FPGA学习按键控制动态数码管

前言提醒

今天的这些内容也只是我自己自学的,有些东西可能理解不是很深刻,还有些东西是直接使用的之前的代码,有解释不清楚的地方还望海涵。

内容介绍

也就是综合之前学的东西,尝试这做了这个小项目,我通过开发板上的四个按钮,对数码管进行操作,其中第一个按钮让数字加一,第二个加十,第三个加一百,第四个加一千;其中,数码管的第二位需要展示出小数点,六位则是展示出负号,今天的这个还算比较简单,更为复杂的加减运算和符号显示今天暂时没有写上去,如果明天做了再分享

原理简述

首先我来根据我自己的理解解释一下这个功能需要的一些技术

按键消抖

附:按键消抖原理看上去容易理解,但是从代码层面上去写还是要花一番功夫,我暂时没有完全独立写出按键消抖代码的能力,所以这里也只是给出了一点理解;不要想着一下子就可以学会,这个不分内容还是比较困难的,主要还是要思考和多加练习。

首先就是按键部分,之前我们使用的按键都是按下触发,但这里我们需要的是:按下按键,但只触发一次,这里提到一个很重要的东西,叫做按键消抖,具体的内容可以在网上自行查看,这里给出我的理解。

由于按键电路是由高低电平来决定的,在按下按钮的过程中,按键信号由高电平转移至低电平(1->0),但是实际情况下,由于按键在按下过程中会有接触不良的部分,虽然在宏观时间上看不出来,但在微观时间上观测,就会显示出一种短时间内高低电平的快速转换,有可能会造成一次按下但触发多次的情况,如果呈现在波形图上,就会展示出一种类似于抖动的状态,而且很不巧,机器能够识别这种微观层面上的电平变化(人类真的好弱啊)。

图我就直接盗了,表达大致意思就行,不想自己画(嘿嘿嘿)~

所以呢,我们希望一次按下只被检测到一次,也就说我们在微观层面上的连续下降沿(1- >0的过程)我们要想办法让机器认为是同一次下降沿。

图我也厚颜无耻的盗了哈哈哈

这里我们的解决方案是使用延迟检测,基本原理是:当我们检测到第一个下降沿的时候,就让机器歇一会,把这次的信号存起来,等待一个在图片和理论上很长,但是宏观层面几乎是一瞬间的时间,等到按键的低电平稳定后,再向需要的位置发送这次信号。(大致是这么个意思,能懂当然最好,不懂我也解释不了更详细的了,比毕竟能力有限,这些也只是帮助理解的方式。)

理解了大致做法,这里也就给出一个解决方案,把这个微观时间控制在20毫秒(20*10-6s),这个时间对于机器来说足够了(另外我也不相信有人能在20毫秒以内连续按下同一个按键两次(乐)),通过多个寄存器对时钟上升沿的检测,将一个短时间内的多次按键下降沿装换成一个上升沿脉冲(其实也可以下降沿,但是上升沿使用起来比较方便,也比较容易读,例:if(key)和if(!key)比较,前者一看就是按下某个按键然后触发,而加上非(!)读起来容易混淆),然后再由这个上升沿去对其他的数据进行操作。

注:这个代码我也没吃透,但是当真是好用的代码,如果你心里没有负担直接用,其实也无可厚非,我无话可说;但若如果你认真吃透,然后自己写出来用自己的,那算你厉害(哈哈哈)。

key_debounce.v代码如下

/**************按键消抖**************/
module key_debounce(
    input   wire            clk,
    input   wire            rst_n,
    input   wire    [3:0]   key,

    output  wire    [3:0]   key_out
);

localparam          MAX_20ms = 20'd100_0000;
reg         [19:0]  cnt_20ms;
reg                 start;
reg         [3:0]   key_r0;
reg         [3:0]   key_r1;
wire                nedge;//下降沿信号
reg         [3:0]   key_r;


/****************20ms设置*******************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt_20ms <= MAX_20ms;
    end
    else if (start) begin
        if (cnt_20ms == 1'd1) begin
            cnt_20ms <= 20'd0;
        end
        else begin
            cnt_20ms <= cnt_20ms - 1'd1;
        end
    end
    else begin
        cnt_20ms <= cnt_20ms;
    end
end
/*-----------------------------------------*/



