Memory inference and read ports

Write a memory as an array, see what this flow builds from it and what a real flow maps it to, and choose a read port by its latency and its read-during-write result.

Lesson
R5.1
Time
30 min
Runs on
Your browser
Check
5 questions

A 1024 x 32 buffer was written as an array with a clocked write and a combinational read. On this site's flow it became 32768 flops and 90859 cells, with 19 levels of logic. An FPGA flow would have used a block RAM; an ASIC flow needs a memory macro, and this flow has none.

Words x bitsCellsFlopsLevels of logic on this flow
4 x 865323
16 x 8 (the lab)3611287
64 x 8148751211
1024 x 32 (the hook)908593276819

The lab's register file with a combinational read, synthesized at four sizes by Yosys 0.70. Every bit is a flop, and the cells grow with the bits.

What an array becomes

An unpacked array such as logic [7:0] mem [16], written in a clocked block, is a memory: words selected by an address. The synthesizer first collects it as one memory cell with a write port and a read port. What happens next depends on the flow.

The 16 x 8 register file after memory_mapArrows carry data and control. With no memory library, every word becomes eight flops, and every output bit picks one of sixteen flops through a mux.
The 16 x 8 register file after memory_mapArrows carry data and control. With no memory library, every word becomes eight flops, and every output bit picks one of sixteen flops through a mux. component Write port we, waddr, wdata logic Address decoder one enable per word register Flop array 16 words x 8 flopswith enables component Read address raddr logic Read mux a 16-way choice perbit output rdata 8 bits, same cycle waddr, weenableswdata128 bitsraddrword
  1. Write

    The decoder turns waddr and we into one enable per word. At the edge, only the enabled word's eight flops load wdata.

  2. Store

    The 128 flops are the storage: 16 words of 8 bits. Measured on this flow, they are 128 flops with an enable each.

  3. Read

    Each of the 8 output bits chooses one of 16 flops by raddr. That choice is the read mux, and it grows with the depth.

Read the diagram as text
  • Write port: we, waddr, wdata
  • Address decoder: one enable per word
  • Flop array: 16 words x 8 flops with enables
  • Read address: raddr
  • Read mux: a 16-way choice per bit
  • rdata: 8 bits, same cycle
  • Write port → Address decoder: waddr, we
  • Address decoder → Flop array: enables
  • Write port → Flop array: wdata
  • Flop array → Read mux: 128 bits
  • Read address → Read mux: raddr
  • Read mux → rdata: word

Yosys 0.70 builds this from the lab design: 361 cells, 128 flops, 7 levels of logic.

The pinned flow runs memory -nomap to collect the array, then memory_map, which builds word-wide flops and address decoders. It has no memory library to map to. A real ASIC flow gives the synthesizer a library of SRAM macros: dense memory blocks made by a memory compiler and placed like one big cell. Yosys can map to such cells with memory_libmap, but only when a library describes them.

Three read ports, one write

The read port is the design choice. A combinational (asynchronous) read returns mem[raddr] in the same cycle. A synchronous read puts a register in the path and returns the word one cycle later. The register can hold the address or the data.

Combinational read

assign rdata = mem[raddr];

Same-cycle data. Fine for flops; most SRAM macros cannot do it.

Registered address

logic [3:0] raddr_q;
always_ff @(posedge clk) raddr_q <= raddr;
assign rdata = mem[raddr_q];

One cycle of latency, the form a synchronous SRAM offers.

A read-during-write happens when one cycle writes and reads the same address. The memory may return the old word or the new one, and the brief must say which. The lab writes 0xa5 into word 5 while reading word 5, which held 0x55.

Reading word 5 while it is writtenEdges 16 to 21 of the lab bench. Each rq row is the value a register after the read port captured at that edge, for one read style.
012345clkraddr39125werq_comb3399cc55a5rq_addr003399cca5rq_data003399cc55a5
  1. Cycle 0

    Edge 16: the combinational read gives word 3, 0x33, in its own cycle. Both registered reads still show the word read before, from address 0.

  2. Cycle 1

    Edge 17: the registered reads give 0x33 now, one cycle late. Each row keeps that one-cycle shift.

  3. Cycle 3

    Edge 19: this cycle writes 0xa5 into word 5 and reads word 5. The combinational read returns the old word, 0x55, because the write lands at this edge.

  4. Cycle 4

    Edge 20: the registered address read returns the new word, 0xa5. The registered data read returns the old word, 0x55, sampled before the write landed.

Measured on the lab bench with each read style. Same memory, three answers to one read-during-write.

The lab: a register file

The register file brief.
PortDirectionWidthMeaning
clkinput1clock
weinput1write at the rising edge when we is 1
waddrinput[$clog2(DEPTH)-1:0]write address
wdatainput[WIDTH-1:0]write data
raddrinput[$clog2(DEPTH)-1:0]read address
rdataoutput[WIDTH-1:0]the word at raddr in the same cycle
Latency
read combinational: rdata is the word at raddr in the same cycle; write at the rising edge
Parameters
DEPTH words of WIDTH bits, 16 x 8 by default; no reset, as in a RAM
Corner cases
read and write of one address in one cycle; a change to the read style; another DEPTH. Read-during-write: a combinational read in the writing cycle returns the old word

Run the lab. The bench writes 0x00, 0x11, up to 0xff, reads words 3, 9 and 12, then reads word 5 while writing it. Then change the read to the registered address of the compare and run again: every word arrives one edge later, and the read-during-write returns the new word.

16 x 8 read portCellsFlopsLevels of logic on this flow
Combinational (the lab)3611287
Registered address3231326
Registered data2891364

