4. 按键控制LED:按键消抖原理、按键检测模块设计、LED控制逻辑、上板验证
大家好,欢迎来到第四章。
这一章我们聊点实在的——按键控制LED。你可能会想,这有什么难的?按一下灯亮,再按一下灯灭,小学生都能做。但实际做起来,你会发现一个很烦人的问题:按键抖动。
我在刚入行那会儿,第一次做按键控制LED的项目,上板一测,灯乱跳。按一下,它闪好几下。我当时还以为板子坏了,折腾了半天。后来老工程师跟我说:「你消抖了吗?」我一脸懵。从那以后,我再也不敢小看按键消抖了。
核心知识点:按键消抖是FPGA实战中绕不开的基础。本章我们讲透原理、写好代码、上板验证。
4.1 按键消抖原理
按键是个机械结构。你按下去,金属触点不会一下子稳定接触。它会弹跳几次,大概持续5~20ms。FPGA跑在50MHz,一个周期才20ns。你想想看,这20ms里FPGA能采样多少次?差不多100万次。
所以,按键信号在FPGA眼里,不是干净的高低电平,而是一串毛刺。这就是抖动。
消抖的思路很简单:等信号稳定了再采样。具体做法有两种:
- 硬件消抖:加RC滤波电路。简单粗暴,但占PCB面积,不灵活。
- 软件消抖:在FPGA内部用计数器延时。我建议用这种方法,省成本、可调参数。
我个人习惯用「延时采样法」。说白了就是:检测到按键电平变化后,等20ms再读一次。如果电平没变,就认为按键真的被按下了。如果变了,就当作抖动,忽略掉。
小提示:20ms是个经验值。不同按键的抖动时间不一样。我建议留点余量,设成20~30ms。太短了消不干净,太长了影响响应速度。
4.2 按键检测模块设计
好,原理讲完了。我们直接上代码。
这个模块的核心是一个计数器。当检测到按键电平变化时,计数器开始计数。计数到设定值(对应20ms)后,再采样一次按键电平。如果电平没变,输出一个脉冲。
module key_debounce (
input clk, // 50MHz 时钟
input rst_n, // 复位,低有效
input key_in, // 按键输入,按下为低电平
output reg key_out // 消抖后的按键信号,按下为高电平
);
// 20ms 延时需要的计数次数
// 50MHz = 20ns 周期
// 20ms / 20ns = 1_000_000
parameter CNT_MAX = 1_000_000;
reg [19:0] cnt;
reg key_in_d0, key_in_d1; // 两级同步,消除亚稳态
reg key_in_sync; // 同步后的按键信号
reg key_out_r;
// 两级同步,消除亚稳态
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_in_d0 <= 1'b1;
key_in_d1 <= 1'b1;
end else begin
key_in_d0 <= key_in;
key_in_d1 <= key_in_d0;
end
end
assign key_in_sync = key_in_d1;
// 消抖计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 20'd0;
key_out_r <= 1'b0;
end else begin
// 检测到按键电平变化,启动计数器
if (key_in_sync != key_out_r) begin
if (cnt == CNT_MAX - 1) begin
cnt <= 20'd0;
key_out_r <= key_in_sync;
end else begin
cnt <= cnt + 1'b1;
end
end else begin
cnt <= 20'd0;
end
end
end
assign key_out = key_out_r;
endmodule
这段代码里,我做了两件事:
- 两级同步:消除亚稳态。这是基本功,别省。
- 延时采样:计数器到20ms后,才更新输出。中间如果电平变了,计数器清零重来。
注意:参数CNT_MAX的值要根据你的时钟频率调整。如果你用100MHz时钟,CNT_MAX要改成2_000_000。我曾经见过有人直接复制代码没改参数,结果消抖时间变成了10ms,上板还是抖。嗯,这种坑我踩过。
4.3 LED控制逻辑
按键消抖搞定了,LED控制就简单了。我们设计一个「按一下切换一次LED状态」的逻辑。
这里有个小技巧:我们检测按键的下降沿。因为按键按下时,key_out从低变高。我们只需要一个脉冲,告诉主控「按键被按了一次」。
module led_control (
input clk,
input rst_n,
input key_debounced, // 消抖后的按键信号
output reg led // LED输出,高电平亮
);
reg key_debounced_d0;
wire key_falling_edge;
// 检测下降沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_debounced_d0 <= 1'b0;
end else begin
key_debounced_d0 <= key_debounced;
end
end
assign key_falling_edge = key_debounced_d0 & ~key_debounced;
// LED状态切换
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
led <= 1'b0;
end else if (key_falling_edge) begin
led <= ~led;
end
end
endmodule
这段逻辑很简单:每次检测到下降沿,LED取反。说白了就是按一下亮,再按一下灭。
个人经验:我习惯把按键检测和LED控制分成两个模块。这样代码清晰,后期改起来也方便。比如你想改成「长按3秒关灯」,只需要改LED控制模块,不用动消抖模块。
4.4 上板验证
代码写完了,该上板了。我建议你按以下步骤来:
- 综合实现:在Vivado里跑一遍综合和实现。看看资源占用,确认没有时序问题。
- 生成比特流:生成.bit文件,下载到板子。
- 连接硬件:把按键和LED接到对应的FPGA引脚。注意看原理图,别接反了。
- 上电测试:按一下按键,看LED是否正常切换。
如果LED乱跳,别慌。先检查消抖模块的CNT_MAX参数对不对。如果参数没问题,用示波器抓一下按键信号,看看抖动时间到底多长。我遇到过一种情况:按键老化,抖动时间长达50ms。这时候把CNT_MAX调大就行了。
验证要点:
- 快速连按:看LED是否每次都能正确切换
- 慢速按:看是否会出现误触发
- 按住不放:看LED是否只切换一次
嗯,到这里,按键控制LED的完整流程就走通了。从原理到代码,从上板到验证,每一步我都踩过坑,也总结出了经验。你照着做,应该一次就能跑通。
下面这张图,是我画的本章知识体系。你可以对照着回顾一下:
这张图把本章的四个核心环节串起来了。你从左边看到右边,就是完整的处理流程。每个环节都有对应的代码和参数,你可以直接拿去用。