4 数码管电子时钟
前篇介绍了FPGA数码管的静态显示与动态显示,本篇将在动态显示的基础上,用FPGA实现电子时钟的功能。
4.1 实现的功能
所用的开发板只有四个数码管,无法做到同时显示时分秒,因此忽略秒的变化,表现为HH.MM的电子时钟。且可根据拨码开关来手动校准当前时间。
4.2 设计思路
程序框图与端口信号
Port name | Direction | Type | Description |
clk_24m | input | wire | 24MHz时钟 |
rstn | input | wire | 复位 |
time_set | input | wire[12:0] | 可设定的时间 |
sm_seg | output | wire[7:0] | 数码管段选 |
sm_bit | output | wire[3:0] | 数码管位选 |
led_13 | output | wire[12:0] | 指示led灯 |
确定计数范围
每过一分钟数码管发生一次跳变,因此主计数器需要精准计数满1min,具体计算如下:开发板提供了一个24MHz的晶振,即时钟周期为(1000/24)ns,计数1min即计数6×1010ns,需要计数6×1010÷(1000/24)=1.44×109,即计数范围0~1_440_000_000-1。
进位设置
当计数满1min时,显示分钟的数码管+1;当计数满60min时,显示分钟的数码管归零,显示时钟的数码管+1;当计数满24h时,显示分钟、时钟的数码管都归零。分钟个位计数0~9,分钟十位计数0~5;时钟个位计数0~9,时钟十位计数02,且时钟计数最大值为23。这里需要注意时钟的进位和普通十进制的不同之处。
位选和段选
与动态显示类似,不过第三个数码管(时钟个位)始终点亮小数点,用于区分时钟和分钟。
手动校准时间
计数器模块counter.v
的程序设计中,其每一位的复位的语句不同于动态显示的赋予0,而是将拨码开关输入的当前时间赋予各位。
引脚分配文件seg_clock.adc
中,根据每一位的最大计数值,恰好把16个拨码开关分为四组。上拨开关后,按下复位键,数码管即显示设置的时间,并以此时间开始计数。为了更加清晰,还点亮了可调节开关对应的LED灯。
4.3 程序设计
计数器模块counter.v
module counter_hour_min
#(
//时钟频率24MHz,则时钟周期(1000/24)ns,则计数一分钟需要60_000_000_000÷(1000/24)=1_440_000_000
parameter CNT_TIME =1_440_000_000-1, //计数一分钟
parameter N =$clog2(CNT_TIME) //即计算log2(12_000_000),向上取整,为24
)
(
input wire clk_24m,
input wire rstn,
input wire[12:0] time_set,
output wire[12:0] time_out
);
//计数器
reg [N-1:0] cnt;
reg [3:0] sm_bit1_num; //分钟个位,0~9
reg [2:0] sm_bit2_num; //分钟十位,0~5
reg [3:0] sm_bit3_num; //时钟个位,0~9,0~3
reg [1:0] sm_bit4_num; //时钟十位,0~2
//——————————————————————————————主计数器,计数1min——————————————————————————————
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt <=0;
else if(cnt==CNT_TIME)
cnt <=0;
else
cnt <=cnt + 1'b1;
end
//——————————————————————————————各位的计数逻辑——————————————————————————————
assign time_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 <=time_set[3:0];
else if(cnt==CNT_TIME) begin
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 <=time_set[6:4];
else if(cnt==CNT_TIME && sm_bit1_num==9) begin
if(sm_bit2_num==5)
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 <=time_set[10:7];
else if(cnt==CNT_TIME && sm_bit2_num==5 && sm_bit1_num==9)begin
if(sm_bit3_num==9 || sm_bit4_num==2 && sm_bit3_num==3)
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 <=time_set[12:11];
else if(cnt==CNT_TIME && sm_bit2_num==5 && sm_bit1_num==9)begin
if(sm_bit4_num==2 && sm_bit3_num==3)
sm_bit4_num <=0;
else if(sm_bit3_num==9)
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 [12:0] time_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 = 4'b0000 ,
S1 = 4'b0001 ,
S2 = 4'b0010 ,
S3 = 4'b0011 ,
S4 = 4'b0100 ,
S5 = 4'b0101 ,
S6 = 4'b0110 ,
S7 = 4'b0111 ,
S8 = 4'b1000 ,
S9 = 4'b1001 ;
//———————————————————————————————————扫描计数器——————————————————————————————————
//cnt_w也是计数器,扫描计数要小于个位数据的变化就行。
always@(posedge clk_24m or negedge rstn) begin
if(~rstn)
cnt_w <=0;
else if(&cnt_w) //按位与
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_bit1_num决定
sm_bit_reg <=4'b1110;
sm_seg_num <=time_out[3:0];
dp <=1;
end
2'b01: begin
sm_bit_reg <=4'b1101;
sm_seg_num <=time_out[6:4];
dp <=1;
end
2'b10: begin
sm_bit_reg <=4'b1011;
sm_seg_num <=time_out[10:7];
dp <=0;
end
2'b11: begin
sm_bit_reg <=4'b0111;
sm_seg_num <=time_out[12:11];
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
指示灯模块led.v
module led_indicate(
output wire[12:0] led_13
);
assign led_13 =0; //指示可供操作的4组共13个led(2+4+3+4)
endmodule
顶层模块top_seg_clock.v
module top_seg_clock
#(
// parameter CNT_TIME = 1_440_000_000-1 //计数一分钟
// parameter CNT_TIME = 24_000_000-1 //计数一秒钟
parameter CNT_TIME = 1_440_000_000-1, //计数一分钟
// parameter CNT_TIME = 300_000-1 //模拟的,快一点
parameter N =$clog2(CNT_TIME)
)
(
input wire clk_24m,
input wire rstn,
input wire[12:0] time_set,
output wire[7:0] sm_seg, //数码管段选
output wire[3:0] sm_bit, //数码管位选
output wire[12:0] led_13
);
wire [12:0] time_out;
led_indicate led_indicate_inst (
.led_13 (led_13)
);
counter_hour_min #(
.CNT_TIME (CNT_TIME)
)
counter_hour_min_inst (
.time_set (time_set),
.clk_24m (clk_24m),
.rstn (rstn),
.time_out (time_out)
);
seg_driver seg_driver_inst (
.clk_24m (clk_24m),
.rstn (rstn),
.time_out (time_out),
.sm_seg (sm_seg),
.sm_bit (sm_bit)
);
endmodule
引脚分配文件seg_clock.adc
#时钟
set_pin_assignment { clk_24m } { LOCATION = K14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
#复位
set_pin_assignment { rstn } { LOCATION = G11; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
#13个LED灯
set_pin_assignment { led_13[0] } { LOCATION = T13; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[1] } { LOCATION = T12; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[2] } { LOCATION = R12; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[3] } { LOCATION = M7; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[4] } { LOCATION = T8; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[5] } { LOCATION = T7; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[6] } { LOCATION = R7; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[7] } { LOCATION = N5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[8] } { LOCATION = P4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[9] } { LOCATION = M5; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[10] } { LOCATION = N4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[11] } { LOCATION = M4; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { led_13[12] } { LOCATION = M3; IOSTANDARD = LVCMOS25; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
#数码管位选
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; }
#13个拨码开关
set_pin_assignment { time_set[0] } { LOCATION = R15; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[1] } { LOCATION = T15; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[2] } { LOCATION = R14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[3] } { LOCATION = T14; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[4] } { LOCATION = P9; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[5] } { LOCATION = R9; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[6] } { LOCATION = N8; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[7] } { LOCATION = N6; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[8] } { LOCATION = P6; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[9] } { LOCATION = M6; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[10] } { LOCATION = T6; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[11] } { LOCATION = R5; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
set_pin_assignment { time_set[12] } { LOCATION = T4; IOSTANDARD = LVCMOS25; PULLTYPE = PULLUP; }
4.4 上板验证
烧录至开发板
在TD5.6.5中新建工程文件,导入四个设计文件,导入引脚文件,编译后将生成的比特流下载到开发板中。具体步骤可参考基于TD5.6.5与vscode的FPGA简易开发教程——以多路选择器为例。
观察进位
将顶层模块top_seg_clock.v
文件中的parameter CNT_TIME = 1_440_000_000-1
修改为其他更小的值,烧录,观察时钟、分钟的进位是否符合电子时钟的逻辑。
手动校准时间
将CNT_TIME
改回原值,重新烧录。根据当前北京时间,调节四组拨码开关的值,长按复位键,当北京时间为整分钟时松开复位键,数码管即显示当前时间。
长时间内多次对比记录,观察该电子时钟是否准确显示北京时间。
如下图,设定时间为01:57,按下复位键,数码管准确显示01.57。
如下图,过了小半个小时再来看一眼,仍准确吻合北京时间。