Block RAM, FIFOs & Memory Inference
Memory inference is pattern matching. Vivado holds a small set of RTL templates and checks whether your code is one of them. Match the template and a hard memory tile appears, costing nothing in the fabric. Miss it by a single line - one asynchronous read, one stray reset - and you get thirty thousand flip-flops and a synthesis run that takes forty minutes to tell you so.
4.1 The templates that infer block RAM
There is one canonical single-port template. Everything else is a variation on it, and every failure to infer is a deviation from it.
module sp_bram #(
parameter int AW = 10, // 1024 words
parameter int DW = 32
) (
input logic clk,
input logic en,
input logic we,
input logic [AW-1:0] addr,
input logic [DW-1:0] din,
output logic [DW-1:0] dout
);
// The array. No initial value needed - an FPGA's memory contents come
// from the bitstream, so an uninitialised array powers up as zeros.
logic [DW-1:0] mem [0:(1<<AW)-1];
always_ff @(posedge clk) begin
if (en) begin
if (we) mem[addr] <= din;
dout <= mem[addr]; // registered read - THE critical line
end
end
endmodule
Four properties of that template are load-bearing. Break any one and inference fails:
| Property | Why the primitive needs it | What you get if you break it |
|---|---|---|
| Read is registered | BRAM has a clocked output latch, period | Flip-flops + a giant mux |
| Array has no reset | The array is SRAM; there is no clear input | Flip-flops, always |
| Single write port | Each BRAM port has one write path | Duplicated memories, or fabric |
| Address is a plain index | Address decode is inside the tile | Fabric, if the "address" is really a decoded one-hot |
Byte enables and initialisation
module bram_byte_en (
input logic clk,
input logic [3:0] wstrb, // one bit per byte lane
input logic [9:0] addr,
input logic [31:0] din,
output logic [31:0] dout
);
logic [31:0] mem [0:1023];
// Load contents at configuration time. The file is read at SYNTHESIS
// time and baked into the bitstream - it is not a runtime operation.
initial $readmemh("boot_rom.hex", mem);
// A generate loop over byte lanes maps onto the BRAM's native write
// enable pins. Writing this as a single masked assignment
// mem[addr] <= (din & mask) | (mem[addr] & ~mask);
// creates a read-modify-write and destroys inference.
always_ff @(posedge clk) begin
for (int i = 0; i < 4; i++)
if (wstrb[i]) mem[addr][i*8 +: 8] <= din[i*8 +: 8];
dout <= mem[addr];
end
endmodule
INIT strings, which end up in the bitstream. This is how you ship a boot ROM,
a sine table or a coefficient set with zero runtime cost. Two caveats: the path is relative
to the working directory, so add the file to the project as a design source rather than
hoping; and UltraRAM cannot be initialised at all - it always powers up as
zeros, so anything needing initial contents must be BRAM.
4.2 Write modes: read-first, write-first, no-change
When a read and a write hit the same address in the same cycle, the primitive has to do something. Which something is a hardware setting, and your RTL selects it by the shape of the always block. Most engineers have never chosen deliberately.
dout during a same-address collision. If your design never collides, choose
NO_CHANGE and take the free power and timing.
// ---- WRITE_FIRST -----------------------------------------------------
// The write is described BEFORE the read, and dout takes din directly.
always_ff @(posedge clk)
if (en) begin
if (we) begin
mem[addr] <= din;
dout <= din; // new data forwarded to the output
end else
dout <= mem[addr];
end
// ---- READ_FIRST ------------------------------------------------------
// The read is unconditional; it sees the array as it was before the write.
always_ff @(posedge clk)
if (en) begin
if (we) mem[addr] <= din;
dout <= mem[addr]; // old contents
end
// ---- NO_CHANGE -------------------------------------------------------
// The read happens ONLY when not writing, so dout simply holds.
always_ff @(posedge clk)
if (en) begin
if (we) mem[addr] <= din;
else dout <= mem[addr];
end
| Mode | Dynamic power | Clock-to-out | Use when |
|---|---|---|---|
NO_CHANGE |
Lowest - output does not toggle during writes | Best | Default choice. Any memory with separate read and write phases |
READ_FIRST |
Medium | Good | You need the previous value; some FIFO pointer schemes rely on it |
WRITE_FIRST |
Highest | Worst - extra forwarding mux in the path | You genuinely need same-cycle write-to-read forwarding |
WRITE_FIRST but the primitive ends up in
READ_FIRST - which happens when a mismatched template forces Vivado to pick a
default - then simulation and hardware disagree on exactly one cycle, only when a
collision occurs. That is the worst class of bug there is: rare, data-dependent, and
invisible in a short testbench. Write the template exactly, and check the synthesis log,
which prints the inferred mode for every RAM.
4.3 True and simple dual-port memories
Every block RAM has two physical ports. What differs is how much of each port you use, and the distinction has a real cost.
| Simple dual-port (SDP) | True dual-port (TDP) | |
|---|---|---|
| Port A | Write only | Read and write |
| Port B | Read only | Read and write |
| Maximum width | 72 bits on a 36 Kb tile | 36 bits per port |
| Clocks | Independent | Independent |
| Collision risk | Only read-during-write on one address | Write-write collisions corrupt data |
| Typical use | FIFOs, line buffers, stream reorder | Shared register files, CPU-accessible scratchpads |
The doubled width on SDP is the headline. A 512-deep by 64-bit buffer fits in one BRAM36 as SDP, and needs two as TDP. If you only need one writer and one reader - which is the overwhelming majority of buffers - say so in the RTL and halve the memory cost.
// Simple dual-port, independent clocks. This is the workhorse: a stream
// crossing from one clock domain into another through a shared buffer.
module sdp_bram #(
parameter int AW = 9, // 512 words
parameter int DW = 64 // 64 bits - fits ONE tile as SDP
) (
input logic wr_clk,
input logic wr_en,
input logic [AW-1:0] wr_addr,
input logic [DW-1:0] wr_data,
input logic rd_clk,
input logic rd_en,
input logic [AW-1:0] rd_addr,
output logic [DW-1:0] rd_data
);
(* ram_style = "block" *)
logic [DW-1:0] mem [0:(1<<AW)-1];
// Write port - one clock
always_ff @(posedge wr_clk)
if (wr_en) mem[wr_addr] <= wr_data;
// Read port - a DIFFERENT clock. The BRAM handles this natively;
// the array itself needs no synchroniser. What DOES need one is the
// control logic that decides rd_addr is safe to use.
always_ff @(posedge rd_clk)
if (rd_en) rd_data <= mem[rd_addr];
endmodule
True dual-port collisions
With TDP, two independent ports can target the same address in the same cycle. The outcomes are defined but unpleasant:
- Write A + Write B, same address, same clock. The stored value is undefined. Not "one of the two" - genuinely undefined, and Vivado's simulation model will emit X.
- Write A + Read B, same address. Port B returns the old data, but only if the clocks are related and the setup relationship is met. With asynchronous clocks the read data is undefined for that cycle.
- Read A + Read B. Always safe.
There is no hardware arbiter. If two masters can address the same location, you must build the arbitration yourself, or partition the address space so a collision is structurally impossible.
4.4 XPM FIFOs and the FIFO primitives
You should almost never write a FIFO by hand on an FPGA. Not because the logic is hard - the Verilog course builds one from first principles - but because the vendor macro is verified, maps onto the hardened FIFO control logic inside the BRAM tile, and gets the CDC constraints right without you writing them.
XPM (Xilinx Parameterized Macros) are the modern answer: plain SystemVerilog modules you instantiate directly in RTL, with no IP catalogue, no generated wrapper files and no regeneration step when you change a parameter. They live in version control like any other source.
// Asynchronous FIFO, 512 x 32, first-word-fall-through.
// No IP catalogue, no .xci file - this is the whole instantiation.
xpm_fifo_async #(
.FIFO_MEMORY_TYPE ("block"), // block | distributed | ultra | auto
.FIFO_WRITE_DEPTH (512),
.WRITE_DATA_WIDTH (32),
.READ_DATA_WIDTH (32),
.READ_MODE ("fwft"), // "fwft" or "std"
.FIFO_READ_LATENCY (0), // must be 0 when READ_MODE is fwft
.CDC_SYNC_STAGES (3), // 2 is the minimum; 3 for high MTBF
.PROG_FULL_THRESH (500),
.PROG_EMPTY_THRESH (10),
.USE_ADV_FEATURES ("0707"), // enables prog_full/prog_empty/counts
.ECC_MODE ("no_ecc")
) u_fifo (
.rst (wr_rst), // async assert, sync deassert to wr_clk
.wr_clk (wr_clk),
.wr_en (wr_en),
.din (wr_data),
.full (full),
.prog_full (almost_full),
.wr_data_count (wr_count),
.rd_clk (rd_clk),
.rd_en (rd_en),
.dout (rd_data),
.empty (empty),
.prog_empty (almost_empty),
.rd_data_count (rd_count),
// Tie off what you do not use - leaving them unconnected is legal
// but makes the synthesis log noisy.
.injectsbiterr (1'b0), .injectdbiterr (1'b0),
.sleep (1'b0),
.sbiterr (), .dbiterr (),
.overflow (), .underflow (),
.wr_ack (), .data_valid (),
.almost_full (), .almost_empty (),
.wr_rst_busy (), .rd_rst_busy ()
);
First-word-fall-through, and why it matters
dout. Reading is a "pop what you already see" operation, which maps directly
onto a valid/ready handshake.
!empty is your tvalid and rd_en is your
tready, with no adaptation logic and no extra cycle. Standard mode forces you
to build a small skid buffer to hold the popped word while the consumer decides - which is
exactly the state machine that goes wrong under back-pressure. The AXI-Stream discussion in
Volume 05 assumes FWFT throughout.
prog_full and the data counts are computed from a
pointer that had to cross clock domains, so they lag reality by the synchroniser depth -
typically two to three cycles of the destination clock, plus more if the clocks are far
apart in frequency. Set PROG_FULL_THRESH with enough headroom to absorb every
write that can still be in flight during that lag. Sizing it as "depth minus one" produces
a FIFO that overflows under burst traffic and works perfectly in every short test.
4.5 When to instantiate instead of infer
Inference is the default and should stay the default: it is portable, readable and simulates without vendor libraries. But four features exist only in the primitive, and no RTL template can reach them.
| Feature | Why inference cannot get there | What to use |
|---|---|---|
| ECC (single-bit correct, double-bit detect) | There is no RTL that means "add Hamming protection" | xpm_memory_sdpram with ECC_MODE, or RAMB36E1 directly |
| Cascade across tiles without fabric mux | The cascade path is a dedicated wire between vertically adjacent tiles | CASCADE_HEIGHT attribute, or explicit instantiation |
| Hardened FIFO control logic | The pointer and flag logic lives inside the tile | xpm_fifo_* or FIFO36E1 |
| Dynamic power gating | The SLEEP pin has no RTL equivalent |
XPM macro's sleep port |
The middle ground - and the right answer nine times out of ten - is an XPM macro. It is source code you instantiate, so it stays readable and version-controlled, but it reaches every feature of the primitive:
// A 4096 x 64 simple dual-port RAM with single-error-correct,
// double-error-detect. Nothing you can write in plain RTL infers this.
xpm_memory_sdpram #(
.MEMORY_SIZE (262144), // bits, not words
.MEMORY_PRIMITIVE ("block"),
.WRITE_DATA_WIDTH_A (64),
.READ_DATA_WIDTH_B (64),
.ADDR_WIDTH_A (12),
.ADDR_WIDTH_B (12),
.READ_LATENCY_B (2), // 2 = use the output register
.ECC_MODE ("both_encode_and_decode"),
.CLOCKING_MODE ("independent_clock")
) u_ram (
.clka (wr_clk), .ena (wr_en), .wea (1'b1),
.addra (wr_addr), .dina (wr_data),
.clkb (rd_clk), .enb (rd_en),
.addrb (rd_addr), .doutb (rd_data),
// Report corrected and uncorrectable errors up to your status logic.
.sbiterrb (ecc_corrected), .dbiterrb (ecc_fatal),
.injectsbiterra (1'b0), .injectdbiterra (1'b0),
.regceb (1'b1), .rstb (1'b0), .sleep (1'b0)
);
Interview grilling - "How would you build a 2048 x 128 buffer, and what does it cost?"
Start with the arithmetic out loud, because that is the actual skill being tested:
- 2048 × 128 = 262,144 bits = 256 Kb.
- A BRAM36 holds 36 Kb, of which 32 Kb is usable data in the wide aspect
ratios. So the floor is
256/32 = 8tiles. - Width matters as much as total bits: as SDP each tile gives 72 bits, so 128 bits needs 2 tiles side by side, and 2048 deep needs 4 tiles stacked - 2 × 4 = 8. The two calculations agree, which is the check that you did it right.
Then the engineering judgement:
- SDP, not TDP - one writer, one reader, so take the 72-bit width.
- Enable the output register and pay two cycles of latency; at 128 bits wide the clock-to-out matters.
- On UltraScale+, consider UltraRAM instead - one URAM288 is 288 Kb and would hold the whole thing in a single tile, freeing eight BRAMs. The catch is that URAM cannot be initialised and both ports share one clock.
- Check the cascade. Four tiles stacked should use the dedicated cascade path; if the tool built a fabric multiplexer instead, you have added a logic level to every read.
The sentence that closes it: "And I would confirm the result in
report_utilization rather than trusting the arithmetic - an off-by-one on
width is the difference between 8 tiles and 16."
Volume 04 recap
| Concept | The one thing to remember |
|---|---|
| Inference | Pattern matching. Registered read, no reset on the array. |
$readmemh |
Runs at synthesis and lands in the bitstream. Not simulation-only. |
| Write modes | NO_CHANGE by default - least power, best timing. |
| SDP vs TDP | SDP gives 72-bit width. One writer + one reader? Say so. |
| Dual-clock BRAM | The array is safe. The pointer protocol is your job. |
| FWFT | !empty = valid, rd_en = ready. Free handshake. |
prog_full |
Lags by the synchroniser depth. Leave headroom. |
| XPM macros | Source you can read, features you cannot infer. |