记使用Verilog实现的计算器
前言
大二上学期数字逻辑实验的课设。
下图是预制好的扩展板,可以插在实验台上使用:

具体功能自由发挥,所以需要先明确一下要做什么。
目标是做一个多功能十进制计算器,能够识别4x6按键以及额外扩展的按钮。支持小数,能实现加、减、乘、除运算。12个数码管用于显示数值,从左到右第一位用于显示符号。
0-9键输入数字,整数部分输完后可以按下"."键进入小数输入,按下"+/-"键可以切换正负,CE可以清除当前输入的数,AC可以将包括存储数值在内的数据全清,退格键可以删掉当前输入的最后一位(小数位都删完后再按一次恢复整数输入)。
使用时,可以先输入第一个操作数,再按下操作符(加减乘除键),输入第二个操作数,按下=键显示结果。或者也支持连续输入计算,如按下1+2后继续按+、3、=,得到结果6。
按下GT键可以显示之前所有结果之和。计算器的储存有最多8个槽位,通过LCD液晶屏显示当前选择槽位的序号、储存的数,可以通过扩展的按钮新建和切换槽位。"M+"键可以将当前显示的数加到储存、"M-"键可以将储存值减去当前显示的数、"MR"键将储存值当作结果显示出来。
%键按下后可以将当前数除以100,额外扩展几个按钮还支持一些函数,比如sinx、cosx、开方、乘方。
如果结果溢出绝对值超过99999999.999(预设最大值),或者运算错误(比如除以0),则数码管最后五位显示"ERROR"。
大概就这么多...
思路启发
在此之前,我不得不提及一下StopWatch项目,这也是课程的一部分,让我们使用所学知识,一步步把一个"数字停表"各模块替换成自己写的组件,复刻出完整的功能。
我设计的计算器很多思想也来自于此,比如说下图展示的四模式状态机,也启发了我计算器课设的状态机实现,这样让整个系统逻辑更清晰,每个模式干相应的事。

设计思路
使用Quartus II 13.0软件进行开发,下图是总体框图 (右边是图形化连接的顶层模块):

在确认选题为计算器后,我开始了ALU的编写,用2位位宽的op_sel输入,取值00、01、10、11分别代表加减乘除操作。
写着写着突然想起来还有小数,考虑到由于要在大概两周时间内抽空做这个,使用浮点数可能工作量过大,于是决定采用定点数(这样乘法器什么的都不用自己写了)。
考虑到三位小数还算是一个比较实用的精度,为了尽量让第三位小数准确,选择60位Q24定点数,即最后24位用于表示小数。这样数码管的显示设计也定下来了,最后三位固定显示三位小数,中间8位是整数部分。
这样的话乘除法需要注意一下,两个Q24定点数相乘后小数位就变成48位了,为了保持精度一致,应该将结果再右移24位。除法同理操作之前应该先将被除数左移24位。
这样思路就大概固定了,我还需要造一个状态机模块作为核心,周围有运算单元和储存单元(主状态机给它们输入,并接收返回的输出,完成功能交互),输入有键盘和按钮,输出有数码管。
状态机有三个状态,分别是第一操作数、第二操作数和结果显示模式,给状态机的输入有keycode,我给各按键预设了一个对应的键值(给无输入也单独分配了一个键值,代表无效),维持一个时钟沿有效。
在第一或者第二操作数模式下按0-9键可以输入数,输入整数部分时,就是把当前操作数乘以10再加上这位数字,退格则除以10并抹掉小数部分,小数部分的输入和退格我是用一个栈记录实现的。
之后做的过程中,发现细节还是很多的,状态机每个按键产生的效果都要考虑一下有没有遗漏,比如说加完一个数之后再按操作符会发生什么,那就设计一个连续计算功能。也有很多构想是编写的过程中产生的,比如说除以零或溢出计算错误,可以展示出来。同时负数也要展示,于是我就用10及以上编码字母E,R,O和负号-,修改一下七段数码管译码器,使这些能在数码管上显示。
在测试状态机的过程中,为了方便边写边测试,我用TestBench模拟按键输入到状态机,把BCD输出通过16进制用$display打印,四个二进制位一位,这样刚好就是数码管显示的内容。
那么要把计算出的数接入显示的话,还得把数表示为12位BCD码,采用的是我在网上搜索到的Double Dabble算法(居然还真有这种东西)。核心逻辑是,在左移过程中,若任一BCD位(4位一组)的值大于等于5,则先给该位加3,然后再继续移位,最终得到这个数的BCD表示,如下图演示:

还有一个重要的步骤是去除前导零(都应该全灭而不是显示0),和小数点的显示(虽然是固定位置,但是进入小数输入之前也不用显示),额外控制一下这两个使能信号。那么还有一个问题来了,整数部分的前导零好办,小数部分的末尾零在输入这位之前也不应该显示,于是需要单独记录指示当前输入小数位位置。
在实现M+/M-/MR操作的过程中,我就又联想到了StopWatch项目,设计了一个多存储槽位选择功能。初步设计打算把当前槽位的序号和内容BCD(也想着可以显示+-*/)再转ascii字符串显示在液晶屏上的(如下图所示,型号LCD1602),正好我们课上实验做的液晶控制封装成模块可以直接用。

至于三角函数功能,是用CORDIC算法实现的,它无需乘法器,将复杂的三角函数运算转化为简单的移位与加减法,逐步迭代逼近目标值。
仿真测试
如上文所述,仿真我是写了个TestBench脚本进行的。进行了各种情况的测试,波形图非常长,主要还是根据$display打印的数据来验证结果的(BCD、en、dp,其中en和dp分别是12个数码管和小数点的使能端),如下图右边所示,a=10是负号的编码。

左边的部分波形图也可以看一下,01010是+的键值,01110是=的键值,用五位编码所以用最后的11111代表无效键值(无输入)。
稍微解释一下,disp_value是状态机输出的显示值,60位Q24定点数表示,16777216就刚好是2的24次方(二进制1后面24个0),再乘以12就是201326592,也就是整数部分为12,小数部分全0。
硬件实测
再说说线下实测的事,我们第一次主要就是去测键盘输入和数码管显示的,由于仿真结果都没什么问题,以为应该很快就能结束。实际上,一开始扩展板没插好,且模式没调对,后来才发现要换到模式4,这时候才知道扩展板不能和液晶屏同时使用,大概是引脚位置的限制,为了替代我就干脆把槽位序号显示在实验台本身的数码管上。
再是我把数码管使能高低弄反了导致什么都没有显示(,然后键盘防抖输入也写炸了,导致输入全都被当作抖动消除了,而我们这个计算器大项目编译又很慢,改了一节课才调好。
不过事先在TestBench仿真确实是很正确的做法,之后再稍微修修补补,就基本完成了整个项目。
总结
这门课,从最开始的八选一模块、三八译码器、七段数码管译码器、RAM模块、分频器、计数器、序列检测器等,一步一步做,最后自行设计完成一个课设,也挺好玩的,更是让我体会到一个计算器诞生的不易(bushi