1、按键基础知识
1.1、按键结构组成
独立按键实际上是一个非自锁的轻触开关,有左右两个触点,当按下时左右两个触点闭合,当松开时左右两个触点断开。
1.2、按键原理
FPGA 的 I/O 通过检测按键按下前后的高低电平变化,来判断按键是否按下。通过程序的控制,就可以实现不同的功能与设置。机械式按键在按下或者释放时,由于机械弹性作用的影响,通常伴随有一定时间的触点机械抖动,然后其触点才稳定下来。抖动时间长短与开关的机械特性有关,一般为 5-10ms。在触点抖动期间检测按键的按下与否,可能会导致判断错误,为了克服机械抖动所产生的影响,必须采取消抖措施,可分为硬件消抖和软件消抖。
1.3、消抖措施
硬件消抖:硬件消抖一般会在按键两端并联电容,通过电容的充放电作用将按键按下时的高频振荡吸收掉。
软件消抖:软件消抖一般是通过延时。当检测到按键按下时,不会立即去检测电平,而是经过短暂的延时之后,再去检测当前引脚的电平。
1.4、按键驱动原理
按键直接接到FPGA的某一个 I/O 上,FPGA 通过读取输入 I/O 引脚的高低电平判断按键是否按下。
2、KEY 原理图
逻辑派 上面一共有三个按键,一个连接 GD32 单片机上面的,另外两个是连接到 FPGA 的 Bank7 上的 D11 和 F10 ,该 Bank 的供电为 3V3 ,如下图所示:
3、实战任务
任务是使用 KEY0 和 KEY1 两个按键来控制 LED0 和 LED1 两个 LED 的亮灭。当 KEY0 按钮被按下时 ,LED0 和 LED1 交替闪烁,间隔时间 500ms,当 KEY1 按钮被按下时 ,LED0 和 LED1 同时闪烁,间隔时间 500ms,其余情况 LED0 和 LED1 都熄灭。
4、系统框图
根据实战任务分析我们需要实现的功能是使用两个按键控制两个 LED 灯以不同的方式闪烁,所以本实验我们设计了两种模式,一种是从 LED0 到 LED1 的交替闪烁,一种是从 LED0 和 LED1 的同时闪烁。
因此,本实验需要四个输入端口:系统时钟和系统复位,两个按键输入信号,输出端口为两个LED灯,模块框图如下图所示:
5、时序图
本实验的计数模块还是使用系统时钟进行计数,计数器计时 0.5s 需要 0.5s/20ns=500000000ns/20ns = 25000000 个时钟周期。
上图可知,count 循环从 0 计数到 24_999_999,表示计时 0.5s。开发板上电,按键都未按下时为高电平,key 信号值为 2’b11,此时给 LED 灯赋值 2’b11 表示两个LED 灯常灭。
长按 KEY1 按键时,key 信号值为 2’b01,此时需要根据 LED 状态控制信号(flag_led)来判断 LED 灯的状态。flag_led 为 1 时,给 led 赋值 2’b01,表示 LED0 灯灭 LED1 灯亮;flag_led 为 0 时,给 led 赋值 2’b10,,表示 LED1 灯灭 LED0 灯亮,依次交替闪烁;
长按 KEY0 按键时,key 信号值为 2’b10,此时需要根据 LED 状态控制信号(flag_led)来判断 LED 灯的状态。flag_led 为 1 时,给 led 赋值 2’b00,表示 LED0 和 LED1 灯亮;flag_led 为 0 时,给 led 赋值 2’b11,表示 LED0 和 LED1 灯灭,依次循环;
6、程序编写
由于我们 逻辑派 的 FPGA 端上面只有两个按键可以用,而我们的系统框图中需要三个按键(程序里面舍弃了复位按键),而我们在实际程序里面只能用到两个按键,而仿真程序中我会按照正常循序来编写。由时序图分析可知,两个按键有四种常用的状态,使用 if 语句实现多分支选择的代码比较繁琐,因此我们可以使用 case 多分支选择语句来实现按键控制 LED 灯闪烁。本节按键控制 LED 灯(key_led.v)代码编写如下:
module key_led(
input sys_clk, //全局系统时钟
// input sys_rst_n, //全局复位,低电平有效
input [1:0] key, //按键
output reg [1:0] led //LED灯
);
reg flag_led; //LED 状态控制信号
reg [24:0] count; //计数器
parameter Scends = 25_000_000; // 0.5S 所需的脉冲
//parameter Scends = 25; // 用于仿真
//定时 0.5 S
always @(posedge sys_clk)begin
// if(!sys_rst_n)
// count <= 25'd0;
// else if(count < Scends - 25'd1)
if(count < Scends - 25'd1) // 如果计数器小于 0.5 秒所需的脉冲数,计数器加 1
count <= count + 25'd1;
else
count <= 25'd0; // 否则,重置计数器
end
always @(posedge sys_clk) begin
// if(!sys_rst_n)
// flag_led <= 1'd0;
// else if(count == (Scends - 25'd1))
if(count == (Scends - 25'd1))
flag_led <= ~flag_led; //定时时间超过 0.5S 后将 flag_led 状态取反
else
flag_led <= flag_led; //其他时间段保存不变
end
always @(posedge sys_clk) begin
// if(!sys_rst_n)
// led <= 2'b11;
// else begin
case(key)
//如果二个按键都未被按下或者都被按下,则二个 LED 都保持常灭
2'b11:
led <= 2'b11;
2'b00:
led <= 2'b11;
//如果 KEY0 按键未被按下,则二个 LED 2'b00 -> 2'b11 ->2'b00 ->2'b11
2'b10:begin
if(flag_led == 1'b1)
led <= 2'b00;
else
led <= 2'b11;
end
//2'b10: led <= flag_led ? 2'b00 : 2'b11; 用条件运算符进行判断输出
//如果 KEY1 按键未被按下,则二个 LED 2'b01 -> 2'b10 ->2'b01 ->2'b10
2'b01:begin
if(flag_led == 1'b1)
led <= 2'b01;
else
led <= 2'b10;
end
//2'b01: led <= flag_led ? 2'b01 : 2'b10; // 用条件运算符进行判断输出
default : led <= 2'b11 ; // 默认情况LED 全灭
endcase
// end
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
程序注释:
- 第 9 行:定义了一个触发信号,用与 LED 灯状态变化的改变。
- 第 26 - 35 行:每隔 0.5S 将 flag_led 的状态取反,保证我们在下面调用的时候进行判断。
- 第 38 - 68 行:通过 case 语句的列表定义不同的按键状态来切换 LED 灯的工作模式。如果当按下 KEY0 ,则 LED0 和 LED1 同时闪烁,当按下 KEY 1 ,则 LED0 和 LED1 交替闪烁,其余情况则是 LED 灯都熄灭。
7、仿真编写
由于我们的 逻辑派 的 FPGA 端只有两个按键,而我们需要三个按键,我们只能在仿真中模拟这一逻辑。实际情况中 LED 灯闪烁间隔 0.5s 所需的时间较长,则我们需要将 0.5S 改为 500ns(25*20ns)。我们需要在仿真中调整时间设置,确保 LED 的闪烁频率符合预期。这将允许我们在有限的硬件资源下,准确模拟和测试所需的功能。
注:1、从这一章节往后我就不在过多说明了,仿真时候需要将 parameter Scends = 50_000_000; 改为 parameter Scends = 50; 用于仿真实验
key_led.v 代码修改如下图所示:
仿真按键控制 LED 灯 mod 模块(key_led_mod.v)代码编写如下:
`timescale 1ns / 1ns // 设置时间单位和时间精度为 1 纳秒
module key_led_mod(); // 定义测试模块
// reg define
reg sys_clk; // 全局系统时钟信号
reg sys_rst_n; // 全局复位信号(低电平有效)
reg [1:0] key; // 2 位宽的按键输入信号
// wire define
wire [1:0] led; // 2 位宽的 LED 输出信号
initial begin
sys_clk <= 1'b0; // 初始化时钟信号为低电平
sys_rst_n <= 1'b0; // 初始化复位信号为低电平(表示复位状态)
// key <= 2'b11; // 可选的按键初始状态
#200; // 等待以确保初始状态稳定
sys_rst_n <= 1'b1; // 解除复位信号,设置为高电平
#200;
// 模拟按键按下的过程
key <= 2'b00; // 设置按键状态为 00
#3000;
key <= 2'b10; // 设置按键状态为 10
#3000;
key <= 2'b01; // 设置按键状态为 01
#3000;
key <= 2'b11; // 设置按键状态为 11
#3000;
end
always #10 sys_clk = ~sys_clk; // 每 10ns 生成一个时钟周期
// 实例化被测模块 key_led
key_led u_key_led(
.sys_clk(sys_clk), // 将测试模块的 sys_clk 连接到被测模块的时钟输入
.sys_rst_n(sys_rst_n), // 将测试模块的复位信号连接到被测模块的复位输入
.key(key), // 将测试模块的 key 连接到被测模块的按键输入
.led(led) // 将被测模块的 LED 输出连接到测试模块的 LED 端口
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
程序注释:
第 14 -- 38 行:我们先将 sys_clk 和 sys_rst_n 初始状态清零,延时200ns后让复位置一整个系统开始工作,26 行就开始模拟 按键都被按的到时候 LED 输出是一个什么样子的状态,延时,让 KEY0 被按下,LED 输出是一个什么样子的状态,延时,让 KEY1 被按下,LED 输出又是一个什么样子的状态。然后我们就可以根据仿真推断出程序是否是跟着我们预期去运行的。
第 39 行:用于产生系统时钟,sys_clk 每隔 10ns 翻转一次,一个完整的时钟周期包含一个高电平和一个低电平,因此系统时钟为 20ns,对应系统时钟频率为 50MHz。
接下来打开 Modelsim 软件对代码进行仿真,需要添加文件如下图所示:
在运行仿真一段时间后,需要将波形放大,将进度到最前面,仿真的波形如下图所示:
由上图可知,当 KEY0 被按下时,LED0 和 LED1 是同时亮灭的,当 KEY1 被按下时, 先 LED1 先亮,然后再是 LED1 灭的同时 LED0 亮,这样开始交替闪烁。
8、I/O 引脚绑定
在仿真验证完成后,接下来使用 Gowin 对时钟 约束 和 引脚 进行分配并上板验证。本实验中,系统时钟、二个 key 按键以及二个 led 端口的管脚分配如下表所示:
信号 | 方向 | 引脚 | 端口作用 | 电平标准 |
---|---|---|---|---|
sys_clk | input | T7 | 时钟 | LVCMOS33 |
key[0] | input | D11 | KEY0 | LVCMOS33 |
key[1] | input | F10 | KEY1 | LVCMOS33 |
led[0] | output | R9 | LED0 | LVCMOS33 |
led[1] | output | N6 | LED1 | LVCMOS33 |
Gowin 软件中 I/O Constraints 界面如下图所示:
9、程序下载
连接开发板的下载器,下载码流文件到开发板里面,下载完成之后,开发板上两个 LED 处于常灭状态。一直按下 KEY0 ,可以看到 LED0 和 LED1 同时闪烁,放开 KEY0 ,再一直按下 KEY 1 ,可以看到 LED0 和 LED1 交替闪烁。实际位置图如下所示: