前篇介绍了利用FPGA进行数码管的静态显示与动态显示,以及如何使用数码管实现电子时钟。本篇将介绍用FPGA数码管实现秒表计数的功能。
5 数码管秒表
5.1 实现的该功能
实现秒表功能,精度为0.01s。按下计时键则开始读秒,最大可读到99秒99,之后归零。计时状态按下计时键则暂停。暂停状态按下计时键则继续计时。任何情况下按下复位键回到00.00。
5.2 设计思路
程序框图与端口信号
Port name | Direction | Type | Description |
clk_24m | input | wire | 24MHz时钟 |
rstn | input | wire | 复位键 |
key_in | input | wire | 计时/暂停键 |
sm_seg | output | wire[7:0] | 数码管段选 |
sm_bit | output | wire[3:0] | 数码管位选 |
确定计数范围
秒表精度为0.01s,即每过0.01s数码管发生一次跳变,因此主计数器需要精准计数满0.01s,具体计算如下:开发板提供了一个24MHz的晶振,即时钟周期为(1000/24)ns,计数0.01s即计数1×107ns,需要计数1×107ns÷(1000/24)ns=2.4×105,即计数范围0~240_000-1。
进位设置
只计秒,最大为99秒99,不存在秒分时的进位,因此采用十进制的规则,即逢九进位。
按键消抖
计数模块中的输入信号key_in
为按键消抖模块的输出信号key_out
,消除了按下的抖动毛刺。
秒表功能
秒表的核心功能是:计时状态,按下key_in
暂停计时;暂停状态,按下key_in
后继续计时。仔细拆解这个功能描述,可得:暂停计时的前提是保证seg_driver.v
模块的输入信号num_out
(也是counter.v
模块的输出信号num_out
)保持不变,这就需要在counter.v
模块内中断各位计数器(该步骤只中断主计数器即可),而这一操作与按下key_in
直接相关。
不难得出,按下key_in
时需要产生一个标志信号flag
,只有满足flag==1
,才能执行主计数器的计数功能。flag==0
时,主计数器的计数信号cnt
保持不变,从而导致四位计数器的技术信号保持不变,进一步使num_out
保持不变。
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(flag)begin //在原先的基础上套一层flag
if(cnt==CNT_MAX)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//else保持省略
end
接下来是flag
信号产生和按下key_in
之间的关系。key_in
默认为高电平,按下后变成低电平,因此flag
信号为key_in
的下降沿检测。
位选和段选
与动态显示类似,不过第三个数码管始终点亮小数点。
5.3 程序设计
按键消抖模块key_filter.v
module key_filter (
input wire clk,
input wire rstn,
input wire key_in,
output reg key_out
);
//20ms,20_000_000÷(1000/24)=480_000
localparam CNT_20MS = 480_000-1;
// localparam CNT_20MS = 100;
reg key_in_d;
wire pos_neg;
reg [$clog2(CNT_20MS)-1:0] cnt;
//边沿检测
always @(posedge clk or negedge rstn) begin
if(~rstn)
key_in_d <=0;
else
key_in_d <=key_in;
end
assign pos_neg =(key_in_d^key_in);
//当上升沿或下降沿时,一律清空计数器
always @(posedge clk or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(pos_neg)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//计数满20ms时,说明稳定了,更新。否则保持原来的值。
always @(posedge clk or negedge rstn) begin
if(~rstn)
key_out <=1'b1;
else if(cnt==CNT_20MS)
key_out <=key_in;
//else保持
end
endmodule
计数器模块counter.v
module counter
#(
parameter CNT_MAX =240_000-1,
parameter N =$clog2(CNT_MAX)
)
(
//计数
input wire clk_24m,
input wire rstn,
input wire key_in,
output wire[15:0] num_out
);
//计数器
reg [N-1:0] cnt;
reg [3:0] sm_bit1_num;
reg [3:0] sm_bit2_num;
reg [3:0] sm_bit3_num;
reg [3:0] sm_bit4_num;
reg key_in_r;
wire neg_edge;
assign neg_edge =(~key_in && key_in_r);
always @(posedge clk_24m or negedge rstn) begin
if(~rstn)
key_in_r <=0;
else
key_in_r <=key_in;
end
reg flag;
always @(posedge clk_24m or negedge rstn) begin
if(~rstn)
flag <=0;
else if(neg_edge)
flag <=~flag;
//else保持省略
end
//——————————————————————————————主计数器——————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(flag)begin
if(cnt==CNT_MAX)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//else保持省略
end
//——————————————————————————————各位的计数逻辑——————————————————————————————
assign num_out ={sm_bit4_num, sm_bit3_num, sm_bit2_num, sm_bit1_num};
//个位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit1_num <=0;
else if(cnt==CNT_MAX) begin //0.1Hz
if(sm_bit1_num==9)
sm_bit1_num <=0;
else
sm_bit1_num <=sm_bit1_num + 1'b1;
end
//else保持,省略
end
//十位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit2_num <=0;
else if(cnt==CNT_MAX && sm_bit1_num==9) begin //个位
if(sm_bit2_num==9)
sm_bit2_num <=0;
else
sm_bit2_num <=sm_bit2_num + 1'b1;
end
//else保持,省略
end
//百位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit3_num <=0;
else if(cnt==CNT_MAX && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit3_num==9)
sm_bit3_num <=0;
else
sm_bit3_num <=sm_bit3_num + 1'b1;
end
//else保持,省略
end
//千位
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
sm_bit4_num <=0;
else if(cnt==CNT_MAX && sm_bit3_num==9 && sm_bit2_num==9 && sm_bit1_num==9) begin
if(sm_bit4_num==9)
sm_bit4_num <=0;
else
sm_bit4_num <=sm_bit4_num + 1'b1;
end
//else保持,省略
end
endmodule
数码管驱动模块seg_driver.v
module seg_driver
(
input wire clk_24m,
input wire rstn,
input wire [15:0] num_out,
output wire [7:0] sm_seg, //数码管段选
output wire [3:0] sm_bit //数码管位选
);
reg [15:0] cnt_w;
reg [3:0] sm_seg_num; //具体数字
reg [7:0] sm_seg_reg; //数码管段选,由具体数字译码而来
reg [3:0] sm_bit_reg; //数码管位选
reg dp;
localparam S0 = 0 ,
S1 = 1 ,
S2 = 2 ,
S3 = 3 ,
S4 = 4 ,
S5 = 5 ,
S6 = 6 ,
S7 = 7 ,
S8 = 8 ,
S9 = 9 ;
//———————————————————————————————————扫描计数器——————————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt_w <=0;
else if(&cnt_w) //按位与,即计数到最大,16位都是1
cnt_w <=0;
else
cnt_w <=cnt_w + 1'b1;
end
//———————————————————————————————————位选与段选——————————————————————————————————
//sm_seg_num用于显示具体的数据
always@(posedge clk_24m or negedge rstn) begin
if(~rstn) begin //这两行自定义复位时哪几个会亮,亮的显示什么
sm_bit_reg <=4'b1111;
sm_seg_num <=0;
end
else begin
case( cnt_w[15:14] )
2'b00: begin //最高两位为00,则点亮第一个管子,且具体数字由sm_bit_num决定
sm_bit_reg <=4'b1110;
sm_seg_num <=num_out[3:0];
dp <=1;
end
2'b01: begin
sm_bit_reg <=4'b1101;
sm_seg_num <=num_out[7:4];
dp <=1;
end
2'b10: begin
sm_bit_reg <=4'b1011;
sm_seg_num <=num_out[11:8];
dp <=0;
end
2'b11: begin
sm_bit_reg <=4'b0111;
sm_seg_num <=num_out[15:12];
dp <=1;
end
endcase
end
end
//段选译码,控制每一位的显示数字
always@(*) begin //共阳极数码管,最高位是小数点,低七位是七段管。0为亮,1为暗。
case ( sm_seg_num )
S0: sm_seg_reg ={dp, 7'b100_0000};
S1: sm_seg_reg ={dp, 7'b111_1001};
S2: sm_seg_reg ={dp, 7'b010_0100};
S3: sm_seg_reg ={dp, 7'b011_0000};
S4: sm_seg_reg ={dp, 7'b001_1001};
S5: sm_seg_reg ={dp, 7'b001_0010};
S6: sm_seg_reg ={dp, 7'b000_0010};
S7: sm_seg_reg ={dp, 7'b111_1000};
S8: sm_seg_reg ={dp, 7'b000_0000};
S9: sm_seg_reg ={dp, 7'b001_0000};
default:sm_seg_reg ={dp, 7'b100_0000};
endcase
end
assign sm_seg =sm_seg_reg;
assign sm_bit =sm_bit_reg;
endmodule
顶层模块top_seg_stopwatch.v
module top_seg_stopwatch
#(
// parameter CNT_MAX =12_000_000-1, //计数0.5s
// parameter CNT_MAX =24_000_000-1, //计数1s
parameter CNT_MAX =240_000-1, //计数0.01s
// parameter CNT_MAX =3_000_000-1, //模拟的,快一点
parameter N =$clog2(CNT_MAX)
)
(
input wire clk_24m,
input wire rstn,
input wire key_in,
output wire[7:0] sm_seg, //数码管段选
output wire[3:0] sm_bit //数码管位选
);
wire [15:0] num_out; //0~9,10个数,需要4bit来存储。现在有四个数码管,需要16bit
wire key_out;
counter # (
.CNT_MAX (CNT_MAX),
.N (N)
)
counter_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.key_in (key_out)
);
seg_driver seg_driver_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.num_out (num_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
key_filter key_filter_inst (
.clk (clk_24m),
.rstn (rstn),
.key_in (key_in),
.key_out (key_out)
);
endmodule
引脚分配文件seg_stopwatch.adc
# 时钟
set_pin_assignment { clk_24m } { LOCATION = K14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 复位
set_pin_assignment { rstn } { LOCATION = G11; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 按下
set_pin_assignment { key_in } { LOCATION = H14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
# 段选与位选
set_pin_assignment { sm_bit[0] } { LOCATION = B1; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[1] } { LOCATION = C3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[2] } { LOCATION = C2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_bit[3] } { LOCATION = F3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[0] } { LOCATION = E3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[1] } { LOCATION = B3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[2] } { LOCATION = F4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[3] } { LOCATION = E4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[4] } { LOCATION = F5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[5] } { LOCATION = D3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[6] } { LOCATION = B2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { sm_seg[7] } { LOCATION = A2; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
5.4 上板验证
烧录至开发板
在TD5.6.5中新建工程文件,导入所有设计文件,导入引脚文件,编译后将生成的比特流下载到开发板中。具体步骤可参考基于TD5.6.5与vscode的FPGA简易开发教程——以多路选择器为例。
观察进位
依次验证以下几条功能:
复位状态,按下计时/暂停键,观察是否正确从00.00开始计时。
计时状态,按下计时/暂停键,观察是否正确暂停,显示SS.SS。
暂停状态,按下计时/暂停键,观察是否从暂停时刻继续计时。
- 在任意时刻按下复位键,观察是否都能成功回到00.00,观察此时若不进行其他操作,是否不会计时。