项目需求
在小脚丫FPGA上实现不同的LED显示效果
需求分析
在小脚丫FPGA核心板上:
· 利用8个单色LED实现不同的LED显示效果
· 通过4个轻触按键控制不同的LED显示模式
· 通过4个拨动开关控制每种显示模式的显示周期
· 在数码管上通过数值显示出相应的模式和周期 - 第一个数码管显示LED的显示模式,第二个数码管显示周期
轻触开关K1 ~ K4,用于切换LED的显示模式:
· K1:循环心跳灯 - 8个LED轮流心跳,每一个LED心跳2个周期(半个周期亮、半个周期灭、半个周期亮、半个周期灭),然后该LED灭掉,下一个LED开始跳动,从第一个LED开始,到第8个LED,然后再从第一个开始,周而复始
· K2:呼吸灯 - 8个LED轮流呼吸,每一个LED呼吸2个周期(半个周期从灭到亮、半个周期从亮到灭、半个周期从灭到亮、半个周期从亮到灭),然后该LED灭掉,下一个LED开始呼吸,从第一个LED开始,到第8个LED,然后再从第一个开始,周而复始
· K3:带渐灭功能的流水灯 - 8个LED构成流动显示的效果,且下面的灯亮度逐渐变暗
· K4:双向渐灭流水灯-两侧点亮的LED同时向对侧移动
实现的方式
代码使用Chisel编写,用GitHub Copilot辅助设计模块,生成Verilog代码在StepFPGA的WebIDE中构建,把构建出的JED下载到FPGA
作品原理
本作品使用STEP MX02-LPC板载的LED实现不同的显示效果。用户通过板载的拨码开关在1s - 4s之间设置循环周期、通过按钮切换4种显示模式,循环周期和模式在数码管上显示。此外当拨码开关输入不是个有效的周期时,LED将会熄灭,显示周期的数码管将播放一个简易的动画作为提示。
Figure 1作品功能框图 (数字表示位宽)
如图1,拨码开关输入的值与一个编码器连接,它会判断输入是否有效并输出二进制的周期值。按钮的输入经过编码器后连接到一个寄存器来设置显示模式;寄存器的值用来切换输出到LED的信号模块,其中流水灯直接用一个模块实现两种显示模式。周期和显示模式被输入到数码管译码器用于驱动数码管显示相应的数字;当周期输入值无效时显示周期的数码管将会播放动画,LED会熄灭。
以下是一些主要模块的工作流程:
心跳灯
Figure 2心跳灯模块工作流程
如图2,心跳灯的工作流程比较简单,输入时钟频率为2Hz,所以在时钟计数器值等于周期值或周期值的2倍时翻转电平;在时钟计数器值等于周期值的2倍时切换要点亮的LED即可。
呼吸灯
Figure 3呼吸灯模块工作流程
如图3,要实现呼吸灯效果,需要一个PWM模块,该模块比较简单,就不在这画出来了。呼吸灯模块要实现使PWM占空比在前半个周期增加、在后半个周期降低。该模块输入时钟频率是100kHz,即每个时钟周期是10μs,所以要在半个周期里让PWM占空比在0 – 100变化,相邻两次变化的间隔就是
(显示周期 * 10^6)/(2 * 100) =5000*显示周期 (μs)
即 显示周期 * 500个时钟周期。所以当时钟计数器达到该值的时候改变占空比就能实现亮度渐变。此外该模块中包含一个方向寄存器,用于指示当前需要增加还是减小占空比;每当占空比减小到0时就切换到下一个LED点亮。
渐灭流水灯
为了方便流水灯渐灭效果的实现,写了个模块用来产生渐灭的信号,如图4,这个模块在复位时设置PWM占空比寄存器值为100,在半个显示周期内使占空比逐渐降为0.
Figure 4渐灭信号模块工作流程
流水灯模块的工作流程如图5:这个模块里包含8个渐灭信号生成器,它们的复位信号连接到一个8位寄存器以便之后复位。在单向模式,每个周期内8个渐灭模块依次被复位,使8个LED依次点亮并慢慢熄灭;在双向模式,每个周期内两侧的LED依次向中间再向两边点亮并逐渐熄灭。输入时钟频率是100kHz,要在一个周期内切换8次,两次之间的间隔时间为
(显示周期 * 10^6)/8 =125000*显示周期 (μs)
即 显示周期 * 12500个时钟周期。所以每当时钟计数器达到这个数值时就切换到下一个要点亮的LED产生复位信号就彳亍了。在单向模式使用计数器切换下一个要点亮的LED;在双向模式使用状态机切换接下来要点亮的LED。
Figure 5流水灯模块工作流程
代码及说明
代码在Intellij IDEA中使用Chisel编写:
abstract class Blinker extends Module {
val io = IO(new Bundle {
val period = Input(UInt(2.W)) // 0 ~ 3 for 1 ~ 4 seconds
val out = Output(UInt(8.W))
})
val realPeriod = Wire(UInt(3.W))
realPeriod := (0.U ## io.period) + 1.U
val clockCnt = RegInit(0.U(20.W)) //register for counting
val currentLed = RegInit(0.U(3.W)) // 3 bit register for current led
}
以上是一个点灯模块的抽象类,它定义了点灯模块的接口和一些它们共有的元素,创建点灯模块时继承这个类即可。
class HeartBeat extends Blinker {
private val ledReg = RegInit(0.U(1.W)) // 1 bit register for led
private val firstCycleFinished = RegInit(false.B) // Whether the first cycle is finished
when(clockCnt < realPeriod * 2.U - 1.U) {
clockCnt := clockCnt + 1.U
when(clockCnt === realPeriod - 1.U) { // half period
ledReg := !ledReg
}
}.otherwise {
clockCnt := 0.U
currentLed := Mux(firstCycleFinished, currentLed + 1.U, currentLed) // Change the current led every 2 cycles
firstCycleFinished := ~firstCycleFinished // Toggle the flag
ledReg := !ledReg
}
io.out := ledReg << currentLed
}
以上是心跳灯模块,每半个周期翻转状态;用firstCycleFinished寄存器标识是否经过了一个周期,来实现每2个周期切换LED。
class PWMGenerator extends Module {
val io = IO(new Bundle {
val duty = Input(UInt(7.W)) // 0-100
val pwmOut = Output(Bool())
})
private val counter = RegInit(0.U(7.W))
counter := counter + 1.U
when(counter >= 100.U) {
counter := 0.U
}
io.pwmOut := (counter < io.duty)
}
以上是PWM模块,在时钟计数器小于占空比时输出高电平,否则输出低电平。
class Breath extends Blinker {
private val pwmDuty = RegInit(1.U(7.W))
private val increaseDuty = RegInit(true.B) // A flag for duty cycle change direction
private val firstCycleFinished = RegInit(false.B) // Whether the first cycle is finished
when(clockCnt < realPeriod * 500.U - 1.U) { // For duty cycle change, change the duty every 1/200 of the period
clockCnt := clockCnt + 1.U
}.otherwise {
clockCnt := 0.U
pwmDuty := Mux(increaseDuty, pwmDuty + 1.U, pwmDuty - 1.U)
when(pwmDuty === 100.U) {
increaseDuty := false.B
}.elsewhen(pwmDuty === 1.U) {
increaseDuty := true.B
}.elsewhen(pwmDuty === 0.U) {
currentLed := Mux(firstCycleFinished, currentLed + 1.U, currentLed) // Change the current led every 2 cycles
firstCycleFinished := ~firstCycleFinished // Toggle the flag
}
}
private val pwmGenerator = Module(new PWMGenerator)
pwmGenerator.clock := clock
pwmGenerator.io.duty := pwmDuty
io.out := pwmGenerator.io.pwmOut << currentLed
}
以上是呼吸灯模块,包含一个占空比寄存器、一个标识占空比变化方向的寄存器。每当要改变占空比时根据标识寄存器修改占空比寄存器的值;当占空比达到极端值时改变标识;当占空比降到0的时候翻转firstCycleFinished寄存器并判断是否切换到下一个LED。
class FadeOut extends Module {
val io = IO(new Bundle {
val realPeriod = Input(UInt(3.W)) // 1 ~ 4 seconds
val out = Output(Bool())
})
val pwmDuty = RegInit(100.U(7.W))
private val clockCnt = RegInit(0.U(12.W)) // For duty cycle change, the longest switching time is 20ms, which is 2000 clock cycles
when(clockCnt < io.realPeriod * 500.U - 1.U) {
clockCnt := clockCnt + 1.U
}.otherwise { // Change the duty every 1/100 of the period
clockCnt := 0.U
pwmDuty := Mux(pwmDuty > 0.U, pwmDuty - 1.U, 0.U)
}
private val pwmGen = Module(new PWMGenerator)
pwmGen.io.duty := pwmDuty
io.out := pwmGen.io.pwmOut
}
以上是渐灭信号模块,它复位后输出的PWM占空比会逐渐从100降至0。
class WaterFlow extends Module {
val io = IO(new Bundle {
val period = Input(UInt(2.W)) // 1 ~ 4 seconds
val bidirectional = Input(Bool())
val out = Output(UInt(8.W))
})
val realPeriod = Wire(UInt(3.W))
realPeriod := (0.U ## io.period) + 1.U
private val fadeOutMods = List.fill(8)(Module(new FadeOut))
private val syncReg = RegInit(0.U(8.W)) // Synchronize the fade-out signals by resetting them
for (i <- 0 until 8) {
fadeOutMods(i).io.realPeriod := realPeriod
fadeOutMods(i).reset := syncReg(i)
}
private val toSync = RegInit(false.B) // A flag for synchronization
private val currentLed = RegInit(0.U(3.W)) // 3 bit register for current led in mono-directional mode
private val led2LightUp = RegInit("b10000001".U(8.W)) // 8 bit register for led to be lit in bidirectional mode
private val direction = RegInit(1.B) // 1: inner, 0: outer
private val clockCnt = RegInit(0.U(17.W)) // Each clock cycle is 10us, the longest switching time is 0.5s, which is 50000 clock cycles
when(clockCnt < realPeriod * 12500.U - 1.U) {
clockCnt := clockCnt + 1.U
}.otherwise {
clockCnt := 0.U
toSync := true.B
}
when(toSync) {
syncReg := Mux(io.bidirectional, led2LightUp, (1.B << currentLed).asUInt)
toSync := false.B
currentLed := currentLed + 1.U
switch(led2LightUp) { // Switch the LEDs with an FSM
is("b10000001".U) {
led2LightUp := "b01000010".U
direction := 1.B
}
is("b01000010".U) {
led2LightUp := Mux(direction, "b00100100".U, "b10000001".U)
}
is("b00100100".U) {
led2LightUp := Mux(direction, "b00011000".U, "b01000010".U)
}
is("b00011000".U) {
led2LightUp := "b00100100".U
direction := 0.B
}
}
}.otherwise {
syncReg := 0.U
}
io.out := Cat(for (led <- fadeOutMods) yield led.io.out)
}
以上是流水灯模块,通过输入信号bidirectional切换单向/双向模式。
class SegDecoder extends Module {
val io = IO(new Bundle {
val data1 = Input(UInt(2.W))
val data2 = Input(UInt(2.W))
val seg1 = Output(UInt(7.W))
val seg2 = Output(UInt(7.W))
})
// Define the lookup table for the 7-segment display
private val lookupTable = VecInit(
"b0110000".U, // 1
"b1101101".U, // 2
"b1111001".U, // 3
"b0110011".U, // 4
)
io.seg1 := lookupTable(io.data1)
io.seg2 := lookupTable(io.data2)
}
以上是数码管译码器,用来把输入的0-3转换成1-4显示输出。
class SegDisplayAnimation extends Module {
val io = IO(new Bundle {
val segAtoF = Output(UInt(6.W))
})
private val segReg = RegInit(0.U(6.W))
segReg := Mux(segReg <= 1.U, 32.U, segReg >> 1.U)
io.segAtoF := segReg
}
以上是数码管动画模块,实际上是个循环的移位寄存器,使数码管转圈。
class LedShow extends Module {
val io = IO(new Bundle {
val buttons = Input(UInt(4.W)) // For mode selection
val switches = Input(UInt(4.W)) // For speed selection
val leds = Output(UInt(8.W))
val seg1 = Output(UInt(7.W))
val seg2 = Output(UInt(7.W))
val dig = Output(UInt(2.W))
val rgb1 = Output(UInt(3.W))
val rgb2 = Output(UInt(3.W))
})
private val clock100kHz = Module(new ClockDivider(12e6.toInt, 1e5.toInt)) // 12MHz -> 100kHz
private val clock2Hz = Module(new ClockDivider(1e5.toInt, 2)) // 100kHz -> 2Hz
clock100kHz.clock := clock
clock2Hz.clock := clock100kHz.io.clkOut
private val modeReg = RegInit(0.U(2.W)) // Blinking mode
modeReg :=
Mux(!io.buttons(0), 0.U,
Mux(!io.buttons(1), 1.U,
Mux(!io.buttons(2), 2.U,
Mux(!io.buttons(3), 3.U, modeReg))))
private val blinkPeriod = // Period - 1s
Mux(io.switches(0), 0.U,
Mux(io.switches(1), 1.U,
Mux(io.switches(2), 2.U, 3.U)))
private val periodValid = (io.switches === "b1000".U) || (io.switches === "b0100".U) || (io.switches === "b0010".U) || (io.switches === "b0001".U)
private val segDecoder = Module(new SegDecoder) // 7-segment decoder
private val segAnimation = Module(new SegDisplayAnimation) // PPlay a simple animation when the switches are invalid
segDecoder.io.data1 := modeReg
segDecoder.io.data2 := blinkPeriod
segAnimation.clock := clock2Hz.io.clkOut
io.seg1 := segDecoder.io.seg1 //Showing the mode
io.seg2 := Mux(periodValid, segDecoder.io.seg2, segAnimation.io.segAtoF ## 0.U(1.W)) //Showing the period or animation
io.dig := 0.U // Always enable the displays
private val heartBeatModule = Module(new HeartBeat) // Blinking Mode 0: Heartbeat
heartBeatModule.clock := clock2Hz.io.clkOut
heartBeatModule.io.period := blinkPeriod
heartBeatModule.reset := ~io.buttons(0)
private val breatheModule = Module(new Breath) // Blinking Mode 1: Breathe
breatheModule.clock := clock100kHz.io.clkOut
breatheModule.io.period := blinkPeriod
breatheModule.reset := ~io.buttons(1)
private val waterFlowModule = Module(new WaterFlow) // Blinking Mode 2, 3: Water-flow
waterFlowModule.clock := clock100kHz.io.clkOut
waterFlowModule.io.period := blinkPeriod
waterFlowModule.io.bidirectional := modeReg(0) // Mode 2: Mono-directional, Mode 3: Bidirectional
waterFlowModule.reset := ~io.buttons(2)
private val blinkingModulesOut = VecInit(heartBeatModule.io.out, breatheModule.io.out,
waterFlowModule.io.out, waterFlowModule.io.out)
io.leds := ~Mux(periodValid, blinkingModulesOut(modeReg), 0.U) // Active low, only play it when the switches are valid
// The RGB LEDs are temporarily unused, so just turn them off
private val rgbModule1 = Module(new RGBController)
private val rgbModule2 = Module(new RGBController)
rgbModule1.io.r_val := 0.U
rgbModule1.io.g_val := 0.U
rgbModule1.io.b_val := 0.U
rgbModule2.io.r_val := 0.U
rgbModule2.io.g_val := 0.U
rgbModule2.io.b_val := 0.U
io.rgb1 := ~rgbModule1.io.out // Active low
io.rgb2 := ~rgbModule2.io.out // Active low
}
以上是顶层模块,它输入拨码开关和按键的信号来设置显示模式和周期,在这里面例化了上述几个点灯模块,连接它们的时钟和复位信号,把显示周期连接到它们的周期输入端口;通过一个Mux选择显示模式输出到LED。此外该作品不需要用到RGB灯,所以也把它们给关了。
object Fuck extends App {
ChiselStage.emitSystemVerilogFile(new LedShow,
firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info", "--split-verilog", "-o=build",
"--lowering-options=disallowLocalVariables,disallowPackedArrays,locationInfoStyle=wrapInAtSquareBracket,noAlwaysComb"))
}
以上是工程的程序入口,在里面调用Chisel的生成Verilog文件方法,添加一些参数来确保生成的代码里面不包含System Verilog特有的语法,因为WebIDE不能正确识别它们。
上述代码已经开源:https://github.com/redstonee/StepFPGA-LEDShow/
(生成的Verilog文件在工程的build目录里)
仿真波形图
Figure 18数码管译码器仿真波形
Figure 19心跳灯仿真波形
Figure 20 120分频模块仿真波形(12MHz -> 100kHz)
Figure 21 PWM模块仿真波形
Figure 22呼吸灯仿真波形
Figure 23渐灭模块仿真波形
Figure 24单向流水灯仿真波形(1)
Figure 25单向流水灯仿真波形(2)
Figure 26双向流水灯仿真波形(1)
Figure 27单向流水灯仿真波形(2)
FPGA的资源利用说明
Figure 28逻辑单元使用情况
Figure 29时钟资源使用情况
Figure 30 IO使用情况