目录
- 一、状态机
- 二、模块设计
- 三、代码实现
- 四、管脚配置及结果展示
上一篇博文:【入门学习二】基于 FPGA 使用 Verilog 实现蜂鸣器响动的代码及原理讲解
概述:前面的两篇文章,其中按键模块采用的是延时消抖的方式,本篇文章采用状态机实现按键功能,只需要一个按键模块,即可使用多个按键,当点击一个按键后,流水灯左移,点击另一个按键后,流水灯右移。
一、状态机
基本概念
- 状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。
- 通俗的话来讲,就是用状态来表示当前信号。
- 种类:
- 若输出只和状态有关而与输入无关,则称为摩尔型(Moore)状态机;
- 输出不仅和状态有关而且和输入有关系,则称为米勒型(Mealy)状态机。
- 编码方式:(采用编码方式描述状态)
- 独热码:
本文的按键状态为 4 个,所以独热码应该为
初始状态——0001
下降状态——0010
保持状态——0100
上升状态——1000 - 自然二进制;
初始状态——00
下降状态——01
保持状态——10
上升状态——11 - 格雷码。
初始状态——00
下降状态——01
保持状态——11
上升状态——10
- 描述方式:
- 一段式:利用一个进程来描述状态的转换及输出信号的定义;
- 二段式:一个为时序电路主要负责状态变量的更新,此进程为同步电路,而另一个进程语句主要是描述下次态变量和输出的更新;
- 三段式:第一个进程主要负责状态变量的更新,第二个进程语句负责描述次态变量,而最后一个则是负责输出信号的更新。
本篇文章的状态机描述
- 对于按键,它的电平为高低变化,有四个状态。
- 本文采用独热码来表示这四个状态,也就是:
初始状态——0001
下降状态——0010
保持状态——0100
上升状态——1000 - 也就是当它为保持状态的时候,取电平值,可以获取按键信号。
- 采用三段式描述,因为三段式的状态机可以易于修改以及维护。
- 先梳理一下,要获取上升沿、下降沿标志,那么就需要一个打拍器,当收到下降沿标志后,要计数 20 ms 再获取当前电平值,所以还需要一个计数器,然后状态机的第一段描述状态转移,第二段描述状态转移规律,第三段描述输出或功能信号(打拍器及计数器的实现)。
二、模块设计
- 这里的按键输入 key_in 的位宽为 2 位,也就是两个按键输入,当然也可以 1 个按键或者更多的按键,使用状态机就不需要重复定义每一个按键的输入了。
- 这个模块设计和第一篇文章的类似,只不过输入为两位宽,表示两个按键输入。
- 这里就不再赘述每个管脚名的含义了。
三、代码实现
顶层模块 fsm_key.v:
- 这里第一排定义了一个全局变量 KEY_W = 2,表示两个按键;
- 模块例化中,“#” 后面表示传参。
module fsm_key #(parameter KEY_W = 2)(
input clk ,
input rst_n ,
input [KEY_W-1:0] key_in ,
output [3:0] led
);
wire [KEY_W-1:0] press;
key_filter #(.KEY_W(KEY_W), .DELAY(1000_000)) u_key_filter(
.clk (clk ),
.rst_n (rst_n ),
.key (key_in ),
.press (press )
);
led_driver #(.KEY_W(KEY_W), .CNT_MAX(25_000_000)) u_led_driver(
.clk (clk ),
.rst_n (rst_n ),
.en (press ),
.led (led )
);
endmodule
按键模块 key_filter.v:
module key_filter #(parameter KEY_W = 1, DELAY = 1000_000)(
input clk ,
input rst_n ,
input [KEY_W-1:0] key ,
output reg [KEY_W-1:0] press
);
localparam IDLE = 4'b0001,
FALL = 4'b0010,
HOLD = 4'b0100,
RISE = 4'b1000;
reg [3 :0] state_c ;
reg [3 :0] state_n ;
reg [19:0] cnt_delay ;
reg [KEY_W-1:0] key_r0 ;
reg [KEY_W-1:0] key_r1 ;
wire [KEY_W-1:0] nedge ;
wire [KEY_W-1:0] pedge ;
wire idle2fall ;
wire fall2idle ;
wire fall2hold ;
wire hold2rise ;
wire rise2idle ;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
always @(*) begin
case(state_c)
IDLE:
begin
if(idle2fall)
state_n = FALL;
else
state_n = state_c;
end
FALL:
begin
if(fall2idle)
state_n = IDLE;
else if(fall2hold)
state_n = HOLD;
else
state_n = state_c;
end
HOLD:
begin
if(hold2rise)
state_n = RISE;
else
state_n = state_c;
end
RISE:
begin
if(rise2idle)
state_n = IDLE;
else
state_n = state_c;
end
endcase
end
assign idle2fall = state_c == IDLE && nedge != 'd0;
assign fall2idle = state_c == FALL && pedge != 'd0 && cnt_delay < DELAY - 1;
assign fall2hold = state_c == FALL && cnt_delay == DELAY - 1;
assign hold2rise = state_c == HOLD && pedge != 'd0;
assign rise2idle = state_c == RISE && cnt_delay == DELAY - 1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_delay <= 0;
end
else if(state_c == FALL || state_c == RISE) begin
if(cnt_delay == DELAY - 1 || pedge != 0)
cnt_delay <= 0;
else
cnt_delay <= cnt_delay + 1;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
key_r0 <= 0;
key_r1 <= 0;
end
else begin
key_r0 <= key;
key_r1 <= key_r0;
end
end
assign nedge = key_r1 & (~key_r0);
assign pedge = key_r0 & (~key_r1);
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
press <= 0;
end
else begin
press <= (fall2hold == 1'b1) ? ~key_r0 : 'd0;
end
end
endmodule
代码执行过程:
- 最开始的时候主要是 state_c 与 state_n 不断地迭代,两个信号只差一个时钟周期,state_c 表示当前按键电平状态,state_n 表示预测 state_c 的下一个周期的电平状态。
- 此时下降沿到来,state_n 由 IDLE 状态变为 FALL 状态,state_c 仍为 IDLE 状态。
- state_n 如何变为 FALL 状态?
- 首先是打拍器检测到下降沿。
- 然后 IDLE 变为 FALL 的标志位就为 1 电平。
- 这个时候,根据第二段的状态转移规律,state_n = FALL;
- 下一个时钟周期,state_c 的状态就为 state_n 的状态,然后 state_n 再次预测 state_c 的状态,也就是 state_c = FALL,state_n = FALL,这里是根据第一段来的。
- 与此同时,state_c = FALL 了,计数器就开始工作了。
- 当检测到上升沿时,FALL2IDLE 标志位就为 1 电平,此时根据 case 选择语句,state_n 就变为了 IDLE 状态,此时,state_c 仍为 FALL 状态,那么计数器就会清零。
- 然后不断重复这一过程,直至 state_c 为 FALL 状态并且计数器已经计满了 20ms,这时,state_n 才为 HOLD 状态,如下图所示:
- 此时,fall2hold 标志信号为 1 ,则输出按键信号高电平。
LED 模块 led_driver.v:
- 接收到按键控制信号后,改变 flag 的值,led 输出模式根据 flag 的改变而改变。
module led_driver #(parameter KEY_W = 2, CNT_MAX = 25_000_000)(
input clk ,
input rst_n ,
input [KEY_W-1:0] en ,
output reg [3:0] led
);
reg flag ;
reg [24:0] cnt ;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 0;
end
else begin
if(cnt == CNT_MAX - 1)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
flag <= 0;
end
else if(en[0]) begin
flag <= 0;
end
else if(en[1]) begin
flag <= 1;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
led <= 4'b0001;
end
else if(cnt == CNT_MAX - 1) begin
if(flag)
led <= {led[0], led[3:1]};
else
led <= {led[2:0], led[3]};
end
end
endmodule
四、管脚配置及结果展示
管脚配置
- 要按照自己开发板的管脚进行配置。
结果展示
- 按 K2 流水灯左移,按 K3 流水灯右移。
下一篇博文:【入门学习四】基于 FPGA 使用 Verilog 实现串口回传通信代码及原理讲解
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)