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依赖。
运行步骤:
- 将上述代码保存为
KeyCountDisplay.scala - 在sbt中执行
runMain KeyCountDisplayMain,生成Verilog文件 - 将生成的Verilog集成到FPGA工程(如Vivado/Quartus),绑定按键引脚和数码管引脚
验证方法:
- 编写Chisel测试(PeekPokeTester),依次模拟按键按下信号,检查计数输出是否正确递增
- 测试用例:连续按下10次,观察计数从0递增到10;按下第100次时回零
- 在FPGA板卡上,每按一次按键,数码管数字应加1;计数到99后再按一次回到00
通过判定标准:仿真中脉冲信号与计数输出时序正确,计数在0~99间循环;硬件上按键响应正常无抖动误触发,数码管显示与按键次数一致。
关键点说明
- 消抖模块采用“同步+延时确认”方式,延时计数假设50MHz时钟,20ms消抖时间对应1,000,000个时钟周期
- 计数器内部使用8位寄存器,高4位存十位、低4位存个位,简化了译码前的拆分逻辑
- 两个译码器实例独立驱动十位和个位数码管,体现模块复用思想