FPGA零基础学习:按键控制LED
本系列将带来FPGA的系统性学习,从最基本的数字电路基础开始,最详细操作步骤,最直白的言语描述,手把手的“傻瓜式”讲解,让电子、信息、通信类专业学生、初入职场小白及打算进阶提升的职业开发者都可以有系统性学习的机会。
系统性的掌握技术开发以及相关要求,对个人就业以及职业发展都有着潜在的帮助,希望对大家有所帮助。后续会陆续更新 Xilinx 的 Vivado、ISE 及相关操作软件的开发的相关内容,学习FPGA设计方法及设计思想的同时,实操结合各类操作软件,会让你在技术学习道路上无比的顺畅,告别技术学习小BUG卡破脑壳,告别目前忽悠性的培训诱导,真正的去学习去实战应用,这种快乐试试你就会懂的。话不多说,上货。
按键控制 LED
作者:郝旭帅 校对:陆辉
利用按键控制LED的要求为:按一下按键,改变一下LED的状态。按键按一次,LED由熄灭变为点亮,按键再按一次,LED由点亮变为熄灭。
硬件介绍
开发板上面有四个按键,当按键按下时,将对应的网络置成低电平;当按键释放时,将对应的网络置成高电平。
开发板上面有四个LED发光二极管,FPGA输出高电平时,LED点亮;FPGA输出低电平时,LED熄灭。
设计原理
通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。
按键抖动会引起一次按键被误读多次。为确保CPU对键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到键释放稳定后再作处理。
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。
我们可以在按键和主控设备之间加入消抖电路(消抖芯片、电容等),此种方法会增大PCB面积和花费一定的物料费用。大多数的板子直接将按键和主控设备相连接,将带有抖动的波形输入到主控设备内部,由内部进行消抖处理。
单片机一般采用延迟重采样的方式进行消抖。当检测到信号为低时,延迟一段时间(一般为20ms),再次检测信号是否为低,如果为低,则证明按键按下,否则认为按键没有按下,继续下一次检查。
在FPGA设计时,笔者推荐另外一种方式:持续采样。当检测到信号持续为低10ms,认为按键按下;当检测到信号持续为高10ms,认为按键释放。
在设计时,需要考虑到外部的按键信号为异步信号,需要进行同步处理。具体请参考附录2 FPGA中的同步信号、异步信号和亚稳态。
每次按键按下的时间的长短不一,经过消抖后,低电平的持续长度长短也不一样。此长度远远大于一个时钟周期的长度。要求每次按下只能够切换一次LED的状态,所以不能够直接用此电平当做输出翻转的使能。
经过消抖的波形,每次按下只有一个下降沿(按键按下时)、只有一个上升沿(按键释放时)。所以通过检测下降沿(上升沿)的变化,产生一个新的信号------脉冲(一个时钟周期的脉冲),利用此脉冲作为翻转的使能即可。利用检测到下降沿的脉冲翻转时,LED的状态会在按下时就会改变;利用检测到上升沿的脉冲翻转时,LED的状态会在释放时发生改变。本设计中采用检测到下降沿的脉冲进行翻转。
设计架构和信号说明
本设计模块命名为key_led。
在设计中,共分为三个模块。
key_filter(按键消抖模块):将外部输入的带有抖动的波形进行消抖。
edge_check(边沿检测模块):将消抖后的波形进行下降沿检测,并产生对应的脉冲。
led_ctrl(led控制模块):利用脉冲,翻转led的输出状态。
key_filter设计实现
本设计采用状态机实现,状态机的具体原理请参看附录3。
对key_n信号为异步信号,需要进行同步两拍,命名为key_n_r和key_n_rr。状态机的判断信号为key_n_rr信号。
本设计共分为四个状态,KEY_OFF(按键释放状态),SHAKE_ON(按键按下时抖动判断状态),KEY_ON(按键按下状态),SHAKE_OFF(按键释放时抖动判断状态)。
按键没有按下时,一直KEY_OFF状态,当按键信号变为低电平时,就转入SHAKE_ON状态,检测低电平的持续时间。如果持续时间没有达到T_10ms就变为高电平,则清零计数器并返回KEY_OFF状态;如果持续时间没有达到T_10ms并且也一直为低电平,则继续在SHAKE_ON状态计数;如果持续时间达到T_10ms并且为低电平,则清零计数器并进入KEY_ON状态。在KEY_ON状态,外部输入为低电平时,则继续在KEY_ON状态;如果外部输出为高电平,则转入SHAKE_OFF状态。在SHAKE_OFF状态,如果持续时间没有到达T_10ms就变为低电平,则清零计数器并返回KEY_ON状态;如果持续时间没有达到T_10ms并且一直为高电平,则继续在SHAKE_OFF状态计数;如果持续时间达到T_10ms并且一直为高电平,则清零计数器并转入KEY_OFF状态。
在KEY_OFF和SHAKE_ON状态,认为按键没有按下;在KEY_ON和SHAKE_OFF状态,认为按键为按下。
状态转移图如下:
设计代码为:
代码语言:javascript复制module key_filter (
input wire clk,
input wire rst_n,
input wire key_n,
output reg okey_n
);
parameter T_10ms = 500_000;
localparam KEY_OFF = 4'b0001;
localparam SHAKE_ON = 4'b0010;
localparam KEY_ON = 4'b0100;
localparam SHAKE_OFF = 4'b1000;
reg [3:0] c_state;
reg [3:0] n_state;
reg key_n_r;
reg key_n_rr;
reg [18:0] cnt;
always @ (posedge clk) key_n_r <= key_n;
always @ (posedge clk) key_n_rr <= key_n_r;
always @ (posedge clk, negedge rst_n) begin
if (rst_n == 1'b0)
c_state <= KEY_OFF;
else
c_state <= n_state;
end
always @ * begin
case (c_state)
KEY_OFF : begin
if (key_n_rr == 1'b1)
n_state = KEY_OFF;
else
n_state = SHAKE_ON;
end
SHAKE_ON : begin
if (key_n_rr == 1'b1)
n_state = KEY_OFF;
else
if (cnt < T_10ms - 1'b1)
n_state = SHAKE_ON;
else
n_state = KEY_ON;
end
KEY_ON : begin
if (key_n_rr == 1'b0)
n_state = KEY_ON;
else
n_state = SHAKE_OFF;
end
SHAKE_OFF : begin
if (key_n_rr == 1'b0)
n_state = KEY_ON;
else
if (cnt < T_10ms - 1'b1)
n_state = SHAKE_OFF;
else
n_state = KEY_OFF;
end
default : n_state = KEY_OFF;
endcase
end
always @ (posedge clk, negedge rst_n) begin
if (rst_n == 1'b0)
cnt <= 19'd0;
else
case (c_state)
KEY_OFF : begin
cnt <= 19'd0;
end
SHAKE_ON : begin
if (key_n_rr == 1'b0 && cnt < T_10ms - 1'b1)
cnt <= cnt 1'b1;
else
cnt <= 19'd0;
end
KEY_ON : begin
cnt <= 19'd0;
end
SHAKE_OFF : begin
if (key_n_rr == 1'b1 && cnt < T_10ms - 1'b1)
cnt <= cnt 1'b1;
else
cnt <= 19'd0;
end
default : cnt <= 19'd0;
endcase
end
always @ (posedge clk, negedge rst_n) begin
if (rst_n == 1'b0)
okey_n <= 1'b1;
else
if (c_state == KEY_ON || c_state == SHAKE_OFF)
okey_n <= 1'b0;
else
okey_n <= 1'b1;
end
endmodule
localparam可以定义参数,与parameter的区别在于,parameter定义的参数可以在例化时进行参数修改,而localparam定义的参数在例化时则不能够修改。定义状态机状态时,一般采用localparam的定义方式。在不希望别人修改参数时,也可以定义为localparam。
edge_check设计实现
在一个波形中,如果当前时刻为低电平,上一个时刻为高电平,则认为波形中有一个下降沿;如果当前时刻为高电平,上一个时刻为低电平,则认为波形中有一个上升沿。
在数字电路设计时,可以采用寄存器来存储上一个时刻的值。
在寄存器电路中,Q的值,永远是上一个CLK的有效边沿所采样的D值。因此Q为上一时刻值,而D为当前时刻的值。
设计代码为:
代码语言:javascript复制module edge_check (
input wire clk, // 50MHz
input wire wave, // wave in
output wire flag_pos, // flag - posedge
output wire flag_neg // flag - negedge
);
reg wave_r;
always @ (posedge clk) wave_r <= wave;
// assign flag_pos = (wave == 1'b1 && wave_r == 1'b0) ? 1'b1 : 1'b0;
assign flag_pos = wave & (~wave_r);
// assign flag_neg = (wave == 1'b0 && wave_r == 1'b1) ? 1'b1 : 1'b0;
assign flag_neg = (~wave) & wave_r;
endmodule
在设计中,注释掉的两行代码和其下方的一行代码的功能是相同的。例:对于上升沿脉冲来说,现在为1,过去为0即为上升沿。由于寄存器每个时钟周期都刷新,满足这个要求的只会存在一个时钟周期,所以flag_pos为一个时钟周期的脉冲。
led_ctrl设计实现
本模块中,利用脉冲进行led状态的翻转即可。
设计代码为:
代码语言:javascript复制module led_ctrl (
input wire clk,
input wire rst_n,
input wire flag,
output reg led
);
always @ (posedge clk, negedge rst_n) begin
if (rst_n == 1'b0)
led <= 1'b0;
else
if (flag == 1'b1)
led <= ~led;
else
led <= led;
end
endmodule
key_led设计实现
本模块只是负责将上述的三个模块按照架构图的方式进行连接,形成最终的设计。
设计代码为:
代码语言:javascript复制module key_led (
input wire clk,
input wire rst_n,
input wire key_n,
output wire led
);
wire okey_n;
wire flag;
key_filter key_filter_inst(
.clk (clk),
.rst_n (rst_n),
.key_n (key_n),
.okey_n (okey_n)
);
edge_check edge_check_inst(
.clk (clk), // 50MHz
.wave (okey_n), // wave in
.flag_pos (), // flag - posedge
.flag_neg (flag) // flag - negedge
);
led_ctrl led_ctrl_inst(
.clk (clk),
.rst_n (rst_n),
.flag (flag),
.led (led)
);
endmodule
在设计中,采用了按键按下时的脉冲(检测到下降沿的脉冲),按键按下时led的状态即可进行翻转。
功能仿真
在仿真时,将按键消抖中的T_10ms的参数修改为20,即持续时间不超过400ns都不认为是有效按下或者抬起。
仿真代码如下:
代码语言:javascript复制`timescale 1ns/1ps
module key_led_tb;
reg clk;
reg rst_n;
reg key_n;
wire led;
key_led key_led_inst(
.clk (clk),
.rst_n (rst_n),
.key_n (key_n),
.led (led)
);
initial clk = 1'b0;
always # 10 clk = ~clk;
initial begin
rst_n = 1'b0;
key_n = 1'b1;
# 201
rst_n = 1'b1;
# 200
//-------------- on shake-------------
@ (posedge clk);
# 2;
key_n = 1'b0;
# 320
@ (posedge clk);
# 2;
key_n = 1'b1;
# 159
@ (posedge clk);
# 2;
key_n = 1'b0;
# 320
@ (posedge clk);
# 2;
key_n = 1'b1;
# 159
//--------------------------------------
//---------key on ---------------------
@ (posedge clk);
# 2;
key_n = 1'b0;
# 5000
//------------------------------------
//---------off shake ----------------
@ (posedge clk);
# 2;
key_n = 1'b0;
# 320
@ (posedge clk);
# 2;
key_n = 1'b1;
# 159
@ (posedge clk);
# 2;
key_n = 1'b0;
# 320
//-------------------------------------
//-----------key off----------------
@ (posedge clk);
# 2;
key_n = 1'b1;
# 10000
$stop;
end
endmodule
将okey_n、flag信号添加出来。
通过RTL仿真图,可以清晰的看到okey_n信号将key_n的抖动滤除掉;flag信号为okey_n信号的下降沿时所产生的脉冲;led在flag信号为高时,反正翻转。
分配管脚、下板测试之前,应该将按键消抖里面的T_10ms参数重新改为500_000,否则下板后可能会达不到消抖的效果。
下板成功后,可以修改在设计中使用上升沿的脉冲,得到的现象应该是按键释放时,LED的状态发生反转。
切记:每次修改代码,一定要进行重新编译,否则更改将不会生效。