Memories
Chisel provides facilities for creating both read only and read/write memories.
ROM
Users can define read-only memories by constructing a Vec
with VecInit
.
VecInit
can accept either a variable-argument number of Data
literals or a Seq[Data]
literals that initialize the ROM.
For example, users can create a small ROM initialized to 1, 2, 4, 8 and loop through all values using a counter as an address generator as follows:
import chisel3._
import chisel3.util.Counter
val m = VecInit(1.U, 2.U, 4.U, 8.U)
val c = Counter(m.length)
c.inc()
val r = m(c.value)
We can create an n value sine lookup table generator using a ROM initialized as follows:
import chisel3._
val Pi = math.Pi
def sinTable(amp: Double, n: Int) = {
val times =
(0 until n).map(i => (i*2*Pi)/(n.toDouble-1) - Pi)
val inits =
times.map(t => Math.round(amp * math.sin(t)).asSInt(32.W))
VecInit(inits)
}
where amp
is used to scale the fixpoint values stored in the ROM.
Read-Write Memories
Memories are given special treatment in Chisel since hardware implementations of memory vary greatly. For example, FPGA memories are instantiated quite differently from ASIC memories. Chisel defines a memory abstraction that can map to either simple Verilog behavioural descriptions or to instances of memory modules that are available from external memory generators provided by foundry or IP vendors.
SyncReadMem
: sequential/synchronous-read, sequential/synchronous-write
Chisel has a construct called SyncReadMem
for sequential/synchronous-read, sequential/synchronous-write memories. These SyncReadMem
s will likely be synthesized to technology SRAMs (as opposed to register banks).
If the same memory address is both written and sequentially read on the same clock edge, or if a sequential read enable is cleared, then the read data is undefined.
Values on the read data port are not guaranteed to be held until the next read cycle. If that is the desired behavior, external logic to hold the last read value must be added.
Read port/write port
Ports into SyncReadMem
s are created by applying a UInt
index. A 1024-entry SRAM with one write port and one read port might be expressed as follows:
import chisel3._
class ReadWriteSmem 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))
// Create one write port and one read port
mem.write(io.addr, io.dataIn)
io.dataOut := mem.read(io.addr, io.enable)
}
Below is an example waveform of the one write port/one read port SyncReadMem
with masks. Note that the signal names will differ from the exact wire names generated for the SyncReadMem
. With masking, it is also possible that multiple RTL arrays will be generated with the behavior below.
Single-ported
Single-ported SRAMs can be inferred when the read and write conditions are mutually exclusive in the same when
chain:
import chisel3._
class RWSmem 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 := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) { rdwrPort := io.dataIn }
.otherwise { io.dataOut := rdwrPort }
}
}
(The DontCare
is there to make Chisel's unconnected wire detection aware that reading while writing is undefined.)
Here is an example single read/write port waveform, with masks (again, generated signal names and number of arrays may differ):
Single-ported SRAMs can also be explicitly generated by using the readWrite
call, which yields a single read/write accessor like so:
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.
Creating asynchronous-read versions of the examples above simply involves replacing SyncReadMem
with Mem
.
Masks
Chisel memories also support write masks for subword writes. Chisel will infer masks if the data type of the memory is a vector. To infer a mask, specify the mask
argument of the write
function which creates write ports. A given masked length is written if the corresponding mask bit is set. For example, in the example below, if the 0th bit of mask is true, it will write the lower byte of the data at corresponding address.
import chisel3._
class MaskedReadWriteSmem extends Module {
val width: Int = 8
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val addr = Input(UInt(10.W))
val mask = Input(Vec(4, Bool()))
val dataIn = Input(Vec(4, UInt(width.W)))
val dataOut = Output(Vec(4, UInt(width.W)))
})
// Create a 32-bit wide memory that is byte-masked
val mem = SyncReadMem(1024, Vec(4, UInt(width.W)))
// Write with mask
mem.write(io.addr, io.dataIn, io.mask)
io.dataOut := mem.read(io.addr, io.enable)
}
Here is an example of masks with readwrite ports:
import chisel3._
class MaskedRWSmem extends Module {
val width: Int = 32
val io = IO(new Bundle {
val enable = Input(Bool())
val write = Input(Bool())
val mask = Input(Vec(2, Bool()))
val addr = Input(UInt(10.W))
val dataIn = Input(Vec(2, UInt(width.W)))
val dataOut = Output(Vec(2, UInt(width.W)))
})
val mem = SyncReadMem(1024, Vec(2, UInt(width.W)))
io.dataOut := DontCare
when(io.enable) {
val rdwrPort = mem(io.addr)
when (io.write) {
when(io.mask(0)) {
rdwrPort(0) := io.dataIn(0)
}
when(io.mask(1)) {
rdwrPort(1) := io.dataIn(1)
}
}.otherwise { io.dataOut := rdwrPort }
}
}
Memory Initialization
Chisel memories can be initialized from an external binary
or hex
file emitting proper Verilog for synthesis or simulation. There are multiple modes of initialization.
For more information, check the experimental docs on 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.
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:
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)
}