From 697b5637c6b5c259dbec0eb73474c90f6310f124 Mon Sep 17 00:00:00 2001 From: Martin Schoeberl Date: Tue, 15 Oct 2024 15:22:59 -0700 Subject: [PATCH] Memory initialization --- build.sbt | 12 +++--- chisel-book.tex | 47 +++++++++++++++++----- src/main/scala/MultiClockMemory.scala | 2 + src/main/scala/memory.scala | 51 ++++++++++++++++++++++- src/test/scala/MemoryTest.scala | 58 ++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index f5bda33..f530b7f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ // scalaVersion := "2.13.8" -// scalaVersion := "2.13.10" -scalaVersion := "2.13.14" +scalaVersion := "2.13.10" +// scalaVersion := "2.13.14" scalacOptions ++= Seq( "-deprecation", @@ -12,12 +12,12 @@ scalacOptions ++= Seq( ) -/* + val chiselVersion = "3.5.6" addCompilerPlugin("edu.berkeley.cs" %% "chisel3-plugin" % chiselVersion cross CrossVersion.full) libraryDependencies += "edu.berkeley.cs" %% "chisel3" % chiselVersion libraryDependencies += "edu.berkeley.cs" %% "chiseltest" % "0.5.6" -*/ + /* val chiselVersion = "3.6.1" @@ -26,12 +26,12 @@ libraryDependencies += "edu.berkeley.cs" %% "chisel3" % chiselVersion libraryDependencies += "edu.berkeley.cs" %% "chiseltest" % "0.6.2" */ - +/* val chiselVersion = "5.3.0" addCompilerPlugin("org.chipsalliance" % "chisel-plugin" % chiselVersion cross CrossVersion.full) libraryDependencies += "org.chipsalliance" %% "chisel" % chiselVersion libraryDependencies += "edu.berkeley.cs" %% "chiseltest" % "5.0.2" - +*/ /* val chiselVersion = "6.5.0" diff --git a/chisel-book.tex b/chisel-book.tex index db14a70..5f6162d 100644 --- a/chisel-book.tex +++ b/chisel-book.tex @@ -3347,14 +3347,12 @@ \section{Memory} \longlist{code/memory_forwarding.txt}{A memory with a forwarding circuit.}{lst:memory:forward} -Chisel also provides \code{Mem}, which represents a memory with synchronous -write and an asynchronous read. As this memory type is usually not directly available -in an FPGA, the synthesize tool will build it out of flip-flops. -Therefore, we recommend using \code{SyncReadMem}. If asynchronous read behavior is needed and -the resources are available in the FPGA you are using (e.g., in the shape of LUTRAM on Xilinx -FPGAs), you can manually implement this as a \code{BlackBox}. Vendors typically provide -code templates that can be used directly for this. +As this pattern is common, Chisel provides an optional parameter in \code{SyncReadMem} +to define the read-during-write behavior. \code{WriteFirst} generates Verilog code that +includes the forwarding if needed. The other two options are \code{ReadFirst} +and \code{Undefined}. +\shortlist{code/memory_write_first.txt} Memories in FPGAs can be initialized with either binary or hexadecimal initialization files. The files are simple ASCII text files with the same number of lines as there are @@ -3367,6 +3365,24 @@ \section{Memory} %yet work in ChiselTest. Either way, Initializations are based around calls to \code{readmemb} or \code{readmemh}. +To initialize on-chip memory from Scala during generation time, we need to first +write the content into a file and then use \code{loadMemoryFromFile}. +Listing~\ref{lst:memory:init} shows an example of initializing a memory with a string. + +\longlist{code/memory_init.txt}{Memory initialization.}{lst:memory:init} + + +Chisel also provides \code{Mem}, which represents a memory with synchronous +write and an asynchronous read. As this memory type is usually not directly available +in an FPGA, the synthesize tool will build it out of flip-flops. +Therefore, we recommend using \code{SyncReadMem}. If asynchronous read behavior is needed and +the resources are available in the FPGA you are using (e.g., as a LUT RAM on Xilinx +FPGAs), you can manually implement this as a \code{BlackBox}. Vendors typically provide +code templates that can be used directly for this. + + + + \section{Exercises} Use the 7-segment encoder from the last exercise and add a 4-bit counter as input @@ -3396,8 +3412,21 @@ \section{Exercises} \shortlist{code/draw_acc.txt} -\todo{Luca: More exercises would be nice. Maybe in the future?} - +As an advanced exercise try using an on-chip memory. Instantiate a \code{SyncReadMem} +and create two communicating state machines. The first state machine writes a string +into the memory. When done, it starts the second state machine that reads back +the string from the memory. Test with simple \code{printf} statements in the +reading state machine. Be careful in the reading, that the read value comes one clock cycle +later then applying the address to the memory. +You can extend that example by writing a Chisel test to test the memory. + +Sometimes, one would like to download content of a memory from a laptop, +e.g., when building a processor to load a program. Assume you have a +serial port (see Section~\ref{sec:uart}) available that connects your FPGA +board to your laptop. Can you envision a protocol on the serial port +with a state machine in the FPGA to download memory content into +the FPGA after configuring it. Downloading at runtime also avoids synthesizing +for the FPGA again, after the memory content changes. \chapter{Input Processing} \label{sec:input} diff --git a/src/main/scala/MultiClockMemory.scala b/src/main/scala/MultiClockMemory.scala index 877e76e..2f32263 100644 --- a/src/main/scala/MultiClockMemory.scala +++ b/src/main/scala/MultiClockMemory.scala @@ -18,10 +18,12 @@ class MultiClockMemory(ports: Int, n: Int = 1024, w: Int = 32) extends Module { val ram = SyncReadMem(n, UInt(w.W)) + /* does not work in Chisel 3.5.6 for (i <- 0 until ports) { val p = io.ps(i) p.datao := ram.readWrite(p.addr, p.datai, p.en, p.we, p.clk.asClock) } + */ } //- end diff --git a/src/main/scala/memory.scala b/src/main/scala/memory.scala index c820fa1..6a73665 100644 --- a/src/main/scala/memory.scala +++ b/src/main/scala/memory.scala @@ -51,6 +51,27 @@ object ForwardingMemory extends App { // emitVerilog(new TrueDualPortMemory(), Array("--target-dir", "generated", "--target:fpga")) } +//- start memory_write_first +class MemoryWriteFirst() extends Module { + val io = IO(new Bundle { + val rdAddr = Input(UInt(10.W)) + val rdData = Output(UInt(8.W)) + val wrAddr = Input(UInt(10.W)) + val wrData = Input(UInt(8.W)) + val wrEna = Input(Bool()) + }) + + //- start memory_write_first + val mem = SyncReadMem(1024, UInt(8.W), SyncReadMem.WriteFirst) + //- end + + io.rdData := mem.read(io.rdAddr) + + when(io.wrEna) { + mem.write(io.wrAddr, io.wrData) + } +} + class TrueDualPortMemory() extends Module { val io = IO(new Bundle { val addrA = Input(UInt(10.W)) @@ -83,7 +104,7 @@ object TrueDualPortMemory extends App { // emitVerilog(new TrueDualPortMemory(), Array("--target-dir", "generated", "--target:fpga")) } -class InitMemory() extends Module { +class InitMemoryFile() extends Module { val io = IO(new Bundle { val rdAddr = Input(UInt(10.W)) val rdData = Output(UInt(8.W)) @@ -92,7 +113,7 @@ class InitMemory() extends Module { val wrEna = Input(Bool()) }) - //- start memory_init + //- start memory_init_file val mem = SyncReadMem(1024, UInt(8.W)) loadMemoryFromFile( mem, "./src/main/resources/init.hex", firrtl.annotations.MemoryLoadFileType.Hex @@ -107,3 +128,29 @@ class InitMemory() extends Module { } //- end } + +class InitMemory() extends Module { + val io = IO(new Bundle { + val rdAddr = Input(UInt(10.W)) + val rdData = Output(UInt(8.W)) + val wrAddr = Input(UInt(10.W)) + val wrData = Input(UInt(8.W)) + val wrEna = Input(Bool()) + }) + + //- start memory_init + val hello = "Hello, World!" + val helloHex = hello.map(_.toInt.toHexString).mkString("\n") + val file = new java.io.PrintWriter("hello.hex") + file.write(helloHex) + file.close() + + val mem = SyncReadMem(1024, UInt(8.W)) + loadMemoryFromFile(mem, "hello.hex") + //- end + + io.rdData := mem.read(io.rdAddr) + when(io.wrEna) { + mem.write(io.wrAddr, io.wrData) + } +} diff --git a/src/test/scala/MemoryTest.scala b/src/test/scala/MemoryTest.scala index b99a379..6fc8b23 100644 --- a/src/test/scala/MemoryTest.scala +++ b/src/test/scala/MemoryTest.scala @@ -29,7 +29,7 @@ class MemoryTest extends AnyFlatSpec with ChiselScalatestTester { dut.io.wrEna.poke(true.B) dut.io.rdAddr.poke(20.U) dut.clock.step() - println(s"Memory data: ${dut.io.rdData.peekInt()}") + // println(s"Memory data: ${dut.io.rdData.peekInt()}") } } @@ -81,7 +81,61 @@ class MemoryTest extends AnyFlatSpec with ChiselScalatestTester { dut.io.rdAddr.poke(20.U) dut.clock.step() dut.io.rdData.expect(123.U) - println(s"Memory data: ${dut.io.rdData.peekInt()}") + // println(s"Memory data: ${dut.io.rdData.peekInt()}") + } + } + + "Memory write first" should "pass" in { + test(new MemoryWriteFirst) { dut => + // Fill the memory + dut.io.wrEna.poke(true.B) + for (i <- 0 to 20) { + dut.io.wrAddr.poke(i.U) + dut.io.wrData.poke((i*10).U) + dut.clock.step() + } + dut.io.wrEna.poke(false.B) + + dut.io.rdAddr.poke(10.U) + dut.clock.step() + dut.io.rdData.expect(100.U) + dut.io.rdAddr.poke(5.U) + dut.io.rdData.expect(100.U) + dut.clock.step() + dut.io.rdData.expect(50.U) + + // Same address read and write + dut.io.wrAddr.poke(20.U) + dut.io.wrData.poke(123.U) + dut.io.wrEna.poke(true.B) + dut.io.rdAddr.poke(20.U) + dut.clock.step() + dut.io.rdData.expect(123.U) + // println(s"Memory data: ${dut.io.rdData.peekInt()}") + } + } + + "Initialized memory" should "pass" in { + test(new InitMemory) { dut => + // Some defaults + dut.io.rdAddr.poke(0.U) + dut.io.wrEna.poke(false.B) + dut.io.wrAddr.poke(0.U) + dut.io.wrData.poke(0.U) + val string = "Hello, World!" + for (i <- 0 until string.length) { + dut.io.rdAddr.poke(i.U) + dut.clock.step() + dut.io.rdData.expect(string(i).U) + } + dut.io.wrAddr.poke(1.U) + dut.io.wrData.poke('a'.U) + dut.io.wrEna.poke(true.B) + dut.clock.step() + dut.io.wrEna.poke(false.B) + dut.io.rdAddr.poke(1.U) + dut.clock.step() + dut.io.rdData.expect('a'.U) } } }