Initial commit
This commit is contained in:
commit
d7704ae61e
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.metals
|
||||||
|
.bsp
|
||||||
|
/out
|
||||||
|
/generated
|
||||||
|
/build
|
||||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# HNY2026 - New Year's Greeting on FPGA
|
||||||
|
|
||||||
|
A Chisel-based FPGA design that transmits a New Year's greeting message via LED patterns on a TangNano 1K development board.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The greeting is encoded as:
|
||||||
|
|
||||||
|
- Green LED = bit '0'
|
||||||
|
- Red LED = bit '1'
|
||||||
|
- 8-bit ASCII (LSB) + parity bit + empty bit
|
||||||
|
|
||||||
|
## FPGA Workflow (TangNano 1K)
|
||||||
|
|
||||||
|
**Full build and load to FPGA SRAM:**
|
||||||
|
```bash
|
||||||
|
mill tangNano1k.load
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full build and burn FPGA flash:**
|
||||||
|
```bash
|
||||||
|
mill tangNano1k.burn
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step-by-step:**
|
||||||
|
```bash
|
||||||
|
mill tangNano1k.generate # Generate SystemVerilog
|
||||||
|
mill tangNano1k.synth # Synthesize with Yosys
|
||||||
|
mill tangNano1k.pnr # Place and route with Nextpnr
|
||||||
|
mill tangNano1k.bitstream # Generate bitstream
|
||||||
|
mill tangNano1k.load # Load to FPGA SRAM
|
||||||
|
mill tangNano1k.burn # Burn to FPGA flash
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run all unit tests:**
|
||||||
|
```bash
|
||||||
|
mill hny2026.test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Scala and [Mill](https://mill-build.org/mill/cli/installation-ide.html)
|
||||||
|
- [Yosys](https://github.com/YosysHQ/yosys) with [Slang plugin](https://github.com/povik/yosys-slang)
|
||||||
|
- [Nextpnr](https://github.com/YosysHQ/nextpnr)
|
||||||
|
- Gowin pack tool (from [Apicula](https://github.com/YosysHQ/apicula) package)
|
||||||
|
- [openFPGALoader](https://github.com/trabucayre/openFPGALoader)
|
||||||
|
- [TangNano 1K](https://wiki.sipeed.com/hardware/en/tang/Tang-Nano-1K/Nano-1k.html) development board (GW1NZ-LV1QN48C6/I5)
|
||||||
|
|
||||||
|
Yosys and other utilities can be taken from the [OSS CAD Suite](https://github.com/YosysHQ/oss-cad-suite-build) package or installed using the package manager [Nix](https://nixos.org/download/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-shell shell.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decoding the Message
|
||||||
|
|
||||||
|
Capture the LED blinking on video (30 fps recommended), then extract the bit pattern: green=0, red=1.
|
||||||
229
build.mill
Normal file
229
build.mill
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
import mill._
|
||||||
|
import mill.scalalib._
|
||||||
|
import mill.javalib._
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import scala.sys.process._
|
||||||
|
import mill.api.Task.Simple
|
||||||
|
|
||||||
|
object hny2026 extends ScalaModule { module =>
|
||||||
|
def scalaVersion = "2.13.18"
|
||||||
|
val chiselVersion = "7.6.0"
|
||||||
|
val scalaTestVersion = "3.2.19"
|
||||||
|
val main = "hny2026.HNY2026"
|
||||||
|
|
||||||
|
def scalacOptions = Seq(
|
||||||
|
"-language:reflectiveCalls",
|
||||||
|
"-language:implicitConversions",
|
||||||
|
"-deprecation"
|
||||||
|
)
|
||||||
|
|
||||||
|
def mvnDeps = Seq(
|
||||||
|
mvn"org.chipsalliance::chisel:$chiselVersion",
|
||||||
|
)
|
||||||
|
|
||||||
|
def scalacPluginMvnDeps = Seq(
|
||||||
|
mvn"org.chipsalliance:::chisel-plugin:$chiselVersion",
|
||||||
|
)
|
||||||
|
|
||||||
|
object test extends ScalaTests with TestModule.ScalaTest {
|
||||||
|
def mvnDeps = module.mvnDeps() ++ Seq(
|
||||||
|
mvn"org.scalatest::scalatest::${module.scalaTestVersion}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common build flow tasks.
|
||||||
|
*/
|
||||||
|
trait Flow extends Module {
|
||||||
|
/**
|
||||||
|
* Optional top selection.
|
||||||
|
*/
|
||||||
|
def top: Option[String] = None
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clock frequency in Hz.
|
||||||
|
*/
|
||||||
|
def clockFrequency: Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* override def staticSrc = Some(Task.Sources("src0.v", "src1.v", ...))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
def staticSrc: Option[Simple[Seq[PathRef]]] = None
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Chisel project.
|
||||||
|
*
|
||||||
|
* @return list of generated SV sources.
|
||||||
|
*/
|
||||||
|
def generate = Task {
|
||||||
|
hny2026.runner().run(Seq(s"clockFreq=$clockFrequency"), hny2026.main)
|
||||||
|
|
||||||
|
File(Task.dest.toURI).listFiles{
|
||||||
|
(_, fileName) => fileName.endsWith((".sv"))
|
||||||
|
}.map(f => PathRef(os.Path(f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gowin flow with Yosys and Nextpnr.
|
||||||
|
*/
|
||||||
|
trait GowinFlow extends Flow {
|
||||||
|
def family: String
|
||||||
|
def device: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overriding example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* override def cstFile = Task.Source("resources/hny2026.cst")
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
def cstFile: Simple[PathRef]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synth with yosys.
|
||||||
|
*
|
||||||
|
* @return path to json netlist.
|
||||||
|
*/
|
||||||
|
def synth = Task {
|
||||||
|
val out = Task.dest
|
||||||
|
val synthJson = out / "synth.json"
|
||||||
|
val genSrc = generate().map(_.path).mkString(" ")
|
||||||
|
|
||||||
|
val stSrc = if (staticSrc.isDefined) {
|
||||||
|
(staticSrc.get)().map(_.path).mkString(" ")
|
||||||
|
} else ""
|
||||||
|
|
||||||
|
val topCmd = top match {
|
||||||
|
case Some(t) => s"-top $t"
|
||||||
|
case _ => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val yosysSlangPlugin = sys.env("YOSYS_SLANG_SO")
|
||||||
|
|
||||||
|
os.call((
|
||||||
|
"yosys", "-m", yosysSlangPlugin, "-l", s"$out/yosys.log", "-p",
|
||||||
|
s"read_slang $genSrc $stSrc; synth_gowin $topCmd -nowidelut -json $synthJson"
|
||||||
|
))
|
||||||
|
|
||||||
|
PathRef(synthJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place and route with nextpnr.
|
||||||
|
*
|
||||||
|
* @return path to PnR json.
|
||||||
|
*/
|
||||||
|
def pnr = Task {
|
||||||
|
val out = Task.dest
|
||||||
|
val pnrJson = out / "placenroute.json"
|
||||||
|
val synthJson = synth().path
|
||||||
|
val clockMhz = (clockFrequency.toDouble / 1000000).floor.toInt
|
||||||
|
|
||||||
|
os.call((
|
||||||
|
"nextpnr-himbaechel",
|
||||||
|
"--json", synthJson,
|
||||||
|
"--write", pnrJson,
|
||||||
|
"--freq", s"$clockMhz",
|
||||||
|
"--device", device,
|
||||||
|
"--vopt", s"family=$family",
|
||||||
|
"--vopt", s"cst=${cstFile().path}",
|
||||||
|
"-l", s"$out/nextpnr.log"
|
||||||
|
))
|
||||||
|
|
||||||
|
PathRef(pnrJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build bitstream.
|
||||||
|
*
|
||||||
|
* @return path to bitstream file.
|
||||||
|
*/
|
||||||
|
def bitstream = Task {
|
||||||
|
val out = Task.dest
|
||||||
|
val bs = out / "bitstream.fs"
|
||||||
|
val pnrJson = pnr().path
|
||||||
|
|
||||||
|
os.call(("gowin_pack", "-d", device, "-o", bs, pnrJson))
|
||||||
|
|
||||||
|
PathRef(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load bitstream into FPGA SRAM.
|
||||||
|
*/
|
||||||
|
def load(args: String*) = Task.Command {
|
||||||
|
val bs = bitstream().path
|
||||||
|
os.call(
|
||||||
|
cmd = ("openFPGALoader", "-c", "ft2232", "-m", bs),
|
||||||
|
stdout = os.Inherit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Birn FPGA flash with bitstream.
|
||||||
|
*/
|
||||||
|
def burn(args: String*) = Task.Command {
|
||||||
|
val bs = bitstream().path
|
||||||
|
os.call(
|
||||||
|
cmd = ("openFPGALoader", "-c", "ft2232", "--unprotect-flash", "-f", bs),
|
||||||
|
stdout = os.Inherit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TangNano 1K target.
|
||||||
|
*
|
||||||
|
* Build command.
|
||||||
|
*
|
||||||
|
* Generate SystemVerilog files from Chisel source:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.generate
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Synthesize the source code using Yosys:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.synth
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Place and route using Nextpnr:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.pnr
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Build bitstream:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.bitstream
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Load bitstream into FPGA's SRAM:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.load
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Burn FPGA flash with a bistream:
|
||||||
|
* ```
|
||||||
|
* $ mill tangNano1k.burn
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Each subsequent command automatically calls the previous one, so when 'burn' is invoked, all
|
||||||
|
* required steps will be performed – SV generation, synthesis, PnR, and bitstream assembly.
|
||||||
|
*/
|
||||||
|
object tangNano1k extends Module with GowinFlow {
|
||||||
|
def top = Some("hny2026_top")
|
||||||
|
def family = "GW1NZ-1"
|
||||||
|
def device = "GW1NZ-LV1QN48C6/I5"
|
||||||
|
def clockFrequency = 27000000
|
||||||
|
|
||||||
|
def staticSrc = Some(Task.Sources("verilog/hny2026_top.v"))
|
||||||
|
def cstFile = Task.Source("resources/hny2026.cst")
|
||||||
|
}
|
||||||
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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
shell.nix
Normal file
46
shell.nix
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
with pkgs;
|
||||||
|
let yosys-slang =
|
||||||
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
|
pname = "yosys-slang";
|
||||||
|
version = "64b44616a3798f07453b14ea03e4ac8a16b77313";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "povik";
|
||||||
|
repo = "yosys-slang";
|
||||||
|
rev = "${finalAttrs.version}";
|
||||||
|
sha256 = "sha256-kfu59/M3+IM+5ZMd+Oy4IZf4JWuVtPDlkHprk0FB8t4=";
|
||||||
|
fetchSubmodules = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
yosys
|
||||||
|
cmake
|
||||||
|
python3
|
||||||
|
];
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/lib
|
||||||
|
cp slang.so $out/lib/slang.so
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
in
|
||||||
|
mkShell {
|
||||||
|
packages = [
|
||||||
|
gnumake boost zlib patchelf mill verilator haskellPackages.sv2v
|
||||||
|
yosys yosys-slang nextpnr python313Packages.apycula openfpgaloader
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
## Shell name
|
||||||
|
export NIX_SHELL_NAME="[hny2026]"
|
||||||
|
export YOSYS_SLANG_SO="${yosys-slang}/lib/slang.so"
|
||||||
|
|
||||||
|
find $HOME/.cache/llvm-firtool/ -type f -executable -exec patchelf --set-interpreter ${pkgs.glibc}/lib64/ld-linux-x86-64.so.2 {} \;
|
||||||
|
find $HOME/.cache/llvm-firtool/ -type f -executable -exec patchelf --add-needed ${zlib}/lib/libz.so.1 {} \;
|
||||||
|
|
||||||
|
find $HOME/.cache/mill/ -type f -executable -exec patchelf --set-interpreter ${pkgs.glibc}/lib64/ld-linux-x86-64.so.2 {} \;
|
||||||
|
find $HOME/.cache/mill/ -type f -executable -exec patchelf --add-needed ${zlib}/lib/libz.so.1 {} \;
|
||||||
|
'';
|
||||||
|
}
|
||||||
18
tangNano1k/resources/hny2026.cst
Normal file
18
tangNano1k/resources/hny2026.cst
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//Copyright (C)2014-2021 Gowin Semiconductor Corporation.
|
||||||
|
//All rights reserved.
|
||||||
|
//File Title: Physical Constraints file
|
||||||
|
//GOWIN Version: 1.9.8
|
||||||
|
//Part Number: GW1NZ-LV1QN48C6/I5
|
||||||
|
//Device: GW1NZ-1
|
||||||
|
//Created Time: Thu 09 16 14:45:08 2021
|
||||||
|
|
||||||
|
IO_LOC "led[2]" 11;
|
||||||
|
IO_PORT "led[2]" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8;
|
||||||
|
IO_LOC "led[1]" 10;
|
||||||
|
IO_PORT "led[1]" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8;
|
||||||
|
IO_LOC "led[0]" 9;
|
||||||
|
IO_PORT "led[0]" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8;
|
||||||
|
IO_LOC "sys_rst_n" 13;
|
||||||
|
IO_PORT "sys_rst_n" IO_TYPE=LVCMOS33 PULL_MODE=UP;
|
||||||
|
IO_LOC "sys_clk" 47;
|
||||||
|
IO_PORT "sys_clk" IO_TYPE=LVCMOS33 PULL_MODE=UP;
|
||||||
26
tangNano1k/verilog/hny2026_top.v
Normal file
26
tangNano1k/verilog/hny2026_top.v
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// %SOURCE_FILE_HEADER%
|
||||||
|
//
|
||||||
|
|
||||||
|
module hny2026_top (
|
||||||
|
input wire sys_clk,
|
||||||
|
input wire sys_rst_n,
|
||||||
|
// 0 - R, 1 - B, 2 - G
|
||||||
|
output wire [2:0] led
|
||||||
|
);
|
||||||
|
|
||||||
|
reg [2:0] rst_sync;
|
||||||
|
wire reset = ~rst_sync[0];
|
||||||
|
always @(posedge sys_clk) rst_sync <= {sys_rst_n, rst_sync[2:1]};
|
||||||
|
|
||||||
|
wire [2:0] led_inv;
|
||||||
|
assign led = ~led_inv;
|
||||||
|
|
||||||
|
HNY2026 hny2026 (
|
||||||
|
.clock(sys_clk),
|
||||||
|
.reset(reset),
|
||||||
|
.io_ledR(led_inv[0]),
|
||||||
|
.io_ledG(led_inv[2]),
|
||||||
|
.io_ledB(led_inv[1])
|
||||||
|
);
|
||||||
|
|
||||||
|
endmodule // hny2026_top
|
||||||
BIN
video/video.mp4
Normal file
BIN
video/video.mp4
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user