文章目录
- 一、数码管简介
- 二、项目分析
- 三、项目源码及分析
- 四、实现效果
- 五、总结
一、数码管简介
请参阅博主以前写过的一篇电子时钟模拟,在此不再赘述。
https://blog.csdn.net/qq_54347584/article/details/130402287
二、项目分析
- 项目说明:本次项目是为了通过数码管实现秒表模拟。其中,六位数码管分别显示秒表的分位,秒位,毫秒位(由于毫秒有三位,在此只取百位和十位),其中分位和秒位,秒位和毫秒位之间用小数点隔开
- 本次项目拟设置四个模块,分别为:按键消抖模块,计数模块,数码管驱动模块,以及顶层模块
- 按键消抖模块要求:传出两个按键的脉冲信号,一个用来暂停/开始秒表的计数,一个用来清空秒表的计数
- 计数模块要求:能传出秒表的各位值以及小数点位置
- 数码管驱动模块要求:能正常显示秒表各位值
三、项目源码及分析
数码管驱动模块:
代码分析:
- 由于本开发板有六位数码管,每位数码管设置显示0-F,因此在此模块中,博主定义了一个24位的din信号(六位每位能显示十六个字符(因此每位需要四位位宽))以此来给每位数码管赋值
- 同时博主将八段式数码管拆分为七段加一段小数点,因此设计了一个六位的point_n以此来控制小数点的显示
- 其余代码与博主以前写的数码管动态显示0-F类似,在此不再赘述(如果是FPGA初学者,没有数码管开发经验,请仔细阅读博主动态显示及电子时钟两篇博客!!)
/**************************************功能介绍***********************************
Date : 2023-08-01 11:08:11
Author : majiko
Version : 1.0
Description: 动态数码管模块(动态扫描)
*********************************************************************************///---------<模块及端口声名>------------------------------------------------------
module seg_driver( input clk ,input rst_n ,input [23:0] din ,//输入6位数码管显示数据,每位数码管占4位input [5:0] point_n ,//输入小数点控制位output reg [5:0] seg_sel ,//输出位选output reg [7:0] seg_dig //输出段选
);
//---------<参数定义>--------------------------------------------------------- parameter TIME_1MS = 50_000;//1ms//数码管显示字符编码localparam NUM_0 = 7'b100_0000,//0NUM_1 = 7'b111_1001,//1NUM_2 = 7'b010_0100,//NUM_3 = 7'b011_0000,//NUM_4 = 7'b001_1001,//NUM_5 = 7'b001_0010,//NUM_6 = 7'b000_0010,//NUM_7 = 7'b111_1000,//NUM_8 = 7'b000_0000,//NUM_9 = 7'b001_1000,//A = 7'b000_1000,//B = 7'b000_0011,//bC = 7'b100_0110,//D = 7'b010_0001,//dE = 7'b000_1110,//E替换为FF = 7'b011_1111,//F替换为--DIV = 7'b011_1111;//---------<内部信号定义>-----------------------------------------------------reg [15:0] cnt_1ms ;//1ms计数器(扫描间隔计数器)wire add_cnt_1ms ;wire end_cnt_1ms ;reg [3:0] disp_data ;//每一位数码管显示的数值reg point_n_r ;//每一位数码管显示的小数点//****************************************************************
//--cnt_1ms
//****************************************************************always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_1ms <= 'd0;end else if(add_cnt_1ms)begin if(end_cnt_1ms)begin cnt_1ms <= 'd0;endelse begin cnt_1ms <= cnt_1ms + 1'b1;end endend assign add_cnt_1ms = 1'b1;//数码管一直亮assign end_cnt_1ms = add_cnt_1ms && cnt_1ms == TIME_1MS - 1;//****************************************************************
//--seg_sel
//****************************************************************always @(posedge clk or negedge rst_n)begin if(!rst_n)beginseg_sel <= 6'b111_110;//循环移位实现时,需要给位选赋初值end else if(end_cnt_1ms)begin seg_sel <= {seg_sel[4:0],seg_sel[5]};//循环左移end end//****************************************************************
//--disp_data
//****************************************************************always @(posedge clk or negedge rst_n)begin if(!rst_n)begindisp_data <= 'd0;point_n_r <= 1'b1;end else begin case (seg_sel)6'b111_110 : begin disp_data <= din[3:0] ; point_n_r <= point_n[0]; end//第一位数码管显示的数值6'b111_101 : begin disp_data <= din[7:4] ; point_n_r <= point_n[1]; end6'b111_011 : begin disp_data <= din[11:8] ; point_n_r <= point_n[2]; end6'b110_111 : begin disp_data <= din[15:12]; point_n_r <= point_n[3]; end6'b101_111 : begin disp_data <= din[19:16]; point_n_r <= point_n[4]; end6'b011_111 : begin disp_data <= din[23:20]; point_n_r <= point_n[5]; enddefault: disp_data <= 'd0;endcaseend end//****************************************************************
//--seg_dig
//****************************************************************// always @(posedge clk or negedge rst_n)begin // if(!rst_n)begin// seg_dig <= 8'hff;//数码管的段选如何赋值好?// end // else begin // case (disp_data)// 0 : seg_dig <= {point_n_r,NUM_0};// 1 : seg_dig <= {point_n_r,NUM_1};// 2 : seg_dig <= {point_n_r,NUM_2};// 3 : seg_dig <= {point_n_r,NUM_3};// 4 : seg_dig <= {point_n_r,NUM_4};// 5 : seg_dig <= {point_n_r,NUM_5};// 6 : seg_dig <= {point_n_r,NUM_6};// 7 : seg_dig <= {point_n_r,NUM_7};// 8 : seg_dig <= {point_n_r,NUM_8};// 9 : seg_dig <= {point_n_r,NUM_9};// 10 : seg_dig <= {point_n_r,A };// 11 : seg_dig <= {point_n_r,B };// 12 : seg_dig <= {point_n_r,C };// 13 : seg_dig <= {point_n_r,D };// 14 : seg_dig <= {point_n_r,E };// 15 : seg_dig <= {point_n_r,F };// default: seg_dig <= 8'hff;// endcase// end // endalways @(*)begin case (disp_data)0 : seg_dig <= {point_n_r,NUM_0};1 : seg_dig <= {point_n_r,NUM_1};2 : seg_dig <= {point_n_r,NUM_2};3 : seg_dig <= {point_n_r,NUM_3};4 : seg_dig <= {point_n_r,NUM_4};5 : seg_dig <= {point_n_r,NUM_5};6 : seg_dig <= {point_n_r,NUM_6};7 : seg_dig <= {point_n_r,NUM_7};8 : seg_dig <= {point_n_r,NUM_8};9 : seg_dig <= {point_n_r,NUM_9};10 : seg_dig <= {point_n_r,A };11 : seg_dig <= {point_n_r,B };12 : seg_dig <= {point_n_r,C };13 : seg_dig <= {point_n_r,D };14 : seg_dig <= {point_n_r,E };15 : seg_dig <= {point_n_r,F };16 : seg_dig <= {point_n_r,DIV };default: seg_dig <= 8'b1101_1111;endcaseendendmodule
计数器模块:
代码分析:
- 由项目分析可以得出,由于数码管资源有限,毫秒位只能实现百位和十位的显示,因此我们不妨设置一个基准单位为10ms,每当计数到10ms时,毫秒计数器才进行加一(这样毫秒计数器只需加到100次即可)
- 除此之外,秒位计数器和分位计数器的技术条件分别为毫秒计数器计满和秒位计数器计满
- 由于本次项目需要引入按键信号进行秒表的暂停、继续以及清空,因此博主引入了两位按键信号。
- 一位按键控制秒表的暂停与继续。由上述分析可知,如果想要暂停秒表的计数,我们只需暂停基准单位10ms的计数即可(其余三个计数器,均要在10ms计数器工作的前提下才能逐级工作),因此我们只需要引入一个中间信号flag,用flag作为其计数的条件即可(博主将flag初值设为0不计数,按键按下后flag反转为1开始计数,再次按下再次反转,停止计数)
- 剩余一位按键用于四个计数器的清零,一旦四个计数器全部清零,传出的数码管数据自然为0
- 在代码的最后博主将毫秒,秒,分计数器的值赋给dout,再将其传入数码管驱动模块即可(之所以是这个顺序是因为博主前面数码管驱动模块对位选信号赋值好像写反了,不过影响不大)
module counter (input wire clk ,input wire rst_n ,input wire [1:0] key_in ,//按键信号输入output wire [23:0] dout ,//数码管各位值输出output wire [5:0] point_out //小数点输出
);//内部参数定义
parameter TIME_10ms = 19'd500_000;//10ms计数器,计满秒表毫秒位加1
parameter TIME_990ms = 7'd100 ;//计数器毫秒位,以10ms为单位
parameter TIME_1s = 6'd60 ;//计数器秒位,计满清零
parameter TIME_1min = 6'd60 ;//计数器分位,计满清零//内部信号定义
reg [18:0] cnt_10ms ;//10毫秒计数器寄存器
reg [6:0] cnt_99ms ;//毫秒位计数寄存器
reg [5:0] cnt_1s ;//秒位计数器寄存器
reg [5:0] cnt_1min ;//分位计数器寄存器wire add_cnt_10ms ;
wire end_cnt_10ms ;wire add_cnt_990ms ;
wire end_cnt_990ms ;wire add_cnt_1s ;
wire end_cnt_1s ;wire add_cnt_1min ;
wire end_cnt_1min ;reg flag ;//运行/暂停标志信号寄存器
reg flag_0 ;//清零信号标志寄存器//10毫秒计数器
always @(posedge clk or negedge rst_n)beginif(!rst_n)begincnt_10ms <= 1'b0;endelse if(add_cnt_10ms)beginif(end_cnt_10ms)begincnt_10ms <= 1'b0;endelse begincnt_10ms <= cnt_10ms + 1'b1;endendelse begincnt_10ms <= cnt_10ms;end
endassign add_cnt_10ms = 1'b1 && flag;//运行标志位有效
assign end_cnt_10ms = (add_cnt_10ms && cnt_10ms == TIME_10ms - 1'b1) || key_in[1];//按下清零按键也会清零//毫秒位计数器
always@(posedge clk or negedge rst_n)beginif(!rst_n)begincnt_99ms <= 1'b0;endelse if(add_cnt_990ms)beginif(end_cnt_990ms)begincnt_99ms <= 1'b0;endelse begincnt_99ms <= cnt_99ms + 1'b1;endendelse begincnt_99ms <= cnt_99ms;end
endassign add_cnt_990ms = end_cnt_10ms;
assign end_cnt_990ms = (add_cnt_990ms && cnt_99ms == TIME_990ms - 1'b1) || key_in[1];//秒位计数器
always@(posedge clk or negedge rst_n)beginif(!rst_n)begincnt_1s <= 1'b0;endelse if(add_cnt_1s)beginif(end_cnt_1s)begincnt_1s <= 1'b0;endelse begincnt_1s <= cnt_1s + 1'b1;endendelse begincnt_1s <= cnt_1s;end
endassign add_cnt_1s = end_cnt_990ms;
assign end_cnt_1s = (add_cnt_1s && cnt_1s == TIME_1s - 1'b1) || key_in[1];//分位计数器
always@(posedge clk or negedge rst_n)beginif(!rst_n)begincnt_1min <= 1'b0;endelse if(add_cnt_1min)beginif(end_cnt_1min)begincnt_1min <= 1'b0;endelse begincnt_1min <= cnt_1min + 1'b1;endendelse begincnt_1min <= cnt_1min;end
endassign add_cnt_1min = end_cnt_1s;
assign end_cnt_1min = (add_cnt_1min && cnt_1min == TIME_1min - 1'b1) || key_in[1];//flag信号控制秒表运行/暂停
always@(posedge clk or negedge rst_n)beginif(!rst_n)beginflag <= 1'b0;endelse if(key_in[0])beginflag <= ~flag;endelse beginflag <= flag;end
end//输出值赋值
assign dout[23:20] = cnt_99ms % 10;
assign dout[19:16] = cnt_99ms / 10;
assign dout[15:12] = cnt_1s % 10;
assign dout[11:8] = cnt_1s / 10;
assign dout[7:4] = cnt_1min % 10;
assign dout[3:0] = cnt_1min / 10;
assign point_out = 6'b110_101 ;endmodule
按键消抖模块:
代码分析:请详细参阅博主所写的按键消抖模块博文,在此不再赘述
module key_filter#(parameter WIDTH = 2)//参数化按键位宽
(input wire clk ,input wire rst_n ,input wire [WIDTH - 1:0] key_in ,//按键输入信号output reg [WIDTH - 1:0] key_out //输出稳定的脉冲信号
);parameter MAX = 20'd1_000_000;reg [19:0] cnt_delay ; //20ms延时计数寄存器
wire add_cnt_delay ; //开始计数的标志
wire end_cnt_delay ; //结束计数的标志reg [WIDTH - 1:0] key_r0 ; //同步
reg [WIDTH - 1:0] key_r1 ; //打一拍
reg [WIDTH - 1:0] key_r2 ; //打两拍wire [WIDTH - 1:0] nedge ; //下降沿寄存器//同步打拍
always @(posedge clk or negedge rst_n) beginif(!rst_n)beginkey_r0 <= {WIDTH{1'b1}};key_r1 <= {WIDTH{1'b1}};key_r2 <= {WIDTH{1'b1}};endelse beginkey_r0 <= key_in; //同步key_r1 <= key_r0; //寄存一拍key_r2 <= key_r1; //寄存两拍end
end//20ms计数器
always @(posedge clk or negedge rst_n)beginif(!rst_n)begincnt_delay <= 1'b0;endelse if(add_cnt_delay )beginif(nedge)begin //检测到下降沿从0开始计数cnt_delay <= 1'b0;endelse if(cnt_delay == MAX - 1'b1)begincnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲endelse begincnt_delay <= cnt_delay + 1'b1;endendelse begincnt_delay <= 1'b0;end
endassign nedge = ~key_r1 & key_r2; //下降沿检测
assign add_cnt_delay = 1'b1;
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX - 1'b1;//key_out脉冲信号赋值
always@(posedge clk or negedge rst_n)beginif(!rst_n)beginkey_out <= 'd0;endelse if(cnt_delay == MAX - 2'd2)begin //计数计满前一个脉冲时产生按键脉冲key_out <= ~key_in;endelse beginkey_out <= 'd0;end
endendmodule
顶层模块:
/****
项目说明:本次项目是为了通过数码管实现秒表模拟。其中,六位数码管分别显示秒表的分位,秒位,毫秒位(百位和十位)。本次项目拟设置四个模块,分别为:按键消抖模块,计数模块,数码管驱动模块,以及顶层模块。本项目按键消抖模块要求传出两个按键的脉冲信号,一个用来暂停/开始秒表的计数,一个用来清空秒表的计数。本项目计数模块要求能传出秒表的各位值以及小数点位置本项目数码管驱动模块要求能正常显示秒表各位值
****/
module top(input wire clk ,input wire rst_n ,input wire [1:0] key_in ,output wire [5:0] sel ,output wire [7:0] seg
);wire [1:0] key_out ;
wire [23:0] din ;
wire [5:0] point_n ;seg_driver u_seg_driver( .clk (clk ),.rst_n (rst_n ),.din (din ),.point_n (point_n),.seg_sel (sel ),.seg_dig (seg )
);key_filter u_key_filter(.clk (clk ),.rst_n (rst_n ),.key_in (key_in ),.key_out (key_out)
);counter u_counter(.clk (clk ),.rst_n (rst_n ),.key_in (key_out ),.dout (din ),.point_out (point_n )
);endmodule
四、实现效果
五、总结
本项目主要锻炼了FPGA的数码管开发和计数器编写,实际仍未FPGA学习基础,博主学习时常常被数码管位选信号的段选信号的赋值绕晕,解决方法也只有自己亲自编写几遍,否则仍然无法理解余晖效应和动态扫面是如何让数码管同时显示不同字符的。后续对于基础部分的学习博主应该还会写几篇关于蜂鸣器的博文,至此算是基础语法学习结束。
再往后博主学习到IP核HLS,通讯协议时,也许会继续编写博客,但也要看博主是否有足够的精力,以及能否自己理解并讲解清楚,否则话的还是请各位自行上网寻找视频教学资料,不管是野火还是正点原子,都有针对的FPGA学习视频。
学海无涯,大家有缘再见。