/*********************下降沿检测****************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_r0 <= 4'b1111;
        key_r1 <= 4'b1111;
    end
    else begin
        key_r0 <= key;      //一拍,同步时钟域
        key_r1 <= key_r0;   //一拍,检测按键下降沿
    end
end
/*--------------------------------------------*/


/************************key_r*****************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_r <= 4'd0000;
    end
    else if (cnt_20ms == 1'd1) begin
        key_r <= ~key_r1;
    end
    else begin
        key_r <= 4'd0000;
    end
end
/*-------------------------------------------*/

/**********************约束start*************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        start <= 1'd0;
    end
    else if (nedge) begin
        start <= 1'd1;
    end
    else if (cnt_20ms == 1'd1) begin
        start <= 1'd0;
    end
    else begin
        start <= start;
    end
end
/*-----------------------------------------*/

assign nedge = (~key_r0[0] && key_r1[0]) || (~key_r0[1] && key_r1[1]) || (~key_r0[2] && key_r1[2]) || (~key_r0[3] && key_r1[3]);//检测到下降沿
assign key_out = key_r;

endmodule
以上代码实现原理解析介绍

突然回归,对以上代码带着新的理解回来了

assign nedge = (~key_r0[0] && key_r1[0]) || (~key_r0[1] && key_r1[1]) || (~key_r0[2] && key_r1[2]) || (~key_r0[3] && key_r1[3]);//检测到下降沿

首先,这段代码的核心在于最后那一排的对nedge,由于在前面定义了一个key_r0和key_r1,这两个寄存器虽然都是由key本身直接复制,但是由于使用了阻塞赋值(<=),所以他们之间有一个一拍(20us)的差值,所以按下key前, ~ key_r0 && key_r1的值(0 && 1)是0,当按下key的时候,key会由高电平转移到低电平(1->0),此时key_r0会首先变成低电平,但是key_r1会保持20us的高电平,此时 ~ key_r0 && key_r1的值(1 && 1)则是1,再下一个20us后,r1也会变成低电平,这个值又重新归零,也就是说,这个地方产生了一个20us的高电平脉冲。而当key重新回到高电平(0->1)的过程中,r0变成0,不会产生任何反应,nedge依然是0.

/**********************约束start*************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        start <= 1'd0;
    end
    else if (nedge) begin
        start <= 1'd1;
    end
    else if (cnt_20ms == 1'd1) begin
        start <= 1'd0;
    end
    else begin
        start <= start;
    end
end
/*-----------------------------------------*/

现在是最有意思的地方。上述原理的过程中,我们知道,按键如果不去对他操作,很有可能产生多个下降沿信号。此处同理,在按下一次按键的过程中,抖动会让nedge产生多个脉冲信号。但是,在最后一个模块中,我们只是当nedge产生脉冲信号的时候,让start信号变成高电平,却并没有在nedge归零的时候让start信号变回低电平,在我们设置的20ms内,start信号一直保持着高电平,直到第一个20ms结束。在这20ms内,产生的脉冲信号除了第一个以外都在做无用功。(他们也想让start变成高电平,但是start本身就是高电平,而start高电平会使时钟计数,这些脉冲不能产生新的计数,所以是无用功。)

也就是说,上边的原理图其实和我们写的代码有一点点出入,不是抖动结束开始计20ms,而是开始抖动就在计数了。

