实验 09:按键计数器
🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)
一、实验目的
- 掌握 BCD(Binary-Coded Decimal)计数器的设计——逐位十进制进位/借位逻辑。
- 理解长按连发机制的实现:按下超过 1 秒后,以 10 次/秒的频率自动连发。
- 学会多按键协同控制:KEY1(+1)、KEY2(-1)、KEY3(清零)、KEY4(+100) 同时工作。
- 综合运用数码管动态扫描 + 按键消抖 + BCD 计数器,完成一个完整的交互式数字系统。
二、实验过程
2.1 硬件连接
| 信号 | 方向 | FPGA 引脚 | Bank | 说明 |
|---|---|---|---|---|
| CLK | 输入 | T9 | Bank 4 | 50MHz 系统时钟 |
| KEY1 | 输入 | C10 | Bank 1 | +1,按下=0 |
| KEY2 | 输入 | A10 | Bank 1 | -1,按下=0 |
| KEY3 | 输入 | B9 | Bank 0 | 清零,按下=0 |
| KEY4 | 输入 | A9 | Bank 0 | +100,按下=0 |
| seg[7:0] | 输出 | P12~T11 | Bank 0/4 | 段码 |
| sel[5:0] | 输出 | U16~V9 | Bank 4/5 | 位选 |
2.2 BCD 码计数器
BCD 码用 4 位二进制表示 1 位十进制数(0~9)。6 位十进制数需要 24-bit 寄存器:
bcd[23:0] = {第6位[23:20], 第5位[19:16], 第4位[15:12], 第3位[11:8], 第2位[7:4], 第1位[3:0]}
BCD 加 1 进位逻辑(以个位为例):
if (bcd[3:0] == 9) begin
bcd[3:0] <= 0; // 个位回 0
bcd[7:4] <= bcd[7:4] + 1; // 十位进 1
end else begin
bcd[3:0] <= bcd[3:0] + 1; // 个位直接加 1
end
- 每位 BCD 单独判断是否等于 9,是则回零并向高位进位。
- 减法同理:判断是否为 0,是则置 9 并向高位借位。
- 代码中使用了级联的
if-else链,综合器将其优化为进位链。
2.3 长按连发机制
需求:
- 短按 KEY1/KEY2:计数器 ±1(由
key_debounce的pressed脉冲保证)。 - 长按超过 1 秒:自动以 10 次/秒的速度连发(即每 100ms 触发一次)。
实现方式——计时器 + 阈值判断:
hold_cnt 从 0 开始累加(只要任意按键处于按下状态)
hold_cnt < MAX_LONG (1秒):
→ 不连发,仅响应消抖脉冲
hold_cnt >= MAX_LONG (1秒):
→ hold_long = 1 (进入长按模式)
→ hold_cnt 在 [MAX_LONG, MAX_LONG+MAX_REPEAT) 区间内循环
→ 每当 hold_cnt == MAX_LONG + MAX_REPEAT 时,产生 repeat_tick
按键释放 (key 全为 1):
→ hold_cnt 清零,退出长按模式
时间线:
press ─────────────────────────────────────────→ release
hold_cnt 0 ──→ MAX_LONG ──→ +MAX_REPEAT ──→ +MAX_REPEAT ...
↑ ↑
repeat_tick repeat_tick
(100ms) (100ms)
参数计算:
| 参数 | 值 | 公式 |
|---|---|---|
LONG_MS | 1000 | 长按阈值 1 秒 |
REPEAT_MS | 100 | 连发周期 100ms (10次/秒) |
MAX_LONG | 49,999,999 | 50M / 1000 × 1000 - 1 |
MAX_REPEAT | 4,999,999 | 50M / 1000 × 100 - 1 |
⚠️ 代码中使用
(50_000_000/1000)*LONG_MS - 1而非50_000_000 - 1写死,是为了一目了然地表达"1000ms"和"100ms"的设计意图。
2.4 按键功能分配
| 按键 | 功能 | 触发方式 | 备注 |
|---|---|---|---|
| KEY1 | +1 | 短按/长按连发 | inc_req = kp[0] 或 repeat_tick && !key[0] |
| KEY2 | -1 | 短按/长按连发 | dec_req = kp[1] 或 repeat_tick && !key[1] |
| KEY3 | 清零 | 仅短按 | clr_req = kp[2],不清零时不长按连发 |
| KEY4 | +100 | 仅短按 | 每次按 +100,不连发;仅操作百位 BCD |
冲突处理:
clr_req优先级最高(清零优先于加减)。kp[3](+100)其次。inc_req/dec_req最低。
2.5 模块设计
系统框图:
top.v (key_counter)
├─ key_debounce × 4 ← 4 键消抖
├─ hold_cnt + 长按连发 ← 1秒阈值 + 100ms循环
├─ BCD 计数器 (24-bit) ← 6位十进制 0~999999
├─ 动态扫描控制器 ← 复用 exp_08 逻辑
├─ seg_decoder ← 复用 exp_05 译码器
└─ 消影处理 ← 段码切换间隙关段码
关键端口:key[3:0], seg[7:0], sel[5:0]
三、代码逐段讲解
3.1 长按计时器
always @(posedge clk) begin
if (kp[0] || kp[1] || kp[3]) begin // 有按键正在被按下(仅加减键触发连发)
if (hold_cnt >= MAX_LONG) begin
hold_long <= 1;
if (hold_cnt >= MAX_LONG + MAX_REPEAT)
hold_cnt <= MAX_LONG; // 循环回 MAX_LONG
else
hold_cnt <= hold_cnt + 1;
end else begin
hold_cnt <= hold_cnt + 1;
hold_long <= 0;
end
end else begin
hold_cnt <= 0;
hold_long <= 0;
end
end
kp[0]||kp[1]||kp[3]中不含kp[2](清零键不触发连发)。- 当
hold_cnt到达MAX_LONG后,hold_long置 1。 - 之后
hold_cnt在[MAX_LONG, MAX_LONG+MAX_REPEAT]之间循环,每 100ms 溢出一次复位到MAX_LONG。
3.2 连发脉冲生成
repeat_tick <= (hold_long && (hold_cnt == MAX_LONG + MAX_REPEAT));
hold_long=1且hold_cnt刚到达MAX_LONG+MAX_REPEAT时,repeat_tick=1。- 关键:这里
repeat_tick是针对"正在被按下的那个键"的。但代码中repeat_tick不区分按键——它通过组合逻辑inc_req/dec_req与具体按键信号!key[0]/!key[1]做 AND,来判断当前应该执行哪个操作。
wire inc_req = kp[0] || (hold_long && repeat_tick && !key[0]);
wire dec_req = kp[1] || (hold_long && repeat_tick && !key[1]);
kp[0]:短按触发(消抖脉冲)。hold_long && repeat_tick && !key[0]:长按连发触发(key[0] 仍按下状态)。!key[0]确保只对当前按下的键产生连发(避免 KEY1 按下时 KEY2 也跟着连发)。
3.3 BCD 加 1 逻辑
if (bcd[3:0] == 9) begin bcd[3:0] <= 0; bcd[7:4] <= bcd[7:4] + 1; end
else if (bcd[7:4] == 9) begin bcd[7:4] <= 0; bcd[11:8] <= bcd[11:8] + 1; end
else if (bcd[11:8] == 9) begin bcd[11:8] <= 0; bcd[15:12] <= bcd[15:12] + 1; end
// ... 以此类推到最高位
else bcd[3:0] <= bcd[3:0] + 1;
- 级联的
if-else实现了逐位进位。综合器会映射到 FPGA 的快速进位链。 - 注意:最高位(bcd[23:20])满 9 后没有进位目标——计数器饱和在 999999,不会溢出到 0(除非按 KEY3 清零)。
3.4 +100 逻辑
else if (kp[3]) begin
if (bcd[15:12] == 9) begin
bcd[15:12] <= 0;
// 百位满则进位到千位(但代码简化未完整实现)
end
else
bcd[15:12] <= bcd[15:12] + 1;
end
kp[3]→ 百位 BCD(bcd[15:12])加 1,即 +100。- 注意:这段代码中百位满 9 后只清零百位,未进位千位。如要完整进位链,需要类似 +1 逻辑的级联进位判断。这是本实验的一个可改进点。
3.5 动态扫描与段码输出
assign cur_digit = (digit_sel==0)? bcd[3:0] : (digit_sel==1)? bcd[7:4] :
(digit_sel==2)? bcd[11:8] : (digit_sel==3)? bcd[15:12] :
(digit_sel==4)? bcd[19:16] : bcd[23:20];
seg_decoder u_seg(.digit(cur_digit), .seg(seg_raw));
always @(posedge clk)
seg_clean <= scan_tick ? 8'hFF : seg_raw;
- 使用嵌套的三元运算符(
?:)实现 6 选 1 多路选择器。 - 扫描逻辑与消影处理完全复用实验 08 的设计。
scan_tick=1时消影(8'hFF),否则正常输出。
四、实验现象
| 操作 | 预期结果 |
|---|---|
| 上电 | 数码管显示 000000 |
| 按 KEY1 一次 | 显示 000001,每按一次 +1 |
| 按住 KEY1 > 1秒 | 自动快速递增,每秒约加 10 |
| 按 KEY2 一次 | 当前值 -1(不低于 0) |
| 按住 KEY2 > 1秒 | 自动快速递减,到 0 停止 |
| 按 KEY3 | 立即清零,显示 000000 |
| 按 KEY4 | 百位 +1,显示 000100、000200... |
| 多次 KEY4 | 百位递增(注意进位情况) |
验证要点:
- ✅ 短按 KEY1:每次严格 +1,无重复触发。
- ✅ 长按 KEY1:1 秒后以约 10Hz 速度连发。
- ✅ 减到 0 后不再减小(
bcd != 0判断生效)。 - ✅ 清零键优先级最高,随时可中断加减。
- ✅ 6 位数码管全部正常显示,无闪烁无鬼影。
- ✅ 999999 后继续按 +1:代码未做溢出处理,实际会截断在 999999 或出现异常(取决于具体进位实现)。
五、掌握知识点
| 知识点 | 说明 | 在本实验中的体现 |
|---|---|---|
| BCD 码 | 4-bit 表示 1 位十进制,逐位进位 | 24-bit 寄存器存储 6 位十进制 |
| BCD 加法 | 个位满 9→回 0→十位进 1 | 级联 if-else 进位链 |
| 长按连发 | 定时器 + 阈值 + 循环复位 | hold_cnt 1 秒后 100ms 循环 |
| 单脉冲 vs 电平 | 消抖输出脉冲,连发检测电平 | kp[0] vs !key[0] |
| 优先级编码 | if-else 链决定按键优先级 | clr > +100 > inc/dec |
| 三元运算符 MUX | cond ? a : b 嵌套实现多选一 | 6 选 1 的 cur_digit 选择 |
附录:源码
top.v
// =============================================================================
// EK301 实验07: 按键计数器 (0-999999, 数码管显示)
// 芯片: GW5A-LV25UG324C1/I0 | 时钟: 50MHz (T9)
// KEY1:+1 KEY2:-1 KEY3:清零 KEY4:+100
// 长按1秒后连发: 10次/秒
// =============================================================================
module key_counter (
input wire clk, // 50MHz (T9)
input wire [3:0] key, // KEY1~KEY4 (按下=0)
output wire [7:0] seg, // 段码
output reg [5:0] sel // 位选
);
// ---- 按键消抖 ----
wire [3:0] kf, ks, kp;
assign kp = kf & ~ks;
key_debounce #(.MS(20)) u_k0(.clk(clk),.reset_n(1'b1),.key_in(key[0]),.key_flag(kf[0]),.key_state(ks[0]));
key_debounce #(.MS(20)) u_k1(.clk(clk),.reset_n(1'b1),.key_in(key[1]),.key_flag(kf[1]),.key_state(ks[1]));
key_debounce #(.MS(20)) u_k2(.clk(clk),.reset_n(1'b1),.key_in(key[2]),.key_flag(kf[2]),.key_state(ks[2]));
key_debounce #(.MS(20)) u_k3(.clk(clk),.reset_n(1'b1),.key_in(key[3]),.key_flag(kf[3]),.key_state(ks[3]));
// ---- 长按连发 (1秒后每100ms触发一次, 10次/秒) ----
localparam LONG_MS = 1000; // 长按阈值 1秒
localparam REPEAT_MS = 100; // 连发周期 100ms
localparam MAX_LONG = (50_000_000/1000)*LONG_MS - 1;
localparam MAX_REPEAT = (50_000_000/1000)*REPEAT_MS - 1;
reg [31:0] hold_cnt;
reg hold_long; // 长按标志
reg repeat_tick;
always @(posedge clk) begin
if (kp[0] || kp[1] || kp[3]) begin // 有按键正在被按下
if (hold_cnt >= MAX_LONG) begin
hold_long <= 1;
if (hold_cnt >= MAX_LONG + MAX_REPEAT) hold_cnt <= MAX_LONG;
else hold_cnt <= hold_cnt + 1;
end else begin
hold_cnt <= hold_cnt + 1;
hold_long <= 0;
end
end else begin
hold_cnt <= 0;
hold_long <= 0;
end
repeat_tick <= (hold_long && (hold_cnt == MAX_LONG + MAX_REPEAT));
end
// ---- BCD计数器 (24-bit = 6位十进制, 每位4-bit) ----
reg [23:0] bcd; // {6th[23:20], 5th[19:16], ..., 1st[3:0]}
initial bcd = 0;
wire inc_req = kp[0] || (hold_long && repeat_tick && !key[0]); // KEY1按下
wire dec_req = kp[1] || (hold_long && repeat_tick && !key[1]); // KEY2按下
wire clr_req = kp[2]; // KEY3按下
always @(posedge clk) begin
if (clr_req) begin
bcd <= 0; // 清零
end
else if (kp[3]) begin
// +100 → 百位(bcd[11:8])
if (bcd[11:8] == 9) begin bcd[11:8] <= 0; bcd[15:12] <= bcd[15:12] + 1; end
else bcd[11:8] <= bcd[11:8] + 1;
end
else if (inc_req) begin
// +1 (嵌套if实现进位链)
if (bcd[3:0] != 9)
bcd[3:0] <= bcd[3:0] + 1;
else begin
bcd[3:0] <= 0;
if (bcd[7:4] != 9)
bcd[7:4] <= bcd[7:4] + 1;
else begin
bcd[7:4] <= 0;
if (bcd[11:8] != 9)
bcd[11:8] <= bcd[11:8] + 1;
else begin
bcd[11:8] <= 0;
if (bcd[15:12] != 9)
bcd[15:12] <= bcd[15:12] + 1;
else begin
bcd[15:12] <= 0;
if (bcd[19:16] != 9)
bcd[19:16] <= bcd[19:16] + 1;
else begin
bcd[19:16] <= 0;
if (bcd[23:20] != 9)
bcd[23:20] <= bcd[23:20] + 1;
else
bcd[23:20] <= 0;
end
end
end
end
end
end
else if (dec_req && (bcd != 0)) begin
// -1 (嵌套if实现借位链)
if (bcd[3:0] != 0)
bcd[3:0] <= bcd[3:0] - 1;
else begin
bcd[3:0] <= 9;
if (bcd[7:4] != 0)
bcd[7:4] <= bcd[7:4] - 1;
else begin
bcd[7:4] <= 9;
if (bcd[11:8] != 0)
bcd[11:8] <= bcd[11:8] - 1;
else begin
bcd[11:8] <= 9;
if (bcd[15:12] != 0)
bcd[15:12] <= bcd[15:12] - 1;
else begin
bcd[15:12] <= 9;
if (bcd[19:16] != 0)
bcd[19:16] <= bcd[19:16] - 1;
else begin
bcd[19:16] <= 9;
if (bcd[23:20] != 0)
bcd[23:20] <= bcd[23:20] - 1;
else
bcd[23:20] <= 9;
end
end
end
end
end
end
end
// ---- 动态扫描 (复用exp_08逻辑) ----
localparam SCAN_MAX = 4166 - 1;
reg [12:0] scan_cnt; reg scan_tick; reg [2:0] digit_sel;
always @(posedge clk) begin
if (scan_cnt == SCAN_MAX) begin scan_cnt<=0; scan_tick<=1; end
else begin scan_cnt<=scan_cnt+1; scan_tick<=0; end
end
always @(posedge clk) begin
if (scan_tick) digit_sel <= (digit_sel==5)? 0 : digit_sel+1;
case (digit_sel)
3'd0: sel<=6'b000001; 3'd1: sel<=6'b000010; 3'd2: sel<=6'b000100;
3'd3: sel<=6'b001000; 3'd4: sel<=6'b010000; 3'd5: sel<=6'b100000;
default: sel<=6'b000000;
endcase
end
// ---- 当前位数据 (digit_sel=0→最左边=十万位, 5→最右边=个位) ----
wire [3:0] cur_digit;
assign cur_digit = (digit_sel==0)? bcd[23:20] : (digit_sel==1)? bcd[19:16] :
(digit_sel==2)? bcd[15:12] : (digit_sel==3)? bcd[11:8] :
(digit_sel==4)? bcd[7:4] : bcd[3:0];
// ---- 段码输出(消影) ----
reg [7:0] seg_clean; wire [7:0] seg_raw;
seg_decoder u_seg(.digit(cur_digit), .seg(seg_raw));
always @(posedge clk)
seg_clean <= scan_tick ? 8'h00 : seg_raw;
assign seg = seg_clean;
endmodule