Skip to content
code0.HDL

Memories

Read-only tables with VecInit and Counter, the combinational-read Mem, the synchronous-read SyncReadMem with its one-cycle latency and undefined-read cases, write masks and the SRAM API, and how to choose between them.

In this chapter

  • Build a constant table with VecInit from a Scala list and read its element width off the ELABORATE line
  • Declare, write, and read Mem and SyncReadMem, and trace the cycle in which each one's read data appears
  • Name the three cases in which SyncReadMem read data is undefined and avoid them with the read enable
  • Choose between a VecInit ROM, Mem, and SyncReadMem from a memory's size and the timing the surrounding logic needs
Concepts in this chapter
  • Memories
Prerequisite concepts

A memory is many words of state behind one address. You could build one from a Vec of registers, but real hardware offers dedicated structures: constant tables folded into logic, register banks made of flip-flops, and SRAM macros supplied by the process. They differ in one property that shapes all the logic around them: when read data becomes available.

Before you start. You will use Vec and VecInit from Bundles and Vecs and the current-value/next-value model from Registers, RegInit, and stepping the clock. Memory writes take effect at the clock edge, just like a register's next value.

A ROM's contents are computed by Scala and fixed during elaboration. The contents of a read-write memory change only at clock edges during simulation.

1. Read latency is the whole difference

MemoryWriteReadLikely implementation
VecInit(...) ROMnoneCombinational: data in the same cycleConstant logic
MemAt the clock edgeCombinational: data in the same cycleRegister bank (flip-flops)
SyncReadMemAt the clock edgeSynchronous: address captured at the edge, data in the next cycleTechnology SRAM

Writes behave the same in both read-write memories; reads differ. A Mem read works like indexing a Vec: the data for this cycle's address is visible in this cycle. A SyncReadMem read registers the address at the edge, so the data for the address you present now appears one cycle later.

Note. Most SRAMs on modern FPGAs and ASIC processes support only that registered read. That is why SyncReadMem is the memory that maps to them, and Mem usually becomes flip-flops.

2. Read-only memories with VecInit

2.1 A table of literals

A ROM is a Vec of literals. VecInit accepts the literals as arguments or as a Scala Seq, so the contents can be the result of any Scala computation: a list, a formula, data read from a file at elaboration time.

Syntax

val name = VecInit(value1, value2, ...)
val name = VecInit(scalaSeq.map(i => expr.U(width.W)))
val squares = VecInit((0 until 8).map(i => (i * i).U(6.W)))
io.out := squares(io.idx)
CodeMeaning
(0 until 8).map(i => (i * i).U(6.W))Scala computes 0, 1, 4, …, 49 and makes eight 6-bit literals
VecInit(...)Turns the Seq of literals into a constant hardware Vec
squares(io.idx)Indexing with a hardware UInt becomes a multiplexer over the constants

Caution. A Vec has one element type. Literals written without a width each get their minimal width, so VecInit widens every element to the widest one. 2.U is 2 bits and 13.U is 4 bits, so a table holding both has 4-bit elements.

2.2 Counter

chisel3.util.Counter(n) is a ready-made version of the wrap counter you built in Registers, RegInit, and stepping the clock: it counts 0 to n - 1 and wraps to 0.

Syntax

val c = Counter(n)
val wrap = c.inc()     // adds the logic that advances c.value
MemberMeaning
c.valueThe current count; Scala picks its width from n
c.inc()Adds the increment logic and returns a Bool that is true in the cycle where the count is n - 1, about to wrap at the next edge

Call inc() inside a when and the counter advances only in cycles where that condition holds.

Note. The form Counter(cond, n) builds a counter that advances only when cond is true and returns a (value, wrap) pair, as in val (value, wrap) = Counter(io.en, 5).

2.3 Example: walking a table of primes

PrimeRom maps the Scala list 2, 3, 5, 7, 11, 13 to literals and builds a VecInit table. An elaboration-time println reports the table's length and element width. Counter(table.length) generates addresses, and inc() is called once, unconditionally, so the counter advances every cycle.

PrimeRom.scala
import chisel3._
import chisel3.util.Counter

class PrimeRom extends Module {
  val io = IO(new Bundle {
    val addr = Output(UInt(3.W))
    val data = Output(UInt(8.W))
    val wrap = Output(Bool())
  })

  // Scala computes the contents; VecInit turns them into constant hardware.
  val primes = Seq(2, 3, 5, 7, 11, 13)
  val table  = VecInit(primes.map(_.U))
  println(s"ELABORATE entries=${table.length} width=${table(0).getWidth}")

