• 状态机设计——从简单的按键消抖开始


      目前笔者正在接受明德扬FPGA网上培训班的培训,讲的内容非常适合新手,且以练习和互动答疑的教学模式让我学到了很多东西。由于是根据自身时间安排进度的,所以战线拉的比较长,发现做些设计总结非常重要,可以帮助自己理清思路,同时也能得到很好的复习。

      之前一直在做altera FPGA的相关学习,对xilinx还不是很熟悉,借着这个契机,将比较基础常用的设计在VIVADO开发环境中过一遍,对我来说是个不错的选择。进入今天的正题,本篇博文旨在通过一个小例子掌握状态机的设计方法。由于设计非常简单,采用常见的三段式状态机来规范设计。后续复杂的例子中,将采用明德扬提出的四段式状态机,个人理解虽然与三段式基本思想相同,但有助于简化设计,理清思路。

      众所周知,硬件按键都存在机械抖动。所以一次人为按下的动作会触发数次按键按下的行为。所谓“按键消抖”模块的功能就是将抖动滤除掉,保证对按键状态的有效识别。单片机的设计思想比较通用,即检测到按键连接端口为低电平(低电平有效)后,延迟一段时间再次确认是否为低。若是则说明此次低电平确实为一次按键行为,否则视为抖动。按键松手检测同理。其大体设计流程如下:

       这是典型的顺序设计思想,但FPGA是并行的。所以这种时间有先后,且操作差异较大的处理过程要用到状态机进行设计。简化后可将上述过程分为四个状态:初始空闲状态、延迟并检测低电平状态、检测释放状态和延迟并检测高电平状态。以下是状态转移图:

      空闲状态下如检测到按键接口低电平进入延迟并确认低电平状态,延迟计数时间设定为10ms。若计数完成且依然为低电平则按下有效进入检测释放状态,若计数期间按键出现高电平说明为抖动回到初始状态。在检测释放状态中若出现高电平进入延迟确认状态,否则持续检测高电平。在延迟确认高电平状态若计数完成且为高电平视为有效松手行为,此时置位有效标志位,按键完成了一次按下到松手的完整有效过程回到IDLE状态再检测下一次按下。如果计数期间出现低电平同样为抖动回到检测释放状态重新检测。

      1 `timescale 1ns / 1ps
      2 
      3 module key_jitter#
      4 (
      5 parameter DELAY_TIME = 2000_000  //延迟10ms
      6 )
      7 (
      8     input clk,
      9     input rst_n,
     10     
     11     input key_i,
     12     output reg led_o
     13     );
     14     
     15     localparam IDLE          = 4'b0001,
     16                DELAY_LOW     = 4'b0010,
     17                CHECK_RELEASE = 4'b0100,
     18                DELAY_HIGH    = 4'b1000;
     19                
     20     reg [20:0] div_cnt;
     21     reg [3:0] state_c,state_n;
     22     reg key_tmp0,key_tmp1;
     23     
     24     wire add_cnt,end_cnt;
     25     wire vld_flag;
     26     wire cnt_during;
     27 
     28     //消除亚稳态
     29     always@(posedge clk or negedge rst_n)begin
     30         if(!rst_n)begin
     31             key_tmp0 <= 0;
     32             key_tmp1 <= 0;
     33         end
     34         else begin
     35             key_tmp0 <= key_i;
     36             key_tmp1 <= key_tmp0;
     37         end
     38     end
     39     
     40     //状态机
     41     always@(posedge clk or negedge rst_n)begin
     42         if(!rst_n)
     43             state_c <= IDLE;
     44         else 
     45             state_c <= state_n;
     46     end
     47     
     48     always@(*)begin
     49         case(state_c)
     50             IDLE:begin  //初始状态检测是否有按键按下 //4'b0001
     51                 if(key_tmp1 == 0)//有按键按下进入延时后再次确认低电平状态
     52                     state_n <= DELAY_LOW;
     53                 else 
     54                     state_n <= state_c;
     55             end
     56             
     57             DELAY_LOW:begin  //延时并再次确认低电平状态  //4'b0010
     58                 if(end_cnt && key_tmp1 == 0)//10ms后依然是低电平则有按键按下,此时检测是否松手
     59                     state_n <= CHECK_RELEASE;
     60                 else if(cnt_during && key_tmp1 == 1)
     61                     state_n <= IDLE;//若未计数完成出现高电平则视为抖动,重新检测按下
     62                 else 
     63                     state_n <= state_c;//计数未完成继续
     64             end
     65             
     66             CHECK_RELEASE:begin  //4'b0100
     67                 if(key_tmp1 == 1)//为高电平则等待并再次确认
     68                     state_n <= DELAY_HIGH;
     69                 else 
     70                     state_n <= state_c;//若没有高电平则持续检测
     71             end
     72             
     73             DELAY_HIGH:begin  //4'b1000
     74                 if(vld_flag)//10ms后依然高电平则按键释放
     75                     state_n <= IDLE;//释放后回到初始状态再次检测下一次的按下
     76                 else if(cnt_during && key_tmp1 == 0)//若延时后为0则松手过程视为抖动
     77                     state_n <= CHECK_RELEASE;
     78                 else 
     79                     state_n <= state_c;//继续计数
     80             end
     81             
     82             default:
     83                 state_n <= IDLE;
     84         endcase
     85     end
     86     
     87     assign cnt_during = add_cnt && div_cnt < DELAY_TIME;
     88     
     89     //延迟计数器
     90     always@(posedge clk or negedge rst_n)begin
     91         if(!rst_n)
     92             div_cnt <= 0;
     93         else if(add_cnt)begin
     94             if(end_cnt)
     95                 div_cnt <= 0;
     96             else 
     97                 div_cnt <= div_cnt + 1'b1;
     98         end
     99         else 
    100             div_cnt <= 0;
    101     end
    102     
    103     assign add_cnt = state_c == DELAY_HIGH || state_c == DELAY_LOW;
    104     assign end_cnt = add_cnt && div_cnt == DELAY_TIME - 1;
    105     //按下一次并释放后表示一次有效的操作,此时led翻转
    106     always@(posedge clk or negedge rst_n)begin
    107         if(!rst_n)
    108             led_o <= 0;//上电复位点亮
    109         else if(state_c == DELAY_HIGH && vld_flag)//可将()内条件作为按键有效输出
    110             led_o <= ~led_o;
    111     end
    112     
    113     
    114     assign vld_flag = end_cnt && key_tmp1 == 1;
    115     
    116 endmodule

       需要注意的知识点是状态机的设计技巧和参数设定。采用三段式状态机设计:一个always块用同步时序方式描述状态转移,另一个模块采用组合逻辑判断状态转移条件,最后给每一个状态输出分配一个时序逻辑块。其优势在于它将同步时序和组合逻辑分别放到不同的always 程序块中实现。这样做的好处不仅仅是便于阅读、理解、维护,更重要的是利于综合器优化代码,利于用户添加合适的时序约束条件,利于布局布线器实现设计。同时采用时序逻辑输出消除了“毛刺”现象,提高设计稳定性。

      另外,参数化设计帮助提高代码可读性和灵活性。verilog中经常使用parameter 和localparam两个关键字定义参数,两者之间有一定的区别:parameter可用作在顶层模块中例化底层模块时传递参数的接口,localparam的作用域仅仅限于当前module,不能作为参数传递的接口。所以这里将延迟时间设定为可传递参数接口,便于顶层模块修改。而状态参数不能改动,只使其作用于当前模块。

      在FPGA设计中,仿真环节必不可少,甚至占用设计周期的大半,极大提高开发效率,让问题尽量在设计前期解决。现在编写测试激励,用modelsim仿真观察按键消抖模块是否完成预期功能。

     1 `timescale 1ns / 1ps
     2 
     3 module key_jitter_tb();
     4     
     5     // reg sys_clk_n,sys_clk_p;
     6     reg clk;
     7     reg rst_n;
     8     reg key_i;
     9     reg [15:0] myrand;
    10     
    11     wire led_o;
    12     
    13     key_jitter key_jitter
    14 (
    15     
    16     .clk(clk),
    17     .rst_n(rst_n),
    18     
    19     .key_i(key_i),
    20     .led_o(led_o)
    21 );
    22 
    23     defparam key_jitter.DELAY_TIME = 50000;//参数重定义 有效时间改为50us便于仿真
    24     parameter RST_TIME = 2,
    25                CYCLE = 5;
    26     
    27     initial begin
    28         clk = 0;
    29         forever #(CYCLE /2) clk = ~clk;
    30     end
    31     
    32     initial begin
    33         rst_n = 1;
    34         #1;
    35         rst_n = 0;
    36         #(CYCLE * RST_TIME);
    37         rst_n = 1;
    38     end
    39     
    40     initial begin
    41         #1;
    42         key_i = 1;//初始未按下
    43         #(CYCLE * RST_TIME);
    44         #(CYCLE * 10);
    45         press_key;
    46         #10_000;
    47         press_key;
    48         $stop;
    49     end
    50     
    51     task press_key;
    52     begin
    53         repeat(20)begin//模拟抖动过程
    54             myrand = {$random}%50000;
    55             #myrand key_i = ~key_i;
    56         end
    57         key_i = 0;
    58         #300_000;
    59         repeat(20)begin
    60             myrand = {$random}%50000;
    61             #myrand key_i = ~key_i;
    62         end
    63         key_i = 1;
    64         #300_000;
    65     end
    66     endtask
    67     
    68 endmodule

      其中使用defparam实现参数重定义,让延迟时间缩短,可以在完成功能验证的前提下缩短仿真时间,提升开发效率。(仿真真的是慢!)在测试激励的编写中,task任务封装绝对是一项利器,可以非常方便地将某项功能封装后反复调用。测试文件中将按键按下释放以及中间的抖动过程作为一个task,在仿真过程中多次调用模拟多次按下释放的行为。通过观察输出波形即可得知功能是否正确。

      在仿真之前,一定要设置好仿真工具、编译库等选项。注意:如果按下Run xx simulation之后一直卡在执行仿真过程中,说明代码中有错误。此时要检查tcl console和log日志文件查看仿真相关警告和错误提示并作出修改。

    modelsim仿真波形:

       可以看到两处红圈处为一次按下释放的有效动作,使led输出翻转。以上便是FPGA实现按键消抖模块的全部设计过程。由于设计比较简单,此处略去上板验证并在线调试的过程,在之后的设计中将给出此处的具体操作流程。这是笔者第一次撰写技术博文,希望大家给出宝贵建议,相互交流学习!

  • 相关阅读:
    原生态Vim使用快捷键
    Django 搭建博客记(二)
    Django搭建博客记(一)
    草稿
    骨骼动画的实现(OpenGL实现)
    场景内容的再现
    实现Ogre的脚本分离
    Bullet物理引擎在OpenGL中的应用
    linux fork进程请谨慎多个进程/线程共享一个 socket连接,会出现多个进程响应串联的情况。
    多开发机别名跳转脚本片段
  • 原文地址:https://www.cnblogs.com/moluoqishi/p/7199220.html
Copyright © 2020-2023  润新知