hny-2026/hny2026/src/Hny2026.scala
2026-01-06 22:43:26 +03:00

199 lines
5.4 KiB
Scala
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 :)")
)
}