跳到主要内容
SoC设计

按键消抖器

入门级数字设计练习:按键消抖器

SOC 入门 按键消抖

按键消抖器 完整练习

1. 模块功能说明

按键消抖器消除机械按键的抖动,输出干净的按键信号。

应用场景:
- 按键输入(计算器、键盘)
- 开关输入
- 继电器/拨码开关
- 任何机械触点信号

2. 按键抖动原理

理想按键波形:
key_in    ────────┐              ┌────
                  └──────────────┘
                  ↑ 按下          ↑ 松开

实际机械按键波形(有抖动):
key_in    ────┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌──
              └┘└┘└┘└┘└┘└┘└┘└┘└┘└┘
              ↑ 抖动时间 ≈ 5-20ms

消抖后波形:
key_out   ────────┐              ┌────
                  └──────────────┘
                  ↑ 消抖后        ↑ 消抖后

为什么会有抖动?

机械按键内部结构:

      ┌─────────────┐
      │    金属片    │ ← 按下时弹起
      │  ┌───────┐  │
      │  │ 触点  │←─┼── 两个触点碰撞
      │  └───────┘  │     产生多次弹跳
      └──────┬──────┘



物理过程:
1. 手指按下 → 金属片开始移动
2. 触点碰撞 → 弹跳多次(每次都会导通/断开)
3. 稳定接触 → 最终稳定在按下状态

3. 完整代码

debounce.sv

//=============================================================================
// Module: debounce
// Description: 按键消抖器
pubDate: 2025-01-01
?n//              支持消抖时间配置,输出消抖后的电平和边沿
// Author: 学习笔记
// Date: 2026-07-13
//=============================================================================

module debounce #(
    parameter CLK_FREQ    = 50_000_000,   // 系统时钟 50MHz
    parameter DEBOUNCE_MS = 20            // 消抖时间 20ms
)(
    input  logic clk,            // 系统时钟
    input  logic rst_n,          // 异步复位(低有效)
    input  logic key_in,         // 原始按键输入
    
    output logic key_level,      // 消抖后的电平(按下=1,松开=0)
    output logic key_pos_edge,   // 上升沿(按下瞬间)
    output logic key_neg_edge    // 下降沿(松开瞬间)
);

    //=========================================================================
    // 参数计算
    //=========================================================================
    // 消抖计数器最大值 = 时钟频率 * 消抖时间(秒)
    // 例: 50MHz * 20ms = 50,000,000 * 0.02 = 1,000,000
    //=========================================================================
    localparam CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_MS;
    localparam CNT_WIDTH = $clog2(CNT_MAX);

    //=========================================================================
    // 信号声明
    //=========================================================================
    logic [CNT_WIDTH-1:0] cnt;          // 消抖计数器
    logic key_sync1, key_sync2;         // 同步寄存器
    logic key_stable;                   // 稳定后的按键值
    logic key_stable_d;                 // 延迟一拍(边沿检测用)

    //=========================================================================
    // 第一步:同步器(消除亚稳态)
    //=========================================================================
    // 按键是异步信号,需要两级同步器
    //=========================================================================
    
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_sync1 <= 1'b1;     // 复位时假设松开(高电平)
            key_sync2 <= 1'b1;
        end else begin
            key_sync1 <= key_in;   // 第一级同步
            key_sync2 <= key_sync1;// 第二级同步
        end
    end

    //=========================================================================
    // 第二步:消抖计数器
    //=========================================================================
    // 原理:
    // - 按键值变化时开始计数
    // - 计数达到阈值后认为按键稳定
    // - 按键值稳定后清零计数器
    //=========================================================================
    
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt       <= '0;
            key_stable <= 1'b1;    // 复位时假设松开
        end else begin
            if (key_sync2 != key_stable) begin
                // 按键值与稳定值不同,开始/继续计数
                if (cnt == CNT_MAX - 1) begin
                    // 计数达到阈值,更新稳定值
                    key_stable <= key_sync2;
                    cnt <= '0;
                end else begin
                    cnt <= cnt + 1;
                end
            end else begin
                // 按键值与稳定值相同,清零计数器
                cnt <= '0;
            end
        end
    end

    //=========================================================================
    // 第三步:边沿检测
    //=========================================================================
    // 检测消抖后信号的上升沿和下降沿
    //=========================================================================
    
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            key_stable_d <= 1'b1;
        else
            key_stable_d <= key_stable;
    end

    //=========================================================================
    // 输出赋值
    //=========================================================================
    
    assign key_level    = key_stable;           // 消抖后的电平
    assign key_pos_edge = key_stable & ~key_stable_d;  // 上升沿
    assign key_neg_edge = ~key_stable & key_stable_d;  // 下降沿

