跳到主要内容

实验 04:按键消抖

🛒 开发板购买: EK303 GW1N FPGA核心板


一、实验目的

  1. 理解机械按键的抖动现象——按下和释放瞬间会产生 5~20ms 的不规则毛刺。
  2. 掌握 FPGA 处理异步信号的核心技术:两级同步器消除亚稳态。
  3. 掌握消抖计数器状态机设计,过滤抖动,输出干净的按键信号。
  4. 理解单周期脉冲的生成方法——按键按下只输出一个时钟周期宽度的高脉冲。

二、实验过程

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_50minput150MHz 时钟
key_ininput1原始按键输入
key_pressedoutput1消抖后的按键脉冲(单周期高脉冲)

内部信号

信号位宽说明
sync_ff1, sync_ff21两级同步寄存器
key_stable1同步后的稳定按键值
state2状态机:IDLE / WAIT / DONE
debounce_cnt20消抖计数器 (0→1,000,000 = 20ms)

逻辑描述

  1. 两级同步器:每个时钟上升沿 sync_ff1 <= key_in; sync_ff2 <= sync_ff1
  2. 边沿检测:sync_ff2 与上一周期的值比对,不同则检测到变化。
  3. 消抖状态机:
  • IDLE:检测到电平变化 → 跳转 WAIT,计数器清零。
  • WAIT:计数器递增。若电平再次变化 → 判定为抖动,回 IDLE。若计满 20ms 且电平稳定 → 跳转 DONE。
  • DONE:输出 key_pressed = 1,下一周期自动回 IDLE。
  1. key_pressed 仅在 DONE 状态维持一个周期的高电平。

2.4 引脚约束

信号约束
key_inIO_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 省去外部上拉电阻

五、思考题

  1. 如果按键是按下为高电平,哪些地方需要修改?
  2. 消抖时间 20ms 是怎么算出来的?(提示:50MHz 时钟,1M 周期 = ? ms)
  3. 如果不用状态机,用连续计数+比较器来消抖可以吗?怎么设计?
  4. 如何区分长按短按?(预习实验 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