基于 FPGA 实现滑动显示、多功能数字时钟【设置年月日时分秒以及闹钟】

  • Post author:
  • Post category:其他



本文内容:

基于 FPGA 实现数字时钟,如果后续有时间可以添加一些额外的功能,比如设置时间、闹钟等等

中间的基础篇和进阶篇主要训练数码管的灵活应用,如果熟悉了并完全掌握的话,可以更加熟练的实现数字时钟



一、数码管原理

  • 我使用的开发板型号为 EP4CE6F17C8,它的数码管有六位,原理图如下:

    在这里插入图片描述
  • 主要是由

    DIG



    SEL

    这两个信号控制 6 位数码管显示,高电平灭,低电平亮,下面主要介绍如何控制



SEL 信号

  • SEL 信号主要用来控制数码管的每一位,共有 6 位,SEL 位宽也就是 6 位,如下图所示:

    在这里插入图片描述

    举个例子,在代码中位宽的表示是低位在右,高位在左,所以在代码中写成

    SEL = 6’b111_110

    时,实际上就是

    第 0 位

    数码管亮,其余五位灭,如下图所示:

    在这里插入图片描述



    SEL = 6’b101_110

    时,实际上就是

    第 0 位



    第 4 位

    数码管亮,如下图所示:

    在这里插入图片描述

    但是只有一个 SEL 信号不足以完全控制每一位数码管中的每一段



DIG 信号

  • DIG 信号主要控制每一位数码管中的 8 个段的数码管亮灭,如下图所示:

    在这里插入图片描述


  • DIG = 8’b1010_0100

    时,一位数码管就显示数字 2,如下图所示:

    在这里插入图片描述

    其它每个数字所对应的 DIG 值就不一一列举出来了
  • 这两个信号时需要相互配合才可以在数码管上呈现我们想要的效果
  • 比如说当

    SEL = 6’b111_101



    DIG = 8’b1010_0100

    时,前面 SEL 表示第 1 位(从 0 开始的)数码管亮,配合 DIG 显示数字 2,那么在开发板上就可以呈现第 1 位数码管显示数字 2 其余数码管灭,代码如下:
module display(
    input                   clk         ,
    input                   rst_n       ,

    output      [7:0]       DIG         ,
    output      [5:0]       SEL
);

    assign SEL = 6'b111_101;
    assign DIG = 8'b1010_0100;
    
endmodule
  • 效果图如下:

    在这里插入图片描述



二、基础篇



2.1 原理及代码

  • 如何让数码管依次显示

    123456

    呢?首先要知道 SEL 和 DIG 如何配合控制数码管显示的
  • 原理图:

    在这里插入图片描述

    (1) 首先设置

    SEL

    显示第

    0

    位,也就是

    SEL = 6’b111_110

    ,再设置

    DIG

    显示

    1

    的比特值,这样的话,数码管就第

    0

    位显示数字

    1

    了,但是其它位是





    (2) 然后设置

    SEL

    显示第

    1

    位,也就是

    SEL = 6’b111_101

    ,再设置

    DIG

    显示

    2

    的比特值,这样的话,数码管就第

    1

    位显示数字

    2

    了,但是其它位是





    (3) 按照上面的套路,依次让每一位显示相应数字,其它位灭,当 SEL 的值改变的

    速度

    不断的

    增加

    ,那么就可以连续显示

    123456

    了,这主要应用到了

    视觉残留

    的机制
  • 仔细好好的理一下逻辑
  • 代码也是十分简单的,当然可以不用

    SEL_num

    来做选择,直接对 SEL 使用

    拼接运算符

    也可以


方法一:普通版

module display #(parameter MS_1 = 17'd100_000)(
    input                   clk         ,
    input                   rst_n       ,

    output  reg [7:0]       DIG         ,
    output  reg [5:0]       SEL
);

    // 参数定义
    parameter SEL_MAX   = 3'd6          ;   // 数码管位数 

// 信号定义
    reg     [ 2:0]          SEL_num     ;   // SEL序号选择      

    reg     [16:0]          cnt_flicker ;   // 数码管闪烁频率计数器

