Initial commit
This commit is contained in:
198
hny2026/src/Hny2026.scala
Normal file
198
hny2026/src/Hny2026.scala
Normal file
@@ -0,0 +1,198 @@
|
||||
package hny2026
|
||||
|
||||
import circt.stage.ChiselStage
|
||||
import chisel3._
|
||||
import chisel3.util._
|
||||
|
||||
/**
|
||||
* Generates a single-cycle strobe pulse at a rate determined by the supplied configuration. The
|
||||
* strobe is used to time the transmission of bits from a character to the LED driver in {@link
|
||||
* CharSender}.
|
||||
*
|
||||
* <p>The strobe generator works by counting clock cycles so that a pulse occurs once every
|
||||
* <code>cntToInt</code> cycles plus a fractional adjustment. The fractional part is implemented by
|
||||
* a second counter that counts <code>fracCntTo</code> pulses before a full <code>cntToInt</code>
|
||||
* cycle is considered complete. This yields a frame rate accurate to within {@code
|
||||
* frameRateAccuracy}.</p>
|
||||
*
|
||||
* @param cfg configuration that contains the clock frequency, frame rate and accuracy target.
|
||||
*/
|
||||
class StrobeGenerator(cfg: HnyConfig) extends Module {
|
||||
val strobe = IO(Output(Bool()))
|
||||
|
||||
val cntTo = (((cfg.clockFreq / cfg.frameRate) / cfg.frameRateAccuracy).round * cfg.frameRateAccuracy)
|
||||
val cntToInt = cntTo.round
|
||||
val cntInt = RegInit(0.U(log2Up(cntToInt+1).W))
|
||||
val cntIntDone = cntInt === 0.U
|
||||
val fracPart = cntTo - cntToInt
|
||||
val cntFracDone = Wire(Bool())
|
||||
|
||||
strobe := cntIntDone
|
||||
|
||||
if (fracPart != 0) {
|
||||
val fracCntTo = (1 / fracPart.abs).toInt
|
||||
val fracCnt = RegInit(0.U(log2Up(fracCntTo).W))
|
||||
|
||||
when(cntIntDone) {
|
||||
when(cntFracDone) {
|
||||
fracCnt := 0.U
|
||||
} otherwise {
|
||||
fracCnt := fracCnt + 1.U
|
||||
}
|
||||
}
|
||||
|
||||
cntFracDone := fracCnt === (fracCntTo-1).U
|
||||
} else {
|
||||
cntFracDone := false.B
|
||||
}
|
||||
|
||||
when(cntIntDone) {
|
||||
when(cntFracDone) {
|
||||
cntInt := {
|
||||
if (fracPart > 0)
|
||||
cntToInt.U
|
||||
else
|
||||
(cntToInt - 2).U
|
||||
}
|
||||
} otherwise {
|
||||
cntInt := (cntToInt - 1).U
|
||||
}
|
||||
} otherwise {
|
||||
cntInt := cntInt - 1.U
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor for a strobe generator.
|
||||
*
|
||||
* @param cfg configuration for the strobe generator.
|
||||
* @return a {@code Bool} that goes high for one cycle at the desired frame rate.
|
||||
*/
|
||||
object StrobeGenerator {
|
||||
def apply(cfg: HnyConfig): Bool = {
|
||||
val sg = Module(new StrobeGenerator(cfg))
|
||||
sg.strobe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a single byte of data to the LED matrix. The module implements a 1-bit wide serial output
|
||||
* using the two signals {@code one} and {@code zero}. When the {@code strobe} from {@link
|
||||
* StrobeGenerator} goes high, the next bit of the current character is shifted out. The MSB is a
|
||||
* parity bit that is the XOR of all data bits. The last bit is a flag that indicates
|
||||
* that the character has been fully transmitted.
|
||||
*
|
||||
* @param cfg configuration that defines the data width and other timing parameters.
|
||||
*/
|
||||
class CharSender(cfg: HnyConfig) extends Module {
|
||||
val io = IO(new Bundle {
|
||||
val data = Flipped(Decoupled(UInt(cfg.dataWidth.W)))
|
||||
val one = Bool()
|
||||
val zero = Bool()
|
||||
})
|
||||
|
||||
val strobe = StrobeGenerator(cfg)
|
||||
val send = RegInit(false.B)
|
||||
val data = Reg(Bits((cfg.dataWidth+2).W)) // Extra bits for parity and done flag
|
||||
val one = RegInit(false.B)
|
||||
val zero = RegInit(false.B)
|
||||
val parity = io.data.bits.xorR
|
||||
|
||||
io.data.ready := !send
|
||||
io.one := one
|
||||
io.zero := zero
|
||||
|
||||
when(send) {
|
||||
when(strobe) {
|
||||
data := data >> 1
|
||||
|
||||
when(data.head(data.getWidth-1) === 0.U) {
|
||||
send := false.B
|
||||
} otherwise {
|
||||
one := data(0)
|
||||
zero := ~data(0)
|
||||
}
|
||||
}
|
||||
} otherwise {
|
||||
one := false.B
|
||||
zero := false.B
|
||||
|
||||
when(io.data.valid) {
|
||||
data := true.B ## parity ## io.data.bits
|
||||
send := true.B
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level module that drives the RGB LEDs to display a string. Each character of the provided
|
||||
* string is sent to the LED driver one after the other. The module uses a {@link CharSender}
|
||||
* instance for the actual bit-streaming and routes the {@code one} and {@code zero} signals to the
|
||||
* red and green LEDs respectively.
|
||||
*
|
||||
* @param cfg configuration that controls the serial timing and data width.
|
||||
* @param str string to display on the LED matrix.
|
||||
*/
|
||||
class HNY2026(cfg: HnyConfig, str: String) extends Module {
|
||||
val io = IO(new Bundle {
|
||||
val ledR = Output(Bool())
|
||||
val ledG = Output(Bool())
|
||||
val ledB = Output(Bool())
|
||||
})
|
||||
|
||||
val sender = Module(new CharSender(cfg))
|
||||
val chars = VecInit(str.map(c => c.toByte.U(cfg.dataWidth.W)))
|
||||
val charCnt = RegInit(UInt(log2Up(str.length()).W), 0.U)
|
||||
|
||||
sender.io.data.valid := true.B
|
||||
sender.io.data.bits := chars(charCnt)
|
||||
|
||||
when(sender.io.data.ready) {
|
||||
when(charCnt === (chars.length - 1).U) {
|
||||
charCnt := 0.U
|
||||
} otherwise {
|
||||
charCnt := charCnt + 1.U
|
||||
}
|
||||
}
|
||||
|
||||
io.ledR := sender.io.one
|
||||
io.ledG := sender.io.zero
|
||||
io.ledB := false.B
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point that parses command-line arguments, creates a {@link HnyConfig} instance and emits
|
||||
* the corresponding SystemVerilog file for the {@link HNY2026} module.
|
||||
*
|
||||
* <pre>
|
||||
* mill hny2026.runMain hny2026.HNY2026
|
||||
* </pre>
|
||||
*
|
||||
* Available arguments:
|
||||
* <ul>
|
||||
* <li><b>clockFreq</b> – clock frequency in Hz (default 27 MHz)</li>
|
||||
* </ul>
|
||||
*/
|
||||
object HNY2026 extends App {
|
||||
val argsMap = args.map { s =>
|
||||
val ss = s.split("=")
|
||||
if (ss.length == 1)
|
||||
(ss(0), "")
|
||||
else
|
||||
(ss(0), ss(1))
|
||||
}.toMap
|
||||
|
||||
val clockFreq = argsMap.getOrElse("clockFreq", "27000000").toInt
|
||||
println(s"Clock frequency = $clockFreq Hz")
|
||||
|
||||
val cfg = HnyConfig(
|
||||
clockFreq = clockFreq,
|
||||
frameRate = 30,
|
||||
frameRateAccuracy = 0.0001,
|
||||
dataWidth = 8
|
||||
)
|
||||
|
||||
ChiselStage.emitSystemVerilogFile(
|
||||
new HNY2026(cfg, "HNY2026! Vsem dobra :)")
|
||||
)
|
||||
}
|
||||
35
hny2026/src/HnyConfig.scala
Normal file
35
hny2026/src/HnyConfig.scala
Normal file
@@ -0,0 +1,35 @@
|
||||
package hny2026
|
||||
|
||||
import chisel3._
|
||||
import chisel3.util._
|
||||
|
||||
/**
|
||||
* Configuration container for the HNY2026 design.
|
||||
*
|
||||
* <p>The parameters control the timing of the strobe generator and the serial transmission of
|
||||
* characters to the LED matrix.</p>
|
||||
*
|
||||
* @param clockFreq The frequency of the input clock in hertz.
|
||||
* Default is 27 MHz.
|
||||
* @param frameRate Desired frame rate in frames-per-second.
|
||||
* The design uses this to calculate the
|
||||
* strobe period; e.g. 30 fps is the
|
||||
* default for my phone camera.
|
||||
* @param frameRateAccuracy The acceptable relative error on the frame
|
||||
* rate. A smaller value yields a more
|
||||
* accurate strobe but may increase the
|
||||
* hardware resource usage. The default
|
||||
* (0.0001) gives <0.01 % error.
|
||||
* @param dataWidth Width in bits of the data payload for each
|
||||
* character. The default is 8 bits, matching
|
||||
* an ASCII byte. The actual transmitted
|
||||
* stream is `dataWidth + 2` bits, the
|
||||
* additional bits being a parity bit and a
|
||||
* empty ending bit.
|
||||
*/
|
||||
case class HnyConfig(
|
||||
clockFreq: Int = 27000000,
|
||||
frameRate: Double = 30.0,
|
||||
frameRateAccuracy: Double = 0.0001,
|
||||
dataWidth: Int = 8
|
||||
)
|
||||
44
hny2026/src/TestGen.scala
Normal file
44
hny2026/src/TestGen.scala
Normal file
@@ -0,0 +1,44 @@
|
||||
package hny2026
|
||||
|
||||
import circt.stage.ChiselStage
|
||||
import chisel3._
|
||||
import chisel3.util._
|
||||
|
||||
/**
|
||||
* Run: mill hny2026.runMain hny2026.TestGen_StrobeGenerator
|
||||
*/
|
||||
object TestGen_StrobeGenerator extends App {
|
||||
println(ChiselStage.emitSystemVerilog(
|
||||
new StrobeGenerator(HnyConfig(27000000, 30.3)),
|
||||
firtoolOpts = Array(
|
||||
"--disable-all-randomization",
|
||||
"--strip-debug-info"
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Run: mill hny2026.runMain hny2026.TestGen_CharSender
|
||||
*/
|
||||
object TestGen_CharSender extends App {
|
||||
println(ChiselStage.emitSystemVerilog(
|
||||
new CharSender(HnyConfig(27000000, 30.3)),
|
||||
firtoolOpts = Array(
|
||||
"--disable-all-randomization",
|
||||
"--strip-debug-info"
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Run: mill hny2026.runMain hny2026.TestGen_HNY2026
|
||||
*/
|
||||
object TestGen_HNY2026 extends App {
|
||||
println(ChiselStage.emitSystemVerilog(
|
||||
new HNY2026(HnyConfig(27000000, 30.3), "Hello!"),
|
||||
firtoolOpts = Array(
|
||||
"--disable-all-randomization",
|
||||
"--strip-debug-info"
|
||||
)
|
||||
))
|
||||
}
|
||||
185
hny2026/test/src/Hny2026.scala
Normal file
185
hny2026/test/src/Hny2026.scala
Normal file
@@ -0,0 +1,185 @@
|
||||
package hny2026.tests
|
||||
|
||||
import chisel3._
|
||||
import chisel3.simulator.scalatest.ChiselSim
|
||||
import chisel3.simulator.HasSimulator
|
||||
import svsim.verilator.Backend.CompilationSettings
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import scala.util.Random
|
||||
|
||||
import hny2026._
|
||||
|
||||
/**
|
||||
* Run all tests:
|
||||
* $ mill hny2026.test
|
||||
*
|
||||
* Run only this test:
|
||||
* $ mill hny2026.test.testOnly hny2026.tests.StrobeGeneratorTest
|
||||
*/
|
||||
class StrobeGeneratorTest extends AnyFlatSpec with ChiselSim {
|
||||
val clockFreq = 10000
|
||||
|
||||
// Enable tracing (see: https://github.com/chipsalliance/chisel/discussions/3957)
|
||||
implicit val verilator: HasSimulator = HasSimulator.simulators
|
||||
.verilator(verilatorSettings =
|
||||
CompilationSettings.default.withTraceStyle(
|
||||
Some(
|
||||
CompilationSettings.TraceStyle(
|
||||
kind = CompilationSettings.TraceKind.Vcd))))
|
||||
|
||||
/**
|
||||
* Use Chisel testbench because ChiselSim don't support fork-join constructions.
|
||||
*
|
||||
* From documentation (https://www.chisel-lang.org/docs/appendix/migrating-from-chiseltest):
|
||||
* > ChiselSim also does not currently have any support for fork-join, so any tests
|
||||
* > using those constructs will need to be rewritten in a single-threaded manner.
|
||||
*
|
||||
* @param frameRate
|
||||
*/
|
||||
class StrobeGeneratorTB(frameRate: Double) extends Module {
|
||||
val io = IO(new Bundle {
|
||||
val duration = Input(UInt(32.W))
|
||||
val strobeCount = Output(UInt(32.W))
|
||||
val start = Input(Bool())
|
||||
val done = Output(Bool())
|
||||
})
|
||||
|
||||
val strobe = StrobeGenerator(HnyConfig(clockFreq, frameRate))
|
||||
val cycles = RegInit(chiselTypeOf(io.duration), 0.U)
|
||||
val strobes = RegInit(chiselTypeOf(io.strobeCount), 0.U)
|
||||
val count = RegInit(false.B)
|
||||
val done = RegInit(false.B)
|
||||
|
||||
io.strobeCount := strobes
|
||||
io.done := done
|
||||
|
||||
when(!done) {
|
||||
when(count) {
|
||||
when(cycles === io.duration) {
|
||||
count := false.B
|
||||
done := true.B
|
||||
}
|
||||
} otherwise {
|
||||
when(io.start) {
|
||||
count := true.B
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when(count) {
|
||||
cycles := cycles + 1.U
|
||||
when(strobe) {
|
||||
strobes := strobes + 1.U
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Add description
|
||||
*
|
||||
* @param dut
|
||||
* @param frameRate
|
||||
* @return
|
||||
*/
|
||||
def check(dut: StrobeGeneratorTB)(implicit frameRate: Double): Unit = {
|
||||
val scale = if ((clockFreq / frameRate).isWhole) 10 else 100
|
||||
val expect = (frameRate * scale).round.toInt
|
||||
val duration = (clockFreq * scale).toInt
|
||||
|
||||
enableWaves()
|
||||
dut.io.duration.poke(duration)
|
||||
dut.io.start.poke(true)
|
||||
dut.clock.stepUntil(dut.io.done, 1, duration + 10)
|
||||
|
||||
val strobes = dut.io.strobeCount.peek().litValue
|
||||
|
||||
println(s"$strobes strobes counted. Expected [${expect-1}..${expect+1}]")
|
||||
assert((strobes >= expect - 1) && (strobes <= expect + 1))
|
||||
}
|
||||
|
||||
behavior of "StrobeGenerator"
|
||||
|
||||
it should "produce 25 strobes per second" in {
|
||||
implicit val frameRate = 25.0
|
||||
simulate(new StrobeGeneratorTB(frameRate))(check)
|
||||
}
|
||||
|
||||
it should "produce 30 strobes per second" in {
|
||||
implicit val frameRate = 30.0
|
||||
simulate(new StrobeGeneratorTB(frameRate))(check)
|
||||
}
|
||||
|
||||
it should "produce 30.1 strobes per second" in {
|
||||
implicit val frameRate = 30.1
|
||||
simulate(new StrobeGeneratorTB(frameRate))(check)
|
||||
}
|
||||
|
||||
it should "produce 30.4 strobes per second" in {
|
||||
implicit val frameRate = 30.4
|
||||
simulate(new StrobeGeneratorTB(frameRate))(check)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run only this test:
|
||||
* $ mill hny2026.test.testOnly hny2026.tests.CharSenderTest
|
||||
*/
|
||||
class CharSenderTest extends AnyFlatSpec with ChiselSim {
|
||||
val cfg = HnyConfig(1000, 25)
|
||||
val dataLength = 10
|
||||
|
||||
// Enable tracing (see: https://github.com/chipsalliance/chisel/discussions/3957)
|
||||
implicit val verilator: HasSimulator = HasSimulator.simulators
|
||||
.verilator(verilatorSettings =
|
||||
CompilationSettings.default.withTraceStyle(
|
||||
Some(
|
||||
CompilationSettings.TraceStyle(
|
||||
kind = CompilationSettings.TraceKind.Vcd))))
|
||||
|
||||
behavior of "CharSender"
|
||||
|
||||
it should s"send $dataLength random bytes" in {
|
||||
simulate(new CharSender(cfg)) { dut =>
|
||||
enableWaves()
|
||||
val data = List.fill(dataLength)(Random.nextInt(Math.pow(2, cfg.dataWidth).toInt))
|
||||
|
||||
data.foreach { x =>
|
||||
dut.io.data.bits.poke(x)
|
||||
dut.io.data.valid.poke(true.B)
|
||||
dut.clock.step()
|
||||
dut.clock.stepUntil(dut.io.data.ready, 1, 1000)
|
||||
dut.io.data.valid.poke(false.B)
|
||||
}
|
||||
|
||||
dut.clock.step((cfg.clockFreq / cfg.frameRate * cfg.dataWidth * 2).toInt)
|
||||
println("See results in the wave diagram.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run only this test:
|
||||
* $ mill hny2026.test.testOnly hny2026.tests.HNY2026Test
|
||||
*/
|
||||
class HNY2026Test extends AnyFlatSpec with ChiselSim {
|
||||
val cfg = HnyConfig(1000, 25)
|
||||
val str = "Hello!"
|
||||
|
||||
// Enable tracing (see: https://github.com/chipsalliance/chisel/discussions/3957)
|
||||
implicit val verilator: HasSimulator = HasSimulator.simulators
|
||||
.verilator(verilatorSettings =
|
||||
CompilationSettings.default.withTraceStyle(
|
||||
Some(
|
||||
CompilationSettings.TraceStyle(
|
||||
kind = CompilationSettings.TraceKind.Vcd))))
|
||||
|
||||
behavior of "HNY2026"
|
||||
|
||||
it should s"send string '$str'" in {
|
||||
simulate(new HNY2026(cfg, str)) { dut =>
|
||||
enableWaves()
|
||||
dut.clock.step((cfg.clockFreq / cfg.frameRate * cfg.dataWidth * str.length() * 2).toInt)
|
||||
println("See results in the wave diagram.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user