Skip to content

VHDL · Chapter 17.2 · FPGA-Oriented VHDL Design

Inferring Block RAM

Block RAM is the FPGA's dedicated memory, and the difference between getting it and getting a wall of flip-flops comes down to one detail. Block RAM has a registered read port, so your code must perform a synchronous read by assigning the read value inside the clocked process. Do that and an array maps to block RAM; read it combinationally and you get distributed LUT RAM instead. This lesson covers the port styles, single, simple dual-port for FIFOs, and true dual-port, the optional output register that adds a cycle of latency for higher speed, and the read-during-write modes. It also explains two FPGA constraints, that block RAM cannot be reset across the whole array so you initialise it with a constant, and that you can force the style with an attribute.

Foundation15 min readVHDLFPGABlock RAMBRAMMemoryInference

1. Engineering intuition — match the code to the block's shape

A block RAM is a physical thing with a fixed shape: an address port, a data port, a write enable, and — critically — a registered output. To make the synthesizer drop your array into one, your code has to look like that shape, and the one feature it must match is the registered read: the read data appears one clock after the address, because the BRAM latches it. If you read combinationally (data follows address in the same instant), you have described something the BRAM cannot be, so the tool falls back to LUT RAM or flip-flops. Everything else — port count, output register, collision behavior — is configuration, but the registered read is the gate. Write the read inside the clocked process and you are speaking the BRAM's language.

2. Formal explanation — the synchronous-read styles