  // Counter(n) is a register that counts 0 to n-1 and wraps.
  val c    = Counter(table.length)
  val wrap = c.inc()

  io.addr := c.value
  io.data := table(c.value)
  io.wrap := wrap
}

The testbench prints address, data, and wrap flag for eight cycles.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new PrimeRom) { dut =>
    for (cycle <- 0 until 8) {
      val a = dut.io.addr.peek().litValue
      val d = dut.io.data.peek().litValue
      val w = dut.io.wrap.peek().litValue
      println(s"cycle=$cycle addr=$a data=$d wrap=$w")
      dut.clock.step()
    }
  }
}

Predict before you run.

  1. How many entries, and what element width, does the ELABORATE line report?
  2. In which cycle is wrap high?
  3. What address and data does cycle 6 show?

Check

The table has six entries and the counter advances every cycle. In which cycle of the trace is wrap high, and what address does cycle 6 show?

Your answer

Check the result.

Output lineWhat it shows
ELABORATESix entries of width 4, widened to fit the widest literal, 13.U
Cycles 0–5The primes 2, 3, 5, 7, 11, 13 at addresses 0 to 5
Cycle 5wrap is high at the last address
Cycles 6–7Back to address 0 with data 2, then address 1

The wrap flag is combinational on the counter's current value. It marks the cycle before the counter returns to zero, which is when downstream logic must act if it wants to react in time.

Try it yourself. Append 17 to the Scala list and predict one change in each phase.

PhaseWhat happens
ElaborationThe line reports seven entries of width 5, because 17 needs 5 bits
SimulationThe counter now wraps after address 6: wrap is high in cycle 6 with data 17, and address 0 returns in cycle 7

You did not touch the counter or the Vec code. Both the table length and the width came from the Scala list.

3. The combinational-read memory Mem

3.1 Declaring, writing, and reading

Mem takes a depth and an element type. write creates a write port and read creates a read port.

Syntax

val name = Mem(depth, elementType)
when(writeCondition) {
  name.write(address, data)
}
val value = name.read(address)

Here is a small register file of four 8-bit registers:

val regs = Mem(4, UInt(8.W))
when(io.wen) {
  regs.write(io.waddr, io.wdata)
}
io.rdata := regs.read(io.raddr)
CodeMeaning
Mem(4, UInt(8.W))A memory of four 8-bit words
when(io.wen) { regs.write(...) }Writes wdata at the edge ending each cycle where wen is high
regs.read(io.raddr)Reads the word at this cycle's raddr in the same cycle

If you write and read the same address in the same cycle, the read returns the old value. The write lands at the edge, so the new value is visible from the next cycle on.

Note. Indexing with parentheses, regs(io.raddr), also creates a read port.

4. The synchronous-read memory SyncReadMem

4.1 Declaration and read latency

SyncReadMem is declared and written exactly like Mem. Its read also accepts an enable; in cycles where enable is low, no read happens.

Syntax

val name = SyncReadMem(depth, elementType)
val value = name.read(address)
val value = name.read(address, enable)

The value read returns is the data for the address captured at the previous edge: an address presented in cycle t produces data in cycle t + 1.

4.2 When read data is undefined

The official documentation leaves SyncReadMem read data undefined in three cases:

ConditionResult
The same address is written and read at the same clock edgeThe read data is undefined
The read enable is lowThe next cycle's read data is undefined
The cycle after read data appearedThe data is not guaranteed to be held; register it yourself if you need it later

4.3 Example: the same addresses from both memories

MemCompare declares a Mem and a SyncReadMem of the same depth and type, sends the same write inputs to both, and reads both at the same address raddr. The SyncReadMem read is enabled only when no write is happening, which avoids the read-during-write case above.

MemCompare.scala
import chisel3._

class MemCompare extends Module {
  val io = IO(new Bundle {
    val wen      = Input(Bool())
    val waddr    = Input(UInt(4.W))
    val wdata    = Input(UInt(8.W))
    val raddr    = Input(UInt(4.W))
    val asyncOut = Output(UInt(8.W))
    val syncOut  = Output(UInt(8.W))
  })

  // Same depth, same element type; only the read timing differs.
  val comb = Mem(16, UInt(8.W))
  val sync = SyncReadMem(16, UInt(8.W))
  when(io.wen) {
    comb.write(io.waddr, io.wdata)
    sync.write(io.waddr, io.wdata)
  }

