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(必做)
要求:
- 模拟真实按键抖动(随机时间、随机次数)
- 测试边界情况
- 自动化检查结果
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. 扩展阅读
- 机械开关特性 - 不同类型开关的抖动特性
- 人机交互设计 - 按键响应时间的用户体验
- 低功耗设计 - 消抖器的低功耗实现
完成本练习后,你应该掌握:
- 机械抖动原理
- 消抖电路设计
- 计数器参数计算
- 同步器使用