2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第十二章 动态数码管显示实验
经过上一章的学习,我们已经知道如何使用数码管静态驱动的方式使数码管显示数字,但
在很多情况下,我们需要让数码管各个位显示不同的数字,这就需要以动态驱动的方式驱动数
码管。本章我们主要介绍数码管动态驱动的原理以及如何使用动态驱动的方式在数码管上显示
变化的数字。
本章包括以下几个部分:
12.1 数码管动态显示简介
12.2 实验任务
12.3 硬件设计
12.4 程序设计
12.5 下载验证
数码管动态显示简介
在“数码管静态显示实验”章节我们详细地介绍了有关数码管方面的知识和数码管的静态
驱动,让大家对数码管的驱动有了基本的了解。由于一般的静态驱动操作虽然方便,但占用的
I/0口较多,例如要驱动6位8段数码管,以静态驱动方式让数码管各个位显示不同的数值,如
“123456”,需要占用6 × 8 = 48个I/O口,虽然对于FPGA这种I/O口较多的芯片而言,在资源
允许的情况下可以使用,但一般不建议浪费宝贵的I/O口资源,尤其在I/O口资源紧张的情况下,
所以对于多位数码管一般采用动态驱动方式使数码管显示数字。那么什么是动态驱动方式呢?
为了更好的理解数码管动态驱动,我们首先了解下市面上常见的多位数码管的内部连接。
以两位数码管为例,其内部连接如下图。由此图可知,两位8段数码管共10个引脚,每位数码
管的阳极连接在一起,为共阳极数码管,每位数码管相同段的led的阴极连接在一起,这样当
给第10和第5脚高电平,给第3脚低电平时,两个数码管的发光二极管A都点亮,对于此种数码
管以静态方式驱动显然不可能显示像“18”这种个位与十位不同的数字。那么该如何显示数字
“18”呢?
图 12.1.1 多位数码管内部连接图
既然同时给第10和第5脚高电平不可行,那么是不是可以先给第5脚高电平,第10脚低电平,
此时,让其显示数字“8”时,左边的数码管不显示,右边的数码管显示数字“8”;然后给第
10脚高电平,第5脚低电平,此时,让其显示数字“1”时,左边的数码管显示数字“1”,右
边的数码管不显示,这样就可以显示数字“18”了。但有一个问题,多长时间切换显示的数码
管呢,时间如果太长就只能看到数字“8”或数字“1”了,时间太短呢,结果是显示不清晰而且显示亮度不够。由于人眼的视觉暂留(人眼在观察景物时,光信号传人大脑神经,需经过一
段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉
的这一现象则被称为“视觉暂留”)及发光二极管的余辉效应(当停止向发光二极管供电时,
发光二极管亮度仍能维持一段时间),每位数码管的点亮时间为1~2ms时,显示效果能满足使
用需要。数码管的这种驱动方式称为数码管的动态驱动,实际上就是分时轮流控制不同数码管
的显示。
实验任务
本节实验任务是使用FPGA开发板上的6位数码管以动态方式从0开始计数,每100ms计数值
增加一,当计数值从0增加到999999后重新从0开始计数。
硬件设计
数码管接口部分的硬件设计原理及本实验中各端口信号的管脚分配与“数码管静态显示实
验”完全相同,请参考“数码管静态显示实验”中的硬件设计部分。
程序设计
由实验任务和动态驱动的原理我们可以知道,若要让6个数码管轮流显示对应的数字,首
先需要一个数码管动态显示模块,能够依次点亮6个数码管,并将对应的数据输出至数码管,
也就是需要分别控制段选和位选信号;同时还需要一个计数模块,能够将0—999999依次输出
至数码管动态显示模块。根据实验任务,我们可以大致规划出系统的控制流程:首先我们需要
一个数码管动态显示模块在数码管上显示数据,其次需要一个计数控制模块实现从0到999999
的变化,并将产生的数值通过数码管动态显示模块在数码管上显示出来。由此画出系统的功能
框图如下所示:
图 12.4.1 数码管动态显示实验系统框图
程序中各模块端口及信号连接如图 12.4.2所示:
图 12.4.2 顶层模块原理图
FPGA顶层(top_seg_led)例化了以下两个模块:计数模块(count)以及数码管动态显示
模块(seg_led)。实现各模块之间数据的交互。计数模块将计数值通过data端口传递给数码
管动态显示模块,使能信号en使能数码管显示数据,小数点显示信号point控制小数点的显示,
符号信号sign可以让数码管显示负号。
计数模块(count):显示的数字每100ms加“1”。
数码管动态显示模块(seg_led):数码管动态显示模块在数码管上以动态方式显示数值。
顶层模块的代码如下:
1
module top_seg_led
(
2 //global clock
3
input sys_clk
, // 全局时钟信号
4
input sys_rst_n
, // 复位信号(低有效)
5
6 //seg_led interface
7
output [5
:0
] sel
, // 控制数码管的亮灭
8
output [7
:0
] seg_led // 控制数码管中的8个灯的亮灭
9
);
10
11 //wire define
12
wire [19
:0
] data
; // 数码管显示的数值
13
wire [ 5
:0
] point
; // 数码管小数点的位置
14
wire en
; // 数码管显示使能信号
15
wire sign
; // 符号位
16
17 //*****************************************************
18 //** main code
19 //*****************************************************
20
21 //例化动态数码管驱动模块
22 seg_led u_seg_led
(
23 //module clock
24
.clk
(sys_clk
), // 时钟信号
25
.rst_n
(sys_rst_n
), // 复位信号
26 //seg_led interface
27
.sel
(sel
), // 位选
28
.seg_led
(seg_led
), // 段选
29 //user interface
30
.data
(data
), // 显示的数值
31
.point
(point
), // 小数点具体显示的位置,从高到低,高电平有效
32
.en
(en
), // 数码管使能信号
33
.sign
(sign
) // 符号位(低电平显示“-”号)
34
);
35
36 //例化计数模块
37 count u_count
(
38 //mudule clock
39
.clk
(sys_clk
), // 时钟信号
40
.rst_n
(sys_rst_n
), // 复位信号
41 //user interface
42
.data
(data
), // 6个数码管要显示的数值
43
.point
(point
), // 小数点具体显示的位置,从高到低,高电平有效
44
.en
(en
), // 数码管使能信号
45
.sign
(sign
) // 符号位
46
);
47
48
endmodule
顶层模块中主要完成对其余模块的例化,并且实现各模块之间信号的交互。计数模块输出
的数值data连接至数码管显示模块的输入端口data,数码管显示模块将输入的数据data输出至
数码管上显示。
计数模块的代码如下所示:
1
module count
(
2 //mudule clock
3
input clk
, // 时钟信号
4
input rst_n
, // 复位信号
5
6 //user interface
7
output reg [19
:0
] data
, // 6个数码管要显示的数值
8
output reg [ 5
:0
] point
, // 小数点的位置,高电平点亮对应数码管位上的小数点
9
output reg en
, // 数码管使能信号
10
output reg sign // 符号位,高电平时显示负号,低电平不显示负号
11
);
12
13 //parameter define
14
parameter MAX_NUM
= 23'd5000_000
; // 计数器计数的最大值
15
16 //reg define
17
reg [22
:0
] cnt
; // 计数器,用于计时100ms
18
reg flag
; // 标志信号
19
20 //*****************************************************
21 //** main code
22 //*****************************************************
23
24 //计数器对系统时钟计数达10ms时,输出一个时钟周期的脉冲信号
25
always @ (posedge clk
or negedge rst_n
) begin
26
if (!rst_n
) begin
27 cnt
<= 23'b0
;
28 flag
<= 1'b0
;
29
end
else if (cnt
< MAX_NUM
- 1'b1
) begin
31 cnt
<= cnt
+ 1'b1
;
32 flag
<= 1'b0
;
33
end
34
else begin
35 cnt
<= 23'b0
;
36 flag
<= 1'b1
;
37
end
38
end
39
40 //数码管需要显示的数据,从0累加到999999
41
always @ (posedge clk
or negedge rst_n
) begin
42
if (!rst_n
)begin
43 data
<= 20'b0
;
44 point
<=6'b000000
;
45 en
<= 1'b0
;
46 sign
<= 1'b0
;
47
end
48
else begin
49 point
<= 6'b000000
; //不显示小数点
50 en
<= 1'b1
; //打开数码管使能信号
51 sign
<= 1'b0
; //不显示负号
52
if (flag
) begin //显示数值每隔0.01s累加一次
53
if(data
< 20'd999999
)
54 data
<= data
+1'b1
;
55
else
56 data
<= 20'b0
;
57
end
58
end
59
end
60
61
endmodule
代码中第14行的参数MAX_NUM为计数的最大计数值,由于是对时钟计数,相当于计时,第25行的always语句块表示的是当计数器cnt计数值小于MAX_NUM - 1'b1时,标志(flag)为“0”,
否则标志(flag)为“1”,并且计数器cnt清零。通过SignalTapII抓到的波形图如下图所示,
可知,计时100ms正确。
图 12.4.3 SignalTap波形图
数码管动态显示模块的代码如下:
1
module seg_led
(
2
input clk
, // 时钟信号
3
input rst_n
, // 复位信号
4
5
input [19
:0
] data
, // 6位数码管要显示的数值
6
input [5
:0
] point
, // 小数点具体显示的位置,从高到低,高电平有效
7
input en
, // 数码管使能信号
8
input sign
, // 符号位(高电平显示“-”号)
9
10
output reg [5
:0
] seg_sel
, // 数码管位选,最左侧数码管为最高位
11
output reg [7
:0
] seg_led // 数码管段选
12
);
13
14 //parameter define
15
localparam CLK_DIVIDE
= 4'd10
; // 时钟分频系数
16
localparam MAX_NUM
= 13'd5000
; // 对数码管驱动时钟(5MHz)计数1ms所需的计数值
17
18 //reg define
19
reg [ 3
:0
] clk_cnt
; // 时钟分频计数器
20
reg dri_clk
; // 数码管的驱动时钟,5MHz
21
reg [23
:0
] num
; // 24位bcd码寄存器
22
reg [12
:0
] cnt0
; // 数码管驱动时钟计数器
23
reg flag
; // 标志信号(标志着cnt0计数达1ms)
24
reg [2
:0
] cnt_sel
; // 数码管位选计数器
25
reg [3
:0
] num_disp
; // 当前数码管显示的数据
26
reg dot_disp
; // 当前数码管显示的小数点
27
28 //wire define
29
wire [3
:0
] data0
; // 个位数
30
wire [3
:0
] data1
; // 十位数
31
wire [3
:0
] data2
; // 百位数
32
wire [3
:0
] data3
; // 千位数
33
wire [3
:0
] data4
; // 万位数
34
wire [3
:0
] data5
; // 十万位数
35
36 //*****************************************************
37 //** main code
38 //*****************************************************
39
40 //提取显示数值所对应的十进制数的各个位
41
assign data0
= data
% 4'd10
; // 个位数
42
assign data1
= data
/ 4'd10
% 4'd10
; // 十位数
43
assign data2
= data
/ 7'd100
% 4'd10
; // 百位数
44
assign data3
= data
/ 10'd1000
% 4'd10
; // 千位数
45
assign data4
= data
/ 14'd10000
% 4'd10
; // 万位数
46
assign data5
= data
/ 17'd100000
; // 十万位数
47
48 //对系统时钟10分频,得到的频率为5MHz的数码管驱动时钟dri_clk
49
always @(posedge clk
or negedge rst_n
) begin
50
if(!rst_n
) begin
51 clk_cnt
<= 4'd0
;
52 dri_clk
<= 1'b1
;
53
end
54
else if(clk_cnt
== CLK_DIVIDE
/2
- 1'd1
) begin
55 clk_cnt
<= 4'd0
;
56 dri_clk
<= ~dri_clk
;
57
end
58
else begin
59 clk_cnt
<= clk_cnt
+ 1'b1
;
60 dri_clk
<= dri_clk
;
61
end
62
end
63
64 //将20位2进制数转换为8421bcd码(即使用4位二进制数表示1位十进制数)
65
always @ (posedge dri_clk
or negedge rst_n
) begin
66
if (!rst_n
)
67 num
<= 24'b0
;
68
else begin
69
if (data5
|| point
[5
]) begin //如果显示数据为6位十进制数,
70 num
[23
:20
] <= data5
; //则依次给6位数码管赋值
71 num
[19
:16
] <= data4
;
72 num
[15
:12
] <= data3
;
73 num
[11
:8
] <= data2
;
74 num
[ 7
:4
] <= data1
;
75 num
[ 3
:0
] <= data0
;
76
end
77
else begin
78
if (data4
|| point
[4
]) begin //如果显示数据为5位十进制数,则给低5位数码管赋值
79 num
[19
:0
] <= {data4
,data3
,data2
,data1
,data0
};
80
if(sign
)
81 num
[23
:20
] <= 4'd11
; //如果需要显示负号,则最高位(第6位)为符号位
82
else
83 num
[23
:20
] <= 4'd10
; //不需要显示负号时,则第6位不显示任何字符
84
end
85
else begin //如果显示数据为4位十进制数,则给低4位数码管赋值
86
if (data3
|| point
[3
]) begin
87 num
[15
: 0
] <= {data3
,data2
,data1
,data0
};
88 num
[23
:20
] <= 4'd10
; //第6位不显示任何字符
89
if(sign
) //如果需要显示负号,则最高位(第5位)为符号位
90 num
[19
:16
] <= 4'd11
;
91
else //不需要显示负号时,则第5位不显示任何字符
92 num
[19
:16
] <= 4'd10
;
93
end
94
else begin //如果显示数据为3位十进制数,则给低3位数码管赋值
95
if (data2
|| point
[2
]) begin
96 num
[11
: 0
] <= {data2
,data1
,data0
};
97 //第6、5位不显示任何字符
98 num
[23
:16
] <= {2
{4'd10
}};
99
if(sign
) //如果需要显示负号,则最高位(第4位)为符号位
100 num
[15
:12
] <= 4'd11
;
101
else //不需要显示负号时,则第4位不显示任何字符
102 num
[15
:12
] <= 4'd10
;
103
end
104
else begin //如果显示数据为2位十进制数,则给低2位数码管赋值
105
if (data1
|| point
[1
]) begin
106 num
[ 7
: 0
] <= {data1
,data0
};
107 //第6、5、4位不显示任何字符
108 num
[23
:12
] <= {3
{4'd10
}};
109
if(sign
) //如果需要显示负号,则最高位(第3位)为符号位
110 num
[11
:8
] <= 4'd11
;
111
else //不需要显示负号时,则第3位不显示任何字符
112 num
[11
:8
] <= 4'd10
;
113
end
114
else begin //如果显示数据为1位十进制数,则给最低位数码管赋值
115 num
[3
:0
] <= data0
;
116 //第6、5位不显示任何字符
117 num
[23
:8
] <= {4
{4'd10
}};
118
if(sign
) //如果需要显示负号,则最高位(第2位)为符号位
119 num
[7
:4
] <= 4'd11
;
120
else //不需要显示负号时,则第2位不显示任何字符
121 num
[7
:4
] <= 4'd10
;
122
end
123
end
124
end
125
end
126
end
127
end
128
end
129
130 //每当计数器对数码管驱动时钟计数时间达1ms,输出一个时钟周期的脉冲信号
131
always @ (posedge dri_clk
or negedge rst_n
) begin
132
if (rst_n
== 1'b0
) begin
133 cnt0
<= 13'b0
;
134 flag
<= 1'b0
;
135
end
136
else if (cnt0
< MAX_NUM
- 1'b1
) begin
137 cnt0
<= cnt0
+ 1'b1
;
138 flag
<= 1'b0
;
139
end
140
else begin
141 cnt0
<= 13'b0
;
142 flag
<= 1'b1
;
143
end
144
end
145
146 //cnt_sel从0计数到5,用于选择当前处于显示状态的数码管
147
always @ (posedge dri_clk
or negedge rst_n
) begin
148
if (rst_n
== 1'b0
)
149 cnt_sel
<= 3'b0
;
150
else if(flag
) begin
151
if(cnt_sel
< 3'd5
)
152 cnt_sel
<= cnt_sel
+ 1'b1
;
153
else
154 cnt_sel
<= 3'b0
;
155
end
156
else
157 cnt_sel
<= cnt_sel
;
158
end
159
160 //控制数码管位选信号,使6位数码管轮流显示
161
always @ (posedge dri_clk
or negedge rst_n
) begin
162
if(!rst_n
) begin
163 seg_sel
<= 6'b111111
; //位选信号低电平有效
164 num_disp
<= 4'b0
;
165 dot_disp
<= 1'b1
; //共阳极数码管,低电平导通
166
end
167
else begin
168
if(en
) begin
169
case (cnt_sel
)
170 3'd0
:begin
171 seg_sel
<= 6'b111110
; //显示数码管最低位
172 num_disp
<= num
[3
:0
] ; //显示的数据
173 dot_disp
<= ~point
[0
]; //显示的小数点
174
end
175 3'd1
:begin
176 seg_sel
<= 6'b111101
; //显示数码管第1位
177 num_disp
<= num
[7
:4
] ;
178 dot_disp
<= ~point
[1
];
179
end
180 3'd2
:begin
181 seg_sel
<= 6'b111011
; //显示数码管第2位
182 num_disp
<= num
[11
:8
];
183 dot_disp
<= ~point
[2
];
184
end
185 3'd3
:begin
186 seg_sel
<= 6'b110111
; //显示数码管第3位
187 num_disp
<= num
[15
:12
];
188 dot_disp
<= ~point
[3
];
189
end
190 3'd4
:begin
191 seg_sel
<= 6'b101111
; //显示数码管第4位
192 num_disp
<= num
[19
:16
];
193 dot_disp
<= ~point
[4
];
194
end
195 3'd5
:begin
196 seg_sel
<= 6'b011111
; //显示数码管最高位
197 num_disp
<= num
[23
:20
];
198 dot_disp
<= ~point
[5
];
199
end
200
default :begin
201 seg_sel
<= 6'b111111
;
202 num_disp
<= 4'b0
;
203 dot_disp
<= 1'b1
;
204
end
205
endcase
206
end
207
else begin
208 seg_sel
<= 6'b111111
; //使能信号为0时,所有数码管均不显示
209 num_disp
<= 4'b0
;
210 dot_disp
<= 1'b1
;
211
end
212
end
213
end
214
215 //控制数码管段选信号,显示字符
216
always @ (posedge dri_clk
or negedge rst_n
) begin
217
if (!rst_n
)
218 seg_led
<= 8'hc0
;
219
else begin
220
case (num_disp
)
221 4'd0
: seg_led
<= {dot_disp
,7'b1000000
}; //显示数字 0
222 4'd1
: seg_led
<= {dot_disp
,7'b1111001
}; //显示数字 1
223 4'd2
: seg_led
<= {dot_disp
,7'b0100100
}; //显示数字 2
224 4'd3
: seg_led
<= {dot_disp
,7'b0110000
}; //显示数字 3
225 4'd4
: seg_led
<= {dot_disp
,7'b0011001
}; //显示数字 4
226 4'd5
: seg_led
<= {dot_disp
,7'b0010010
}; //显示数字 5
227 4'd6
: seg_led
<= {dot_disp
,7'b0000010
}; //显示数字 6
228 4'd7
: seg_led
<= {dot_disp
,7'b1111000
}; //显示数字 7
229 4'd8
: seg_led
<= {dot_disp
,7'b0000000
}; //显示数字 8
230 4'd9
: seg_led
<= {dot_disp
,7'b0010000
}; //显示数字 9
231 4'd10
: seg_led
<= 8'b11111111
; //不显示任何字符
232 4'd11
: seg_led
<= 8'b10111111
; //显示负号(-)
233
default:
234 seg_led
<= {dot_disp
,7'b1000000
};
235
endcase
236
end
237
end
238
239
endmodule
数码管动态显示模块不仅可以将数值显示在数码管上,而且可以控制小数点的显示以及显
示负数。数码管驱动模块没有在高位填充“0”,除非该位显示小数点。结合第131行开始的always
语句块可知,cnt每1ms的时间变化一次;而从第161行的case语句块可知,cnt控制数码管的位
选和段选。下图为该模块运行时SignalTapII抓取到的波形图:
图 12.4.4 SignalTapb波形图
由该波形图可知,当flag信号拉高时,切换显示信号cnt加1。
下载验证
首先我们打开数码管动态显示实验工程,在工程所在的路径下打开top_seg_led/par文件
夹,在里面找到“top_seg_led.qpf”并双击打开。注意工程所在的路径名只能由字母、数字
以及下划线组成,不能出现中文、空格以及特殊字符等。工程打开后如图 12.5.1所示。
图 12.5.1 数码管动态显示实验工程
然后将下载器一端连接电脑,另一端与开发板上的JTAG下载口相连,最后连接
电源线并打
开电源开关。
接下来我们下载程序,验证数码管动态显示的功能。
工程打开后通过点击工具栏中的“Programmer”图标打开下载界面,通过“Add File”按
钮选择top_seg_led/par/output_files目录下的“top_seg_led.sof”文件。开发板电源打开
后,在程序下载界面点击“Hardware Setup”,在弹出的对话框中选择当前的硬件连接为“USB-
Blaster[USB-0]”。然后点击“Start”将工程编译完成后得到的sof文件下载到开发板中,如
图 12.5.2所示。
图 12.5.2 程序下载界面
下载完成后观察到开发板上数码管显示的值从“0”增加到“999999”,如下图所示,说
明数码管动态显示实验程序下载验证成功。
图 12.5.3 动态数码管态显示实验结果显示