20. 数码管的动态显示

在上一章节我们已经对静态数码管做了详细的讲解,但单单掌握静态数码管的显示方式是远远不够的。我们知道静态数码管六个数码管显示的数字是一样的,但是在实际应用中我们需要显示的信息往往不会那么简单,那么如何节让六个数码管分别显示不同的字符呢?本章节就为大家讲解可用以显示不同字符的数码管驱动方式:动态驱动。

20.1. 理论学习

20.1.1. 数码管动态显示简介

上一章节我们已经知道了什么是静态显示,而什么是动态显示呢?下面将通过图 26‑1为大家讲解动态驱动原理。

segdan002

图 26‑1 六位数码管等效电路图

如图 26‑1,在上一章节我们知道静态显示是让六个数码管的8位段选信号连在8根线上且六个数码管的位选信号同时选中点亮。但是如果我们每次只选中一个数码管点亮呢?这样我们段选信号点亮的就只是我们选中数码管的值了,那是不是就可以给每个数码管显示不一样的值了?但是这样我们又会发现一个新的问题:每次只点亮一个 数码管,那么同一时间六个数码管就只能看到一个数码管在亮,那不是同时显示不了六个不同的字符了吗?针对这个问题先为大家介绍两种现象:

首先是人眼视觉暂留:人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉影像并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”。

其次是数码管的余晖效应:当停止向发光二极管供电时发光二极管亮度仍能维持一段时间。

根据这两种现象我们可以想到,如果让数码管轮流显示,而且轮流显示速度很快,这样会不会看起来六个数码管都在显示呢?事实证明是可以的,这种方式称为动态扫描。为帮助大家理解,打个比方:若一个数码管在1s内点亮两次,那么我们可以很明显的看到其亮了两次,若1s内点亮10次呢?我们可能只能看到其在快速的闪烁,若点 亮100次1000次呢?总有一个速度我们人眼是分辨不出来在闪烁的。所以说一个数码管让我们人眼感觉一直在亮并不用一直给其点亮,只要我们让其亮的间隔足够短就行。这样我们就可以用不在点亮的时间去点亮其他数码管,让其他数码管也达到这样的效果,这样就可以让我们人眼感觉所有数码管都在同时点亮了。那么多长的动态扫 描间隔可以让人眼感觉所有数码管都在亮而不会有闪烁感呢?实验证明,当我们的扫描间隔为1ms时不会有闪烁感。

最后为大家总结我们的动态驱动数码管的方式:使用1ms的刷新时间让六个数码管轮流显示:第1ms点亮第一个数码管,第2ms点亮第二个数码管,以此类推依次点亮六个数码管,6ms一个轮回,也就是说每个数码管每6ms点亮1ms,这样就能让人眼感觉到数码管一直在亮了。点亮相应数码管的时候给其显示相应的值,这样就 可以使六个数码管显示不同的值了,这就是驱动数码管动态显示的方法。

20.2. 实战演练

20.2.1. 实验目标

让六位数码管显示从十进制数0开始计数,每0.1s加1,一直到加到十进制数999999。到达999999之后回到0开始重新计数。

20.2.2. 硬件资源

该硬件资源与“数码管的静态显示”章节的硬件资源是一样的,这里就不再过多介绍了。

20.2.3. 程序设计

硬件资源介绍完毕,我们开始实验工程的程序设计。

20.2.3.1. 整体说明

我们根据实验任务可以先画出我们的系统框图,根据框图我们可以更加明确的看到我们该如何完成这个实验。

segdan003

图 26‑2 数码管动态显示系统框图

根据框图可以看到实验一共分为6个模块,下面分模块为大家介绍。

表格 26‑1 数码管动态显示工程模块简介

模块名称

功能描述

data_gen

数据生成模块

seg_danamic

数码管动态显示驱动模块

bcd_8421

二进制转bcd码模块

hc595_ctrl

74HC595控制模块

seg_595_dynamic

数码管动态显示模块

top_seg_595

顶层模块

20.2.3.2. 数据生成模块

模块框图

segdan004

图 26‑3 数据生成模块框图

表格 26‑2 数据生成模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

系统时钟,频率50MHz

sys_rst_n

1bit

Input

复位信号,低有效

point

6bit

Output

输出小数点

data

20bit

Output

输出数据

seg_en

1bit

Output

数码管使能信号

sign

1bit

Output

输出符号

因为8位数码管是可以显示小数点和负号的,虽然本实验不用显示小数点和负号,但是我们还是给其预留了小数点和负号输出口,以增强模块的复用性。seg_en为数码管使能信号,当其为高电平数码管才显示我们的数据。data为显示的数据,因为需显示的最大值为十进制的999999,故其位宽为20bit。

数据生成波形图绘制

segdan005

图 26‑4 数据生成模块波形图

如图 26‑4所示:

cnt_100ms:我们实验的要求是每0.1s让显示的数据加1,所以我们需要用到一个间隔为0.1s的循环计数器。计数器其从0开始计数,计到0.1s(4999999)时归0开始下一个0.1s计数。因为计数器的时钟为50MHz,一个时钟为1/50MHz(s),也就是20ns,所以0.1s=4999_99 9*20ns。

