diff --git a/docs/src/explanations/memories.md b/docs/src/explanations/memories.md index 6e3bedd5231..ae996c303af 100644 --- a/docs/src/explanations/memories.md +++ b/docs/src/explanations/memories.md @@ -200,3 +200,55 @@ Chisel memories can be initialized from an external `binary` or `hex` file emitt For more information, check the experimental docs on [Loading Memories](../appendix/experimental-features#loading-memories) feature. +## SRAM + +Chisel provides an API to generate `SRAMs`, an alternative APIs for `SyncReadMem`. + +The key difference between the `SRAM` and `SyncReadMem` APIs is the former's capability to declare a specific number of read, write, and read-write memory ports, which are interacted with using explicit bundles. + +```scala mdoc:silent +import chisel3.util._ + +class ModuleWithSRAM(numReadPorts: Int, numWritePorts: Int, numReadwritePorts: Int) extends Module { + val width: Int = 8 + + val io = IO(new SRAMInterface(1024, UInt(width.W), numReadPorts, numWritePorts, numReadwritePorts)) + + // Generate a SyncReadMem representing an SRAM with an explicit number of read, write, and read-write ports + io :<>= SRAM(1024, UInt(width.W), numReadPorts, numWritePorts, numReadwritePorts) +} +``` + +To interact with a desired port, use the `readPorts`, `writePorts`, and `readwritePorts` fields: + +```scala mdoc:silent +class TopModule extends Module { + // Declare a 2 read, 2 write, 2 read-write ported SRAM with 8-bit UInt data members + val mem = SRAM(1024, UInt(8.W), 2, 2, 2) + + // Whenever we want to read from the first read port + mem.readPorts(0).address := 100.U + mem.readPorts(0).enable := true.B + + // Read data is returned one cycle after enable is driven + val foo = WireInit(UInt(8.W), mem.readPorts(0).data) + + // Whenever we want to write to the second write port + mem.writePorts(1).address := 5.U + mem.writePorts(1).enable := true.B + mem.writePorts(1).data := 12.U + + // Whenever we want to read or write to the third read-write port + // Write: + mem.readwritePorts(2).address := 5.U + mem.readwritePorts(2).enable := true.B + mem.readwritePorts(2).isWrite := true.B + mem.readwritePorts(2).writeData := 100.U + + // Read: + mem.readwritePorts(2).address := 5.U + mem.readwritePorts(2).enable := true.B + mem.readwritePorts(2).isWrite := false.B + val bar = WireInit(UInt(8.W), mem.readwritePorts(2).readData) +} +``` \ No newline at end of file diff --git a/src/main/scala/chisel3/util/SRAM.scala b/src/main/scala/chisel3/util/SRAM.scala new file mode 100644 index 00000000000..2c9ec0e0927 --- /dev/null +++ b/src/main/scala/chisel3/util/SRAM.scala @@ -0,0 +1,273 @@ +package chisel3.util + +import chisel3._ + +import chisel3.internal.Builder +import chisel3.experimental.SourceInfo +import chisel3.internal.sourceinfo.{MemTransform, SourceInfoTransform} +import scala.language.reflectiveCalls +import scala.language.experimental.macros + +/** A bundle of signals representing a memory read port. + * + * @tparam tpe The data type of the memory port + * @param addrWidth The width of the address signal + */ +class MemoryReadPort[T <: Data](tpe: T, addrWidth: Int) extends Bundle { + val address = Input(UInt(addrWidth.W)) + val enable = Input(Bool()) + val data = Output(tpe) +} + +/** A bundle of signals representing a memory write port. + * + * @tparam tpe The data type of the memory port + * @param addrWidth The width of the address signal + * @param masked Whether this read/write port should have an optional mask. + * + * @note `masked` is only valid if tpe is a Vec; if this is not the case no mask will be initialized + * regardless of the value of `masked`. + */ +class MemoryWritePort[T <: Data] private[chisel3] (tpe: T, addrWidth: Int, masked: Boolean) extends Bundle { + val address = Input(UInt(addrWidth.W)) + val enable = Input(Bool()) + val data = Input(tpe) + val mask: Option[Vec[Bool]] = if (masked) { + val maskSize = tpe match { + case vec: Vec[_] => vec.size + case _ => 0 + } + Some(Input(Vec(maskSize, Bool()))) + } else { + None + } +} + +/** A bundle of signals representing a memory read/write port. + * + * @tparam tpe The data type of the memory port + * @param addrWidth The width of the address signal + * @param masked Whether this read/write port should have an optional mask. + * + * @note `masked` is only valid if tpe is a Vec; if this is not the case no mask will be initialized + * regardless of the value of `masked`. + */ +class MemoryReadWritePort[T <: Data] private[chisel3] (tpe: T, addrWidth: Int, masked: Boolean) extends Bundle { + val address = Input(UInt(addrWidth.W)) + val enable = Input(Bool()) + val isWrite = Input(Bool()) + val readData = Output(tpe) + val writeData = Input(tpe) + val mask: Option[Vec[Bool]] = if (masked) { + val maskSize = tpe match { + case vec: Vec[_] => vec.size + case _ => 0 + } + Some(Input(Vec(maskSize, Bool()))) + } else { + None + } +} + +/** A IO bundle of signals connecting to the ports of a wrapped `SyncReadMem`, as requested by + * `SRAMInterface.apply`. + * + * @param memSize The size of the memory, used to calculate the address width + * @tparam tpe The data type of the memory port + * @param numReadPorts The number of read ports + * @param numWritePorts The number of write ports + * @param numReadwritePorts The number of read/write ports + */ +class SRAMInterface[T <: Data]( + memSize: BigInt, + tpe: T, + numReadPorts: Int, + numWritePorts: Int, + numReadwritePorts: Int, + masked: Boolean = false) + extends Bundle { + if (masked) { + require( + tpe.isInstanceOf[Vec[_]], + s"masked writes require that SRAMInterface is instantiated with a data type of Vec (got $tpe instead)" + ) + } + + val addrWidth = log2Up(memSize + 1) + + val readPorts: Vec[MemoryReadPort[T]] = Vec(numReadPorts, new MemoryReadPort(tpe, addrWidth)) + val writePorts: Vec[MemoryWritePort[T]] = Vec(numWritePorts, new MemoryWritePort(tpe, addrWidth, masked)) + val readwritePorts: Vec[MemoryReadWritePort[T]] = + Vec(numReadwritePorts, new MemoryReadWritePort(tpe, addrWidth, masked)) +} + +object SRAM { + + /** Generates a [[SyncReadMem]] within the current module, connected to an explicit number + * of read, write, and read/write ports. This SRAM abstraction has both read and write capabilities: that is, + * it contains at least one read accessor (a read-only or read-write port), and at least one write accessor + * (a write-only or read-write port). + * + * @param size The desired size of the inner `SyncReadMem` + * @tparam T The data type of the memory element + * @param numReadPorts The number of desired read ports >= 0, and (numReadPorts + numReadwritePorts) > 0 + * @param numWritePorts The number of desired write ports >= 0, and (numWritePorts + numReadwritePorts) > 0 + * @param numReadwritePorts The number of desired read/write ports >= 0, and the above two conditions must hold + * + * @return A new `SRAMInterface` wire containing the control signals for each instantiated port + * @note This does *not* return the `SyncReadMem` itself, you must interact with it using the returned bundle + * @note Read-only memories (R >= 1, W === 0, RW === 0) and write-only memories (R === 0, W >= 1, RW === 0) are not supported by this API, and will result in an error if declared. + */ + def apply[T <: Data]( + size: BigInt, + tpe: T, + numReadPorts: Int, + numWritePorts: Int, + numReadwritePorts: Int + )( + implicit sourceInfo: SourceInfo + ): SRAMInterface[T] = + memInterface_impl(size, tpe)(numReadPorts, numWritePorts, numReadwritePorts, Builder.forcedClock) + + /** Generates a [[SyncReadMem]] within the current module, connected to an explicit number + * of read, write, and read/write ports, with masking capability on all write and read/write ports. + * This SRAM abstraction has both read and write capabilities: that is, it contains at least one read + * accessor (a read-only or read-write port), and at least one write accessor (a write-only or read-write port). + * + * @param size The desired size of the inner `SyncReadMem` + * @tparam T The data type of the memory element + * @param numReadPorts The number of desired read ports >= 0, and (numReadPorts + numReadwritePorts) > 0 + * @param numWritePorts The number of desired write ports >= 0, and (numWritePorts + numReadwritePorts) > 0 + * @param numReadwritePorts The number of desired read/write ports >= 0, and the above two conditions must hold + * + * @return A new `SRAMInterface` wire containing the control signals for each instantiated port + * @note This does *not* return the `SyncReadMem` itself, you must interact with it using the returned bundle + * @note Read-only memories (R >= 1, W === 0, RW === 0) and write-only memories (R === 0, W >= 1, RW === 0) are not supported by this API, and will result in an error if declared. + */ + def masked[T <: Data]( + size: BigInt, + tpe: T, + numReadPorts: Int, + numWritePorts: Int, + numReadwritePorts: Int + )( + implicit evidence: T <:< Vec[_], + sourceInfo: SourceInfo + ): SRAMInterface[T] = + masked_memInterface_impl(size, tpe)(numReadPorts, numWritePorts, numReadwritePorts, Builder.forcedClock) + + private def memInterface_impl[T <: Data]( + size: BigInt, + tpe: T + )(numReadPorts: Int, + numWritePorts: Int, + numReadwritePorts: Int, + clock: Clock + )( + implicit sourceInfo: SourceInfo + ): SRAMInterface[T] = { + val isValidSRAM = ((numReadPorts + numReadwritePorts) > 0) && ((numWritePorts + numReadwritePorts) > 0) + + if (!isValidSRAM) { + val badMemory = + if (numReadPorts + numReadwritePorts == 0) + "write-only SRAM (R + RW === 0)" + else + "read-only SRAM (W + RW === 0)" + Builder.error( + s"Attempted to initialize a $badMemory! SRAMs must have both at least one read accessor and at least one write accessor." + ) + } + + val _out = Wire(new SRAMInterface(size, tpe, numReadPorts, numWritePorts, numReadwritePorts)) + val mem = SyncReadMem(size, tpe) + + for (i <- 0 until numReadPorts) { + _out.readPorts(i).data := mem.read(_out.readPorts(i).address, _out.readPorts(i).enable, clock) + } + + for (i <- 0 until numWritePorts) { + when(_out.writePorts(i).enable) { + mem.write(_out.writePorts(i).address, _out.writePorts(i).data, clock) + } + } + + for (i <- 0 until numReadwritePorts) { + _out.readwritePorts(i).readData := mem.readWrite( + _out.readwritePorts(i).address, + _out.readwritePorts(i).writeData, + _out.readwritePorts(i).enable, + _out.readwritePorts(i).isWrite, + clock + ) + } + + _out + } + + private def masked_memInterface_impl[T <: Data]( + size: BigInt, + tpe: T + )(numReadPorts: Int, + numWritePorts: Int, + numReadwritePorts: Int, + clock: Clock + )( + implicit sourceInfo: SourceInfo, + evidence: T <:< Vec[_] + ): SRAMInterface[T] = { + val isValidSRAM = ((numReadPorts + numReadwritePorts) > 0) && ((numWritePorts + numReadwritePorts) > 0) + + if (!isValidSRAM) { + val badMemory = + if (numReadPorts + numReadwritePorts == 0) + "write-only SRAM (R + RW === 0)" + else + "read-only SRAM (W + RW === 0)" + Builder.error( + s"Attempted to initialize a $badMemory! SRAMs must have both at least one read accessor and at least one write accessor." + ) + } + + val _out = Wire(new SRAMInterface(size, tpe, numReadPorts, numWritePorts, numReadwritePorts, true)) + val mem = SyncReadMem(size, tpe) + + for (i <- 0 until numReadPorts) { + _out.readPorts(i).data := mem.read(_out.readPorts(i).address, _out.readPorts(i).enable, clock) + } + + for (i <- 0 until numWritePorts) { + when(_out.writePorts(i).enable) { + mem.write( + _out.writePorts(i).address, + _out.writePorts(i).data, + _out.writePorts(i).mask.get, + clock + ) + } + } + + for (i <- 0 until numReadwritePorts) { + _out.readwritePorts(i).readData := mem.readWrite( + _out.readwritePorts(i).address, + _out.readwritePorts(i).writeData, + _out.readwritePorts(i).mask.get, + _out.readwritePorts(i).enable, + _out.readwritePorts(i).isWrite, + clock + ) + } + + _out + } + + // Helper util to generate portedness descriptors based on the input parameters + // supplied to SRAM.apply + private[chisel3] def portedness(rd: Int, wr: Int, rw: Int): String = { + val rdPorts: String = if (rd > 0) s"${rd}R" else "" + val wrPorts: String = if (wr > 0) s"${wr}W" else "" + val rwPorts: String = if (rw > 0) s"${rw}RW" else "" + + s"$rdPorts$wrPorts$rwPorts" + } +} diff --git a/src/test/scala/chiselTests/Mem.scala b/src/test/scala/chiselTests/Mem.scala index c78b9a9910e..781775b6a68 100644 --- a/src/test/scala/chiselTests/Mem.scala +++ b/src/test/scala/chiselTests/Mem.scala @@ -407,3 +407,122 @@ class MemorySpec extends ChiselPropSpec { ChiselStage.emitCHIRRTL(new TrueDualPortMemory(4, 32)) } } + +class SRAMSpec extends ChiselFunSpec { + describe("SRAM") { + val portCombos: Seq[(Int, Int, Int)] = + for { + numRD <- 0 until 3 + numWR <- 0 until 3 + numRW <- 0 until 3 + if (numRD + numWR + numRW) > 0 + if (numRD + numRW) > 0 + if (numWR + numRW) > 0 + } yield (numRD, numWR, numRW) + + portCombos.foreach { + case (numRD, numWR, numRW) => + val portedness: String = { + val rdPorts: String = if (numRD > 0) s"${numRD}R" else "" + val wrPorts: String = if (numWR > 0) s"${numWR}W" else "" + val rwPorts: String = if (numRW > 0) s"${numRW}RW" else "" + + s"$rdPorts$wrPorts$rwPorts" + } + it(s"should generate a $portedness memory") { + class TestModule(val rd: Int, val wr: Int, val rw: Int) extends Module { + val mem = SRAM(32, UInt(8.W), rd, wr, rw) + + dontTouch(mem) + + for (i <- 0 until rd) { + mem.readPorts(i) := DontCare + } + for (i <- 0 until wr) { + mem.writePorts(i) := DontCare + } + for (i <- 0 until rw) { + mem.readwritePorts(i) := DontCare + } + } + val chirrtl = ChiselStage.emitCHIRRTL(new TestModule(numRD, numWR, numRW), args = Array("--full-stacktrace")) + + // Check that the chirrtl ports actually exist and the signals + // are properly connected + for (rd <- 0 until numRD) { + val rdPortName = s"mem_out_readPorts_${rd}_data_MPORT" + chirrtl should include(s"when mem.readPorts[$rd].enable") + chirrtl should include(s"read mport $rdPortName") + chirrtl should include(s"mem.readPorts[$rd].data <= $rdPortName") + } + + for (wr <- 0 until numWR) { + val wrPortName = s"mem_MPORT${if (wr == 0) "" else s"_$wr"}" + chirrtl should include(s"when mem.writePorts[$wr].enable") + chirrtl should include(s"write mport $wrPortName") + chirrtl should include(s"$wrPortName <= mem.writePorts[$wr].data") + } + + for (rw <- 0 until numRW) { + val rwPortName = s"mem_out_readwritePorts_${rw}_readData_MPORT" + chirrtl should include(s"when mem.readwritePorts[$rw].enable") + chirrtl should include(s"rdwr mport $rwPortName") + chirrtl should include(s"when mem.readwritePorts[$rw].isWrite") + chirrtl should include(s"$rwPortName <= mem.readwritePorts[$rw].writeData") + } + } + } + } + + it(s"should support masking with Vec-valued data") { + class TestModule(val wr: Int, val rw: Int) extends Module { + val mem = SRAM.masked(32, Vec(3, UInt(8.W)), 0, wr, rw) + + dontTouch(mem) + + for (i <- 0 until wr) { + mem.writePorts(i) := DontCare + } + for (i <- 0 until rw) { + mem.readwritePorts(i) := DontCare + } + } + val chirrtl = ChiselStage.emitCHIRRTL(new TestModule(1, 1), args = Array("--full-stacktrace")) + + chirrtl should include( + "writePorts : { flip address : UInt<6>, flip enable : UInt<1>, flip data : UInt<8>[3], flip mask : UInt<1>[3]}[1]" + ) + chirrtl should include( + "readwritePorts : { flip address : UInt<6>, flip enable : UInt<1>, flip isWrite : UInt<1>, readData : UInt<8>[3], flip writeData : UInt<8>[3], flip mask : UInt<1>[3]}[1]" + ) + + for (i <- 0 until 3) { + chirrtl should include(s"when mem.writePorts[0].mask[$i]") + chirrtl should include(s"mem_MPORT[$i] <= mem.writePorts[0].data[$i]") + + chirrtl should include(s"when mem.readwritePorts[0].mask[$i]") + chirrtl should include(s"mem_out_readwritePorts_0_readData_MPORT[$i] <= mem.readwritePorts[0].writeData[$i]") + } + } + describe("Read-only SRAM") { + it(s"should error") { + class TestModule extends Module { + val mem = SRAM(32, Vec(3, UInt(8.W)), 1, 0, 0) + } + intercept[Exception] { + ChiselStage.emitCHIRRTL(new TestModule, args = Array("--full-stacktrace")) + } + } + } + + describe("Write-only SRAM") { + it(s"should error") { + class TestModule extends Module { + val mem = SRAM(32, Vec(3, UInt(8.W)), 0, 1, 0) + } + intercept[Exception] { + ChiselStage.emitCHIRRTL(new TestModule, args = Array("--full-stacktrace")) + } + } + } +}