endmodule

4. Testbench

tb_debounce.sv

//=============================================================================
// Testbench: tb_debounce
// Description: 按键消抖器测试平台
pubDate: 2025-01-01
?n//=============================================================================

`timescale 1ns / 1ps

module tb_debounce;

    //=========================================================================
    // 参数
    //=========================================================================
    parameter CLK_PERIOD = 10;      // 时钟周期 10ns
    parameter CLK_FREQ = 100_000_000; // 100MHz(仿真用高频,加快计数)
    parameter DEBOUNCE_MS = 1;      // 消抖时间 1ms(仿真用短时间)

    //=========================================================================
    // 信号
    //=========================================================================
    logic clk;
    logic rst_n;
    logic key_in;
    logic key_level;
    logic key_pos_edge;
    logic key_neg_edge;

    //=========================================================================
    // 时钟生成
    //=========================================================================
    initial clk = 0;
    always #(CLK_PERIOD/2) clk = ~clk;

    //=========================================================================
    // 实例化被测模块
    //=========================================================================
    debounce #(
        .CLK_FREQ    (CLK_FREQ),
        .DEBOUNCE_MS (DEBOUNCE_MS)
    ) uut (
        .clk          (clk),
        .rst_n        (rst_n),
        .key_in       (key_in),
        .key_level    (key_level),
        .key_pos_edge (key_pos_edge),
        .key_neg_edge (key_neg_edge)
    );

    //=========================================================================
    // 任务:模拟按键抖动
    //=========================================================================
    task press_key_with_bounce;
        integer i;
        begin
            $display("时间=%0t: 开始按下按键(带抖动)", $time);
            
            // 模拟按下抖动
            key_in = 0;
            repeat(3) begin
                #500;
                key_in = 1;
                #300;
                key_in = 0;
            end
            
            // 稳定按下
            key_in = 1;
            $display("时间=%0t: 按键稳定按下", $time);
        end
    endtask

    task release_key_with_bounce;
        integer i;
        begin
            $display("时间=%0t: 开始松开按键(带抖动)", $time);
            
            // 模拟松开抖动
            key_in = 1;
            repeat(3) begin
                #400;
                key_in = 0;
                #350;
                key_in = 1;
            end
            
            // 稳定松开
            key_in = 0;
            $display("时间=%0t: 按键稳定松开", $time);
        end
    endtask

    //=========================================================================
    // 测试激励
    //=========================================================================
    initial begin
        // 初始化
        rst_n = 0;
        key_in = 0;        // 默认松开状态(低有效按键)
        
        // 等待复位释放
        #100;
        rst_n = 1;
        #100;
        
        $display("=== 按键消抖器测试开始 ===");
        $display("消抖时间: %0d ms", DEBOUNCE_MS);
        $display("");
        
        //=====================================================================
        // 测试 1: 正常按键(无抖动)
        //=====================================================================
        $display("--- 测试 1: 正常按键(无抖动)---");
        key_in = 1;
        #1000;
        key_in = 0;
        #1000;
        $display("");
        
        //=====================================================================
        // 测试 2: 带抖动的按键
        //=====================================================================
        $display("--- 测试 2: 带抖动的按键 ---");
        press_key_with_bounce;
        #2000;  // 等待消抖
        release_key_with_bounce;
        #2000;
        $display("");
        
        //=====================================================================
        // 测试 3: 快速抖动(不应该通过)
        //=====================================================================
        $display("--- 测试 3: 快速抖动(应该被过滤)---");
        repeat(5) begin
            #200;
            key_in = 1;
            #150;
            key_in = 0;
        end
        #2000;
        $display("");
        
        //=====================================================================
        // 测试 4: 长按
        //=====================================================================
        $display("--- 测试 4: 长按 10ms ---");
        key_in = 1;
        #10_000;  // 10ms
        key_in = 0;
        #2000;
        $display("");
        
        //=====================================================================
        // 测试 5: 快速连按
        //=====================================================================
        $display("--- 测试 5: 快速连按 ---");
        // 第一次按
        key_in = 1;
        #3000;
        key_in = 0;
        #1000;
        // 第二次按
        key_in = 1;
        #3000;
        key_in = 0;
        #1000;
        // 第三次按
        key_in = 1;
        #3000;
        key_in = 0;
        #2000;
        
        $display("");
        $display("=== 按键消抖器测试结束 ===");
        $finish;
    end

    //=========================================================================
    // 波形输出
    //=========================================================================
    initial begin
        $dumpfile("wave_debounce.vcd");
        $dumpvars(0, tb_debounce);
    end

    //=========================================================================
    // 监控输出
    //=========================================================================
    always @(posedge clk) begin
        if (key_pos_edge)
            $display("时间=%0t: >>> 按键按下(消抖后)<<<", $time);
        if (key_neg_edge)
            $display("时间=%0t: >>> 按键松开(消抖后)<<<", $time);
    end

endmodule

5. 仿真波形分析

理想消抖波形:

key_in     ────┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌──────────────
               └┘└┘└┘└┘└┘└┘└┘└┘└┘└┘
               ↑ 抖动区间

key_sync1  ────────┐                    ┌─────────
                   └────────────────────┘

key_sync2  ──────────┐                    ┌───────
                     └────────────────────┘

cnt        ──────────┐                    ┌───────
                     │  计数中...         │ 清零
                     └────────────────────┘

key_stable ──────────────────┐                    ┌──
                             └────────────────────┘
                             ↑ 消抖后稳定

key_pos_edge ────────────────┐
                            └┘
                            ↑ 按下脉冲

6. 练习任务

任务 1:基础验证(必做)

# 编译
iverilog -o tb_debounce tb_debounce.sv debounce.sv

# 运行仿真
vvp tb_debounce

# 查看波形
gtkwave wave_debounce.vcd

观察并记录:

  • 消抖后信号是否干净?
  • 快速抖动是否被过滤?
  • 边沿脉冲是否正确?

任务 2:参数调整(必做)

修改参数,观察不同消抖时间的效果:

// 测试不同消抖时间
parameter DEBOUNCE_MS = 5;   // 5ms
parameter DEBOUNCE_MS = 10;  // 10ms
parameter DEBOUNCE_MS = 50;  // 50ms

记录:

  • 消抖时间 vs 最小按键时间的关系
  • 不同消抖时间对快速连按的影响

任务 3:功能扩展(选做)

添加以下功能:

// 1. 长按检测
output logic key_long_press,    // 长按(>1秒)

// 2. 连按计数
output logic [3:0] press_count, // 连按次数
output logic press_count_clear, // 清除计数

// 3. 双击检测
output logic key_double_click   // 双击

任务 4:写一个完整的 Testbench(必做)

要求:

  1. 模拟真实按键抖动(随机时间、随机次数)
  2. 测试边界情况
  3. 自动化检查结果

7. 常见错误

错误原因解决方法
消抖不完全计数器太小增大 DEBOUNCE_MS
按键响应太慢消抖时间太长减小 DEBOUNCE_MS
边沿检测不到忘记同步确保使用两级同步器
复位后状态错误复位值不对确保 key_stable 复位为 1

8. 知识点总结

消抖时间选择

应用场景推荐消抖时间说明
普通按键10-20ms人手按压速度
轻触开关5-10ms抖动较小
拨码开关50-100ms抖动较大
继电器100-200ms机械惯性大

消抖算法对比

1. 计数器法(本设计)
   - 简单可靠
   - 延迟 = 消抖时间
   - 适合大多数场景

2. 采样法
   - 连续采样 N 次,全部相同才更新
   - 延迟 = N * 采样周期
   - 适合高速采样

3. 平均法
   - 多次采样取平均
   - 延迟可预测
   - 适合模拟消抖

4. 软件法(CPU处理)
   - 灵活可配置
   - 占用 CPU 资源
   - 适合复杂交互

9. 扩展阅读

  • 机械开关特性 - 不同类型开关的抖动特性
  • 人机交互设计 - 按键响应时间的用户体验
  • 低功耗设计 - 消抖器的低功耗实现

完成本练习后,你应该掌握:

  • 机械抖动原理
  • 消抖电路设计
  • 计数器参数计算
  • 同步器使用