  // Mem: data for raddr appears in the same cycle.
  io.asyncOut := comb.read(io.raddr)
  // SyncReadMem: raddr is captured at the edge; data appears next cycle.
  io.syncOut := sync.read(io.raddr, !io.wen)
}

The testbench writes 11, 22, 33, 44 to addresses 0 to 3, then presents the addresses 2, 0, 3, 1, 1, one per cycle, and prints both outputs. In the first read cycle no enabled read came before, so the syncmem value is undefined and printed as --.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new MemCompare) { dut =>
    // Write 11, 22, 33, 44 into addresses 0 to 3.
    dut.io.wen.poke(true.B)
    for (a <- 0 until 4) {
      dut.io.waddr.poke(a.U)
      dut.io.wdata.poke(((a + 1) * 11).U)
      dut.clock.step()
    }

    // Read the same address sequence from both memories.
    dut.io.wen.poke(false.B)
    for ((a, cycle) <- Seq(2, 0, 3, 1, 1).zipWithIndex) {
      dut.io.raddr.poke(a.U)
      val m = dut.io.asyncOut.peek().litValue
      // Cycle 0 has no earlier enabled read, so its sync value is not shown.
      val s = if (cycle == 0) "--" else dut.io.syncOut.peek().litValue.toString
      println(s"cycle=$cycle raddr=$a mem=$m syncmem=$s")
      dut.clock.step()
    }
  }
}

Practice this concept

Use this concept in a short Chisel problem as your next step.

Predict before you run. The first two rows are filled in; continue for cycles 2 to 4.

Read cycleraddrmemsyncmem
0233-- (the previous cycle was a write with the read disabled)
101133 (data for address 2, captured at the edge ending cycle 0)
  1. How is the syncmem column related to the mem column?
  2. Is there a cycle where both columns show the same value?

Check

Read cycle 2 presents raddr = 3. What do the mem and syncmem columns show?

Your answer

Check the result.

ColumnWhat it shows
memFollows the address column directly: 33, 11, 44, 22, 22
syncmemThe mem column shifted down one line: --, 33, 11, 44, 22
Cycle 4The address repeats cycle 3's, so both columns show 22

Here the repeated address makes the two columns agree. Different addresses can also return equal data, so equal outputs alone do not prove equal read latency.

Try it yourself. Make the Mem read synchronous by hand: change its output to io.asyncOut := RegNext(comb.read(io.raddr)), and predict the mem column.

CyclememWhy
011The register's copy of mem[0], because raddr was 0 during the last write cycle
1–433, 11, 44, 22Identical to syncmem

A register after a combinational read gives the same visible timing as a synchronous-read memory.

Caution. Same timing, different hardware: this is a register bank plus an output register, not an SRAM.

5. Ports, masks, and the SRAM API

5.1 Write masks

Declare the element type as a Vec and write accepts a mask, so a write can change only some lanes. Byte enables are the typical use.

Syntax

val name = SyncReadMem(depth, Vec(lanes, elementType))
name.write(address, data, mask)   // mask: Vec(lanes, Bool())
val mem = SyncReadMem(256, Vec(4, UInt(8.W)))
when(io.wen) {
  mem.write(io.addr, io.wdata, io.mask)
}
io.rdata := mem.read(io.addr, !io.wen)

Only lanes i with mask(i) high take wdata(i); the others keep their old contents.

5.2 Single-ported memories and SRAM

FeatureHow it is writtenWhat it gives
Single-ported memorymem.readWrite(address, writeData, enable, isWrite), or read and write in mutually exclusive branches of one whenOne shared read/write port, as in a single-port SRAM
chisel3.util.SRAMSRAM(depth, type, nRead, nWrite, nReadWrite)A SyncReadMem with an explicit number of ports, driven through port bundles
val mem = SyncReadMem(16, UInt(8.W))
io.rdata := mem.readWrite(io.addr, io.wdata, io.en, io.isWrite)

Note. A single-ported memory read inside a when leaves the output undriven in other cycles, so the official example first assigns io.dataOut := DontCare to satisfy the unconnected-wire check.

Note. Memories can also be initialized from a binary or hex file. The official documentation marks that feature experimental, and this course does not use it.

6. Choosing a memory