bram_styles.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
type ram_t is array (0 to 1023) of std_logic_vector(31 downto 0);
signal ram : ram_t;                                 -- NO array-wide reset (BRAM can't) — init via constant
 
-- SINGLE-PORT, SYNCHRONOUS read → BLOCK RAM.
process (clk) begin
  if rising_edge(clk) then
    if we = '1' then ram(to_integer(unsigned(addr))) <= din; end if;   -- write
    dout <= ram(to_integer(unsigned(addr)));                           -- REGISTERED read → BRAM
  end if;
end process;
 
-- SIMPLE DUAL-PORT (1 write + 1 read, e.g. FIFO): independent write and read addresses.
process (clk) begin if rising_edge(clk) then
  if we='1' then ram(to_integer(unsigned(waddr))) <= din; end if;      -- write port
  dout <= ram(to_integer(unsigned(raddr)));                            -- read port (registered)
end if; end process;
 
-- OPTIONAL OUTPUT REGISTER: add a second register → +1 cycle latency, higher Fmax.
-- dout <= dout_pre;   (dout_pre <= ram(...) registered, then dout registers it again)

The rule: a synchronous (registered) read inside the clocked process infers block RAM; an asynchronous read infers distributed RAM. Port configs: single, simple dual (1W/1R), true dual (2 R/W). An optional output register adds latency for speed. BRAM has no array-wide reset — initialize the array with a constant. ram_style (16.8) can force block vs distributed.

3. Production usage — read-during-write and FPGA caveats

bram_caveats.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- READ-DURING-WRITE (same address, same cycle) — the chosen ordering selects the BRAM mode:
--   WRITE-FIRST : read returns the NEW data (write bypasses to the read port)
--   READ-FIRST  : read returns the OLD data (read happens before the write)
--   NO-CHANGE   : read output holds (unchanged) during a write
-- The exact statement ordering in the clocked process picks which mode is inferred.
 
-- INITIAL CONTENTS via a constant (NOT a reset): BRAM supports init, not per-cycle array reset.
constant INIT : ram_t := ( 0 => x"DEADBEEF", others => (others => '0') );
signal ram : ram_t := INIT;                          -- power-up contents
 
-- FORCE the style if inference picks wrong (16.8):
attribute ram_style : string;
attribute ram_style of ram : signal is "block";      -- "block" | "distributed"
 
-- BYTE ENABLES: write selected bytes by slicing the word under per-byte write enables.

What hardware does this become? One (or more) BRAM primitives: a 1024×32 array is a single block, written and read a word per cycle with the registered output the BRAM provides. Read-during-write ordering maps to the BRAM's hardware collision mode (write-first/read-first/no-change) — important when a read and write hit the same address. The output register option uses a second BRAM register stage to push Fmax up at the cost of another cycle of latency. And because BRAM has no array-wide reset, you give power-up values via a constant initializer, not a synchronous clear — trying to reset the whole array is exactly what kicks it out of BRAM into flip-flops.

4. Structural interpretation — block RAM inference

block RAM: write port into array, registered read port producing data one cycle later, optional output registerwriteregistered readoptionalwrite portwe, waddr, din (clocked)BRAM array1024 × 32 (no array reset)read registersynchronous read → BRAMoptional output reg+1 cycle, higher Fmax12
Block RAM inference on an FPGA hinges on a registered read. The write port clocks din into the array at the write address when write-enable is high; the read port assigns the array output into a register, so the read data appears one cycle after the address — matching the block RAM's physical registered output port. An optional second output register adds another cycle of latency for higher Fmax. Port configuration (single, simple-dual, true-dual) sets how many read/write ports exist; read-during-write ordering selects the collision mode. Block RAM cannot be reset array-wide, so contents are initialized via a constant. This is a memory-inference structure; the waveform below shows the registered-read latency.

5. Simulation interpretation — registered-read latency

Block RAM read latency: 1 cycle (read reg), or 2 with the output register

8 cycles
Block RAM read latency: 1 cycle (read reg), or 2 with the output registerdout_1: 1-cycle latency (read register) — data for addr 5dout_1: 1-cycle latenc…dout_2: 2-cycle latency (read reg + output reg) — higher Fmaxdout_2: 2-cycle latenc…clkaddr56777777dout_1--M5M6M7M7M7M7M7dout_2----M5M6M7M7M7M7t0t1t2t3t4t5t6t7
A block RAM read is registered, so dout_1 reflects the addressed word one clock after the address — the signature of BRAM (vs a same-cycle combinational read, which would be LUT RAM). Adding the optional output register makes dout_2 lag by two cycles, buying higher Fmax. Surrounding logic (FIFOs, pipelines) must account for this fixed read latency.

6. Debugging example — the memory that would not map to BRAM

Expected: a large array uses one block RAM. Observed: synthesis builds it from flip-flops/LUT RAM (huge utilization), or reports the memory could not map to block RAM. Root cause (FPGA-specific): the read was asynchronous (a concurrent dout <= ram(addr)), or the array had an array-wide reset (BRAM cannot reset all locations per cycle), or an unsupported port/read-during-write style — any of which makes the BRAM template not match, so the tool falls back. Fix: use a synchronous (registered) read inside the clocked process, remove the array-wide reset (initialize via a constant instead), keep to a supported port count and read-during-write mode, and force with ram_style => "block" if inference still picks distributed. Engineering takeaway: BRAM needs a registered read and no array-wide reset — an async read or a full-array clear forces LUT RAM/flops; register the read, initialize by constant, and use ram_style to force the block.

register_read_no_array_reset.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: array-wide reset + (often) async read → can't map to BRAM, falls back to flops.
-- if rst='1' then ram <= (others => (others => '0')); end if;   -- BRAM can't do this
-- FIX: synchronous read, NO array reset (init via constant), force block if needed.
process (clk) begin if rising_edge(clk) then
  if we='1' then ram(to_integer(unsigned(addr))) <= din; end if;
  dout <= ram(to_integer(unsigned(addr)));    -- registered read → BRAM
end if; end process;

7. Common mistakes & what to watch for

  • Asynchronous read. BRAM needs a registered read; a combinational read infers distributed/LUT RAM.
  • Array-wide reset. BRAM cannot reset every location per cycle; initialize contents with a constant, not a synchronous clear.
  • Ignoring read latency. A registered read is 1 cycle (2 with the output register); account for it in FIFOs and pipelines.
  • Undefined read-during-write. Choose write-first/read-first/no-change deliberately via statement ordering; same-address collisions matter.
  • Wrong inferred style. If the tool picks distributed RAM, force block with the ram_style attribute (16.8).

8. Engineering insight & continuity

Block RAM inference on an FPGA comes down to a registered read — write the array's read inside the clocked process and it maps to dedicated BRAM, with port configuration, an optional output register (extra latency for Fmax), and a chosen read-during-write mode — while remembering BRAM has no array-wide reset (initialize via constant) and ram_style can force the choice. This is how data-heavy designs avoid drowning the fabric in flip-flops. The other dedicated resource for compute-heavy designs is the multiplier/MAC, covered next: Inferring DSP and Multiplier Blocks — mapping arithmetic onto the FPGA's DSP slices.