FPGA

BRAM inference vs instantiation in FPGA

By CourseTron Team ·  3 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    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.

    Two roads to a block RAM Behavioral RTL reg [W-1:0] mem[0:D-1] Instantiated RAMB36E2 / IP core Synthesis pattern match BRAM tile 36 Kb / M20K binds straight to the primitive CourseTron · coursetron.com
    Inference walks through pattern recognition; instantiation binds straight to the primitive.

    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.

    Read-during-write modes Same address read and written in one cycle: mem[a] holds OLD, din carries NEW. Read-first read assigned before the write dout = OLD Write-first write assigned before the read dout = NEW No-change output register holds its previous value dout = held CourseTron · coursetron.com
    Collision behavior of a BRAM tile when one address is read and written in the same cycle.

    A moving picture: the registered read pipeline

    Registered read: one cycle of latency clk addr dout A0A1A2A3not yet validD0 = mem[A0]D1 = mem[A1]D2 = mem[A2] Address A0 is presented on one edge; its data D0 appears on the next. CourseTron · coursetron.com
    Address presented on one edge; registered read data emerges on the next — the behavior a BRAM gives you for free.

    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.
    DimensionInferenceInstantiation
    Portability across vendorsHigh — plain RTLNone — vendor-locked
    Readability / reviewExcellentOpaque black box
    Access to ECC / FIFO / asymmetryLimitedFull
    Risk of wrong map (LUT RAM)Real if template brokenNone
    Simulation setupZero extraNeeds 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.

    Advertisement