Skip to content

Commit

Permalink
Memory initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
schoeberl committed Oct 15, 2024
1 parent ef12e5e commit 697b563
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 19 deletions.
12 changes: 6 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
Expand All @@ -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"
Expand Down
47 changes: 38 additions & 9 deletions chisel-book.tex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/MultiClockMemory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 49 additions & 2 deletions src/main/scala/memory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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)
}
}
58 changes: 56 additions & 2 deletions src/test/scala/MemoryTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()}")
}
}

Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit 697b563

Please sign in to comment.