// 逻辑实现
    // 闪烁频率计数器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_flicker <= 17'd0;
        end
        else if (cnt_flicker >= MS_1 - 17'd1) begin
            cnt_flicker <= 17'd0;
        end
        else begin
            cnt_flicker <= cnt_flicker + 17'd1; 
        end
    end

    // SEL序号选择
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            SEL_num <= 3'd0;
        end
        else if (cnt_flicker >= MS_1 - 17'd1) begin
            if (SEL_num >= SEL_MAX - 3'd1) begin
                SEL_num <= 3'd0;
            end
            else begin
                SEL_num <= SEL_num + 3'd1;
            end
        end
        else begin
            SEL_num <= SEL_num;
        end
    end

    // SEL信号输出
    always @(*) begin
        case (SEL_num)
            3'd0 : SEL = 6'b111_110;
            3'd1 : SEL = 6'b111_101;
            3'd2 : SEL = 6'b111_011;
            3'd3 : SEL = 6'b110_111;
            3'd4 : SEL = 6'b101_111;
            3'd5 : SEL = 6'b011_111;
            default: SEL = 6'b111_111;
        endcase
    end

    // DIG信号输出
    always @(*) begin
        case (SEL_num)
            3'd0 : DIG = 8'b1111_1001;
            3'd1 : DIG = 8'b0010_0100;
            3'd2 : DIG = 8'b1011_0000;
            3'd3 : DIG = 8'b0001_1001;
            3'd4 : DIG = 8'b1001_0010;
            3'd5 : DIG = 8'b1000_0010;
            default: DIG = 8'b1111_1111; 
        endcase
    end
    
endmodule


方法二:拼接运算版

module display #(parameter MS_1 = 17'd100_000)(
    input                               clk         ,
    input                               rst_n       ,

    output  reg     [7:0]               DIG         ,
    output  reg     [5:0]               SEL
);

// 信号定义    
    reg             [16:0]              cnt_flicker ;   // 数码管闪烁频率计数器
    wire            [ 0:0]              SEL_change  ;

// 逻辑实现
    // 闪烁频率计数器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_flicker <= 17'd0;
        end
        else if (SEL_change) begin
            cnt_flicker <= 17'd0;
        end
        else begin
            cnt_flicker <= cnt_flicker + 17'd1; 
        end
    end

    assign SEL_change = cnt_flicker >= MS_1 - 17'd1 ? 1'b1 : 1'b0;

    // SEL信号输出
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            SEL <= 6'b111_110;
        end
        else if (SEL_change) begin
            SEL <= {SEL[4:0], SEL[5]};
        end
        else begin
            SEL <= SEL;
        end
    end

    // DIG信号输出
    always @(*) begin
        case (SEL)
            6'b111_110 : DIG = 8'b1111_1001;
            6'b111_101 : DIG = 8'b0010_0100;
            6'b111_011 : DIG = 8'b1011_0000;
            6'b110_111 : DIG = 8'b0001_1001;
            6'b101_111 : DIG = 8'b1001_0010;
            6'b011_111 : DIG = 8'b1000_0010;
            default: DIG = 8'b1111_1111; 
        endcase
    end
    
endmodule



2.2 验证结果

  • 效果图如下:

    在这里插入图片描述



三、进阶篇



3.1 原理及代码

  • 该部分主要进一步探究数码管 SEL 与 DIG 之间配合显示


实现目标:

让 6 位数码管从右往左滑动显示 0 – 9,当显示 9 时,后面连续显示 “-”,直到 9 消失,又从 0 开始滑动

  • 可以自己画图分析如何实现,这里我将数码管显示分为 16 个状态,每经过 0.5 s 的时间就向左滑动一下,也就是从一个状态跳转到下一个状态
  • 将一些特殊的情况罗列出来,可以发现用

    状态序号

    +

    位选号

    来确定每一位所对应的 DIG 信号
  • 进一步分析,随着状态的转移,它们之间的和就大于 9 了,那么就需要分情况讨论

  • 和 <= 9


    直接输出对应的 DIG 二进制


    15>= 和 >= 10


    输出 “-” 对应的 DIG 二进制


    和 >= 16


    输出

    和 – 16

    对应的 DIG 二进制

    在这里插入图片描述
  • 现在思路清晰了,实现代码如下:
module dig_demo #(parameter MS_1    = 17'd100_000,
                            MS_200  = 25'd2500_0000)(
    input                               clk                     ,   // 50MHz时钟
    input                               rst_n                   ,   // 复位信号

    output  reg     [ 7:0]              DIG                     ,   // 输出DIG
    output  reg     [ 5:0]              SEL                         // 输出SEL
);

// 信号定义    
    reg             [16:0]              cnt_flicker             ;   // SEL刷新频率计数器
    wire            [ 0:0]              end_cnt_flicker         ;   // cnt_flicker停止计数信号
    
    reg             [24:0]              cnt_200ms               ;   // 200ms计数器
    wire            [ 0:0]              end_cnt_200ms           ;   // cnt_200ms停止计数使能信号

    reg             [ 3:0]              cnt_16state             ;   // 16 个状态计数器
    wire            [ 0:0]              end_cnt_16state         ;   // 结束计时

    reg             [ 2:0]              SEL_now                 ;   // SEL现态
    wire            [ 4:0]              cnt_16state_and_SEL_now ;   // cnt_16state + SEL_now
    reg             [ 1:0]              DIG_now_status          ;   // DIG现态所处的情况
    reg             [ 3:0]              DIG_now                 ;   // DIG现态

// 逻辑实现
    // SEL刷新频率计数器
        /*
        每过10_0000个时钟周期刷新SEL值
        */
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_flicker <= 17'd0;
        end
        else if (end_cnt_flicker) begin
            cnt_flicker <= 17'd0;
        end
        else begin
            cnt_flicker <= cnt_flicker + 17'd1; 
        end
    end

    assign end_cnt_flicker = cnt_flicker >= MS_1 - 17'd1 ? 1'b1 : 1'b0;

    // SEL现态
        /*
        当cnt_flicker计数到最大值10_0000个时钟周期后
        SEL就从第n位跳到第n+1位
        当跳到第5位后,就回到第0位
        */
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            SEL_now <= 'd0;
        end
        else if (end_cnt_flicker) begin
            if (SEL_now >= 'd5) begin
                SEL_now <= 'd0;
            end
            else begin
                SEL_now <= SEL_now + 'd1;
            end
        end
        else begin
            SEL_now <= SEL_now;
        end
    end

    // SEL信号输出
        /*
        根据SEL_now选择SEL输出二进制
        */
    always @(*) begin
        case (SEL_now)
          3'd0 : SEL = 6'b111_110;
          3'd1 : SEL = 6'b111_101;
          3'd2 : SEL = 6'b111_011;
          3'd3 : SEL = 6'b110_111;
          3'd4 : SEL = 6'b101_111;
          3'd5 : SEL = 6'b011_111;
          default: SEL = 6'b111_111;
        endcase
    end

    // cnt_200ms
        /*
        计数200ms
        每过200ms,数码管就向左滑动一下
        */
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_200ms <= 'd0;
        end
        else if (end_cnt_200ms) begin
            cnt_200ms <= 'd0;
        end
        else begin
            cnt_200ms <= cnt_200ms + 'd1;
        end
    end

    assign end_cnt_200ms = cnt_200ms >= MS_200 - 'd1 ? 1'b1 : 1'b0;

    // cnt_16state
        /*
        数码管滑动分为16个状态
        每个状态持续200ms
        */
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_16state <= 'd0;
        end
        else if (end_cnt_200ms) begin
            if (end_cnt_16state) begin
                cnt_16state <= 'd0;
            end
            else begin
                cnt_16state <= cnt_16state + 'd1;
            end
        end
        else begin
            cnt_16state <= cnt_16state;
        end
    end

    assign end_cnt_16state = cnt_16state >= 4'd15 ? 1'b1 : 1'b0;

    // 计算当前16个状态中的一个状态值与SEL现在位数的和
    assign cnt_16state_and_SEL_now = cnt_16state + SEL_now;

    // DIG现态值判断
        /*
        根据上面计算出的和
        判断当前数码管处于3个状态中的哪一个状态
            0:数码管每一位显示数字
            1:数码管前面显示数字,后面显示"-"
            2:数码管前面显示"-",后面显示数字
        */
    always @(*) begin
        if (cnt_16state_and_SEL_now > 9 && cnt_16state_and_SEL_now < 16) begin
            DIG_now_status <= 'd1;
        end
        else if (cnt_16state_and_SEL_now >= 16) begin
            DIG_now_status <= 'd2;
        end
        else begin
            DIG_now_status <= 'd0;
        end
    end

    // DIG现态
        /*
        根据前面状态的判断计算出当前SEL所对应的DIG的值
        */
    always @(*) begin
        if (DIG_now_status == 'd0) begin
            DIG_now = cnt_16state_and_SEL_now;
        end
        else if (DIG_now_status == 'd1) begin
            DIG_now = 'd10;
        end
        else if (DIG_now_status == 'd2) begin
            DIG_now = cnt_16state_and_SEL_now - 'd16;
        end
        else begin
            DIG_now = DIG_now;
        end
    end

    // DIG对应数字输出
    always @(*) begin
        case (DIG_now)
            4'd0  : DIG = 8'b1100_0000;
            4'd1  : DIG = 8'b1111_1001;
            4'd2  : DIG = 8'b1010_0100;
            4'd3  : DIG = 8'b1011_0000;
            4'd4  : DIG = 8'b1001_1001;
            4'd5  : DIG = 8'b1001_0010;
            4'd6  : DIG = 8'b1000_0010;
            4'd7  : DIG = 8'b1111_1000;
            4'd8  : DIG = 8'b1000_0000;
            4'd9  : DIG = 8'b1001_0000;
            4'd10 : DIG = 8'b1011_1111;
            default : DIG = 8'b1111_1111;
        endcase
    end

endmodule



3.2 验证结果

数码管滑动显示



四、数字时钟



4.1 原理及代码


需求分析:

  1. 数码管显示拥有多个界面

    (1)时分秒:显示界面、设置界面

    (2)年月日:显示界面、设置界面

    (3)闹钟:状态界面(开启/关闭)、设置界面
  2. 年月日:

    (1)默认主界面为时分秒显示界面,通过按键可调出年月日界面,并显示三秒钟后自动跳转回主界面(时分秒界面)

    (2)进入到年月日界面后,可通过设置按键设置年月日的值,设置完成后自动保存
  3. 时分秒:

    (1)默认主界面为时分秒显示界面,设置按键可设置当前显示界面时分秒的值,设置完成后自动保存
  4. 闹钟:

    (1)闹钟设置为闹钟显示界面,如果开启闹钟,则显示设置的闹钟时间,如果关闭闹钟,则显示连续的 “-”,表示关闭了闹钟

    (2)当时间达到闹钟设的值时,蜂鸣器播放歌曲,播放歌曲期间按键任意按键即可关闭闹钟


原理讲解:

  • 说实话,这原理其实真没啥好讲的,根据上面写出来的需求一步一步添加相应的条件或者使能信号就实现出来了
  • 上面两部分倒是有些原理可讲,如果能熟练写出上面的代码,那么对于一些信号的灵活应用以及代码编写的能力也能提高很多,而这时钟是真不好讲,所以只说说模块划分吧

    在这里插入图片描述
  • 数字时钟,无非就是用按键设置时钟的值,这里空口白话的说也理不清楚里面的逻辑关系,涉及到很多使能信号之间的条件关系
  • 可以仿真出来看看,就很清楚明白了
  • 工程文件链接:

    https://pan.baidu.com/s/1NR6dzZ2G6TEWzoaGswhYww?pwd=30gh

    ——提取码:

    30gh
  • 简单说一下工程内的文件

    在这里插入图片描述
  • 源码主要是在

    rtl

    下面的

    .v

    文件

    在这里插入图片描述
  • 看看上面那张系统设计的图,就可以知道每个文件之间的依赖关系
  • 贴一张代码图片,看着还不算脑袋疼吧,有点些许的强迫症

    在这里插入图片描述



4.2 验证结果

  • 最后来看看在开发板上的效果吧

FPGA多功能数字时钟

  • 视频中可以看到设置闹钟的分,递减到 59 就减不下去了,这是由于代码中闹钟模块设置分的部分,条件写岔劈了,有点粗心大意,这个我改了,将大于(>)改成了小于(<)
  • 同时有一个小 bug ,在 display.v 模块中,也就是数码管显示模块,处于闹钟关闭状态下,如果一直按设置键,即使当前闹钟显示关闭状态,也还是可以设值,只是我们看不到而已
  • 这是由于给 select_place 信号赋值的条件少写了,这个我还没改了,有时间可以自行研究



版权声明:本文为ssj925319原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。