MemoryGood fitCost
VecInit ROMSmall constant tables: lookup tables, coefficients, microcodeSynthesizes into logic, so area grows quickly with size
MemA small memory where a same-cycle read keeps the surrounding logic simple, such as a small register fileFlip-flop area
SyncReadMemA memory large enough that an SRAM mattersOne cycle of read latency; the address or the operation consuming the data must be lined up one cycle

A Mem followed by RegNext matches the timing of SyncReadMem, so you can switch between them while keeping a testbench valid. The choice then comes down to the implementation the synthesis tool can infer.

7. Common mistakes

7.1 Writing without a when

// Wrong: writes wdata to waddr in every cycle
regs.write(io.waddr, io.wdata)

// Right: write only when wen is high
when(io.wen) {
  regs.write(io.waddr, io.wdata)
}

write fires in every cycle where its surrounding condition holds. Without a condition, the wen input is never connected to the write port, and the memory changes even in cycles where wen is low.

7.2 Reading and writing one SyncReadMem address at the same time

// Wrong: the read stays enabled during writes
when(io.wen) { sync.write(io.addr, io.wdata) }
io.rdata := sync.read(io.addr)

// Right: disable the read while writing
when(io.wen) { sync.write(io.addr, io.wdata) }
io.rdata := sync.read(io.addr, !io.wen)

Writing and reading the same address at the same edge leaves SyncReadMem read data undefined. A simulation may happen to show a plausible value, and an SRAM may not.

7.3 Sending ROM values through a narrow port

// Wrong: 4-bit elements, 3-bit output
val out = Output(UInt(3.W))
io.out := table(io.idx)   // 11 comes out as 3, 13 as 5

// Right: an output as wide as the elements
val out = Output(UInt(4.W))
io.out := table(io.idx)

Connecting a wider value to a narrower port with := silently drops the high bits. When ROM values lose their top bits, check the element width and the width of every port the data passes through.

7.4 Declaring a large memory as Mem

// Wrong choice: a combinational read cannot map to SRAM
val big = Mem(4096, UInt(32.W))

// Right choice: synchronous read, with the one-cycle latency designed in
val big = SyncReadMem(4096, UInt(32.W))

Nothing fails, but synthesis builds over 130,000 flip-flops and a huge read multiplexer. When a memory is big enough to need an SRAM, use SyncReadMem and design the surrounding logic around its one-cycle read.

8. Summary

TopicKey point
ROMA Vec of literals built with VecInit; contents and element width come from the Scala list during elaboration
Countervalue is the current count; inc() adds the increment and is true in the cycle before the wrap
MemWrites at the edge, reads in the same cycle; usually becomes flip-flops
SyncReadMemCaptures the address at the edge and returns data one cycle later; maps to SRAM
Undefined readsSame-address read-during-write, the cycle after a disabled read, and the cycle after data appeared
Masks and portsVec elements plus a mask give per-lane writes; readWrite and SRAM set the port configuration

9. Check your understanding

  1. Why does the ROM's element width change from 4 to 5 bits when 17 is added, and in which phase is that width decided?

    Show answer

    Literals without a width get their minimal width, and VecInit widens every element to the widest one. 17 needs 5 bits, so the elements become 5 bits wide. That is decided during elaboration.

  2. In which cycle relative to the wrap does Counter's inc() return true?

    Show answer

    In the cycle where the count is n - 1, just before it wraps to 0 at the next edge. In PrimeRom that is the cycle with address 5.

  3. In the MemCompare trace, why is the first syncmem value not printed?

    Show answer

    The SyncReadMem data visible in the first read cycle is the result of the previous cycle's read. The previous cycle was a write cycle with wen high, so the read enable was low and that data is undefined.

  4. If a Mem is written and read at the same address in the same cycle, what does the read return?

    Show answer

    The old value. The write takes effect at the edge ending the cycle, and the combinational read sees the contents before it. The new value is visible from the next cycle.

  5. Which two signals would you compare to show that RegNext(comb.read(...)) and SyncReadMem have the same timing, and from which cycle are they expected to agree?

    Show answer

    asyncOut and syncOut, printed as the mem and syncmem columns. The first read cycle's syncmem value is undefined, so the columns should agree from read cycle 1 on.

10. Practice

Two Practice problems sit below this chapter, both linked to the read-latency example. In the first, you add a valid flag to a scratchpad built on SyncReadMem, so a client can take the data whenever the flag is high; right now the flag rises in a different cycle from the data. In the second, both operand ports of a register file return the previous instruction's registers, and you find out why and fix it. For both, write the cycle-by-cycle read table before you press Run.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows