实验 04:按键消抖
🛒 开发板购买: EK303 GW1N FPGA核心板
一、实验目的
- 理解机械按键的抖动现象——按下和释放瞬间会产生 5~20ms 的不规则毛刺。
- 掌握 FPGA 处理异步信号的核心技术:两级同步器消除亚稳态。
- 掌握消抖计数器状态机设计,过滤抖动,输出干净的按键信号。
- 理解单周期脉冲的生成方法——按键按下只输出一个时钟周期宽度的高脉冲。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | 引脚 | 说明 |
|---|---|---|---|
| KEY | 输入 | 69 | 轻触按键,按下时接地(低电平),默认上拉高电平 |
2.2 设计思路
按键信号处理流程:
按键物理引脚
↓ (异步信号,可能有亚稳态)
[两级同步器] → 消除亚稳态,产生稳定采样值
↓
[边沿检测] → 检测电平变化 (0→1 或 1→0)
↓
[消抖计数器] → 检测到变化后等待 20ms,期间电平稳定则确认
↓
[脉冲输出] → 输出一个周期宽度的 key_pressed 脉冲
为什么需要两级同步器?
按键信号与 FPGA 时钟不同步,直接采样可能进入亚稳态。用两级 DFF 将异步信号同步到系统时钟域。
消抖状态机:
IDLE ──(检测到电平变化)──→ WAIT ──(20ms计数满且电平稳定)──→ DONE ──→ IDLE
│
└──(20ms内电平又变了)──→ IDLE (抖动了,忽略)
2.3 Verilog 模块设计
模块名:key_debounce
端口:
| 端口 | 方向 | 位宽 | 说明 |
|---|---|---|---|
clk_50m | input | 1 | 50MHz 时钟 |
key_in | input | 1 | 原始按键输入 |
key_pressed | output | 1 | 消抖后的按键脉冲(单周期高脉冲) |
内部信号:
| 信号 | 位宽 | 说明 |
|---|---|---|
sync_ff1, sync_ff2 | 1 | 两级同步寄存器 |
key_stable | 1 | 同步后的稳定按键值 |
state | 2 | 状态机:IDLE / WAIT / DONE |
debounce_cnt | 20 | 消抖计数器 (0→1,000,000 = 20ms) |
逻辑描述:
- 两级同步器:每个时钟上升沿
sync_ff1 <= key_in; sync_ff2 <= sync_ff1。 - 边沿检测:
sync_ff2与上一周期的值比对,不同则检测到变化。 - 消抖状态机:
- IDLE:检测到电平变化 → 跳转 WAIT,计数器清零。
- WAIT:计数器递增。若电平再次变化 → 判定为抖动,回 IDLE。若计满 20ms 且电平稳定 → 跳转 DONE。
- DONE:输出
key_pressed = 1,下一周期自动回 IDLE。
key_pressed仅在 DONE 状态维持一个周期的高电平。
2.4 引脚约束
| 信号 | 约束 |
|---|---|
key_in | IO_LOC 对应按键引脚,PULL_MODE=UP 使用内部上拉 |
三、实验现象
对比观察(用示波器或逻辑分析仪):
| 信号 | 现象 |
|---|---|
key_in(原始) | 按下时波形充满密集毛刺,持续 5~20ms |
key_pressed(消抖后) | 干净的单周期高脉冲,每个有效按键只产生一个脉冲 |
直观验证:将 key_pressed 接到 LED 翻转逻辑——每按一次键,LED 精确翻转一次,不会"按一次跳两次"。
四、掌握知识点
| 知识点 | 说明 |
|---|---|
| 机械按键抖动 | 5~20ms 不规则毛刺的成因 |
| 亚稳态 | 异步信号直接采样可能导致触发器输出不确定 |
| 两级同步器 | sync_ff1 <= async_in; sync_ff2 <= sync_ff1 |
| 消抖状态机 | IDLE→WAIT(20ms)→DONE 三段式 |
| 单周期脉冲 | key_pressed 只维持一个周期,便于下游模块使用 |
| 内部上拉 | PULL_MODE=UP 省去外部上拉电阻 |
五、思考题
- 如果按键是按下为高电平,哪些地方需要修改?
- 消抖时间 20ms 是怎么算出来的?(提示:50MHz 时钟,1M 周期 = ? ms)
- 如果不用状态机,用连续计数+比较器来消抖可以吗?怎么设计?
- 如何区分长按和短按?(预习实验 06)
附录:源码清单
top.v
// =============================================================================
// top.v - 按键消抖验证 (Key Debounce + Toggle LED)
// 芯片: GW1N-LV9LQ144C6/I5
// 功能: 按键消抖 + LED Toggle — 按一次按键,LED翻转亮灭
// =============================================================================
module key_debounce_top (
input wire clk_50m, // 50MHz (Pin 98)
input wire key_in, // 按键 (Pin 69, 按下=0)
output reg led // LED 指示 (Pin 8, 高亮)
);
// =========================================================================
// 子模块: key_debounce — 消抖 + 下降沿检测 + 单脉冲输出
// =========================================================================
wire key_pressed;
key_debounce #(
.DEBOUNCE_MS(20)
) u_debounce (
.clk_50m (clk_50m),
.key_in (key_in),
.key_pressed (key_pressed)
);
// =========================================================================
// Toggle LED: 每次 key_pressed 脉冲翻转一次 LED
// 按下 → LED亮, 再按 → LED灭, 交替切换
// =========================================================================
always @(posedge clk_50m) begin
if (key_pressed)
led <= ~led;
end
// 上电初值: LED灭
initial led = 1'b0;
endmodule
// =============================================================================
// 按键消抖子模块 (Key Debounce) — 可被其他工程例化复用
// 参数 DEBOUNCE_MS: 消抖时间 (ms), 默认20ms
// 输出 key_pressed: 单周期高脉冲, 只响应下降沿 (按键按下)
// =============================================================================
module key_debounce #(
parameter DEBOUNCE_MS = 20
) (
input wire clk_50m,
input wire key_in, // 原始按键 (按下=0, 默认上拉=1)
output reg key_pressed // 消抖后单周期高脉冲 (下降沿触发)
);
localparam DEBOUNCE_MAX = (50_000_000 * DEBOUNCE_MS / 1000) - 1;
localparam STATE_IDLE = 2'd0;
localparam STATE_WAIT = 2'd1;
localparam STATE_DONE = 2'd2;
// 两级同步器
reg sync_ff1, sync_ff2;
wire key_synced;
always @(posedge clk_50m) begin
sync_ff1 <= key_in;
sync_ff2 <= sync_ff1;
end
assign key_synced = sync_ff2;
// 上一周期值(用于边沿检测)
reg key_prev;
always @(posedge clk_50m) key_prev <= key_synced;
// 下降沿: 上一周期=1, 当前=0 → 按下
wire key_falling = (key_prev == 1'b1) && (key_synced == 1'b0);
reg [1:0] state;
reg [19:0] debounce_cnt;
always @(posedge clk_50m) begin
case (state)
STATE_IDLE: begin
key_pressed <= 1'b0;
if (key_falling) begin
state <= STATE_WAIT;
debounce_cnt <= 20'd0;
end
end
STATE_WAIT: begin
key_pressed <= 1'b0;
if (debounce_cnt == DEBOUNCE_MAX) begin
if (key_synced == 1'b0)
state <= STATE_DONE; // 确认有效按下
else
state <= STATE_IDLE; // 已松开, 视为抖动
end else begin
debounce_cnt <= debounce_cnt + 1'b1;
end
end
STATE_DONE: begin
key_pressed <= 1'b1;
state <= STATE_IDLE;
end
default: begin
state <= STATE_IDLE;
key_pressed <= 1'b0;
end
endcase
end
endmodule