cnt_flag:当计数器计到0.1s时拉高一个标志信号,让这个标志信号去控制数据的加1。

data:输出数据。当检测到0.1s到来的标志信号为高时让其加1,当加到999999并检测到0.1s到来的标志信号时让其归0,开始下一轮的数据显示。

point:输出小数点。在这里我们让其高电平有效(以本次使用的数码管为例,即当小数点对应段选信号为低,位选信号为高时点亮有效),本次实验不需要显示小数点,让每一位都为0即可。

seg_en:数码管使能信号。因为我们一直在显示,所以一直给其拉高就行

sign:负号显示,高电平有效。因本次实验不显示负号,给其一直为低电平即可。

本设计思路只做参考,并非唯一方法,读者可利用所学知识,按照自己思路进行设计。

代码编写

代码清单 26‑1 数据生成模块参考代码(data_gen.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
module data_gen
#(
parameter CNT_MAX = 23'd4999_999, //100ms计数值
parameter DATA_MAX= 20'D999_999 //显示的最大值
)
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效

 output reg [19:0] data , //数码管要显示的值
 output reg [5:0] point , //小数点显示,高电平有效
 output reg seg_en , //数码管使能信号,高电平有效
 output reg sign //符号位,高电平显示负号
 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //reg define
 reg [22:0] cnt_100ms ; //100ms计数器
 reg cnt_flag ; //100ms标志信号

 ////
 //\* Main Code \//
 ////

 //不显示小数点以及负数
 assign point = 6'b000_000;
 assign sign = 1'b0;

 //cnt_100ms:用50MHz时钟从0到4999_999计数即为100ms
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_100ms <= 23'd0;
 else if(cnt_100ms == CNT_MAX)
 cnt_100ms <= 23'd0;
 else
 cnt_100ms <= cnt_100ms + 1'b1;

 //cnt_flag:每100ms产生一个标志信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_flag <= 1'b0;
 else if(cnt_100ms == CNT_MAX - 1'b1)
 cnt_flag <= 1'b1;
 else
 cnt_flag <= 1'b0;

 //数码管显示的数据:0-999_999
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 data <= 20'd0;
 else if((data == DATA_MAX) && (cnt_flag == 1'b1))
 data <= 20'd0;
 else if(cnt_flag == 1'b1)
 data <= data + 1'b1;
 else
 data <= data;

 //数码管使能信号给高即可
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 seg_en <= 1'b0;
 else
 seg_en <= 1'b1;

 endmodule

如上所示代码完全是根据波形图来编写的。大家可以根据波形图的描述一个一个信号去进行编写,这样的话编写的时候思路就比较清晰,不容易出错。此模块是数据生成模块,如果要显示其他字符,在此模块进行更改即可。

20.2.3.3. 二进制转BCD码模块

从工程整体的模块框图中可以看到我们里面设计了一个二进制转BCD码模块(bcd_8421),我们为什么要设计这个模块呢?

根据实验目标可知我们要让数码管显示的数是十进制数,而我们数码管是通过段选信号和位选信号去控制每个数码管进行显示的,一位数码管显示的是一个十进制数,而我们的十进制数是以二进制进行编码的,如果我们直接使用二进制编码进行显示,那么每个数码管显示的数就不是十进制数而是十六进制数了。所以我们需要将二进制编码的 十进制数转换为BCD编码的十进制数进行显示。为什么转换为BCD码就能进行十进制的显示了呢?下面我们先了解一下什么是BCD码。

BCD码(Binary-Coded Decimal‎),又称二 - 十进制码,使用4位二进制数来表示1位十进制数中的0~9这10个数码,是一种二进制的数字编码形式,用二进制编码的十进制代码。

BCD码根据权值的有无可分为“有权码”和“无权码”。其中的权字表示的是权值,有权码的四位二进制数中的每一位都有一个固定的权值,而无权码是没有权值的。常见的有权码有8421码、5421码和2421码,8421码它的权值从左到右是8421,5421码的权值从左到右是5421,2421码的权值是2421, 其中8421码是最为常用的BCD编码,本实验中使用的也是这种编码。常用的无权码有余3码、余3循环码,还有前面所讲的格雷码。各编码方式所对应的十进制数如图 26‑5所示:

segdan006

图 26‑5 十进制数对应的BCD编码

那么如何用BCD码来表示我们的十进制数呢?以本次实验我们使用的8421BCD编码为例,比如说十进制数5,它的8421BCD码为0101,那么我们怎么通过0101得到我们的数字5呢,这里需要用到一个算法,就是将其每位二进制数乘以它的权值然后相加。十进制5的8421BCD码为0101,即:1\(\times\)1 + 0\(\times\)2 + 1\(\times\)4+ 0\(\times\)8 = 5。其它有权码也是这种计算方式,而无权码的计算方式这里就不过多讲解了,感兴趣的读者可自行查找相关资料进行了解。

讲完BCD码的相关知识后我们再回到前面的问题,为什么转换为BCD码就能进行十进制的显示了呢?这里我们举个例子,例如我们需要显示十进制数据234,根据我们前面讲的动态显示原理,在第一个显示周期我们点亮数码管1,并让其显示值4;在第二个显示周期我们点亮数码管2,并让其显示值3;在第三个显示周期我们点亮数 码管3,并让其显示值2;第四、五、六个周期我们不点亮其余数码管即可,以此循环就完成了十进制数234的显示。那个我们如何才能在点亮相应的值时给其显示相应的值呢?我们先看看234的十进制数的二进制表示为:1110_1010;234的8421BCD码为:0010_0011_0100;可以发现如果我们使用二 进制数赋值我们并不能准确的给到2、3、4值,而如果我们用8421BCD码给其赋值的话,每4位8421BCD码代表一个十进制数,那么就能完美的进行显示了。

上面我们介绍了为什么要用BCD码进行显示,但是我们输入的数据是以二进制编码表示的十进制数,我们要如何将其转化为8421BCD码表示的十进制数呢?下面我们通过一个例子来为大家介绍怎么进行转换。

这里我们以十进制数234为例,通过图 26‑6为大家讲解。

segdan007

图 26‑6 二进制转BCD码步骤

如上图所示,十进制数234其对应的二进制数为1110_1010,首先第一步我们在其前面补上若干个0,那么这个0的数量是如何决定的呢?参与转换的十进制有多少位,就需要多少个相应的BCD码,比如234,该十进制数是3位,而一位十进制数的BCD码是四位,所以这里我们就需要12位BCD码,故我们就在前面补1 2个0。其余位数的十进制补0数量也是这样进行计算。

第二步我们需要进行判断运算移位操作,首先判断每一个BCD码其对应的十进制数是否大于4,如果大于4就对BCD码做加3操作,若小于等于4就让其值保持不变。当对每一个BCD码进行判断运算后,都需要将运算后的数据像左移1位。移完位后我们仍按前面所述进行判断运算,判断运算后需再次移位,以此循环,当我们进行8次 判断移位后的BCD码部分数据就是我们转换的数据,如图 26‑6所示,当第8次移位后的8421BCD码数据对应的十进制正是234。这里需要注意的是我们输入转换的二进制码有多少位我们就需要进行多少次判断移位操作,这里输入的是8位二进制,我们就进行8次判断移位操作。

根据以上方法我们就能进行任意位数的二进制码转BCD码的操作了。下面我们看看本次实验中的二进制码转BCD码的代码该如何实现。

模块框图

segdan008

图 26‑7 二进制转BCD码模块框图

表格 26‑3 二进制转BCD码模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

系统时钟,频率50MHz

sys_rst_n

1bit

Input

复位信号,低有效

data

20bit

Input

输入以二进制表示的十进制数

unit

1bit

Output

个位BCD码

ten

1bit

Output

十位BCD码

hun

1bit

Output

百位BCD码

tho

1bit

Output

千位BCD码

t_tho

1bit

Output

万位BCD码

h_hun

1bit

Output

十万位BCD码

如图 26‑7、表格 26‑3所示:由于我们数据生成模块生成的数据位宽为20位,所以里我们输入转换的二进制编码表示的十进制数也是20位宽,同时我们输出了六个BCD码位,一个BCD码代表十进制数的一个位宽,这样我们将各个位的BCD码分别给相应的数码管显示就行了。

波形图绘制

segdan009

图 26‑8 二进制转BCD码波形图

根据图 26‑8为大家讲解如何实现二进制转BCD码。

输入信号中的时钟复位这里就不过多讲解了,首先是data信号,该信号是输入的需要转换的二进制表示的十进制数,该信号由数据生成模块传来。

cnt_shift:移位判断计数器,前面我们说到我们输入转换的二进制码有多少位我们就需要进行多少次判断移位操作,这里我们data数据的位宽为20位,所以这里我们声明移位判断计数器对移位20次进行判断控制。

data_shift:移位判断数据寄存器,该寄存器用于存储移位判断操作过程中的数据,这里我们输入的二进制位宽为20位,待转换成的BCD码位宽为24位,所以这里我们声明该寄存器的位宽为输入的二进制位宽和待转换完成的BCD码位宽之和,即44位。根据波形图可知,这里我们设计当移位计数器等于0时寄存器的低2 0位即为待转换数据,而由于还没开始进行转换,高24位的BCD码我们补0即可。

shift_flag:移位判断操作标志信号。前面说到我们需要对数据进行移位和判断,判断在前移位在后,所以这里我们声明一个标志信号,用于控制判断和移位的先后顺序,当shift_flag为低时对数据进行判断,当shift_flag为高时对数据进行移位。需要注意的是无论是移位操作和判断操作都是在单个系统时 钟下完成的,故我们判断20次移位20次在40个系统时钟内就能完成。

unit、ten、hum、tho、t_tho、h_hun:六路BCD码,前面我们说到我们开发板上有六位数码管,故可以显示的最大值是六位十进制数,随意这里我们声明了六路BCD码,一个BCD码代表十进制的一个位数,其中unit代表个位、ten代表十位、hun代表百位、tho代表千位、t_tho代表万位、 h_hun代表十万位。当我们的移位判断计数器等于21是说明20位二进制转BCD码的移位判断操作已经完成,此时data_shift里寄存的数据就是转换的寄存数据,该数据的高24位即转换完成后的BCD码。所以当cnt_shift等于20是就将寄存的BCD码值赋值为相对应的各个位数。

代码编写

波形图讲解完之后我们看看代码是如何实现的,模块参考代码,具体见代码清单 26‑2。

代码清单 26‑2 二进制转BCD码模块参考代码(bcd_8421.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
module bcd_8421
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire [19:0] data , //输入需要转换的数据

output reg [3:0] unit , //个位BCD码
output reg [3:0] ten , //十位BCD码
output reg [3:0] hun , //百位BCD码
 output reg [3:0] tho , //千位BCD码
 output reg [3:0] t_tho , //万位BCD码
 output reg [3:0] h_hun //十万位BCD码
 );

 ////
 //\* Parameter And Internal Signal \//
 ////

 //reg define
 reg [4:0] cnt_shift ; //移位判断计数器
 reg [43:0] data_shift ; //移位判断数据寄存器
 reg shift_flag ; //移位判断标志信号

 ////
 //\* Main Code \//
 ////

 //cnt_shift:从0到21循环计数
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_shift <= 5'd0;
 else if((cnt_shift == 5'd21) && (shift_flag == 1'b1))
 cnt_shift <= 5'd0;
 else if(shift_flag == 1'b1)
 cnt_shift <= cnt_shift + 1'b1;
 else
 cnt_shift <= cnt_shift;

 //data_shift:计数器为0时赋初值,计数器为1~20时进行移位判断操作
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 data_shift <= 44'b0;
 else if(cnt_shift == 5'd0)
 data_shift <= {24'b0,data};
 else if((cnt_shift <= 20) && (shift_flag == 1'b0))
 begin
 data_shift[23:20] <= (data_shift[23:20] > 4) ?
 (data_shift[23:20] + 2'd3) : (data_shift[23:20]);
 data_shift[27:24] <= (data_shift[27:24] > 4) ?
 (data_shift[27:24] + 2'd3) : (data_shift[27:24]);
 data_shift[31:28] <= (data_shift[31:28] > 4) ?
 (data_shift[31:28] + 2'd3) : (data_shift[31:28]);
 data_shift[35:32] <= (data_shift[35:32] > 4) ?
 (data_shift[35:32] + 2'd3) : (data_shift[35:32]);
 data_shift[39:36] <= (data_shift[39:36] > 4) ?
 (data_shift[39:36] + 2'd3) : (data_shift[39:36]);
 data_shift[43:40] <= (data_shift[43:40] > 4) ?
 (data_shift[43:40] + 2'd3) : (data_shift[43:40]);
 end
 else if((cnt_shift <= 20) && (shift_flag == 1'b1))
 data_shift <= data_shift << 1;
 else
 data_shift <= data_shift;

 //shift_flag:移位判断标志信号,用于控制移位判断的先后顺序
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 shift_flag <= 1'b0;
 else
 shift_flag <= ~shift_flag;

 //当计数器等于20时,移位判断操作完成,对各个位数的BCD码进行赋值
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 begin
 unit <= 4'b0;
 ten <= 4'b0;
 hun <= 4'b0;
 tho <= 4'b0;
 t_tho <= 4'b0;
 h_hun <= 4'b0;
 end
 else if(cnt_shift == 5'd21)
 begin
 unit <= data_shift[23:20];
 ten <= data_shift[27:24];
 hun <= data_shift[31:28];
 tho <= data_shift[35:32];
 t_tho <= data_shift[39:36];
 h_hun <= data_shift[43:40];
 end

 endmodule

代码是根据我们绘制的波形图讲解的,这里我们对部分代码进行特别说明。代码47~58行我们使用三目运算符对移位数据进行判断,若各个位数大于4则进行加三操作,否则不变。代码61行进行移位操作,每当判断之后我们都需将数据左移移位后再进行判断。

20.2.3.4. 数码管动态显示驱动模块

模块框图

segdan010

图 26‑9 数码管动态显示驱动模块框图

表格 26‑4 数码管动态显示驱动模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

系统时钟,频率50MHz

sys_rst_n

1bit

Input

复位信号,低有效

point

6bit

Iutput

输入小数点

data

20bit

Iutput

输入数据

seg_en

1bit

Iutput

数码管使能信号

sign

1bit

Iutput

输入符号

sel

6bit

Iutput

数码管位选信号

seg

8bit

Output

数码管段选信号

可以看到该模块内例化了我们前面所讲的二进制转BCD码模块,这样我们在数码管动态显示驱动模块中就能完成对输入十进制数的动态显示,这样能增大模块的复用性,方便其它工程调用。

波形图绘制

segdan011

图 26‑10 数码管动态显示波形图

根据图 26‑10我们对各个信号逐一为大家讲解。

point:输入小数点控制信号,高电平有效,这里我们假设要让第二个数码管显示小数点,其余数码管不显示小数点,那么此时point的输入的值就应该是6’b000010。

seg_en:数码管使能信号,这里一直让其拉高即可。

data:输入的十进制数据,假设这里我们输入的十进制数为9876。

sign:符号位控制信号,高电平有效。假设我们需要显示的是负数,那么这里就让符号位控制信号为高即可。

unit、ten、hun、tho、t_tho、h_hun:这六个信号就是我们例化的bcd_8421模块转化的的8421BCD码,也就是说这六个BCD码就是输入十进制数9876各个位的BCD码。所以这里个位(unit)是6,十位(ten)是7,百位(hun)是8,千位(tho)是9,万位和十万位都为0 。

data_reg:数码管待显示内容寄存器,因为这里我们假设输入要显示的十进制数为9876,并且显示负号,所以前五个数码管就会显示-9876的数值,此时最高位数码管什么都不显示,我们用X表示,所以这里六个数码管显示的内容就是:X-9876。

cnt_1ms:前面讲到要让显示的数码管不会有闪烁感,我们需要使用1ms的扫描时间去扫描各个数码管。所以这里我们需要一个1ms的计数器对1ms进行循环计数。

flag_1ms:1ms计数标志信号,当1ms计数器计到1ms时拉高该标志信号,我们使用该标志信号去控制位选数码管计数器的计数。

cnt_sel:位选数码管计数器。我们在理论学习中说到动态扫描方式是用1ms的刷新时间让六个数码管轮流显示:第1ms点亮第一个数码管,第2ms点亮第二个数码管,以此类推依次点亮六个数码管,6ms一个轮回,也就是说每个数码管每6ms点亮一次。那问题是我们怎么去选中这个要显示的数码管并且给其要显示的值呢 ?这个时候我们就引入了一个cnt_sel信号,让其从0~5循环计数,1个数代表一个数码管,可以看做是给数码管编号。这样的话我们只要选择计数器的值就相当于选中了其中对应的数码管。特别要说明的是我们的cnt_sel计数器必须与数码管的刷新状态一致,也就是1ms计1个数。

sel_reg:数码管位选信号寄存器,为了让数码管位选信号和段选信号同步,这里我们先将位选信号进行寄存。刷新到哪个数码管就将sel中对应位(6个位宽,每一位对应一个数码管)给高点亮即可。选中点亮的数码管后我们需要给其要显示的值,所以我们引入一个新的信号。

data_disp:当前点亮数码管显示的值。若我们此时点亮的是第一个数码管,那么我们就需要给第一个数码管显示值6,若刷新到第二个数码管,那么我们就需要给第二个数码管显示值7,以此类推;当刷新到第五个数码管时,此时显示的是负号,那么我们该如何表示呢?这里我们让该信号的值为10来表示,也就是说当data _disp的值为10时就让数码管显示负号,同理这里我们定义data_disp的值为11时让数码管什么也不显示,即不点亮数码管。

dot_disp:当前数码管显示的小数点,我们输入的point信号是点亮第二个数码管的小数点,而我们的数码管是低电平点亮,所以这里当扫描到第二个数码管时让dot_disp信号为低即可。

seg:数码管段选信号,我们根据数码管编码译码表当扫描到哪个数码管显示需要显示的值时,我们将对于的段点亮即可。

sel:数码管位选信号。将数码管位选信号寄存器打一拍即可,这样就能实现数码管段选信号和位选信号的同步。

代码编写

数码管动态扫描的基本逻辑关系我们通过波形图已经讲解完了,下面就可以进行代码的编写了。模块参考代码,具体见代码清单 26‑3。

代码清单 26‑3 数码管动态显示模块参考代码(seg_dynamic.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
module seg_dynamic
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [19:0] data , //数码管要显示的值
input wire [5:0] point , //小数点显示,高电平有效
input wire seg_en , //数码管使能信号,高电平有效
input wire sign , //符号位,高电平显示负号
output reg [5:0] sel , //数码管位选信号
output reg [7:0] seg //数码管段选信号
);
////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CNT_MAX = 16'd49_999; //数码管刷新时间计数最大值
//wire define
wire [3:0] unit ; //个位数
wire [3:0] ten ; //十位数
wire [3:0] hun ; //百位数
wire [3:0] tho ; //千位数
wire [3:0] t_tho ; //万位数
wire [3:0] h_hun ; //十万位数
//reg define
reg [23:0] data_reg ; //待显示数据寄存器
reg [15:0] cnt_1ms ; //1ms计数器
reg flag_1ms ; //1ms标志信号
reg [2:0] cnt_sel ; //数码管位选计数器
reg [5:0] sel_reg ; //位选信号
reg [3:0] data_disp ; //当前数码管显示的数据
reg dot_disp ; //当前数码管显示的小数点
////
//\* Main Code \//
////
//data_reg:控制数码管显示数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_reg <= 24'b0;
//若显示的十进制数的十万位为非零数据或需显示小数点,则六个数码管全显示
else if((h_hun) \|\| (point[5]))
data_reg <= {h_hun,t_tho,tho,hun,ten,unit};
//若显示的十进制数的万位为非零数据或需显示小数点,则值显示在5个数码管上
//打比方我们输入的十进制数据为20’d12345,我们就让数码管显示12345而不是012345
else if(((t_tho) \|\| (point[4])) && (sign == 1'b1))//显示负号
data_reg <= {4'd10,t_tho,tho,hun,ten,unit};//4'd10我们定义为显示负号
else if(((t_tho) \|\| (point[4])) && (sign == 1'b0))
data_reg <= {4'd11,t_tho,tho,hun,ten,unit};//4'd11我们定义为不显示
//若显示的十进制数的千位为非零数据或需显示小数点,则值显示4个数码管
else if(((tho) \|\| (point[3])) && (sign == 1'b1))
data_reg <= {4'd11,4'd10,tho,hun,ten,unit};
else if(((tho) \|\| (point[3])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,tho,hun,ten,unit};
//若显示的十进制数的百位为非零数据或需显示小数点,则值显示3个数码管
else if(((hun) \|\| (point[2])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd10,hun,ten,unit};
else if(((hun) \|\| (point[2])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,4'd11,hun,ten,unit};
//若显示的十进制数的十位为非零数据或需显示小数点,则值显示2个数码管
else if(((ten) \|\| (point[1])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd11,4'd10,ten,unit};
else if(((ten) \|\| (point[1])) && (sign == 1'b0))
data_reg <= {4'd11,4'd11,4'd11,4'd11,ten,unit};
//若显示的十进制数的个位且需显示负号
else if(((unit) \|\| (point[0])) && (sign == 1'b1))
data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd10,unit};
//若上面都不满足都只显示一位数码管
else
data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd11,unit};
//cnt_1ms:1ms循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_1ms <= 16'd0;
else if(cnt_1ms == CNT_MAX)
cnt_1ms <= 16'd0;
else
cnt_1ms <= cnt_1ms + 1'b1;
//flag_1ms:1ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
flag_1ms <= 1'b0;
else if(cnt_1ms == CNT_MAX - 1'b1)
flag_1ms <= 1'b1;
else
flag_1ms <= 1'b0;
//cnt_sel:从0到5循环数,用于选择当前显示的数码管
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sel <= 3'd0;
else if((cnt_sel == 3'd5) && (flag_1ms == 1'b1))
 cnt_sel <= 3'd0;
 else if(flag_1ms == 1'b1)
 cnt_sel <= cnt_sel + 1'b1;
 else
 cnt_sel <= cnt_sel;

 //数码管位选信号寄存器
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 sel_reg <= 6'b000_000;
 else if((cnt_sel == 3'd0) && (flag_1ms == 1'b1))
 sel_reg <= 6'b000_001;
 else if(flag_1ms == 1'b1)
 sel_reg <= sel_reg << 1;
 else
 sel_reg <= sel_reg;

 //控制数码管的位选信号,使六个数码管轮流显示
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 data_disp <= 4'b0;
 else if((seg_en == 1'b1) && (flag_1ms == 1'b1))
 case(cnt_sel)
 3'd0: data_disp <= data_reg[3:0] ; //给第1个数码管赋个位值
 3'd1: data_disp <= data_reg[7:4] ; //给第2个数码管赋十位值
 3'd2: data_disp <= data_reg[11:8] ; //给第3个数码管赋百位值
 3'd3: data_disp <= data_reg[15:12]; //给第4个数码管赋千位值
 3'd4: data_disp <= data_reg[19:16]; //给第5个数码管赋万位值
 3'd5: data_disp <= data_reg[23:20]; //给第6个数码管赋十万位值
 default:data_disp <= 4'b0;
 endcase
 else
 data_disp <= data_disp;

 //dot_disp:小数点低电平点亮,需对小数点有效信号取反
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 dot_disp <= 1'b1;
 else if(flag_1ms == 1'b1)
 dot_disp <= ~point[cnt_sel];
 else
 dot_disp <= dot_disp;

 //控制数码管段选信号,显示数字
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 seg <= 8'b1111_1111;
 else
 case(data_disp)
 4'd0 : seg <= {dot_disp,7'b100_0000}; //显示数字0
 4'd1 : seg <= {dot_disp,7'b111_1001}; //显示数字1
 4'd2 : seg <= {dot_disp,7'b010_0100}; //显示数字2
 4'd3 : seg <= {dot_disp,7'b011_0000}; //显示数字3
 4'd4 : seg <= {dot_disp,7'b001_1001}; //显示数字4
 4'd5 : seg <= {dot_disp,7'b001_0010}; //显示数字5
 4'd6 : seg <= {dot_disp,7'b000_0010}; //显示数字6
 4'd7 : seg <= {dot_disp,7'b111_1000}; //显示数字7
 4'd8 : seg <= {dot_disp,7'b000_0000}; //显示数字8
 4'd9 : seg <= {dot_disp,7'b001_0000}; //显示数字9
 4'd10 : seg <= 8'b1011_1111 ; //显示负号
 4'd11 : seg <= 8'b1111_1111 ; //不显示任何字符
 default:seg <= 8'b1100_0000;
 endcase

 //sel:数码管位选信号赋值
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 sel <= 6'b000_000;
 else
 sel <= sel_reg;

 ////
 //\* Instantiation \//
 ////

 //---------- bsd_8421_inst ----------
 bcd_8421 bcd_8421_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n), //复位信号,低电平有效
 .data (data ), //输入需要转换的数据

 .unit (unit ), //个位BCD码
 .ten (ten ), //十位BCD码
 .hun (hun ), //百位BCD码
 .tho (tho ), //千位BCD码
 .t_tho (t_tho ), //万位BCD码
 .h_hun (h_hun ) //十万位BCD码
 );

 endmodule

代码第43行到75行是对数码管显示的值进行控制。我们的实验任务是从0显示到999999,所以刚开始只有一个数码管在显示,然后是两个,三个,最后才是六个数码管在同时显示。所以当我们只显示一位或显示其它位时,我们需要让不显示的位数显示负号或什么都不显示。所以我们的data_disp多出了4’d10和4’ d11两个状态,这两个数字并不是我们要显示的值,而是代表显示负号和什么字符都不显示的状态。当点亮段选信号时这两个状态给其点亮显示负号或什么字符不显示即可。

20.2.3.5. 74HC595控制模块

在“数码管的静态显示”章节我们已经对该模块进行了详细的讲解,这里我们直接调用即可。

20.2.3.6. 数码管动态显示模块

我们将数码管动态显示驱动模块和74HC595控制模块整合到一个模块之中,后面我们工程要用到数码管动态显示时我们直接例化这一个模块即可,较为方便。其模块框图如图 26‑11所示。

模块框图

segdan012

图 26‑11 数码管动态显示模块

该模块主要是对数码管动态显示驱动模块和74HC595控制模块的实例化,以及对应信号的连接,各输入输出如表格 26‑5所示。

表格 26‑5 数码管动态显示模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

系统时钟,频率50MHz

sys_rst_n

1bit

Input

复位信号,低有效

point

6bit

Iutput

输入小数点

data

20bit

Iutput

输入数据

seg_en

1bit

Iutput

数码管使能信号

sign

1bit

Iutput

输入符号

stcp

1bit

Output

存储寄存器时钟

shcp

1bit

Output

移位寄存器时钟

ds

1bit

Output

串行数据

oe

1bit

Output

输出使能,低有效

代码编写

该模块代码编写较为容易,无需波形图的绘制。数码管动态显示模块参考代码,具体见代码清单 26‑4。

代码清单 26‑4 数码管动态显示模块参考代码(seg_595_dynamic.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
module seg_595_dynamic
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [19:0] data , //数码管要显示的值
input wire [5:0] point , //小数点显示,高电平有效
input wire seg_en , //数码管使能信号,高电平有效
input wire sign , //符号位,高电平显示负号

 output wire stcp , //输出数据存储寄时钟
 output wire shcp , //移位寄存器的时钟输入
 output wire ds , //串行数据输入
 output wire oe //输出使能信号

 );

 ////
 //\* Parameter And Internal Signal \//
 ////

 //wire define
 wire [5:0] sel; //数码管位选信号
 wire [7:0] seg; //数码管段选信号

 ////
 //\* Main Code \//
 ////

 seg_dynamic seg_dynamic_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n), //复位信号,低有效
 .data (data ), //数码管要显示的值
 .point (point ), //小数点显示,高电平有效
 .seg_en (seg_en ), //数码管使能信号,高电平有效
 .sign (sign ), //符号位,高电平显示负号

 .sel (sel ), //数码管位选信号
 .seg (seg ) //数码管段选信号

 );

 hc595_ctrl hc595_ctrl_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n), //复位信号,低有效
 .sel (sel ), //数码管位选信号
 .seg (seg ), //数码管段选信号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe )

 );

 endmodule

20.2.3.7. 顶层模块

模块框图

segdan013

图 26‑12 数码管动态显示顶层模块

顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,各输入输出如表格 26‑6所示。

表格 26‑6 顶层模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

系统时钟,50MHz

sys_rst_n

1bit

Input

复位信号,低有效

stcp

1bit

Output

存储寄存器时钟

shcp

1bit

Output

移位寄存器时钟

ds

1bit

Output

串行数据

oe

1bit

Output

输出使能,低有效

代码编写

顶层模块代码编写较为容易,无需波形图的绘制。顶层参考代码具体见代码清单 26‑5。

代码清单 26‑5 数码管动态显示顶层模块(top_seg_595.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
module top_seg_595
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效

output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
output wire oe //输出使能信号

 );

 ////
 //\* Parameter And Internal Signal \//
 ////

 //wire define
 wire [19:0] data ; //数码管要显示的值
 wire [5:0] point ; //小数点显示,高电平有效top_seg_595
 wire seg_en ; //数码管使能信号,高电平有效
 wire sign ; //符号位,高电平显示负号

 ////
 //\* Main Code \//
 ////


 //-------------data_gen_inst--------------
 data_gen data_gen_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n), //复位信号,低电平有效

 .data (data ), //数码管要显示的值
 .point (point ), //小数点显示,高电平有效
 .seg_en (seg_en ), //数码管使能信号,高电平有效
 .sign (sign ) //符号位,高电平显示负号

 );

 //-------------seg7_dynamic_inst--------------
 seg_595_dynamic seg_595_dynamic_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .data (data ), //数码管要显示的值
 .point (point ), //小数点显示,高电平有效
 .seg_en (seg_en ), //数码管使能信号,高电平有效
 .sign (sign ), //符号位,高电平显示负号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号

 );
 endmodule

20.2.3.8. RTL视图

顶层模块介绍完毕,使用Quartus II软件对实验工程进行编译,工程通过编译后查看实验工程RTL视图。工程RTL视图,具体见图 26‑12、图 26‑13。由图可知,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。

segdan014

图 26‑13 顶层模块rtl视图

segdan015

图 26‑14 数码管动态显示模块rtl视图

20.2.3.9. 仿真验证

仿真代码编写

编写仿真代码,对参考代码进行仿真验证。仿真参考代码,具体见代码清单 26‑6。

代码清单 26‑6 数码管动态显示仿真参考代码(tb_top_seg_595.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
\`timescale 1ns/1ns
module tb_top_seg_595();

////
//\* Parameter and Internal Signal \//
////
//wire define
wire stcp ; //输出数据存储寄时钟
wire shcp ; //移位寄存器的时钟输入
 wire ds ; //串行数据输入
 wire oe ; //输出使能信号

 //reg define
 reg sys_clk ;
 reg sys_rst_n ;

 ////
 //\* Main Code \//
 ////

 //对sys_clk,sys_rst_n赋初始值
 initial
 begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #100
 sys_rst_n <= 1'b1;
 end

 //clk:产生时钟
 always #10 sys_clk <= ~sys_clk;

 //重新定义参数值,缩短仿真时间
 defparam top_seg_595_inst.seg_595_dynamic_inst.seg_dynamic_inst.
 CNT_MAX=19;
 defparam top_seg_595_inst.data_gen_inst.CNT_MAX = 49;

 ////
 //\* Instantiation \//
 ////
 //------------- seg_595_static_inst -------------
 top_seg_595 top_seg_595_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低电平有效

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号
 );

 endmodule

代码第34行和第36行是对1ms和100ms定义的比实际的小一些,这样仿真波形图能比较快的跑出我们想要的实验结果。

仿真波形分析

配置好仿真文件,使用ModelSim对参考代码进行仿真,仿真结果如下。

segdan016

图 26‑15 数码管动态显示仿真波形图(一)

如图 26‑15为数据生成模块仿真整体波形图,可以看到的是date是一直有值的,其它的什么也看不清。我们放大其局部来看看数据生成模块是否满足我们的设计要求。

segdan017

图 26‑16 数码管动态显示仿真波形图(二)

如图 26‑16为数据生成模块的开始波形图,可以看到数据(data)从0开始往后开始相加了,与我们绘制的数据生成模块的开头部分是一致的。

segdan018

图 26‑17 数码管动态显示仿真波形图(三)

如图 26‑17为数据生成模块计数到最大值的波形图,可以看到数据(data)计数计到最大值时跳回0从头开始计数了,与我们所绘制的数据生成波形图的结尾部分是一致的。通过上面上个波形图说明数据生成模块的设计是正确的,能达到我们的实验要求。下面我们看看二进制转BCD码模块能否正确转换。

segdan019

图 26‑18 数码管动态显示仿真波形图(四)

如图 26‑18为二进制转BCD模块仿真波形图,由于我们转换需要22个计数周期,所以转换后的BCD码会延迟22个计数周期,可以看到上图中数据是可以正确转换的。下面我们看看数码管是否能正确显示。

segdan020

图 26‑19 数码管动态显示仿真波形图(五)

如图 26‑19所示,截取的是动态驱动模块的仿真波形图,可以看到其跟我们所绘制的时序波形图是一样的,这里需要注意的是由于我们缩短了显示数据跳转的时间,而没有缩短刷新时间,所以一个数据我们没有完全显示就跳转到下一个数据了。大家可以在仿真文件中增大数据跳转时间或者缩小数码管刷新时间可解决此问题,这里就不 过多讲解了。

20.3. 上板调试

20.3.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 26‑7所示。

表格 26‑7 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

时钟

sys_rst_n

Input

M15

复位

stcp

Output

K9

存储寄存器时钟

shcp

Output

B1

移位寄存器时钟

ds

Output

R1

串行数据

oe

Output

L11

输出使能

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下所示。

segdan021

图 26‑20 管脚分配

20.3.1.1. 结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,连接号之后上电,如图 26‑21所示。

segdan022

图 26‑21 下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 26‑22所示。

segdan023

图 26‑22 下载成功界面

下载成功后可以看到数码管上显示的数据从0开始一直往上加,说明验证成功。

20.4. 章末总结

学习该章节主要是要理解动态扫描的方法,同时我们所设计的数码管动态显示模块是一个复用性较强的模块,当我们要显示其他数值时,我们只需输入要显示的数值给该模块即可。

20.5. 拓展训练

  1. 更改数据生成模块,生成一个时钟(时分秒)显示在数码管上。

  2. 在1的基础上加入按键信号,可通过按键去设置时钟值。