Block RAM vs distributed RAM: where your memory actually goes

Write reg [7:0] mem [0:255]; and the synthesizer has a choice to make: build it from dedicated block RAM, or assemble it from the LUTs themselves (distributed RAM). The two have different superpowers, and knowing which you'll get, and how to steer the choice, is one of those small skills that pays off in every design.

Two kinds of silicon

Block RAM is dedicated SRAM: 36 Kb blocks (AMD), 20 Kb M20K (Intel), 18 Kb EBR (ECP5). True dual-port, optional output registers, byte enables. You have a fixed number of blocks and each is all-or-nothing, a 40-bit-deep scratchpad consumes a whole block, which is why the BRAM estimator exists.

Distributed RAM exploits a beautiful fact: a LUT is a small memory (that's the glossary definition). Fabrics that support it (AMD's SLICEM, some others) let you write to it at runtime, a 6-LUT becomes a 64x1 RAM. Assemble a few and you have a small, wide, everywhere-available memory that lives inside the logic it serves.

The decisive differences

Block RAM Distributed RAM
Best size KBytes and up up to a few hundred bits deep
Read synchronous (1-cycle latency) asynchronous (same-cycle)
Location fixed columns on the die anywhere, inside your logic
Cost model whole blocks LUTs you'd otherwise use for logic
Ports true dual-port 1 write + 1-few reads

The sleeper issue is the read latency. BRAM reads are registered, address in this cycle, data next cycle. Distributed RAM reads are combinational, data falls out the same cycle, straight into your logic. That single difference decides most close calls:

What the tools decide (and how to overrule them)

Synthesizers use depth thresholds: tiny memories → distributed, big ones → BRAM, with a gray zone (roughly 64–1024 entries) decided by heuristics. When you disagree:

(* ram_style = "block" *)       reg [7:0] mem [0:511];   // AMD/Xilinx
(* ram_style = "distributed" *) reg [7:0] mem [0:63];
// Intel: (* ramstyle = "M20K" *) or "MLAB"; Quartus also honors logic

Two classic interventions:

  1. Out of BRAMs? Push the small-but-many memories (FIFOs of depth 32, lookup tables) to distributed and reclaim whole blocks, check the tiling math with the estimator first.
  2. Fmax suffering on an async-read path? That combinational read is in your critical path; move to BRAM and absorb the pipeline stage, or register the distributed read yourself.

The inference recipe that always works

For BRAM, keep the template simple and registered, the Verilog cheatsheet version:

always @(posedge clk) begin
    if (we) mem[waddr] <= wdata;
    rdata <= mem[raddr];        // registered read → BRAM
end

Read the write port and read port in the same always block for write-first behavior, separate blocks for read-first, and if you need a memory with asynchronous read (assign rdata = mem[raddr];), you've just asked for distributed RAM whether you meant to or not. On iCE40, note there's no distributed RAM at all: small memories cost real EBR blocks or flops, which changes the math for tiny FPGAs (board picker has the capacities).

Rule-of-thumb summary

Memory placement is one of the few areas where a one-line attribute can free 20% of your chip. Worth the five minutes.

memory