In short: Prefer inference — write a plain synchronous read/write memory array in RTL and let the synthesis tool map it to block RAM — and fall back to instantiation of a vendor primitive or IP core only when you need a feature the inference templates cannot express, such as true dual-port asymmetric widths, built-in ECC, or a hard FIFO.
Ask any FPGA engineer where their design's area really went and the honest answer is usually "the memories." Modern parts carry hundreds to thousands of dedicated block-RAM tiles — AMD/Xilinx 7-series and UltraScale call them BRAM36 (36 Kb each, splittable into two 18 Kb halves), and Intel/Altera devices expose M20K blocks. How you describe a memory in Verilog or VHDL decides whether the tool quietly drops it into one of those tiles or scatters it across thousands of LUTs. This article unpacks the two ways to get there — inference and instantiation — and the exact coding styles that steer the mapper.
Two roads to a block RAM
Inference means you write behavioral RTL — an array indexed on a clock edge — and the synthesis engine (AMD Vivado, Intel Quartus, Synopsys Synplify, or Siemens' Precision) recognizes the pattern and infers a BRAM. Instantiation means you drop a concrete component into your netlist: either a low-level primitive (RAMB36E2, altsyncram) or a generated IP core (Vivado's Block Memory Generator, Quartus' RAM IP).
Rule of thumb: if two senior engineers reading your module can both tell it is a RAM from the RTL alone, the tool can infer it too. Inference keeps the source portable and readable; instantiation locks you to one vendor but exposes every hardware feature.
The canonical inference template
The single most important rule for inferring a block RAM is: register the read. A BRAM tile has a synchronous read port — the read data appears one clock after the address is presented. If your RTL reads combinationally (address in, data out same cycle), the tool physically cannot use a BRAM and will build distributed RAM out of LUTs instead. A simple single-port synchronous RAM:
reg [31:0] mem [0:1023];
reg [31:0] dout;
always @(posedge clk) begin
if (we) mem[addr] <= din;
dout <= mem[addr]; // registered read -> BRAM
end
That one module reliably maps to a single 36 Kb tile (1024 × 32 = 32 Kb). Drop the dout <= register and read mem[addr] directly, and the same 1024 × 32 array explodes into roughly 512 LUTs of distributed RAM plus select logic — fine for a 32-deep buffer, ruinous for a 1024-deep one.
A 2 K × 36 memory inferred correctly uses two BRAM36 tiles. Written as an unregistered array it can consume well over 1,000 LUTs — on a mid-range part that is a measurable fraction of the whole fabric, gone to a single table.
Read-during-write: the detail that bites
BRAM tiles have a configurable collision behavior when you read and write the same address in the same cycle: read-first (old data out), write-first / transparent (new data out), or no-change (output holds). The order of statements in your always block tells the tool which mode you want. Reading before the write assignment implies read-first; writing before reading implies write-first. Get this wrong and simulation and hardware agree — but neither matches your intent, which is worse. Vendor coding guides (Xilinx UG901, the Quartus HDL style guide) publish the exact templates; match them character-for-character when a mode matters.
A moving picture: the registered read pipeline
When inference is not enough — reach for instantiation
Inference templates cover single-port, simple dual-port, and true dual-port RAMs, plus ROMs initialized from a file. They do not cleanly express:
- Asymmetric port widths — e.g. write 72-bit, read 8-bit. Most tools will not infer this; instantiate the Block Memory Generator /
altsyncram. - Built-in ECC — the hardened 64-bit SECDED logic inside UltraScale BRAM is only reachable through the primitive or IP.
- Hard FIFO mode — the FIFO36 controller around the tile saves fabric; use the FIFO IP.
- Byte-write enables with exotic granularity or cascaded-tile deep memories where you want explicit control over placement.
| Dimension | Inference | Instantiation |
|---|---|---|
| Portability across vendors | High — plain RTL | None — vendor-locked |
| Readability / review | Excellent | Opaque black box |
| Access to ECC / FIFO / asymmetry | Limited | Full |
| Risk of wrong map (LUT RAM) | Real if template broken | None |
| Simulation setup | Zero extra | Needs vendor sim model |
Even AMD and Intel now blur the line with synthesis attributes: (* ram_style = "block" *) in Vivado or ramstyle = "M20K" in Quartus forces an inferred array into a BRAM even when the tool was tempted to use LUTs — giving you inference's readability with instantiation's certainty. Use the attribute rather than rewriting readable code into a primitive. For a deeper comparison of when a memory belongs in BRAM versus LUT fabric, see our companion piece on block RAM vs distributed RAM, and for the timing closure angle read BRAM output-register timing.
If your project is moving toward custom silicon, the same read/write templates reappear in ASIC memory compilers — a topic our partners at Avecas VLSI design services handle with Synopsys and Siemens memory-BIST flows.
A disciplined default
Write inferred RTL first, confirm the synthesis report shows the expected RAMB36/M20K count, and reach for a primitive only when a feature forces it. Pin the mapping with a ram_style attribute rather than hoping, and always keep an eye on the utilization report — a memory that silently landed in LUTs is one of the most common causes of a design that "suddenly won't fit."
Want to drill inference templates, read-during-write modes, and timing closure on real hardware? The CourseTron FPGA & RTL design track builds these muscles with lab-graded exercises.
Frequently asked questions
Why did my memory synthesize to LUTs instead of BRAM?
Almost always because the read is combinational, the memory is too small to be worth a tile, or an initialization/reset pattern the BRAM cannot support forced the tool to use distributed RAM. Register the read output and check for an asynchronous reset on the array.
Is instantiation faster than inference?
No — both end up in the same physical tile, so Fmax is identical. Instantiation only guarantees which features and configuration you get; it does not make the memory quicker.
Can I force inference to use a block RAM?
Yes. Apply a synthesis attribute: (* ram_style = "block" *) in Vivado/Synplify or ramstyle = "M20K" in Quartus on the array declaration. The tool will then honor BRAM mapping or warn if it is impossible.
Does a reset on the memory array hurt BRAM inference?
Yes — a BRAM tile has no reset on its storage, only on the optional output register. If your RTL clears the whole array on reset, the tool cannot infer a BRAM and falls back to flops or LUT RAM. Reset the output register only.