FPGA学习—通过数码管实现电子秒表模拟

文章目录

一、数码管简介

请参阅博主以前写过的一篇电子时钟模拟,在此不再赘述。

https://blog.csdn.net/qq_54347584/article/details/130402287

二、项目分析

  • 项目说明:本次项目是为了通过数码管实现秒表模拟。其中,六位数码管分别显示秒表的分位,秒位,毫秒位(由于毫秒有三位,在此只取百位和十位),其中分位和秒位,秒位和毫秒位之间用小数点隔开
  • 本次项目拟设置四个模块,分别为:按键消抖模块,计数模块,数码管驱动模块,以及顶层模块
  • 按键消抖模块要求:传出两个按键的脉冲信号,一个用来暂停/开始秒表的计数,一个用来清空秒表的计数
  • 计数模块要求:能传出秒表的各位值以及小数点位置
  • 数码管驱动模块要求:能正常显示秒表各位值

三、项目源码及分析

数码管驱动模块:

代码分析:

  • 由于本开发板有六位数码管,每位数码管设置显示0-F,因此在此模块中,博主定义了一个24位的din信号(六位每位能显示十六个字符(因此每位需要四位位宽))以此来给每位数码管赋值
  • 同时博主将八段式数码管拆分为七段加一段小数点,因此设计了一个六位的point_n以此来控制小数点的显示
  • 其余代码与博主以前写的数码管动态显示0-F类似,在此不再赘述(如果是FPGA初学者,没有数码管开发经验,请仔细阅读博主动态显示电子时钟两篇博客!!)
