zl程序教程

您现在的位置是:首页 >  其他

当前栏目

FPGA极易入门教程----数码管篇(2)动态显示

入门教程 ---- FPGA 数码管 动态显示
2023-09-11 14:14:48 时间

写在前面

       接上篇:FPGA极易入门教程(1)----数码管静态显示篇,建议一起阅读,有助于理解。


1、动态显示原理

       在静态显示章节我们实现了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、其他

  • 创作不易,如果本文对您有帮助,还请多多点赞、评论和收藏。您的支持是我持续更新的最大动力!
  • 关于本文,您有什么想法均可在评论区留言交流。如果需要整个工程,请在评论留下邮箱或者私信我邮箱(注意保护隐私)。
  • 自身能力不足,如有错误还请多多指出!
  • 预告:下一篇更新代码编写与仿真测试的小技巧。

版本信息

        文件:V1.0

        编号:0012

        Modelsim:Modelsim SE-64 10.4

        Quartus II:Quartus II 13.1 (64-bit)