4. 按键计数显示

任务说明

本案例实现一个按键计数显示电路:每次按下按键,计数器加1,并将计数值通过数码管显示。核心目标是掌握Chisel中多模块的连接方式——将按键消抖模块、计数器模块和数码管驱动模块组合为完整系统。

模块划分与连接关系

系统由三个模块组成,连接关系如下:

  • Debounce:对按键输入进行消抖,输出单周期脉冲(每次按下只产生一个时钟周期的上升沿信号)
  • Counter:接收脉冲信号,计数范围0~99(两位十进制数),溢出自动回零
  • SegDisplay:将计数值的十位和个位分别译码为七段数码管段码输出

完整实现代码

import chisel3._
import chisel3.util._

// 按键消抖模块:检测按下动作,输出单周期脉冲
class Debounce extends Module {
  val io = IO(new Bundle {
    val btnIn   = Input(Bool())   // 原始按键输入(低有效)
    val pulseOut = Output(Bool()) // 消抖后的单周期脉冲
  })

  // 两级同步寄存器消除亚稳态
  val syncReg = RegInit(true.B)
  val syncReg2 = RegInit(true.B)
  syncReg := io.btnIn
  syncReg2 := syncReg

  // 边沿检测:按下瞬间(从高到低)产生脉冲
  val debounced = RegInit(true.B)
  val pressCnt = RegInit(0.U(20.W))

  // 简单延时消抖:稳定20ms(假设50MHz时钟)
  when(syncReg2 =/= debounced) {
    pressCnt := 0.U
    debounced := syncReg2
  }.elsewhen(pressCnt < 1000000.U) {
    pressCnt := pressCnt + 1.U
  }

  // 下降沿检测(按下动作)
  val prevDebounced = RegNext(debounced)
  io.pulseOut := (!debounced) && prevDebounced
}

// 计数器模块:记录脉冲次数,0~99循环
class Counter extends Module {
  val io = IO(new Bundle {
    val pulse = Input(Bool())
    val count = Output(UInt(8.W)) // 低4位个位,高4位十位
  })

  val countReg = RegInit(0.U(8.W))
  when(io.pulse) {
    when(countReg === 99.U) {
      countReg := 0.U
    }.otherwise {
      countReg := countReg + 1.U
    }
  }
  io.count := countReg
}

// 七段数码管译码模块(共阴极)
class SegDecoder extends Module {
  val io = IO(new Bundle {
    val value = Input(UInt(4.W))  // 0~9
    val seg   = Output(UInt(7.W)) // a~g
  })

  // 共阴极段码表:a,b,c,d,e,f,g
  io.seg := MuxLookup(io.value, 0.U)(Seq(
    0.U -> "b0111111".U,
    1.U -> "b0000110".U,
    2.U -> "b1011011".U,
    3.U -> "b1001111".U,
    4.U -> "b1100110".U,
    5.U -> "b1101101".U,
    6.U -> "b1111101".U,
    7.U -> "b0000111".U,
    8.U -> "b1111111".U,
    9.U -> "b1101111".U
  ))
}

// 顶层模块:连接三个子模块
class KeyCountDisplay extends Module {
  val io = IO(new Bundle {
    val btn     = Input(Bool())
    val segTens = Output(UInt(7.W)) // 十位段码
    val segOnes = Output(UInt(7.W)) // 个位段码
  })

  // 实例化消抖模块
  val debounce = Module(new Debounce())
  debounce.io.btnIn := io.btn

  // 实例化计数器
  val counter = Module(new Counter())
  counter.io.pulse := debounce.io.pulseOut

  // 拆分十位和个位
  val tens = (counter.io.count >> 4) & 0xF.U
  val ones = counter.io.count & 0xF.U

  // 实例化两个译码器
  val segTensMod = Module(new SegDecoder())
  segTensMod.io.value := tens
  io.segTens := segTensMod.io.seg

  val segOnesMod = Module(new SegDecoder())
  segOnesMod.io.value := ones
  io.segOnes := segOnesMod.io.seg
}

// 生成Verilog
object KeyCountDisplayMain extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new KeyCountDisplay(), Array("--target-dir", "generated"))
}

运行与验证

环境要求:Chisel 3.6+ / Scala 2.13,sbt项目已配置chisel依赖。

运行步骤:

  1. 将上述代码保存为 KeyCountDisplay.scala
  2. 在sbt中执行 runMain KeyCountDisplayMain,生成Verilog文件
  3. 将生成的Verilog集成到FPGA工程(如Vivado/Quartus),绑定按键引脚和数码管引脚

验证方法:

  • 编写Chisel测试(PeekPokeTester),依次模拟按键按下信号,检查计数输出是否正确递增
  • 测试用例:连续按下10次,观察计数从0递增到10;按下第100次时回零
  • 在FPGA板卡上,每按一次按键,数码管数字应加1;计数到99后再按一次回到00

通过判定标准:仿真中脉冲信号与计数输出时序正确,计数在0~99间循环;硬件上按键响应正常无抖动误触发,数码管显示与按键次数一致。

关键点说明

  • 消抖模块采用“同步+延时确认”方式,延时计数假设50MHz时钟,20ms消抖时间对应1,000,000个时钟周期
  • 计数器内部使用8位寄存器,高4位存十位、低4位存个位,简化了译码前的拆分逻辑
  • 两个译码器实例独立驱动十位和个位数码管,体现模块复用思想