数码管篇(2)动态显示

文章正文
发布时间:2024-07-12 18:02

       在静态显示章节我们实现了6个数码管的显示,在所有数码管的位选信号都选通的情况下,6个数码管显示都是一致的。这就有点难搞了,我在实际开发中怎么可能用6个数码管来显示同一个数字,我用一个不就得了?所以说数码管的静态显示这种方法不太实用,仅仅能帮助我们如何学习使用FPGA来控制数码管。看来得想点办法让6个数码管显示不同的数字。

       大家应该都清楚电影的基本显示原理:视觉暂留。科学实验证明,人眼在某个视像消失后,仍可使该物像在视网膜上滞留0.1-0.4秒左右。电影胶片以每秒24格画面匀速转动,一系列静态画面就会因视觉暂留作用而造成一种连续的视觉印象,产生逼真的动感。

        我们的开发板一般都是独立控制6个数码管的电源(位选),然后可以控制所有的数码管的显示内容(段选)。假设现在需要显示一个数字:123456。那么可以先让数码管显示6,由于所有数码管的段选均连在一起,假设所有数码管供电(位选控制),那么数码管应该会显示:666666,这显然不符合我们的需求。可以只保留最右边的数码管亮,其他全部熄灭,那么6个数码管应该显示:、、、、、6(、表示数码管熄灭),让这个状态先保持一定的时间,比如说1ms。接着,让数码管显示5,同时除了右数第二个数码管供电外,其他数码管一概熄灭,那么6个数码管应该显示:、、、、5、(、表示数码管熄灭),这个状态仍然保持1ms。然后重复这个过程,来使得剩下的4个数码管分别显示4~1。

       显然,由于人眼的“视觉暂留”效应,第一次看到的数字“6”会保持一定的时间,第二次看到的数字“5”也会保持一定的时间,```````最后一次看到的数字“1”也会保持一定的时间,这样看起来就好像数字“123456”同时出现在了眼前。

        上图是显示数字“1234”的示意GIF,因为切换速度不够快,所以还是可以看到明显的切换过程,但是实际上已经有一点“暂留“的感觉了。可以预见,一旦这个切换速度够快,那么人眼应该是无法察觉切换过程的,也就是说到时只能看到数字”1234“了,就好像数字”1234“同时在显示一样。依靠这个原理我们就可以实现数码管的动态显示了。

2、动态显示驱动

       了解了原理后,开始编写数码管动态显示的驱动代码。

2.1、端口

        驱动模块输入输出端口如下图:

        各信号含义如下:

                输入端:

                        sys_clk:系统时钟,我的开发板是50M,周期20ns

                        sys_rst_n:低电平有效的异步复位信号

                        num[19:0]:显示的数字,10进制,可显示范围:0-999999,需要20bit的位宽才能表示

                输出端

                        dis_seg[6:0]:数码管7位段选,从高到底分别控制二极管a~g,低电平有效

                        dis_sel[5:0]:数码管位宽,共6个数码管所以共需要6bit,低电平有效

                        dp:小数点二极管控制信号,低电平有效

2.2、Verilog代码 2.2.1、驱动模块代码

       不妨先构思一下具体的思路:

计时(计数)模块

        让数码管实现动态显示的效果的关键,是要以一个较快的时间来切换供电的数码管,这个时间一般可以设置为1ms或者其他。那么我们必然需要一个计时模块来计数,这个模块重复计时到1ms。

移位模块

        让数码管实现动态显示的效果的另一个关键,是要每隔1ms让一个数码管来显示数字的对应位,此时其他位的数码管必须均灭。比如第1ms显示个位的数码管,下一ms显示十位的数码管······直到显示十万位的数码管后再循环。而数码管的亮灭是使用位选信号控制的,也就是说我们需要控制位选信号的移动,每隔1ms,让位选信号左移1位即可。

BCD转换模块

        在本设计里,输入的是十进制数,因为这样使用起来是比较直观和方便的。但是使用者方便了,FPGA就不一定“方便”了。在数字世界中,数字的表示都是采用二进制数来表示。那么如果想要完整的表示十进制中的10个单个数字0~9,则最少需要用4位二进制表示(‘d9 == 'b1001)。但是直接用4位二级制表示,也会有显示问题。比如我现在输入数字:15(’d15 == 'hf),那么我肯定希望数码管显示的是两个数字1--5,而不是一个字母F吗,毕竟喜欢在日常生活中用十六进制的人应该不多。所以我们需要将显示数字从二进制转换为BCD进制码。BCD码可以简单地理解为,将所以位一一拆开,每位均用4位2进制码表示。例如十进制的123,BCD码则为:0001_0010_0011。

        二进制码转BCD码的方法有很多(以后再说),本文采用一种比较直观(但是资源消耗很多)的方法:通过求商、求余的方法来求得6位数的每一位。

        求得6位数的每一位(共6位)后,还需要从高位到低位依次进行判断:

                该位是否非0。若所有位均不为0,则应是完整的6位数,此时将各个位上的BCD码统一寄存到一个变量。

                若最高位为0,则说明这是一个5位数。在这里显示5位数的时候,我们希望最高位会被熄灭,而不是显示0,比如012345就看起来很别扭。所以最高位的BCD码(也就是0)就不用被寄存,替代寄存一个其他约定好的数('ha~'hf均可,这里用'ha)来表示熄灭,后面将BCD码解析成数码管的编码时,就接卸成熄灭所有段选。或者直接用熄灭的数码管编码代替,省略一道解码过程。

                若最高2位均为0,则说明这个是一个4位数。最高位、此高位的BCD码均不用寄存,用熄灭的BCD码代替。

                其他位情况相似。

判断模块

        移位模块让位选有效信号定时定时移动了起来。在判断模块,需要根据当前有效的位选信号,来判断对应数码管应该显示什么数值。例如,在当前时刻,仅右数第一个数码管点亮,那么此时数码管应该显示的值即为高位数的BCD码······

译码模块

        根据输入的BCD码来让数码管点亮对应的段选信号,可以看作是BCD码到段选码的译码过程。

        根据以上,可以编写出完整的代码如下:

//6位8段式数码管动态显示驱动 //端口定义 module dis_dyn_dri ( input sys_clk , //时钟信号 input sys_rst_n , //复位信号(低有效) input [19:0] num, //数码管显示的十进制数,10进制,范围0-999999 output reg [5:0] dis_sel, //数码管位选 output reg [6:0] dis_seg, //数码管段选 output dis_dp //数码管小数点 ); reg [31:0] cnt_1ms; //1ms计数器 reg [23:0] split_num; //拆分的数字 reg [3:0] dis_num; //显示的数字 wire [3:0] num_r1; //右数第1位,即个位 wire [3:0] num_r2; //右数第2位,即十位 wire [3:0] num_r3; //右数第3位,即百位 wire [3:0] num_r4; //右数第4位,即千位 wire [3:0] num_r5; //右数第5位,即万位 wire [3:0] num_r6; //右数第6位,即十万位 //NO.1------------------------------------------------------------------------------------------------------------ //1ms计时模块,1ms/20ns=50_000,从0开始,只要计数到49999 always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) cnt_1ms <= 0; //复位清零 else if(cnt_1ms == 49_999) //计时到 cnt_1ms <= 0; //清零计时 else //计时未到 cnt_1ms <= cnt_1ms + 1; //继续计时 end //控制数码管位选信号(低电平有效),每次1ms循环向左移位1位,实现从右到左数码管的依次点亮 always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) dis_sel <= 6'b111110; //复位熄灭所有数码管 else if(cnt_1ms == 49_999) //计时到 dis_sel <= {dis_sel[4:0],dis_sel[5]}; //位选信号左移1位 else dis_sel <= dis_sel; //复位完成后给所有数码管供电 end //NO.2------------------------------------------------------------------------------------------------------------ assign dis_dp = 1'b1; //小数点,我们暂时不用,使其无效即可 assign num_r1 = num % 10; //个位 assign num_r2 = num / 10 % 10; //十位 assign num_r3 = num / 100 % 10; //百位 assign num_r4 = num / 1000 % 10; //千位 assign num_r5 = num / 10000 % 10; //万位 assign num_r6 = num / 100000 % 10; //十万位 //简单的BCD转换 always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) split_num <= 24'haaaaaa; //复位熄灭 else if(num_r6) //十万位有数字 //把拆分出来的各个位分别赋值给待显示数字变量dis_num split_num <= {num_r6,num_r5,num_r4,num_r3,num_r2,num_r1}; else if(num_r5)begin //万位有数字,但是十万位没有数字 //把拆分出来的各个位分别赋值给待显示数字变量dis_num(除了十万位,因为其不存在) split_num[19:0] <= {num_r5,num_r4,num_r3,num_r2,num_r1}; //十万位因为其不存在,所以不能直接赋值,需要赋值一个约定的数,用以控制熄灭数码管 split_num[23:20] <= 4'ha; //4'ha表示熄灭(每个数最多到9,可选4'ha-4'hf来作为特殊约定) end else if(num_r4)begin //千位有数字,但是十万位、万位没有数字 //把拆分出来的各个位分别赋值给待显示数字变量dis_num(除了十万位、万位,因为其不存在) split_num[15:0] <= {num_r4,num_r3,num_r2,num_r1}; //十万位因为其不存在,所以不能直接赋值,需要赋值一个约定的数,用以控制熄灭数码管 split_num[23:16] <= 8'haa; //4'ha表示熄灭(每个数最多到9,可选4'ha-4'hf来作为特殊约定) end else if(num_r3)begin //百位有数字,但是十万位、万位、千位没有数字 split_num[11:0] <= {num_r3,num_r2,num_r1}; split_num[23:12] <= 12'haaa; end else if(num_r2)begin //十位有数字,但是十万位、万位、千位、百位没有数字 split_num[7:0] <= {num_r2,num_r1}; split_num[23:8] <= 16'haaaa; end else begin //仅仅个位有数字 split_num[3:0] <= num_r1; split_num[23:4] <= 20'haaaaa; end end //NO.3------------------------------------------------------------------------------------------------------------ //根据当前被点亮的数码管,判断应该显示什么数值 always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) dis_num <= 0; else case(dis_sel) 6'b111110: dis_num <= split_num[3:0]; //当前工作的数码管是最右边的数码管,所以应该显示个位数 6'b111101: dis_num <= split_num[7:4]; //当前工作的数码管是次右边的数码管,所以应该显示十位数 6'b111011: dis_num <= split_num[11:8]; // 6'b110111: dis_num <= split_num[15:12]; // 6'b101111: dis_num <= split_num[19:16]; // 6'b011111: dis_num <= split_num[23:20]; //当前工作的数码管是最左边的数码管,所以应该显示十万位数 default: dis_num <= 0; endcase end //根据数码管显示的数值,控制段选信号(低电平有效) always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) dis_seg <= 7'b111_1111; //复位时熄灭数码管(这一条用处不大,因为复位时数码管也不供电) else case (dis_num) //根据要显示的数字来对数码管编码 4'h0 : dis_seg <= 7'b000_0001; //显示数字“0”,则数码管的段选编码为7'b000_0001 4'h1 : dis_seg <= 7'b100_1111; 4'h2 : dis_seg <= 7'b001_0010; 4'h3 : dis_seg <= 7'b000_0110; 4'h4 : dis_seg <= 7'b100_1100; 4'h5 : dis_seg <= 7'b010_0100; 4'h6 : dis_seg <= 7'b010_0000; 4'h7 : dis_seg <= 7'b000_1111; 4'h8 : dis_seg <= 7'b000_0000; 4'h9 : dis_seg <= 7'b000_0100; //显示数字“9”,则数码管的段选编码为7'b000_0100 default : dis_seg <= 7'b111_1111; //其他数字(16进制的数字相对10进制无效)则熄灭数码管 endcase end endmodule 2.2.2、数据生成模块代码

        那么现在静态显示的驱动写好了,我们还需要写个数据生成模块,也就是我们要想办法写入数据到这个驱动来进行显示。

        这个模块只有两个always块,第1个 always块做一个100ms的计时器,该计时器循环计时100ms。该模块可以顺序生成123456(方便我们测试每一个数码管)-999999,每隔100ms,数据累加1。

//数字生成模块,每隔100ms生成10进制数字,该数字从123456开始,到999999结束,然后循环 //端口定义 module data_generate ( input sys_clk , //时钟信号,50M input sys_rst_n , //复位信号(低有效) output reg [19:0] data //4位二进制数字 ); reg [31:0] cnt_100ms; //100ms计数器 //1ms计时模块,该模块循环计数到100ms always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) cnt_100ms <= 0; //复位计数器为0 else if(cnt_100ms == (5_000_000 - 1)) //计数器计数到了100ms,每个时钟周期20ns,则从0开始需要计数(100_000_000/20 - 1) cnt_100ms <= 0; //计数器清零重新开始计数 else cnt_100ms <= cnt_100ms + 1; //没有计数到则每个周期计数1次 end //数据生成模块,从123456开始累加(每100ms累加一次),到999999结束。然后循环 always @ (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) // data <= 0; //复位时熄灭数码管(这一条用处不大,因为复位时数码管也不供电) data <= 123456; //从123456开始,方便仿真验证 else if(cnt_100ms == (5_000_000 - 1))begin //每次计数到100ms if(data == 999999) //数据生成到了999999则重新开始从0生成 data <= 0; else //数据没有生成到9999999则累加1 data <= data + 1; end end endmodule 2.2.3、顶层模块

        有了驱动模块和数据生成模块,我们再写一个顶层模块,顶层模块调用这两个模块并完成连接,实现模块化设计。顶层模块的代码如下:

//6位8段式数码管动态顶层文件 //例化动态显示驱动模块和数据生成模块,将数据生成模块生成的数字,通过动态驱动,用数码管显示出来 //端口定义 module dis_dyn_top ( input sys_clk , //时钟信号 input sys_rst_n , //复位信号(低有效) output [5:0] dis_sel, //数码管位选 output [6:0] dis_seg, //数码管段选 output dis_dp //数码管小数点 ); wire [19:0] data; //需要显示的数字 //例化动态显示驱动模块 dis_dyn_dri dis_dyn_dri_inst( .sys_clk (sys_clk ), //时钟信号 .sys_rst_n (sys_rst_n ), //复位信号(低有效) .num (data ), //数码管显示的十进制数 .dis_sel (dis_sel ), //数码管位选 .dis_seg (dis_seg ), //数码管段选 .dis_dp (dis_dp ) ); //例化数据生成模块 data_generate data_generate_inst ( .sys_clk (sys_clk ), //时钟信号,50M .sys_rst_n (sys_rst_n ), //复位信号(低有效) .data (data ) //4位二进制数字 ); endmodule 2.3、Testbench及仿真结果

        由于我们的工程中,我已经设计了激励,所以在TB文件中,我们仅仅需要提供时钟、复位即可。完整的TB文件如下:

//动态显示仿真激励TESTBENCH `timescale 1ns/1ns //时间单位/精度 //------------<模块及端口声明>---------------------------------------- module tb_dis_dyn_top(); reg sys_clk; //时钟信号 reg sys_rst_n; //复位信号(低有效) wire [5:0] dis_sel; //数码管位选 wire [6:0] dis_seg; //数码管段选 wire dis_dp; //数码管小数点 //------------<例化被测试模块>---------------------------------------- dis_dyn_top dis_dyn_top_inst( .sys_clk (sys_clk ), .sys_rst_n (sys_rst_n ), .dis_sel (dis_sel ), .dis_seg (dis_seg ), .dis_dp (dis_dp ) ); //------------<设置初始测试条件>---------------------------------------- initial begin sys_clk = 1'b0; //初始时钟为0 sys_rst_n <= 1'b0; //初始复位 #25 //25个时钟周期后 sys_rst_n <= 1'b1; //拉高复位,系统进入工作状态 end //------------<设置时钟>---------------------------------------------- always #10 sys_clk = ~sys_clk; //系统时钟周期20ns endmodule

        仿真结果如下(只截取了部分,仿真时间太长,后面会写怎么优化测试过程):

        从上图中可以看到,从123456开始,每隔100ms输入的数据(要显示的数字)累加1。接下来看下数字123456的具体显示过程:

        上图中的信号:

                小数点dp:高电平无效,小数点熄灭

                数码管位选信号dis_sel:低电平有效,111110 = 最右边的数码管亮、其他灭 == 显示个位数。每隔1ms,分别显示个、十、百、千、万、十万。

                数码管段选信号dis_seg:低电平有效,从共阳极数码管编码表得:0000110 = 3 = 显示3;1111111 = 熄灭。所以对应个、十、百、千、万、十万分别是6、5、4、3、2、1。

2.4、上板验证

        绑定对应管脚,全编译整个文件,将sof文件通过JTAG接口下载进FPGA开发板,观察其实验现象。(开发环境在最后)。

        需要注意的是由于CSDN对视频的支持不是很友好,我把实验现象的结果视频转换成了GIF(时长较短,为了满足上传图片5M的大小限制)。实验结果如下(治好了我多年的颈椎病):

3、其他

创作不易,如果本文对您有帮助,还请多多点赞、评论和收藏。您的支持是我持续更新的最大动力!

关于本文,您有什么想法均可在评论区留言交流。如果需要整个工程,请在评论留下邮箱或者私信我邮箱(注意保护隐私)。

自身能力不足,如有错误还请多多指出!

首页
评论
分享
Top