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
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
-- 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
5. Simulation interpretation — registered-read latency
Block RAM read latency: 1 cycle (read reg), or 2 with the output register
8 cycles6. 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.
-- 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_styleattribute (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.