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 上板验证

代码写完了,该上板了。我建议你按以下步骤来:

  1. 综合实现:在Vivado里跑一遍综合和实现。看看资源占用,确认没有时序问题。
  2. 生成比特流:生成.bit文件,下载到板子。
  3. 连接硬件:把按键和LED接到对应的FPGA引脚。注意看原理图,别接反了。
  4. 上电测试:按一下按键,看LED是否正常切换。

如果LED乱跳,别慌。先检查消抖模块的CNT_MAX参数对不对。如果参数没问题,用示波器抓一下按键信号,看看抖动时间到底多长。我遇到过一种情况:按键老化,抖动时间长达50ms。这时候把CNT_MAX调大就行了。

验证要点:

  • 快速连按:看LED是否每次都能正确切换
  • 慢速按:看是否会出现误触发
  • 按住不放:看LED是否只切换一次

嗯,到这里,按键控制LED的完整流程就走通了。从原理到代码,从上板到验证,每一步我都踩过坑,也总结出了经验。你照着做,应该一次就能跑通。

下面这张图,是我画的本章知识体系。你可以对照着回顾一下:

按键控制LED - 知识体系 按键消抖原理 机械抖动 5~20ms 按键检测模块 两级同步 + 延时采样 LED控制逻辑 下降沿检测 + 状态切换 上板验证 综合 → 实现 → 下载 → 测试 核心思路:先消抖,再检测,最后控制 参数可调,适应不同按键和时钟频率

这张图把本章的四个核心环节串起来了。你从左边看到右边,就是完整的处理流程。每个环节都有对应的代码和参数,你可以直接拿去用。

可可精品资料库