跳到主要内容

实验 09:按键计数器

🛒 开发板: EK301 GW5A FPGA 开发板 芯片: GW5A-LV25UG324C1/I0 (Arora V 系列)


一、实验目的

  1. 掌握 BCD(Binary-Coded Decimal)计数器的设计——逐位十进制进位/借位逻辑。
  2. 理解长按连发机制的实现:按下超过 1 秒后,以 10 次/秒的频率自动连发。
  3. 学会多按键协同控制:KEY1(+1)、KEY2(-1)、KEY3(清零)、KEY4(+100) 同时工作。
  4. 综合运用数码管动态扫描 + 按键消抖 + BCD 计数器,完成一个完整的交互式数字系统。

二、实验过程

2.1 硬件连接

信号方向FPGA 引脚Bank说明
CLK输入T9Bank 450MHz 系统时钟
KEY1输入C10Bank 1+1,按下=0
KEY2输入A10Bank 1-1,按下=0
KEY3输入B9Bank 0清零,按下=0
KEY4输入A9Bank 0+100,按下=0
seg[7:0]输出P12~T11Bank 0/4段码
sel[5:0]输出U16~V9Bank 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_debouncepressed 脉冲保证)。
  • 长按超过 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_MS1000长按阈值 1 秒
REPEAT_MS100连发周期 100ms (10次/秒)
MAX_LONG49,999,99950M / 1000 × 1000 - 1
MAX_REPEAT4,999,99950M / 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=1hold_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,显示 000100000200...
多次 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
三元运算符 MUXcond ? 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