From 23367aaf3ba4b6f298b81660f1aeaa98bcad6159 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:36:15 +0000 Subject: [PATCH] Implement read-write memory accessors for SyncReadMem (#3190) (#3214) Adds new APIs to generate explicit read-write (rdwr) ports for a SyncReadMem, and all masked or clocked variants. --------- Co-authored-by: Megan Wachs (cherry picked from commit 1e6f2b4ed41e3d130f416cccbfe182c84105a9fc) Co-authored-by: Jared Barocsi <82000041+jared-barocsi@users.noreply.github.com> --- core/src/main/scala/chisel3/Mem.scala | 203 ++++++++++++++++++ docs/src/explanations/memories.md | 18 ++ .../sourceinfo/SourceInfoTransform.scala | 8 + src/test/scala/chiselTests/Mem.scala | 168 +++++++++++++++ 4 files changed, 397 insertions(+) diff --git a/core/src/main/scala/chisel3/Mem.scala b/core/src/main/scala/chisel3/Mem.scala index e82da9e7a21..5b77282fce4 100644 --- a/core/src/main/scala/chisel3/Mem.scala +++ b/core/src/main/scala/chisel3/Mem.scala @@ -384,4 +384,207 @@ sealed class SyncReadMem[T <: Data] private[chisel3] (t: T, n: BigInt, val readU } // note: we implement do_read(addr) for SyncReadMem in terms of do_read(addr, en) in order to ensure that // `mem.read(addr)` will always behave the same as `mem.read(addr, true.B)` + + /** Generates an explicit read-write port for this SyncReadMem. Note that this does not infer + * port directionality based on connection semantics and the `when` context unlike SyncReadMem.apply(), + * so the behavior of the port must be controlled by changing the values of the input parameters. + * + * @param idx memory element index to write into + * @param writeData new data to write + * @param enable enables access to the memory + * @param isWrite performs a write instead of a read when enable is true; the return + * value becomes undefined when this parameter is true + * + * @return The read data of the memory, which gives the value at idx when enable is true and isWrite is false, + * or an undefined value otherwise, on the following clock cycle. + * + * @example Controlling a read/write port with IO signals + * {{{ + * class MyMemWrapper extends Module { + * val width = 2 + * + * val io = IO(new Bundle { + * val address = Input(UInt()) + * val wdata = Input(UInt(width.W)) + * val enable = Input(Bool()) + * val isWrite = Input(Bool()) + * val rdata = Output(UInt(width.W)) + * }) + * + * val mem = SyncReadMem(2, UInt(width.W)) + * io.rdata := mem.readWrite(io.address, io.wdata, io.enable, io.isWrite) + * } + * + * }}} + */ + def readWrite(idx: UInt, writeData: T, en: Bool, isWrite: Bool): T = macro SourceInfoTransform.idxDataEnIswArg + + /** @group SourceInfoTransformMacro */ + def do_readWrite(idx: UInt, writeData: T, en: Bool, isWrite: Bool)(implicit sourceInfo: SourceInfo): T = + _readWrite_impl(idx, writeData, en, isWrite, Builder.forcedClock, true) + + /** Generates an explicit read-write port for this SyncReadMem, using a clock that may be + * different from the implicit clock. + * + * @param idx memory element index to write into + * @param writeData new data to write + * @param enable enables access to the memory + * @param isWrite performs a write instead of a read when enable is true; the return + * value becomes undefined when this parameter is true + * @param clock clock to bind to this read-write port + * + * @return The read data of the memory, which gives the value at idx when enable is true and isWrite is false, + * or an undefined value otherwise, on the following clock cycle. + */ + def readWrite(idx: UInt, writeData: T, en: Bool, isWrite: Bool, clock: Clock): T = + macro SourceInfoTransform.idxDataEnIswClockArg + + /** @group SourceInfoTransformMacro */ + def do_readWrite( + idx: UInt, + data: T, + en: Bool, + isWrite: Bool, + clock: Clock + )( + implicit sourceInfo: SourceInfo + ): T = + _readWrite_impl(idx, data, en, isWrite, clock, true) + + /** @group SourceInfoTransformMacro */ + private def _readWrite_impl( + addr: UInt, + data: T, + enable: Bool, + isWrite: Bool, + clock: Clock, + warn: Boolean + )( + implicit sourceInfo: SourceInfo + ): T = { + val a = Wire(UInt()) + a := DontCare + + var port: Option[T] = None + when(enable) { + a := addr + port = Some(super.do_apply_impl(a, clock, MemPortDirection.RDWR, warn)) + + when(isWrite) { + port.get := data + } + } + port.get + } + + /** Generates an explicit read-write port for this SyncReadMem, with a bytemask for + * performing partial writes to a Vec element. + * + * @param idx memory element index to write into + * @param writeData new data to write + * @param mask the write mask as a Seq of Bool: a write to the Vec element in + * memory is only performed if the corresponding mask index is true. + * @param enable enables access to the memory + * @param isWrite performs a write instead of a read when enable is true; the return + * value becomes undefined when this parameter is true + * + * @return The read data Vec of the memory at idx when enable is true and isWrite is false, + * or an undefined value otherwise, on the following clock cycle + * + * @example Controlling a read/masked write port with IO signals + * {{{ + * class MyMaskedMemWrapper extends Module { + * val width = 2 + * + * val io = IO(new Bundle { + * val address = Input(UInt()) + * val wdata = Input(Vec(2, UInt(width.W))) + * val mask = Input(Vec(2, Bool())) + * val enable = Input(Bool()) + * val isWrite = Input(Bool()) + * val rdata = Output(Vec(2, UInt(width.W))) + * }) + * + * val mem = SyncReadMem(2, Vec(2, UInt(width.W))) + * io.rdata := mem.readWrite(io.address, io.wdata, io.mask, io.enable, io.isWrite) + * } + * }}} + * + * @note this is only allowed if the memory's element data type is a Vec + */ + def readWrite( + idx: UInt, + writeData: T, + mask: Seq[Bool], + en: Bool, + isWrite: Bool + )( + implicit evidence: T <:< Vec[_] + ): T = masked_readWrite_impl(idx, writeData, mask, en, isWrite, Builder.forcedClock, true) + + /** Generates an explicit read-write port for this SyncReadMem, with a bytemask for + * performing partial writes to a Vec element and a clock that may be different from + * the implicit clock. + * + * @param idx memory element index to write into + * @param writeData new data to write + * @param mask the write mask as a Seq of Bool: a write to the Vec element in + * memory is only performed if the corresponding mask index is true. + * @param enable enables access to the memory + * @param isWrite performs a write instead of a read when enable is true; the return + * value becomes undefined when this parameter is true + * @param clock clock to bind to this read-write port + * + * @return The read data Vec of the memory at idx when enable is true and isWrite is false, + * or an undefined value otherwise, on the following clock cycle + * + * @note this is only allowed if the memory's element data type is a Vec + */ + def readWrite( + idx: UInt, + writeData: T, + mask: Seq[Bool], + en: Bool, + isWrite: Bool, + clock: Clock + )( + implicit evidence: T <:< Vec[_] + ): T = masked_readWrite_impl(idx, writeData, mask, en, isWrite, clock, true) + + private def masked_readWrite_impl( + addr: UInt, + data: T, + mask: Seq[Bool], + enable: Bool, + isWrite: Bool, + clock: Clock, + warn: Boolean + )( + implicit evidence: T <:< Vec[_] + ): T = { + implicit val sourceInfo = UnlocatableSourceInfo + val a = Wire(UInt()) + a := DontCare + + var port: Option[T] = None + when(enable) { + a := addr + port = Some(super.do_apply_impl(a, clock, MemPortDirection.RDWR, warn)) + val accessor = port.get.asInstanceOf[Vec[Data]] + + when(isWrite) { + val dataVec = data.asInstanceOf[Vec[Data]] + if (accessor.length != dataVec.length) { + Builder.error(s"Mem write data must contain ${accessor.length} elements (found ${dataVec.length})") + } + if (accessor.length != mask.length) { + Builder.error(s"Mem write mask must contain ${accessor.length} elements (found ${mask.length})") + } + + for (((cond, p), datum) <- mask.zip(accessor).zip(dataVec)) + when(cond) { p := datum } + } + } + port.get + } } diff --git a/docs/src/explanations/memories.md b/docs/src/explanations/memories.md index 10759f25ec9..6e3bedd5231 100644 --- a/docs/src/explanations/memories.md +++ b/docs/src/explanations/memories.md @@ -114,6 +114,24 @@ Here is an example single read/write port waveform, with [masks](#masks) (again, ![read/write ports example waveform](https://svg.wavedrom.com/github/freechipsproject/www.chisel-lang.org/master/docs/src/main/resources/json/smem_rw.json) +Single-ported SRAMs can also be explicitly generated by using the `readWrite` call, which yields a single read/write accessor like so: + +```scala mdoc:silent +class RDWR_Smem extends Module { + val width: Int = 32 + val io = IO(new Bundle { + val enable = Input(Bool()) + val write = Input(Bool()) + val addr = Input(UInt(10.W)) + val dataIn = Input(UInt(width.W)) + val dataOut = Output(UInt(width.W)) + }) + + val mem = SyncReadMem(1024, UInt(width.W)) + io.dataOut := mem.readWrite(io.addr, io.dataIn, io.enable, io.write) +} +``` + ### `Mem`: combinational/asynchronous-read, sequential/synchronous-write Chisel supports random-access memories via the `Mem` construct. Writes to `Mem`s are combinational/asynchronous-read, sequential/synchronous-write. These `Mem`s will likely be synthesized to register banks, since most SRAMs in modern technologies (FPGA, ASIC) tend to no longer support combinational (asynchronous) reads. diff --git a/macros/src/main/scala/chisel3/internal/sourceinfo/SourceInfoTransform.scala b/macros/src/main/scala/chisel3/internal/sourceinfo/SourceInfoTransform.scala index b693723ce13..c48927e87df 100644 --- a/macros/src/main/scala/chisel3/internal/sourceinfo/SourceInfoTransform.scala +++ b/macros/src/main/scala/chisel3/internal/sourceinfo/SourceInfoTransform.scala @@ -223,6 +223,14 @@ class SourceInfoTransform(val c: Context) extends AutoSourceTransform { q"$thisObj.$doFuncTerm($idx, $en, $clock)($implicitSourceInfo)" } + def idxDataEnIswArg(idx: c.Tree, writeData: c.Tree, en: c.Tree, isWrite: c.Tree): c.Tree = { + q"$thisObj.$doFuncTerm($idx, $writeData, $en, $isWrite)($implicitSourceInfo)" + } + + def idxDataEnIswClockArg(idx: c.Tree, writeData: c.Tree, en: c.Tree, isWrite: c.Tree, clock: c.Tree): c.Tree = { + q"$thisObj.$doFuncTerm($idx, $writeData, $en, $isWrite, $clock)($implicitSourceInfo)" + } + def xEnArg(x: c.Tree, en: c.Tree): c.Tree = { q"$thisObj.$doFuncTerm($x, $en)($implicitSourceInfo)" } diff --git a/src/test/scala/chiselTests/Mem.scala b/src/test/scala/chiselTests/Mem.scala index 0ed32e9bee3..c78b9a9910e 100644 --- a/src/test/scala/chiselTests/Mem.scala +++ b/src/test/scala/chiselTests/Mem.scala @@ -188,6 +188,156 @@ private class TrueDualPortMemory(addrW: Int, dataW: Int) extends RawModule { } } +class MemReadWriteTester extends BasicTester { + val (cnt, _) = Counter(true.B, 6) + val mem = SyncReadMem(2, UInt(2.W)) + + // The address to write to, alternating between 0 and 1 each cycle + val address = Wire(UInt()) + address := DontCare + + // The data to write into the read-write port + val wdata = Wire(UInt(8.W)) + wdata := DontCare + + // Enable signal + val enable = Wire(Bool()) + enable := true.B // By default, memory access is on + + // Write signal + val isWrite = Wire(Bool()) + isWrite := false.B // By default, writes are off + + val rdata = mem.readWrite(address, wdata, enable, isWrite) + + switch(cnt) { + is(0.U) { // Cycle 1: Write 3.U to address 0 + address := 0.U + enable := true.B + isWrite := true.B + wdata := 3.U + } + is(1.U) { // Cycle 2: Write 2.U to address 1 + address := 1.U + enable := true.B + isWrite := true.B + wdata := 2.U + } + is(2.U) { // Cycle 3: Read from address 0 (data returned next cycle) + address := 0.U; + enable := true.B; + isWrite := false.B; + } + is(3.U) { // Cycle 4: Expect RDWR port to contain 3.U, then read from address 1 + address := 1.U; + enable := true.B; + isWrite := false.B; + assert(rdata === 3.U) + } + is(4.U) { // Cycle 5: Expect rdata to contain 2.U + assert(rdata === 2.U) + } + is(5.U) { // Cycle 6: Stop + stop() + } + } +} + +class MemMaskedReadWriteTester extends BasicTester { + val (cnt, _) = Counter(true.B, 11) + val mem = SyncReadMem(2, Vec(4, UInt(8.W))) + + // The address to write to, alternating between 0 and 1 each cycle + val address = Wire(UInt()) + address := DontCare + + // The data to write into the read-write port + val wdata = Wire(Vec(4, UInt(8.W))) + wdata := DontCare + + // The bytemask used for masking readWrite + val mask = Wire(Vec(4, Bool())) + mask := DontCare + + // Enable signal + val enable = Wire(Bool()) + enable := true.B // By default, memory access is on + + // Write signal + val isWrite = Wire(Bool()) + isWrite := false.B // By default, writes are off + + val rdata = mem.readWrite(address, wdata, mask, enable, isWrite) + + switch(cnt) { + is(0.U) { // Cycle 1: Write (1.U, 2.U, 3.U, 4.U) with mask (1, 1, 1, 1) to address 0 + address := 0.U + enable := true.B + isWrite := true.B + mask := VecInit.fill(4)(true.B) + wdata := VecInit(1.U, 2.U, 3.U, 4.U) + } + is(1.U) { // Cycle 2: Write (5.U, 6.U, 7.U, 8.U) with mask (1, 1, 1, 1) to address 1 + address := 1.U + enable := true.B + isWrite := true.B + mask := VecInit.fill(4)(true.B) + wdata := VecInit(5.U, 6.U, 7.U, 8.U) + } + is(2.U) { // Cycle 3: Read from address 0 (data returned next cycle) + address := 0.U; + enable := true.B; + isWrite := false.B; + } + is(3.U) { // Cycle 4: Expect RDWR port to contain (1.U, 2.U, 3.U, 4.U), then read from address 1 + assert(rdata === VecInit(1.U, 2.U, 3.U, 4.U)) + + address := 1.U; + enable := true.B; + isWrite := false.B; + } + is(4.U) { // Cycle 5: Expect rdata to contain (5.U, 6.U, 7.U, 8.U) + assert(rdata === VecInit(5.U, 6.U, 7.U, 8.U)) + } + is(5.U) { // Cycle 6: Write (0.U, - , - , 0.U) with mask (1, 0, 0, 1) to address 0 + address := 0.U + enable := true.B + isWrite := true.B + mask := VecInit(true.B, false.B, false.B, true.B) + // Bogus values for 2nd and 3rd indices to make sure they aren't actually written + wdata := VecInit(0.U, 100.U, 100.U, 0.U) + } + is(6.U) { // Cycle 7: Write (- , 0.U , 0.U , -) with mask (0, 1, 1, 0) to address 1 + address := 1.U + enable := true.B + isWrite := true.B + mask := VecInit(false.B, true.B, true.B, false.B) + // Bogus values for 1st and 4th indices to make sure they aren't actually written + wdata := VecInit(100.U, 0.U, 0.U, 100.U) + } + is(7.U) { // Cycle 8: Read from address 0 (data returned next cycle) + address := 0.U; + enable := true.B; + isWrite := false.B; + } + is(8.U) { // Cycle 9: Expect RDWR port to contain (0.U, 2.U, 3.U, 0.U), then read from address 1 + // NOT (0.U, 100.U, 100.U, 0.U) + assert(rdata === VecInit(0.U, 2.U, 3.U, 0.U)) + + address := 1.U; + enable := true.B; + isWrite := false.B; + } + is(9.U) { // Cycle 10: Expect rdata to contain (5.U, 0.U, 0.U, 8.U) + // NOT (100.U, 0.U, 0.U, 100.U) + assert(rdata === VecInit(5.U, 0.U, 0.U, 8.U)) + } + is(10.U) { // Cycle 11: Stop + stop() + } + } +} + class MemorySpec extends ChiselPropSpec { property("Mem of Vec should work") { assertTesterPasses { new MemVecTester } @@ -214,6 +364,24 @@ class MemorySpec extends ChiselPropSpec { assertTesterPasses { new SyncReadMemWithZeroWidthTester } } + property("SyncReadMems should be able to have an explicit number of read-write ports") { + // Check if there is exactly one MemReadWrite port (TODO: extend to Nr/Nw?) + val chirrtl = ChiselStage.emitCHIRRTL(new MemReadWriteTester) + chirrtl should include(s"rdwr mport rdata = mem[_rdata_T_1], clock") + + // Check read/write logic + assertTesterPasses { new MemReadWriteTester } + } + + property("SyncReadMem masked read-writes should work") { + // Check if there is exactly one MemReadWrite port (TODO: extend to Nr/Nw?) + val chirrtl = ChiselStage.emitCHIRRTL(new MemMaskedReadWriteTester) + chirrtl should include(s"rdwr mport rdata = mem[_rdata_T_1], clock") + + // Check read/write logic + assertTesterPasses { new MemMaskedReadWriteTester } + } + property("Massive memories should be emitted in Verilog") { val addrWidth = 65 val size = BigInt(1) << addrWidth