Measured with Yosys 0.70. The extra flops hold the 4-bit address or the 8-bit word. Fewer cells here is how the synthesizer mapped each design, not a rule.

Synthesize each, then change DEPTH to 4 and to 64 and synthesize again. The flops follow the bits exactly. The read mux follows the depth: each output bit chooses among DEPTH flops.

Asked in interviews
  1. What does an unpacked array become in synthesis?

    A memory. With a memory library it maps to an SRAM macro or block RAM if the ports fit. Without one, it becomes flops with write enables and a read mux per output bit.

  2. Synchronous or asynchronous read?

    An asynchronous read returns the word in the same cycle, which flops can do and most SRAMs cannot. A synchronous read registers the address or the data and returns the word one cycle later.

  3. What is read-during-write behaviour?

    What a read returns when the same cycle writes that address: the old word or the new one. It differs between memories, so the brief states it and the bench checks it.

Try it

Edges 0 to 15 write the pattern. Each later line shows the read address of the cycle that ended at that edge, and rq, the word a register captured there. A combinational read needs no extra cycle, so each address meets its own word.

With the registered address, the lines read 0x00, 0x33, 0x99, 0xcc, then 0xa5 at edges 20 and 21. With the registered data, they read 0x00, 0x33, 0x99, 0xcc, 0x55 and 0xa5.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e16 raddr=3 rq=0x33 e17 raddr=9 rq=0x99 e18 raddr=12 rq=0xcc e19 raddr=5 write 5<-0xa5 rq=0x55 e20 raddr=5 rq=0xa5 e21 raddr=5 rq=0xa5

Original design · 361 cells · 128 flip-flops · 0 latches · 7 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic       we = 0;
  logic [3:0] waddr = 0, raddr = 0;
  logic [7:0] wdata = 0, rdata;
  logic [7:0] rq;                // a register after the read port: what it captures at each edge
  always @(posedge clk) rq <= rdata;
  bit show = 0;
  task automatic write(input logic [3:0] a, input logic [7:0] d);
    we = 1; waddr = a; wdata = d; tick(); we = 0;
  endtask
  regfile dut (.clk(clk), .we(we), .waddr(waddr), .wdata(wdata), .raddr(raddr), .rdata(rdata));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (show && we) $display("e%0d raddr=%0d write %0d<-0x%h rq=0x%h", edge_n, raddr, waddr, wdata, rq);
    else if (show)  $display("e%0d raddr=%0d rq=0x%h", edge_n, raddr, rq);
  endtask
  initial begin
    for (int a = 0; a < 16; a++) write(4'(a), 8'(a * 17));   // 0x00, 0x11, ... 0xff
    show = 1;
    raddr = 3;  tick();
    raddr = 9;  tick();
    raddr = 12; tick();
    we = 1; waddr = 5; wdata = 8'ha5; raddr = 5; tick();       // read and write address 5 in one cycle
    we = 0; tick();
    tick();
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe register file with its combinational read holds 0x3c in word 2. In one cycle the bench writes 0xc3 into word 2 and reads word 2. A register after the read port captures rdata at the edge that ends the cycle. What does it print?
// regfile: DEPTH words of WIDTH bits, one write port and one read port.
// The write lands at the rising edge when we is 1. The read is combinational:
// rdata follows raddr in the same cycle, with no clock in the path.
module regfile #(parameter int DEPTH = 16, parameter int WIDTH = 8) (
  input  logic                     clk,
  input  logic                     we,
  input  logic [$clog2(DEPTH)-1:0] waddr,
  input  logic [WIDTH-1:0]         wdata,
  input  logic [$clog2(DEPTH)-1:0] raddr,
  output logic [WIDTH-1:0]         rdata
);
  logic [WIDTH-1:0] mem [DEPTH];          // the storage: no reset, like a real RAM

  always_ff @(posedge clk)
    if (we) mem[waddr] <= wdata;          // the write port

  assign rdata = mem[raddr];              // the read port: combinational
endmodule

module tb;
  logic clk = 0, we = 0;
  logic [3:0] waddr = 0, raddr = 0;
  logic [7:0] wdata = 0, rdata, rq;
  regfile dut (.*);
  always @(posedge clk) rq <= rdata;            // a register after the read port
  initial begin
    we = 1; waddr = 2; wdata = 8'h3c;           // write 0x3c into word 2
    #1 clk = 1; #1 clk = 0;
    wdata = 8'hc3; raddr = 2;                   // next cycle: write 0xc3 into word 2 and read word 2
    #1 clk = 1; #1 clk = 0;
    $display("rq=0x%h", rq);
    $finish(0);
  end
endmodule
Spot the bugEach option is the read port of a 16 x 8 register file. The bench changes the read address every cycle and expects the word one edge later. Which one fails?
ConceptWhy does a 16 x 8 flop RAM need a 16-way mux for every bit of rdata?
ConceptA brief asks for a 4096 x 32 buffer in an ASIC with a memory compiler. Why does the designer write it with a synchronous read?
Read the waveformThis is the lab bench's read-during-write, edges 18 to 21. The cycle that ends at edge 19 writes 0xa5 into word 5 and reads word 5, which held 0x55. Which read style gives these rq values?
One read style
0123raddr125werq99cc55a5

Values captured at edges 18 to 21, left to right.

Remember

  • An unpacked array written in a clocked block is a memory; what it becomes depends on the flow's memory library.
  • With no library, Yosys maps a memory to one flop per bit with write enables and a read mux per output bit; the cells grow with the bits.
  • A combinational read returns the word in the same cycle; a synchronous read registers the address or the data and returns it one cycle later, the form SRAM macros offer.
  • Read-during-write returns the old or the new word depending on the read port; the brief says which and the bench checks it.