/************************key_r*****************/
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        key_r <= 4'd0000;
    end
    else if (cnt_20ms == 1'd1) begin
        key_r <= ~key_r1;
    end
    else begin
        key_r <= 4'd0000;
    end
end
/*-------------------------------------------*/

20ms之后,按键已经趋于平稳,这个时候再把已经变成低电平的key信号或者任何与key相同的信号,如r0,r1,赋给输出信号。这里对信号做了一点微小的处理,仅当20ms到达的一瞬间把按键信号取反进行复制,而在这之前之后都置零,这样输出的处理后的信号就也是一个仅有20us的脉冲信号,便于我们的后序使用。

注:这里基本上是确定了一个按键按下的抖动时长在20ms以内,而20ms又在人的感官中是一个瞬间,也就是说,在处理按键消抖的过程中,我们选择消抖时间不一定非要一个固定的时间,只要这个时间足够让按键稳定,而人的感官中,这个时间又是一瞬间即可(又是微观宏观概念)。如25ms,30ms,50ms这种微量级别的时间都可以拿来使用,不用拘泥一格非要20ms。我们使用20ms的理由大概是因为这个数在计数的时候正好是十的倍数,毕竟一次计数20us嘛,看着舒服。

动态数码管操作

其实这部分操作并不是很难,在FPGA开发板上,如果只有段选,那虽然麻烦,但是还是比较简单的,静态数码管都可以解决大部分问题;但是如果位选和段选都有,那要使用动态数码管,还是需要对微观和宏观有削微的理解嘿嘿嘿。

大致解释:在有位选位选的FPGA开发板上,展示数字,则当段选是多少,所有被位选标记的数位都会被段选的位置触发,所以要想同时在所有的数位上展示不同的数字,数位不断变化,而数字在不同的数位上则显示不同的内容。

这里我选择的时间是2毫秒,以确保人的肉眼不会观察到数位的变化,也就是超级快速的流水灯原理,然后再在状态机里,让不同的数字去找到对应的数位,在肉眼上看到的也就是全部点亮,而且每个数位的内容就不一样了。

这个部分比较上按键还是比较好理解的,然后就是我的各种按键照过来操作一下,因为我当时有点蠢,没有把按键控制数字和数字控制数码管分开,所以就将就一下看吧。更多的理解就直接看代码里的注释吧

sel_control.v代码如下

module sel_control(
    input   wire            clk     ,
    input   wire            rst_n   ,
    input   wire    [3:0]   key_out ,//检测出入按键信号上升沿脉冲

    output  wire    [5:0]   sel     ,//位选
    output  wire    [7:0]   seg      //段选
);

    parameter   MAX_2ms = 17'd10_0000;

    reg [05:0]  sel_r;//位选
    reg [07:0]  seg_r;//段选
    reg [16:0]  cnt_2ms;//让位选器不断选位的计数器

    reg [19:0]  num_single;//某一位数字
    reg [19:0]  num_all;//整体数字

    //对数码管数字进行储存,方便后序使用
    localparam  S0 = 7'b100_0000,
                S1 = 7'b111_1001,
                S2 = 7'b010_0100,
                S3 = 7'b011_0000,
                S4 = 7'b001_1001,
                S5 = 7'b001_0010,
                S6 = 7'b000_0010,
                S7 = 7'b111_1000,
                S8 = 7'b000_0000,
                S9 = 7'b001_0000,
                SF = 7'b011_1111;
    
    //2毫秒计数器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_2ms <= 17'd0; 
        end
        else if (cnt_2ms == MAX_2ms - 1'd1) begin
            cnt_2ms <= 17'd0;
        end
        else begin
            cnt_2ms <= cnt_2ms + 1'd1;
        end
    end

    /***
    每过2毫秒,位选器向前位移一位,由于人眼看不见2毫秒的变化(也可
    能是因为数码管2毫秒还不能熄灭),使得不同位能够显示不同的数字
    ***/
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sel_r <= 6'b011_111;
        end
        else if (cnt_2ms == MAX_2ms - 1'd1) begin
            sel_r <= {sel_r[0],sel_r[5:1]};
        end
        else begin
            sel_r <= sel_r;
        end
    end

    /***
    对不同按键进行操作
    这里希望按键一使整体数据加一
    按键二使整体数据加十
    按键三使整体数据加一百
    按键四使整体数据加一千
    ***/
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            num_all <= 20'd0;
        end
        else if (key_out[0]) begin
            num_all <= num_all + 1'd1;
        end
        else if (key_out[1]) begin
            num_all <= num_all + 4'd10;
        end
        else if (key_out[2]) begin
            num_all <= num_all + 7'd100;
        end
        else if (key_out[3]) begin
            num_all <= num_all + 10'd1000;
        end
        else begin
            num_all <= num_all;
        end
    end

    /***
    将整体的数字转换成单个位数的数字,取到各个10进制数
    ***/
    always @(*) begin
        case (sel_r)
            6'b011_111  :   num_single = num_all % 10;//对整体数字取余,和num_single = num_all - num_all / 10 * 10效果一致,但是更加方便,也比较好读
            6'b101_111  :   num_single =    (num_all / 10) % 10;//一下算式同上,这里没有做整理是方便比较对比
            6'b110_111  :   num_single =   ((num_all / 10) / 10) % 10;
            6'b111_011  :   num_single =  (((num_all / 10) / 10) / 10) % 10;
            6'b111_101  :   num_single = ((((num_all / 10) / 10) / 10) / 10) % 10;
            6'b111_110  :   num_single = ((((num_all / 10) / 10) / 10) / 10) / 10;//最后一位直接除取得结果即可,因为没有更高位数的数字了
            default     :   num_single = 20'd0;
        endcase
        
    end

    /***
    使各个数字的数位拥有相应的数码管触发
    这里额外加入了第二位后显示小数点
    第六位从数字改为了负号,表示负数
    ***/
    always @(*) begin
        if (sel_r == 6'b101_111) begin
            seg_r[7] = 1'b0;//显示小数点
            case (num_single)
                20'd0   :   seg_r[6:0] = S0;
                20'd1   :   seg_r[6:0] = S1;
                20'd2   :   seg_r[6:0] = S2;
                20'd3   :   seg_r[6:0] = S3;
                20'd4   :   seg_r[6:0] = S4;
                20'd5   :   seg_r[6:0] = S5;
                20'd6   :   seg_r[6:0] = S6;
                20'd7   :   seg_r[6:0] = S7;
                20'd8   :   seg_r[6:0] = S8;
                20'd9   :   seg_r[6:0] = S9;
                default :   seg_r[6:0] = SF;
            endcase
        end
        else if (sel_r == 6'b111_110) begin
            seg_r = {1'b1,SF};//显示负号,且不显示小数点
        end
        else begin
            seg_r[7] = 1'b1;//不显示小数点
            case (num_single)
                20'd0   :   seg_r[6:0] = S0;
                20'd1   :   seg_r[6:0] = S1;
                20'd2   :   seg_r[6:0] = S2;
                20'd3   :   seg_r[6:0] = S3;
                20'd4   :   seg_r[6:0] = S4;
                20'd5   :   seg_r[6:0] = S5;
                20'd6   :   seg_r[6:0] = S6;
                20'd7   :   seg_r[6:0] = S7;
                20'd8   :   seg_r[6:0] = S8;
                20'd9   :   seg_r[6:0] = S9;
                default :   seg_r[6:0] = SF;
            endcase
        end
    end

    assign seg = seg_r;
    assign sel = sel_r;

endmodule

顶层模块

顶层模块还是老朋友了,就是吧我们的信号全部当成是物理电路里面的电线就可以了,这里我当时也是脑子抽了,顶层模块的名字都是乱取的(捂脸)。

key_control.v代码如下

module key_control(
    input   wire            clk     ,
    input   wire            rst_n   ,
    input   wire    [3:0]   key     ,

    output  wire    [5:0]   sel     ,
    output  wire    [7:0]   seg      
);

wire [3:0] key_out;

sel_control u_sel_control(
    .clk        (clk        ),
    .rst_n      (rst_n      ),
    .key_out    (key_out    ),

    .sel        (sel        ),
    .seg        (seg        ) 
);

key_debounce u_key_debounce(
    .clk        (clk        ),
    .rst_n      (rst_n      ),
    .key        (key        ),

    .key_out    (key_out    ) 
);

endmodule

总结

今天的内容就到此为止吧,结果我就不放出来了,各位有兴趣可以自己试试,反正我成功了(绝对不是因为手机不在身边懒得录),嘿嘿嘿。
FPGA的学习还是比较有意思的,自己取做点小项目也是很有成就感的,有不懂的地方去搞懂也很有成就感的,继续努力吧。拜了个拜~

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值