17_数码管的动态显示
1. 实验目标
让六位数码管显示从十进制数 0 开始计数,每 0.1s 加 1,一直到加到十进制数999999。到达 999999 之后回到 0 开始重新计数。
2. 系统框图
2.1 数据生成模块
2.2 二进制转 BCD 码模块
二进制转BCD 步骤
二进制转 BCD 码模块输入输出信号描述:
二进制转 BCD 码模块框图:
2.3 数码管动态显示驱动模块
2.4 74HC595 控制模块
2.5 数码管动态显示模块
2.6 顶层模块
3. 波形图
3.1 数据生成模块
3.2 二进制转 BCD 码模块
3.3 数码管动态显示驱动模块
3.4 74HC595 控制模块
4. RTL
4.1 数据生成模块
module data_gen
#(
parameter CNT_MAX = 23'd4999_999, //100ms 计数值,0-999999 每隔0.12秒产生一个
parameter DATA_MAX= 20'd999_999 //显示的最大值
)
(
input wire sys_clk , //系统时钟,频率 50MHz
input wire sys_rst_n , //复位信号,低电平有效
/*
输出数据。当检测到 0.1s 到来的标志信号为高时让其加 1
当加到 999999 并检测到 0.1s 到来的标志信号时让其归 0,开始下一轮的数据显示。
*/
output reg [19:0] data , //数码管要显示的值
/*
point:输出小数点。在这里我们让其高电平有效(以本次使用的数码管为例,即当小
数点对应段选信号为低,位选信号为高时点亮有效),本次实验不需要显示小数点,
让每一位都为 0 即可。
*/
output wire [5:0] point , //小数点显示,高电平有效 // 数码管有6个小数点显示管
/*
seg_en:数码管使能信号。因为我们一直在显示,所以一直给其拉高就行
*/
output reg seg_en , //数码管使能信号,高电平有效
/*
sign:负号显示,高电平有效。因本次实验不显示负号,给其一直为低电平即可。
本设计思路只做参考,并非唯一方法,读者可利用所学知识,按照自己思路进行设
*/
output wire sign //符号位,高电平显示负号
);
// 0.1s 切换显示 0-999999
reg [22:0] cnt_100ms ; //100ms 计数器
reg cnt_flag ; //100ms 标志信号
//不显示小数点以及负数
assign point = 6'b000_000;
assign sign = 1'b0;
// //cnt_100ms:用 50MHz 时钟从 0 到 4999_999 计数即为 100ms 5000000次
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 + 23'd1;
//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 + 20'd1;
else
data <= data;
//数码管显示的数据:0-999_999
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
4.2 二进制转 BCD 码模块
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 码
);
/*
cnt_shift:移位判断计数器,
前面我们说到我们输入转换的二进制码有多少位我们就需
要进行多少次判断移位操作,这里我们 data 数据的位宽为 20 位,
所以这里我们声明移位
判断计数器对移位 20 次进行判断控制。
此外:还需要加上 2位
第一位:在刚开始赋值全为0
第二位:在最后确定最终数的位
cnt_shift 计数值为22
*/
reg [4:0] cnt_shift ; //移位判断计数器
/*
data_shift:移位判断数据寄存器,
该寄存器用于存储移位判断操作过程中的数据,
这里我们输入的二进制位宽为 20 位(二进制),
待转换成的 BCD 码位宽为 24 位(4*6=24),
所以这里我们声明该
寄存器的位宽为 44 位。
根据波形图可知,这里我们设计当移位计数器等于 0 时寄存器的低
20 位即为待转换数据,而由于还没开始进行转换,高 24 位的 BCD 码我们补 0 即可。
*/
reg [43:0] data_shift ; //移位判断数据寄存器
/*
shift_flag:移位判断操作标志信号。前面说到我们需要对数据进行移位和判断,
判断在前移位在后,所以这里我们声明一个标志信号,用于控制判断和移位的先后顺序,
当shift_flag 为低时对数据进行判断,
当 shift_flag 为高时对数据进行移位。
需要注意的是无论是移位操作和判断操作都是在单个系统时钟下完成的,
故我们判断 20 次移位 20 次在 40 个系统时钟内就能完成。
相当于一个分频器
*/
//shift_flag:移位判断标志信号,用于控制移位判断的先后顺序
reg shift_flag ; //移位判断标志信号
// shift_flag 高电平 就是一个时钟周期
// 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;
// cnt_shift:从 0 到 21 循环计数
// 数的是 shift_flag 的时钟周期
// 注意!! 判断为0 要在之前判断
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 + 5'd1;
else
cnt_shift <= cnt_shift;
//data_shift:计数器为 0 时赋初值,计数器为 1~20 时进行移位判断操作
// 首先判断每一个 BCD 码其对应的十进制数是否大于 4
// 如果大于 4 就对 BCD 码做加 3 操作
// 若小于等于 4 就让其值保持不变
// 当对每一个 BCD 码进行判断运算后,都需要将运算后的数据像左移 1 位
// 判断运算后需再次移位,以此循环
// 当我们进行 20 次判断移位后 BCD 码部分数据就是我们转换的数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_shift <= 44'b0;
// 在cnt_shift 在0处 和21处 做特殊处理
// 在cnt_shift 在0处 把data二进制之前的高位赋值为0
else if(cnt_shift == 5'd0)
data_shift <= {
24'b0,data};
// 当shift_flag 为低时对数据进行判断,
else if((cnt_shift <= 20) && (shift_flag == 1'b0))
begin
// 注意 !!!!!!!!!
// 二进制能与10进制的数 直接比较大小
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
// 当 shift_flag 为高时对数据进行移位。
else if((cnt_shift <= 20) && (shift_flag == 1'b1))
data_shift <= data_shift << 1;
else
data_shift <= data_shift;
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
else
begin
unit <= unit;
ten <= ten;
hun <= hun;
tho <= tho;
t_tho <= t_tho;
h_hun <= h_hun;
end
endmodule
4.3 数码管动态显示驱动模块
module seg_dynamic
(
input wire sys_clk , //系统时钟,频率 50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire [19:0] data , ///数码管要显示的值
/*
point:输入小数点信号,高电平有效,这里我们假设要让第二个数码管显示小数点,
其余数码管不显示小数点,那么此时 point 的输入的值就应该是 6’b000010。
*/
input wire [5:0] point , //小数点显示,高电平有效
input wire seg_en , //数码管使能信号,高电平有效
/*
sign:符号位控制信号,高电平有效。假设我们需要显示的是负数,
那么这里就让符号位控制信号为高即可。
*/
input wire sign , //符号位,高电平显示负号
output reg [5:0] sel , //数码管位选信号
output reg [7:0] seg //数码管段选信号
);
//parameter define
//1ms 人眼看起来是连续的
parameter CNT_MAX = 16'd49_999; //数码管刷新时间计数最大值
// wire define
wire [3:0] unit ; //个位 BCD 码
wire [3:0] ten ; //十位 BCD 码
wire [3:0] hun ; //百位 BCD 码
wire [3:0] tho ; //千位 BCD 码
wire [3:0] t_tho ; //万位 BCD 码
wire [3:0] h_hun ; //十万位 BCD 码
//reg define
/*
data_reg:数码管待显示内容寄存器,因为这里我们假设输入要显示的十进制数为
9876,并且显示负号,所以前五个数码管就会显示-9876 的数值,
此时最高位数码管什么都不显示,我们用 X 表示,所以这里六个数码管显示的内容就是:X-9876。
*/
reg [23:0] data_reg ; //待显示数据寄存器
/*
cnt_1ms:前面讲到要让显示的数码管不会有闪烁感,我们需要使用 1ms 的扫描时间去
扫描各个数码管。所以这里我们需要一个 1ms 的计数器对 1ms 进行循环计数。
*/
reg [15:0] cnt_1ms ; //1ms 计数器
reg flag_1ms ; //1ms 标志信号
/*
cnt_sel:位选数码管计数器。我们理论学习已经学习到动态扫描方式是用 1ms 的刷新
时间让六个数码管轮流显示:第 1ms 点亮第一个数码管,第 2ms 点亮第二个数码管,以此
类推依次点亮六个数码管,6ms 一个轮回,也就是说每个数码管每 6ms 点亮一次。
那问题是我们怎么去选中这个要显示的数码管并且给其要显示的值呢?
这个时候我们就引入了一个cnt_sel 信号
让其从 0~5 循环计数,1个数代表一个数码管,可以看做是给数码管编号。
这样的话我们只要选择计数器的值就相当于选中了其中对应的数码管。特别要说明的是我
们的 cnt_sel 计数器必须与数码管的刷新状态一致,也就是 1ms 计 1 个数。
*/
reg [2:0] cnt_sel ; //数码管位选计数器
/*
sel_reg:数码管位选信号寄存器,为了让数码管位选信号和段选信号同步,这里我们
先将位选信号进行寄存。刷新到哪个数码管将 sel 中对应位(6 个位宽,每一位表示一个数
码管)给高点亮即可。选中点亮的数码管后我们需要给其要显示的值,所以我们引入一个
新的信号。
*/
reg [5:0] sel_reg ; //位选信号
/*
data_disp:当前点亮数码管显示的值。若我们此时点亮的是第一个数码管,那么我们
就需要给第一个数码管显示的值 6,若刷新到第二个数码管就让该信号的值为 7 让第二个
数码管显示,以此类推;当刷新到第五个数码管时,此时显示的是负号,那么我们该如何
表示呢?这里我们让该信号的值为 10 来表示,也就是说当 data_disp 的值为 10 时就让数码
管显示负号,同理这里我们定义 data_disp 的值为 11 时让数码管什么也不显示,即不点亮
数码管。
*/
reg [3:0] data_disp ; //当前数码管显示的数据
/*
dot_disp:当前数码管显示的小数点,我们输入的 point 信号是点亮第二个数码管的小
数点,而我们的数码管是低电平点亮,所以这里当扫描到第二个数码管时让 dot_disp 信号
为低即可。
*/
reg dot_disp ; //当前数码管显示的小数点
// 1. 首先把传进的data 数据变成各种 BCD 码 表示的形式
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 码
);
// 2. 根据 BCD 码 判断,是否为0等。交给data_reg做拼接操作
//data_reg:控制数码管显示数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_reg <= 24'b0;
//若显示的十进制数的十万位为非零数据或需显示小数点,则六个数码管全显示
//如果显示小数点0.1234 1.1234 前面的数字都需要显示出来
else if((h_hun) || (point[5])) // 例如 999999 9.99999 0.12345
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};
// 3. 拼接完成后开始每隔1ms 显示 data_reg 数
//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 <= 6'b000_000;
else if((seg_en == 1'b1) && (flag_1ms == 1'b1))
begin
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
end
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
begin
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
end
//sel:数码管位选信号赋值 延迟一拍
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sel <= 6'b000_000;
else
sel <= sel_reg;
endmodule
4.4 74HC595 控制模块
module hc595_ctrl
(
input wire sys_clk , //系统时钟,频率 50MHz
input wire sys_rst_n , //系统时钟,频率 50MHz
input wire [5:0] sel , //数码管位选信号
input wire [7:0] seg , //数码管段选信号
/*
stcp:存储寄存器时钟,
当我们 14 位数码管控制信号传输完之后我们需要拉高一个
stcp 时钟来将信号存入存储寄存器之中
最后一个数据是在 cnt_bit=13 且 cnt=2 时传输的,
所以我们就在下一个时钟(cnt_bit=13 且 cnt=3 时)将 stcp 拉高一个时钟产生上升沿即可
*/
output reg stcp , //数据存储器时钟
/*
shcp:移位寄存器时钟,上升沿时将数据写入移位寄存器中
其频率即为系统时钟四分频(12.5MHz)
我们在 ds 数据的中间状态拉高产生上升沿,
这样可以使 shcp 采得的 ds 数据更加稳定。如波形图所示我们在 cnt=2
时拉高,cnt=0 时拉低,
*/
output reg shcp , //移位寄存器时钟
//ds:串行数据输出
output reg ds , //串行数据输入
output wire oe //使能信号,低有效
);
reg [1:0] cnt_4 ; //分频计数器,产生四分频的时钟
//cnt_bit:传输位数计数器。我们知道我们需要传输 14bit 的数据
reg [3:0] cnt_bit ; //传输位数计数器
wire [13:0] data ; //数码管信号寄存
//将数码管信号寄存
assign data={
seg[0],seg[1],seg[2],seg[3],seg[4],seg[5],seg[6],seg[7],sel};
//将复位取反后赋值给其即可
assign oe = ~sys_rst_n;
//分频计数器:0~3 循环计数
//产生四分频的时钟(当是这个四分频的时钟有区别)
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_4 <= 2'd0;
else if(cnt_4 == 2'd3)
cnt_4 <= 2'd0;
else
cnt_4 <= cnt_4 + 2'd1;
//cnt_bit:每输入一位数据加一,传输位数计数器
// 为什么 cnt_bit 会在 cnt_4 == 2'd3 加一呢?
// 因为传送数据是由 移位寄存器时钟 决定的
// 一个shcp周期 传输一位 这是规定好的
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 4'd0;
// 注意顺序的问题
// 注意两个条件都满足的问题
else if(cnt_bit == 4'd13 && cnt_4 == 2'd3)
cnt_bit <= 4'd0;
else if(cnt_4 == 2'd3)
cnt_bit <= cnt_bit + 4'd1;
else
cnt_bit <= cnt_bit;
//stcp:14 个信号传输完成之后产生一个上升沿
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
stcp <= 1'd0;
else if(cnt_bit == 4'd13 && cnt_4 == 2'd3)
stcp <= 1'd1;
else
stcp <= 1'b0;
shcp:产生四分频移位时钟
// 这里时钟的产生注意,为了性能的优化,产生的时钟方式有所不同
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
shcp <= 1'b0;
else if(cnt_4 >= 4'd2)
shcp <= 1'b1;
else
shcp <= 1'b0;
//ds:将寄存器里存储的数码管信号输入即
//ds 传输的时间是一个shcp的时钟周期
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ds <= 1'b0;
else if(cnt_4 == 2'd0)
ds <= data[cnt_bit];
else
ds <= ds;
endmodule
4.5 数码管动态显示模块
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 //输出使能信号
);
wire [5:0] sel; //数码管位选信号
wire [7:0] seg; //数码管段选信号
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
4.6 顶层模块
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 //输出使能信号
);
wire [19:0] data ; //数码管要显示的值
wire [5:0] point ; //小数点显示,高电平有效 top_seg_595
wire seg_en ; //数码管使能信号,高电平有效
wire sign ; //符号位,高电平显示负号
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 ) //符号位,高电平显示负号
);
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
5. testbench
5.1 数据生成模块
`timescale 1ns/1ns
module tb_data_gen();
//因为 testbench 不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测 RTL 模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在 initial 块和 always 块中被赋值的变量一定要是 reg 型
//在 testbench 中待测试 RTL 模块的输入永远是 reg 型变量
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
reg sys_clk;
reg sys_rst_n;
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
wire [19:0] data ;
wire [5:0] point ;
wire sign ;
wire seg_en ;
//初始化值在没有特殊要求的情况下给 0 或 1 都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim 中的波形显示红色)
initial
//initial 只在通电执行一次
//在仿真中 begin...end 块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在 rtl 代码中 begin...end 相当于括号的作用, begin...end 在 Testbench 中的用法及意义(区别 -----------------------------------------------------)
//在同一个 always 块中给多个变量赋值的时候要加上
begin
sys_clk = 1'b1; //时钟信号的初始化为 1,且使用“=”赋值,
//其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为 0
#20 //延时20ns
sys_rst_n <= 1'b1; //初始化 20ns 后,复位释放,因为是低电平复位
end
//产生时钟信号
always #10 sys_clk = ~sys_clk;
data_gen
#(
.CNT_MAX(23'd9), //100ms 计数值,0-999999 每隔0.1秒产生一个
.DATA_MAX(20'd9) //显示的最大值
)
data_gen_inst
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.data(data),
.point(point),
.sign(sign),
.seg_en(seg_en )
);
endmodule
5.2 二进制转 BCD 码模块
`timescale 1ns/1ns
module tb_bcd_8421();
//因为 testbench 不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测 RTL 模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在 initial 块和 always 块中被赋值的变量一定要是 reg 型
//在 testbench 中待测试 RTL 模块的输入永远是 reg 型变量
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
reg sys_clk ; //系统时钟,频率 50MHz
reg sys_rst_n; //复位信号,低电平有效
reg [19:0] data; //数码管要显示的值
wire [3:0] unit ; //个位 BCD 码
wire [3:0] ten ; //十位 BCD 码
wire [3:0] hun ; //百位 BCD 码
wire [3:0] tho ; //千位 BCD 码
wire [3:0] t_tho ; //万位 BCD 码
wire [3:0] h_hun ;//十万位 BCD 码
//初始化值在没有特殊要求的情况下给 0 或 1 都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim 中的波形显示红色)
initial
//initial 只在通电执行一次
//在仿真中 begin...end 块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在 rtl 代码中 begin...end 相当于括号的作用, begin...end 在 Testbench 中的用法及意义(区别 -----------------------------------------------------)
//在同一个 always 块中给多个变量赋值的时候要加上
begin
sys_clk = 1'b1; //时钟信号的初始化为 1,且使用“=”赋值,
//其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为 0
data <= 20'd0;
#30 //延时20ns
sys_rst_n <= 1'b1; //初始化 20ns 后,复位释放,因为是低电平复位
data <= 20'd123_456;
# 3000
data <= 20'd987_654;
# 3000
data <= 20'd9876;
end
//产生时钟信号
always #10 sys_clk = ~sys_clk;
bcd_8421 bcd_8421_inst
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.sys_clk(sys_clk),
.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
5.3 数码管动态显示驱动模块
`timescale 1ns/1ns
module tb_seg_dynamic();
//因为 testbench 不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测 RTL 模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在 initial 块和 always 块中被赋值的变量一定要是 reg 型
//在 testbench 中待测试 RTL 模块的输入永远是 reg 型变量
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
reg sys_clk ; //系统时钟,频率 50MHz
reg sys_rst_n ; //复位信号,低电平有效
reg [19:0] data ; ///数码管要显示的值
/*
point:输入小数点信号,高电平有效,这里我们假设要让第二个数码管显示小数点,
其余数码管不显示小数点,那么此时 point 的输入的值就应该是 6’b000010。
*/
reg [5:0] point ; //小数点显示,高电平有效
reg seg_en ; //数码管使能信号,高电平有效
/*
sign:符号位控制信号,高电平有效。假设我们需要显示的是负数,
那么这里就让符号位控制信号为高即可。
*/
reg sign ; //符号位,高电平显示负号
wire [5:0] sel ; //数码管位选信号
wire [7:0] seg ; //数码管段选信号
//初始化值在没有特殊要求的情况下给 0 或 1 都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim 中的波形显示红色)
initial
//initial 只在通电执行一次
//在仿真中 begin...end 块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在 rtl 代码中 begin...end 相当于括号的作用, begin...end 在 Testbench 中的用法及意义(区别 -----------------------------------------------------)
//在同一个 always 块中给多个变量赋值的时候要加上
begin
sys_clk = 1'b1; //时钟信号的初始化为 1,且使用“=”赋值,
//其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为 0
data <= 20'd0;
point <= 6'b0;
sign <= 1'b0;
seg_en <= 1'b0;
#30 //延时20ns
sys_rst_n <= 1'b1; //初始化 20ns 后,复位释放,因为是低电平复位
data <= 20'd9876;
point <= 6'b000_010;
sign <= 1'b1;
seg_en <= 1'b1;
end
//产生时钟信号
always #10 sys_clk = ~sys_clk;
defparam seg_dynamic_inst.CNT_MAX = 20'd5;
seg_dynamic seg_dynamic_inst
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.sys_clk(sys_clk) , //系统时钟,频率 50MHz
.sys_rst_n(sys_rst_n) , //复位信号,低电平有效
.data(data) , ///数码管要显示的值
/*
point:输入小数点信号,高电平有效,这里我们假设要让第二个数码管显示小数点,
其余数码管不显示小数点,那么此时 point 的输入的值就应该是 6’b000010。
*/
.point(point) , //小数点显示,高电平有效
.seg_en(seg_en) , //数码管使能信号,高电平有效
/*
sign:符号位控制信号,高电平有效。假设我们需要显示的是负数,
那么这里就让符号位控制信号为高即可。
*/
.sign(sign) , //符号位,高电平显示负号
.sel(sel) , //数码管位选信号
.seg (seg)//数码管段选信号
);
endmodule
5.4 74HC595 控制模块
`timescale 1ns/1ns
module tb_hc595_ctrl();
//因为 testbench 不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测 RTL 模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在 initial 块和 always 块中被赋值的变量一定要是 reg 型
//在 testbench 中待测试 RTL 模块的输入永远是 reg 型变量
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
reg sys_clk;
reg sys_rst_n;
reg [5:0] sel ;//数码管位选信号
reg [7:0] seg ;//数码管段选信号
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
wire stcp ;//数据存储器时钟
wire shcp ; //移位寄存器时钟
wire ds ; //串行数据输入
wire oe ;//使能信号,低有效
//初始化值在没有特殊要求的情况下给 0 或 1 都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim 中的波形显示红色)
initial
//initial 只在通电执行一次
//在仿真中 begin...end 块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在 rtl 代码中 begin...end 相当于括号的作用, begin...end 在 Testbench 中的用法及意义(区别 -----------------------------------------------------)
//在同一个 always 块中给多个变量赋值的时候要加上
begin
sys_clk = 1'b1; //时钟信号的初始化为 1,且使用“=”赋值,
//其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为 0
sel <= 6'b111111;
seg <= 8'b1100_0000;
#20 //延时20ns
sys_rst_n <= 1'b1; //初始化 20ns 后,复位释放,因为是低电平复位
#500000000
seg <= 8'b1111_1001;
//都是顺序执行的
end
//产生时钟信号
always #10 sys_clk = ~sys_clk;
hc595_ctrl hc595_ctrl_inst
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.sel(sel),
.seg(seg),
.stcp(stcp),//数据存储器时钟
.shcp(shcp), //移位寄存器时钟
.ds(ds), //串行数据输入
.oe(oe)//使能信号,低有效
);
endmodule
5.5 顶层模块
`timescale 1ns/1ns
module tb_top_seg_595();
//因为 testbench 不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测 RTL 模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在 initial 块和 always 块中被赋值的变量一定要是 reg 型
//在 testbench 中待测试 RTL 模块的输入永远是 reg 型变量
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是 wire 型变量(在 testbench 中待测试 RTL 模块的输出永远是 wire 型变量)
//wire define
wire stcp ; //输出数据存储寄时钟
wire shcp ; //移位寄存器的时钟输入
wire ds ; //串行数据输入
wire oe ; //输出使能信号
//reg define
reg sys_clk ;
reg sys_rst_n ;
//初始化值在没有特殊要求的情况下给 0 或 1 都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim 中的波形显示红色)
initial
//initial 只在通电执行一次
//在仿真中 begin...end 块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在 rtl 代码中 begin...end 相当于括号的作用, begin...end 在 Testbench 中的用法及意义(区别 -----------------------------------------------------)
//在同一个 always 块中给多个变量赋值的时候要加上
begin
sys_clk = 1'b1; //时钟信号的初始化为 1,且使用“=”赋值,
//其他信号的赋值都是用“<=”
sys_rst_n <= 1'b0; //因为低电平复位,所以复位信号的初始化为 0
#100
sys_rst_n <= 1'b1; //初始化 20ns 后,复位释放,因为是低电平复位
end
//产生时钟信号
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;
top_seg_595 top_seg_595_inst
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds ), //串行数据输入
.oe (oe ) //输出使能信号
);
endmodule