设计规划
8个数码管显示从十进制0开始计数,每0.1s加1,一直加到十进制数9999_9999,然后回到0重新计数。
理论基础
上一节介绍了静态显示,每个数码管显示的数字是一样的,但是实际生活中远远不能满足需求。因此我们要学习显示不同字符的数码管驱动方式-动态驱动。静态驱动的原理是位选信号为1111_1111,即8个数码管同时选中,再用段选信号让数码管显示理想数值,那么8个数码管的字符就一样。那么如果我们每次只选中一个数码管,再利用段选信号让它显示理想数值,就可以显示不同数值了。由于视觉暂留以及数码管的余晖效应 即使数码管不是同时点亮,我们在视觉效果上看到的也是同时点亮。(视觉暂留:视觉影像不会立即消失,动画就是利用这个原理,当一秒内帧数达到24就会感觉比较流畅。余晖效应:停止供电发光二极管亮度会维持一段时间。)实验证明,扫描间隔为1ms,即一秒点亮1000次,不会有闪烁感。那么我们只需要第1ms点亮第一个数码管,第2ms点亮第二个数码管…
BCD码:2-10进制码,将1位十进制数和4位二进制数对应的码。8421码比较常见,第0位权重为1,第1位权重为2,第三位权重为4,第4位权重为8,0对应0000,1对应0001,...,9对应1001。那么例如一个二进制数10010,对应十进制是18,对应BCD码是0001_1000。
编写代码
通过系统框图可以看出,分为6个模块:数据生成模块,二进制转BCD模块,数码管动态显示驱动模块,74HC595模块,数码管动态显示模块,顶层模块。
1、数据生成模块data_gen
应该具有输入:时钟和复位,输出:数据data和使能seg_en。中间信号有100ms计数器cnt_100ms,标志位cnt_flag。
module data_gen
#(
parameter CNT_MAX = 23'd4999_999, //100ms计数值
parameter DATA_MAX= 27'd9999_9999 //显示的最大值
)
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
output reg [26:0] data , //数码管要显示的值
output reg seg_en //数码管使能信号,高电平有效
);
//reg define
reg [22:0] cnt_100ms ; // 100ms计数器
reg cnt_flag ; //100ms标志信号
//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-9999_9999
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data <= 27'd0;
else if((data == DATA_MAX) && (cnt_flag == 1'b1))
data <= 27'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
参数定义:首先确定计数和显示的最大值,计数100ms,用50MHz的时钟需要从0记到4999_999,显示的最大值是999_999,前者是23位,后者是27位。
cnt_100ms:复位有效和记满时归零,其他情况+1
cnt_flag:复位有效时归0,计满时拉高,其他情况都是保持低电平
data:复位有效时归0,数据达到最大值且标志位拉高时+1(数据要在999_999保持0.1ms,如果计满就+1就只能维持一个时钟周期,这就是flag的意义),其他情况保持
seg_en:复位有效时归0,其他情况都是高电平有效。
2、BCD模块bcd_8421
输入是时钟、复位和数据data,data是2进制数,要先转换成10进制,再将10进制的每一位转换成BCD码,才能使每一个数码管显示对应的字符。我们看二进制码如何变为BCD码。输出是8个数码管对应8位,每位转换BCD码是4位。输出是:个位,十位,百位,千位,万位,十万位,百万位,千万位。中间信号是移位判断计数器cnt_shift,移位判断数据寄存器data_shift,移位判断标志信号shift_flag。
以十进制数234为例,二进制是1110_1010,BCD码应该是0010_0011_1000,那么先进行补0操作,输入的二进制码不足12位,在高位补0。再将二进制码的最高位作为BCD的最低位进行移位,并判断每一个BCD码其对应的十进制数是否大于4,如果大于4就对BCD码做加3操作,若小于等于4就让其值保持不变。然后继续移位,每次移位后都要进行判断。
module bcd_8421(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire [26: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码
output reg [3:0] t_hun , //百万位BCD码
output reg [3:0] h_tho //千万位BCD码
);
//reg define
reg [4:0] cnt_shift ; //移位判断计数器
reg [58:0] data_shift ; //移位判断数据寄存器
reg shift_flag ; //移位判断标志信号
//cnt_shift:从0到28循环计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_shift <= 5'd0;
else if((cnt_shift == 5'd28) && (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~27时进行移位判断操作
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_shift <= 59'b0;
else if(cnt_shift == 5'd0)
data_shift <= {32'b0,data};
else if((cnt_shift <= 27) && (shift_flag == 1'b0))
begin
data_shift[30:27] <= (data_shift[30:27] > 4) ?
(data_shift[30:27] + 2'd3) : (data_shift[30:27]);
data_shift[34:31] <= (data_shift[34:31] > 4) ?
(data_shift[34:31] + 2'd3) : (data_shift[34:31]);
data_shift[38:35] <= (data_shift[38:35] > 4) ?
(data_shift[38:35] + 2'd3) : (data_shift[38:35]);
data_shift[42:39] <= (data_shift[42:39] > 4) ?
(data_shift[42:39] + 2'd3) : (data_shift[42:39]);
data_shift[46:43] <= (data_shift[46:43] > 4) ?
(data_shift[46:43] + 2'd3) : (data_shift[46:43]);
data_shift[50:47] <= (data_shift[50:47] > 4) ?
(data_shift[50:47] + 2'd3) : (data_shift[50:47]);
data_shift[54:51] <= (data_shift[54:51] > 4) ?
(data_shift[54:51] + 2'd3) : (data_shift[54:51]);
data_shift[58:55] <= (data_shift[58:55] > 4) ?
(data_shift[58:55] + 2'd3) : (data_shift[58:55]);
end
else if((cnt_shift <= 27) && (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;
//当计数器等于28时,移位判断操作完成,对各个位数的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;
t_hun <= 4'b0;
h_tho <= 4'b0;
end
else if(cnt_shift == 5'd28)
begin
unit <= data_shift[30:27];
ten <= data_shift[34:31];
hun <= data_shift[38:35];
tho <= data_shift[42:39];
t_tho <= data_shift[46:43];
h_hun <= data_shift[50:47];
t_hun <= data_shift[54:51];
h_tho <= data_shift[58:55];
end
endmodule
cnt_shift:移位判断计数器,data有27位,一共要判断27次,cnt_shift位数为5位,从0开始计数到28,其中1-27是移位判断操作。复位有效时归0,计满且flag为高点平(移位结束)时归0,flag为高点平(每次移位结束)都+1,其他情况保持不变
data_shift:移位判断数据寄存器,移位数据放在高位,data数据放在低位,输出数据一共要32位,data有27位,一共需要59位。计数器cnt_shift为0时赋初值,计数器为1-27时移位判断。复位有效时归0,计数器为0时赋初值,高32位为0,后27位是data初值。当计数器不到27且flag为低电平(还没开始判断时),就要开始判断移位了,具体是:判断data_shift[30:27]是否大于4(0-26是data,27-30是移位数据的低4位),大于4则将[30:27]+3,不满足就保持原值。...判断data_shift[58:55]是否大于4,大于4则+3,不满足保持原值,判断结束后,data_shift左移一位,其他情况维持不变
shift_flag:随着时钟周期高低变化的移位判断标志信号,复位有效时归0,其他情况取反,即一个时钟周期内为低电平表示判断,下一个时钟周期内为高点平表示移位
输出:复位有效时,个位-千万位的输出都为0。由于data有27位,要判断移位27次,计数器计数到27时结束最后一次判断移位,计数到28时进行输出的赋值。个位的4位BCD码是移位数据的低四位data_shift[31:27],千万位的4位BCD码是移位数据的高4位data_shift[58:55]
3、数码管动态显示驱动模块seg_dynamic
我们由上一节知道数码管是由位选和段选信号进行选择和控制的,那么这个模块里要将输入的data转换成对应的段选位选信号。因此,输入为时钟、复位、data、seg_en,输出为sel,seg。因为第1ms第一个数码管亮,第2ms第二个数码管亮,这里需要用1ms时钟去控制位选信号即哪个数码管亮。每100ms数码管显示的十进制数要+1,因此时钟还要控制段选信号即选中的数码管显示什么值。中间信号有实例化后的输出unit,...,h_tho,data_reg,cnt_1ms,flag_1ms,cnt_sel,sel_reg,data_disp。data_reg是数码管带显示内容寄存器,假设输入要显示的十进制数是9999_9999,那么不考虑符号时8个数码管显示9999_9999。cnt_1ms时1ms计数器。flag_1ms是标志信号,计满时拉高,控制位选。cnt_sel是位选数码管计数器,8个数码管每1ms换一个亮,也就是每个数码管8ms亮一次。cnt_sel从0-7计数,相当于给数码管编码。sel_reg是数码管位选信号寄存器,选中点亮的数码管后给他显示的值。data_disp:当前点亮数码管显示的值。
module seg_dynamic
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [26:0] data , //数码管要显示的值
input wire seg_en , //数码管使能信号,高电平有效
output reg [7:0] sel , //数码管位选信号
output reg [7:0] seg //数码管段选信号
);
//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 ; //十万位数
wire [3:0] t_hun ;
wire [3:0] h_tho ;
//reg define
reg [31:0] data_reg ; //待显示数据寄存器
reg [15:0] cnt_1ms ; //1ms计数器
reg flag_1ms ; //1ms标志信号
reg [2:0] cnt_sel ; //数码管位选计数器
reg [7:0] sel_reg ; //位选信号
reg [3:0] data_disp; //当前数码管显示的数据
//data_reg:控制数码管显示数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_reg <= 32'b0;
//不考虑小数点,若显示的数据是8位都非0,则8个数码管全显示
else if(h_tho)
data_reg <= {h_tho,t_hun,h_hun,t_tho,tho,hun,ten,unit};
//若显示的数据7位非0,比如1234567不是显示01234567,则7个数码管亮
else if(t_hun)
data_reg <= {4'd10,t_hun,h_hun,t_tho,tho,hun,ten,unit};//4'd10我们定义为不显示
//若显示的数据6位非0,则6个数码管亮
else if(h_hun)
data_reg <= {4'd10,4'd10,h_hun,t_tho,tho,hun,ten,unit};
//若显示的数据5位非0,则5个数码管亮
else if(t_tho)
data_reg <= {4'd10,4'd10,4'd10,t_tho,tho,hun,ten,unit};
//若显示的数据4位非0,则4个数码管亮
else if(tho)
data_reg <= {4'd10,4'd10,4'd10,4'd10,tho,hun,ten,unit};
//若显示的数据3位非0,则3个数码管亮
else if(hun)
data_reg <= {4'd10,4'd10,4'd10,4'd10,4'd10,hun,ten,unit};
//若显示的数据2位非0,则2个数码管亮
else if(ten)
data_reg <= {4'd10,4'd10,4'd10,4'd10,4'd10,4'd10,ten,unit};
//若上面都不满足都只显示一位数码管
else
data_reg <= {4'd10,4'd10,4'd10,4'd10,4'd10,4'd10,4'd10,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到7循环数,用于选择当前显示的数码管
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_sel <= 3'd0;
else if((cnt_sel == 3'd7) && (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 <= 8'b0000_0000;
else if((cnt_sel == 3'd0) && (flag_1ms == 1'b1))
sel_reg <= 8'b0000_0001;
else if(flag_1ms == 1'b1)
sel_reg <= sel_reg < < 1;
else
sel_reg <= sel_reg;
//控制数码管的位选信号,使8个数码管轮流显示
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个数码管赋十万位值
3'd6: data_disp <= data_reg[27:24]; //给第7个数码管赋百万位值
3'd7: data_disp <= data_reg[31:28]; //给第8个数码管赋千万位值
default:data_disp <= 4'b0;
endcase
else
data_disp <= data_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 <= {1'b1,7'b100_0000}; //显示数字0
4'd1 : seg <= {1'b1,7'b111_1001}; //显示数字1
4'd2 : seg <= {1'b1,7'b010_0100}; //显示数字2
4'd3 : seg <= {1'b1,7'b011_0000}; //显示数字3
4'd4 : seg <= {1'b1,7'b001_1001}; //显示数字4
4'd5 : seg <= {1'b1,7'b001_0010}; //显示数字5
4'd6 : seg <= {1'b1,7'b000_0010}; //显示数字6
4'd7 : seg <= {1'b1,7'b111_1000}; //显示数字7
4'd8 : seg <= {1'b1,7'b000_0000}; //显示数字8
4'd9 : seg <= {1'b1,7'b001_0000}; //显示数字9
4'd10 : 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 <= 8'b0000_0000;
else
sel <= sel_reg;
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码
.t_hun (t_hun ), //百万位BCD码
.h_tho (h_tho ) //千万位BCD码
);
endmodule
参数定义:1ms计数2500000个,最大计数值49_999。
data_reg:控制数码管要显示的数据,复位有效时归0,因为没有小数点,我们考虑到有几位就只让几个数码管进行显示。不显示的数码管定义为4'd11
cnt_1ms:复位有效或计满时归0,其他情况+1
flag_1ms:1ms标志位,复位有效时归0,计满数值-1时拉高,其他情况保持低电平
cnt_sel:数码管编号,从0-7计数,复位有效时归0,计满7个数且标志位拉高时归0,标志位拉高时+1,其他情况保持不变
控制数码管位选信号:复位有效时data_disp归0,使能为高电平有效且标志信号为高时,分情况讨论,cnt_sel为0,第一个数码管赋data_reg的低四位,第一个数码管显示个位,...,需要有default语句,data_disp赋0即可,其他情况,data_disp保持不变
控制数码管段选信号:复位有效时seg所有位赋1表示每一段都不点亮,其他分情况讨论,data_disp为0时数码管显示0,0对应的段选码上一节讨论了,最高位是小数点不用都为1表示灭,低7位对应abcdefg=100_0000,以此类推,加上default语句
sel:数码管位选信号赋值,复位有效时归0,其他情况将sel_reg赋值给
实例化BCD
直接用上一节的代码,需要注意的是hc595_ctrl.v中第23行对data的赋值,把最右边的数码管作为个位比较符合我们的书写习惯,所以位选信号的顺序要改,原语句是assign data={sel,seg[0],seg[1],...,seg[7]};要改做assign data={sel[0],sel[1],...,sel[7],seg[0],seg[1],...,seg[7]};否则,数码管最左边是个位,不符合书写习惯。
5、数码管动态显示模块
该模块主要是对数码管动态显示驱动模块和74HC595控制模块的实例化,以及对应信号的连接
module seg_595_dynamic
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [26:0] data , //数码管要显示的值
input wire seg_en , //数码管使能信号,高电平有效
output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds //串行数据输入
);
//wire define
wire [7:0] sel; //数码管位选信号
wire [7:0] seg; //数码管段选信号
seg_dynamic seg_dynamic_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低有效
.data (data ), //数码管要显示的值
.seg_en (seg_en ), //数码管使能信号,高电平有效
.sel (sel ), //数码管位选信号
.seg (seg ) //数码管段选信号
);
hc595_ctrl hc595_ctrl_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低有效
.sel (sel ), //数码管位选信号
.seg (seg ), //数码管段选信号
.en (1'b1),
.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds ) //串行数据输入
);
endmodule
两个模块的实例化
6、顶层模块top_seg_595
顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接
module top_seg_595
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds //串行数据输入
);
//wire define
wire [26:0] data ; //数码管要显示的值
wire seg_en ;//数码管使能信号,高电平有效
data_gen data_gen_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n), //复位信号,低电平有效
.data (data ), //数码管要显示的值
.seg_en (seg_en) //数码管使能信号,高电平有效
);
seg_595_dynamic seg_595_dynamic_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低有效
.data (data ), //数码管要显示的值
.seg_en (seg_en ), //数码管使能信号,高电平有效
.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds ) //串行数据输入
);
endmodule
数据产生和动态显示模块的实例化
编写testbench
`timescale 1ns/1ns
module tb_top_seg_595();
//wire define
wire stcp ; //输出数据存储寄时钟
wire shcp ; //移位寄存器的时钟输入
wire ds ; //串行数据输入
//reg define
reg sys_clk ;
reg sys_rst_n ;
//对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;
top_seg_595 top_seg_595_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.stcp (stcp ), //输出数据存储寄时钟
.shcp (shcp ), //移位寄存器的时钟输入
.ds (ds )//串行数据输入
);
endmodule
testbench已经很熟悉了。
对比波形
先看6个模块的从属关系,再分别检查6个模块的波形,比较容易debug。
数据产生模块
后面几个模块不细列。
分配管脚
和上一节一样
全部0条评论
快来发表一下你的评论吧 !