基于FPGA的计算器设计3——(逐位输入与输出控制模块)
0赞之前写了两篇关于计算器的模块,一个是键盘扫描,一个是数码管消零,今天我总结一下第三个模块,也就是标题写的逐位输入与输出控制模块。我们平时使用过计算器都应该了解,我们每按一个键,显示屏上就多一个数,并且新输入的键放在了最低位,以前输入的数依次左移,在输入操作符的时候,显示的数保持不变,输入第二个操作数的时候,第一个操作数被清空,显示屏显示第二个操作数。
相信很多人都会开始觉得这不很简单吗?不就是移位吗?像流水灯一样。没错,但是如果你觉得它和流水灯一样简单,你就大错特错了。这个模块的难点不在于移位操作,而在于它的时序关系。当然这个实验还不涉及到时序约束的问题,只是你怎么保证,你按下一个键,数码管就不多不少,不迟不早的显示你按下的键。这才是问题的关键所在。
我记得,我第一次调试的时候,按下第三个键,才能显示第一个按下的数字,由此可见,我当时的输出是比我的输入慢了2拍的,甚至有人估计会遇到按一个键,满屏都是这个键所代表的数字。这些都是什么原因呢?
首先回顾一下,我在按键扫描中做了一件事,那就是每确认有键按下,就输出一个flag,这个flag的作用就是避免按一次键,出现满屏相同的数字,因为我们按键按下的时间一般是20ms以上,我用来驱动这个控制电路的时钟和按键扫描的时钟都是用的1khz,就是为了保证flag传过来的脉冲宽度,如果没有这个flag,控制电路检测到按键按下的次数就是20+次了。而这个flag,我是检测的按键弹起的瞬间,至于为什么,我在前面键盘扫描的时候已经解释过了,不了解的可以回头看一下。http://blog.chinaaet.com/yocan/p/5100018107
具体怎么实现,我们还是先来贴代码吧。
这是整个控制部分的代码,代码量不是很多,主要的核心部分是一个两段式的状态机。我定义了两个变量num_reg1和num_reg2用来保存两个操作数,opcode_reg用来保存操作符,在一个每个时钟下只有一个操作数能通过num_out输出到数码管。需要注意的是,我们这里输入的操作数是暂时以BCD码的形式保存,在进行计算时,还需要将BCD转换成二进制或十进制数进行运算。但是数码管是以BCD的形式接收数据和显示的,所以不用考虑BCD转二进制的问题(当然你也可以令数码管以二进制的形式接收数据并显示,我在数字钟的实验中就是以二进制的形式来接收数据和显示的,这个需要和其他模块进行相应的配合,看怎样比较合适就进行怎样的设计)。
在状态机中,S0状态下,接收第一个数据,通过检测接收到的数据是0-9还是a-d,来判断第一个操作数是否输入完毕,在输入操作数的时候,通过循环移位的方式,来把输入的操作数保存在num_reg1寄存器中,至于操作数的输出,我是通过组合逻辑的方式,把num_reg1,传递给了num_out。
至于为什么用组合逻辑呢?如果你用时序逻辑,在num_reg1 <= {num_reg1[19:0],key_value};之后,再num_out <= num_reg1的话,会是一种什么现象?由于这里,我们给了一个按键按下的flag限制,所以在第一个按键按下的时候,num_reg1的低4位会得到当前按下的键值,但是由于是时序逻辑和非阻塞赋值,所以赋给num_out的值此时还是为0。当下一个按键到来的时候,num_out的值才会被赋予第一个按键的值。所以这里就会慢一拍了,加上前面在矩阵键盘扫描里慢下的那一拍,就是为什么我第一次调试的时候慢了两拍的原因。(像这里需要与按键配合调试,不太好写testbench的情况,我推荐大家用signaltap或者chipscope来调试)
在S0状态,如果检测到a-d的键按下,说明输入的是操作符,我们先把操作符保存在opcode_reg中,而不是马上输出,先让状态跳转到S1。因为可能存在误输操作符的情况,在S1状态如果继续输入操作符,那么我们就更新操作符寄存器。如果是输入的0-9,那么代表着第二个操作数开始输入,我们类似对操作数1的处理,把第二个操作数保存起来,直到检测到等号,跳转到S3状态,S3状态直接把计算结果赋给第一个操作数。很多人也许会惊讶,为什么要这样做?我来解释一下这样做有几点好处:
1,输出的处理变得简单,代码中有红色下划线的地方就是决定输出哪个信号,一句话就搞定;
2,可以实现连续运算,如果得到计算结果后,我继续输入的是操作符,那么状态跳回S1,可以继续计算;如果继续输入的是操作数,则表示用户自己放弃连续运算,状态调回S0,重新开始新一轮计算(具体请看S3)。
总结一下,这段代码有几点比较好的地方:
1,状态机状态比较少,各状态之间关系简单明了,我相信很多人写计算器第一反应估计会罗列出4,5种状态,先要输入第一个操作数,然后操作符,然后第二个操作数,然后等号,可能还有一个初始态。这样当然也能实现,只是状态越多,状态之间的跳变就会越复杂。
2,输入输出关系简单明了,我曾经也纠结过怎样建立num_out和各个操作数以及运算结果之间的联系,目前这种方式,是我能想到的最简单有效的方式了,希望能给需要的人一点帮助。
计算器模块还剩最后一个模块了,就是计算以及BCD和二进制之间的转码了,这个控制模块只是实现了最基础的功能,就是正整数的输入,如果要实现负数和小数的运算,这个模块还需要做很大的改动,有兴趣的可以自己思考一下,我会在写下一个模块的时候上传整个工程的代码。小数部分我也只是实现了定点小数而已,浮点小数好像真的挺难的,最近培训,每天安排都很满,继续优化计算器了。这周在做一个IIC控制EEPROM的读写,等把计算器写完了就开始写IIC。