FPGA:用状态机点亮一个LED灯(结合讲清楚按键消抖!!!)
实现功能:按键控制LED,按一下亮,按一下灭,就这么简单;
其实有很多方法都能实现这个功能,最简单的
如下:
module led_ctrl( input wire sys_clk, input wire sys_rstn, input wire key, output reg led ); always@(posedge sys_clk or negedge sys_rstn) if(sys_rstn==1'b0) led <= 1'b1; else if(key==1'b0) led <= ~led; else led <= led; endmodule
下面这个图是上面代码综合出的RTL视图,
但是实际上这里我在做的时候出现了一个问题,就是有时候按一下就灭了,再按下的时候不亮了,我再按久一点,它又亮了,这是为什么呢,这里存在一个硬件上的问题,就是机械按键在按下去的时候信号会有抖动,什么意思呢,见下图:
我本来是按照这样一个波形图去设计的,但是实际上的输入是下面这样的波形:
这就是机械按键自带的抖动,按下之后会有,弹回之前也会有,也就是说,我无法保证中间状态抖了多少次,所以最后的led输出状态也不确定。
怎么去除这两个状态呢,有两个方法:硬件和软件,硬件的话就是加一个RS触发器如下图,这样在跳变期间会保持状态而不至于抖动。
但是这样会增加电路元件,在寸土寸金的电路板上,我们大概率不会考虑这样的方法,所以软件消抖比较实用也比较好实现,软件消抖的原理是什么呢,就是延时检测,抖动时间一般小于10ms,把那段跳动避过去,就OK了,具体怎么实现呢,通过一个计数器计时,按键按下为低电平,一般一次按下那段稳定的低电平信号会持续20ms左右,当电平为低时,开始计数,当电平为高,计数归0,只要计时到20ms了,那都稳定这么久了,那信号肯定稳了,给个信号输出来就行。就像这样:
你们可能会奇怪为什么要计数要到999_999,是因为我的开发板晶振频率f是50MHz,1/f换成时间就是20ns一周期,要计时20ms就是要10_000_000*20ns,所以从0计到999_999,所以计数多少看你们自己情况,计时时长15ms~20ms,写成代码:
module key_filter #( parameter CNT_MAX = 20'd999_999 //计数器计数最大值 ) ( input wire sys_clk , //系统时钟50Mhz input wire sys_rst_n , //全局复位 input wire key_in , //按键输入信号 output reg key_flag //key_flag为1时表示消抖后检测到按键被按下 //key_flag为0时表示没有检测到按键被按下 ); //reg define reg [19:0] cnt_20ms ; //计数器 //cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) cnt_20ms <= 20'b0; else if(key_in == 1'b1) cnt_20ms <= 20'b0; else if(cnt_20ms == CNT_MAX && key_in == 1'b0) cnt_20ms <= cnt_20ms; else cnt_20ms <= cnt_20ms + 1'b1; //key_flag:当计数满20ms后产生按键有效标志位 //且key_flag在999_999时拉高,维持一个时钟的高电平 always@(posedge sys_clk or negedge sys_rst_n) if(sys_rst_n == 1'b0) key_flag <= 1'b0; else if(cnt_20ms == CNT_MAX - 1'b1) key_flag <= 1'b1; else key_flag <= 1'b0; endmodule
把这个结合到刚刚那个里面去再来一次:代码如下:
1 module led_ctrl( 2 input wire sys_clk, 3 input wire sys_rstn, 4 input wire key, 5 6 output reg led 7 ); 8 9 wire key_flag; 10 11 always@(posedge sys_clk or negedge sys_rstn) 12 if(sys_rstn==1'b0) 13 led <= 1'b1; 14 else if(key_flag==1'b1) 15 led <= ~led; 16 else 17 led <= led; 18 19 key_filter 20 #( 21 .CNT_MAX(20'd999_999) //计数器计数最大值 22 ) 23 key_filter_inst 24 ( 25 .sys_clk (sys_clk ), //系统时钟50Mhz 26 .sys_rst_n (sys_rstn ), //全局复位 27 .key_in (key ), //按键输入信号 28 29 .key_flag (key_flag ) //key_flag为1时表示消抖后检测到按键被按下 30 //key_flag为0时表示没有检测到按键被按下 31 ); 32 33 endmodule
这是综合出来的RTL视图,相比于之前那个,前面多了一个按键消抖模块。
OK,讲到这里,才把按键消抖模块讲清楚,今天的主角还没上呢,好了,主角登场:状态机,一般说状态机指有限状态机啊,就是FSM(Finite State Machine)。
状态机的按照类型分呢,主要分类两种:Moore型状态机和Mealy型状态机,Moore型状态机的输出只与当前状态有关,Mealy型状态机的输出既与当前状态还和输入有关系;由于今天讲的东西呢,只有两个状态,所以我们以Moore型状态机来举例哈,以为讲完了吗?还没有,
另外一种分类方式呢,就是代码风格分类,分为一段式、二段式和三段式,
一段式就是把输入,状态变化条件和状态变化,输出全写在一起,比较精简吧,但是后期难以维护;
二段式就是把时序逻辑和组合逻辑分开,时序逻辑内进行当前状态和下一状态的切换,组合逻辑内实现各个输入、输出以及状态判断,二段式相较于一段式好维护,但是组合逻辑输出易出现毛刺等常见问题;
三段式就是一段时序逻辑内进行当前状态和下一状态的切换,一段组合逻辑实现状态判断,再来一段时序逻辑输出;解决了毛刺问题,但是耗费的资源比二段式要多。
好了,状态机概念先讲到这,下面实现目的,点灯!!!
状态机,首先要明白状态怎么变,所以要画状态转换图,如下;
看图写代码,如下(加上消抖模块!!!不然会出现和之前一样的情况)
module led_ctrl( input wire sys_clk, input wire sys_rstn, input wire key, output wire led ); parameter OFF=1'b0,ON=1'b1; //定义状态 reg state,nstate ; //定义状态变量 wire key_flag ; always@(posedge sys_clk or negedge sys_rstn) begin if(sys_rstn==1'b0) state <= OFF; else state <= nstate; end always@(*) begin case(state) OFF: nstate = key_flag ? ON:OFF; ON : nstate = key_flag ? OFF:ON; default:nstate = OFF; endcase end assign led=(state==OFF); key_filter #( .CNT_MAX(20'd999_999) //计数器计数最大值 ) key_filter_inst ( .sys_clk (sys_clk ), //系统时钟50Mhz .sys_rst_n (sys_rstn ), //全局复位 .key_in (key ), //按键输入信号 .key_flag (key_flag ) //key_flag为1时表示消抖后检测到按键被按下 //key_flag为0时表示没有检测到按键被按下 ); endmodule
比之前要复杂,同样实现功能,但是用的是不一样的方法,还有个方法,叫边缘检测,大家可以试一下;
点个灯可能很简单,但是要用学到的新东西去实现功能,活学活用才是硬道理;
注:用的硬件平台是野火FPGA征途系列开发板,软件平台是quartus II + notepad++