c 复制代码
/**************************************功能介绍***********************************
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,//0
               NUM_1 = 7'b111_1001,//1
               NUM_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,//b
               C     = 7'b100_0110,//
               D     = 7'b010_0001,//d
               E     = 7'b000_1110,//E替换为F
               F     = 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)begin
            cnt_1ms <= 'd0;
        end 
        else if(add_cnt_1ms)begin 
            if(end_cnt_1ms)begin 
                cnt_1ms <= 'd0;
            end
            else begin 
                cnt_1ms <= cnt_1ms + 1'b1;
            end 
        end
    end 
    
    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)begin
            seg_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)begin
            disp_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]; end
                6'b111_011 : begin disp_data <= din[11:8] ; point_n_r <= point_n[2]; end
                6'b110_111 : begin disp_data <= din[15:12]; point_n_r <= point_n[3]; end
                6'b101_111 : begin disp_data <= din[19:16]; point_n_r <= point_n[4]; end
                6'b011_111 : begin disp_data <= din[23:20]; point_n_r <= point_n[5]; end
                default: disp_data <= 'd0;
            endcase
        end 
    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 
    // end

    always @(*)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;
        endcase
    end


endmodule

计数器模块:

代码分析:

  • 由项目分析可以得出,由于数码管资源有限,毫秒位只能实现百位和十位的显示,因此我们不妨设置一个基准单位为10ms,每当计数到10ms时,毫秒计数器才进行加一(这样毫秒计数器只需加到100次即可)
  • 除此之外,秒位计数器和分位计数器的技术条件分别为毫秒计数器计满和秒位计数器计满
  • 由于本次项目需要引入按键信号进行秒表的暂停、继续以及清空,因此博主引入了两位按键信号。
  • 一位按键控制秒表的暂停与继续。由上述分析可知,如果想要暂停秒表的计数,我们只需暂停基准单位10ms的计数即可(其余三个计数器,均要在10ms计数器工作的前提下才能逐级工作),因此我们只需要引入一个中间信号flag,用flag作为其计数的条件即可(博主将flag初值设为0不计数,按键按下后flag反转为1开始计数,再次按下再次反转,停止计数)
  • 剩余一位按键用于四个计数器的清零,一旦四个计数器全部清零,传出的数码管数据自然为0
  • 在代码的最后博主将毫秒,秒,分计数器的值赋给dout,再将其传入数码管驱动模块即可(之所以是这个顺序是因为博主前面数码管驱动模块对位选信号赋值好像写反了,不过影响不大)
c 复制代码
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)begin
    if(!rst_n)begin
        cnt_10ms <= 1'b0;
    end
    else if(add_cnt_10ms)begin
        if(end_cnt_10ms)begin
            cnt_10ms <= 1'b0;
        end
        else begin
            cnt_10ms <= cnt_10ms + 1'b1;
        end
    end
    else begin
        cnt_10ms <= cnt_10ms;
    end
end

assign 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)begin
    if(!rst_n)begin
        cnt_99ms <= 1'b0;
    end
    else if(add_cnt_990ms)begin
        if(end_cnt_990ms)begin
            cnt_99ms <= 1'b0;
        end
        else begin
            cnt_99ms <= cnt_99ms + 1'b1;
        end
    end
    else begin
        cnt_99ms <= cnt_99ms;
    end
end

assign 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)begin
    if(!rst_n)begin
        cnt_1s <= 1'b0;
    end
    else if(add_cnt_1s)begin
        if(end_cnt_1s)begin
            cnt_1s <= 1'b0;
        end
        else begin
            cnt_1s <= cnt_1s + 1'b1;
        end
    end
    else begin
        cnt_1s <= cnt_1s;
    end
end

assign 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)begin
    if(!rst_n)begin
        cnt_1min <= 1'b0;
    end
    else if(add_cnt_1min)begin
        if(end_cnt_1min)begin
            cnt_1min <= 1'b0;
        end
        else begin
            cnt_1min <= cnt_1min + 1'b1;
        end
    end
    else begin
        cnt_1min <= cnt_1min;
    end
end

assign 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)begin
    if(!rst_n)begin
        flag <= 1'b0;
    end
    else if(key_in[0])begin
        flag <= ~flag;
    end
    else begin
        flag <= 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

按键消抖模块:

代码分析:请详细参阅博主所写的按键消抖模块博文,在此不再赘述

c 复制代码
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) begin
    if(!rst_n)begin
        key_r0 <= {WIDTH{1'b1}};
        key_r1 <= {WIDTH{1'b1}};
        key_r2 <= {WIDTH{1'b1}};
    end
    else begin
        key_r0 <= key_in; //同步
        key_r1 <= key_r0; //寄存一拍
        key_r2 <= key_r1; //寄存两拍
    end
end

//20ms计数器
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_delay <= 1'b0;
    end
    else if(add_cnt_delay )begin
        if(nedge)begin //检测到下降沿从0开始计数
            cnt_delay <= 1'b0;
        end
        else if(cnt_delay == MAX - 1'b1)begin
            cnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲
        end
        else begin
            cnt_delay <= cnt_delay + 1'b1;
        end
    end
    else begin
        cnt_delay <= 1'b0;
    end
end

assign 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)begin
    if(!rst_n)begin
        key_out <= 'd0;
    end
    else if(cnt_delay == MAX - 2'd2)begin //计数计满前一个脉冲时产生按键脉冲
        key_out <= ~key_in;
    end
    else begin
        key_out <= 'd0;
    end
end

endmodule

顶层模块:

c 复制代码
/****
项目说明:本次项目是为了通过数码管实现秒表模拟。其中,六位数码管分别显示秒表的分位,秒位,毫秒位(百位和十位)。

本次项目拟设置四个模块,分别为:按键消抖模块,计数模块,数码管驱动模块,以及顶层模块。

本项目按键消抖模块要求传出两个按键的脉冲信号,一个用来暂停/开始秒表的计数,一个用来清空秒表的计数。

本项目计数模块要求能传出秒表的各位值以及小数点位置

本项目数码管驱动模块要求能正常显示秒表各位值
****/
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学习视频。

学海无涯,大家有缘再见。

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
ZPC82105 天前
docker 镜像备份
人工智能·算法·fpga开发·机器人
ZPC82105 天前
docker 使用GUI ROS2
人工智能·算法·fpga开发·机器人
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习