VHDL · Chapter 13.5 · Advanced Data Structures
Memory and RAM/ROM Modeling
There is no memory keyword in VHDL. A RAM or ROM is just an array of words written in a coding style the synthesizer recognizes and maps to dedicated storage. A ROM is a constant array indexed at runtime, mapping to block ROM or LUTs. A RAM is a signal array with a clocked write, and the decisive detail is the read: a synchronous, registered read infers dedicated block RAM, while an asynchronous read infers distributed LUT RAM instead. You also choose the read-during-write behaviour, provide initial contents, and pick single-port or simple dual-port with one write and one read port. Getting the style right is what makes the tool infer the memory primitive you intended instead of a wall of flip-flops. This lesson covers the ROM, single-port, and dual-port styles and the synchronous-read rule for block RAM.
Foundation15 min readVHDLMemoryRAMROMBlock RAMSynthesis
1. Engineering intuition — memory is a recognized pattern, not a keyword
FPGAs and ASICs have dedicated memory blocks — block RAM, LUT RAM, ROM — and the synthesizer maps your code onto them by recognizing a pattern, not by a special type. That pattern is: an array of words, a clocked write, and a read. The single most important knob is whether the read is registered: a block RAM physically has a registered read port, so to infer one your code must read through a register (synchronous read). Read combinationally (asynchronous) and the tool cannot use block RAM, so it falls back to distributed/LUT RAM or flops. So memory modeling is really about writing the array access in the exact shape the target primitive expects.
2. Formal explanation — ROM and single-port RAM styles
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- ROM: a CONSTANT array indexed at runtime → block ROM / LUTs.
type rom_t is array (0 to 255) of std_logic_vector(7 downto 0);
constant ROM : rom_t := ( 0 => x"3A", 1 => x"7F", others => x"00" ); -- initial contents
-- (registered read recommended for block ROM inference)
process (clk) begin
if rising_edge(clk) then dout <= ROM(to_integer(unsigned(addr))); end if;
end process;
-- SINGLE-PORT RAM with SYNCHRONOUS read → infers BLOCK RAM.
type ram_t is array (0 to 1023) of std_logic_vector(31 downto 0);
signal ram : ram_t;
process (clk) begin
if rising_edge(clk) then
if we = '1' then ram(to_integer(unsigned(addr))) <= din; end if; -- clocked write
dout <= ram(to_integer(unsigned(addr))); -- REGISTERED read → BRAM
end if;
end process;
-- (An ASYNCHRONOUS read — dout <= ram(addr) as a concurrent statement — infers DISTRIBUTED/LUT RAM.)A ROM is a constant array indexed by a runtime address; a RAM is a signal array with a clocked write.
The read style decides the primitive: a registered read (assigned inside the clocked process) infers block
RAM; an asynchronous read infers distributed RAM. Initial contents come from the array's initializer.
3. Production usage — read-during-write and simple dual-port
-- SIMPLE DUAL-PORT: one write port, one read port (e.g. for FIFOs, buffers).
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))); -- independent read port
end if;
end process;
-- READ-DURING-WRITE choice when waddr = raddr in the same cycle:
-- READ-FIRST (read OLD data): read assigned from the array BEFORE/independent of the write
-- WRITE-FIRST (read NEW data): bypass — dout takes din when writing the same address
-- The exact ordering in the clocked process selects which BRAM mode the tool infers.What hardware does this become? With a synchronous read, both the single- and dual-port styles map to a
block RAM primitive: 1024×32 storage with registered output, one or two ports. The read-during-write
ordering you write (assign dout from the array vs bypass din) selects the BRAM's read-first/write-first mode —
which matters when a read and write hit the same address in one cycle. Choosing dual-port lets reads and writes
proceed independently, the shape FIFOs and line buffers need. The array is the memory; the access style picks
the primitive and its collision behavior.
4. Structural interpretation — synchronous-read RAM inference
5. Simulation interpretation — the one-cycle synchronous read
Synchronous-read RAM: write address 5, then read it back one cycle late
8 cycles6. Debugging example — the RAM that became a wall of flip-flops
Expected: a large array infers block RAM. Observed: synthesis builds thousands of flip-flops (or fails to
fit), or reports the memory could not be mapped to block RAM. Root cause: the read was asynchronous (a
concurrent dout <= ram(addr)), or the array was reset/initialized in a way block RAM cannot support, or it had
too many ports / an unsupported read-during-write style — so the tool could not use a block RAM primitive and fell
back to registers/LUT RAM. Fix: use a synchronous (registered) read inside the clocked process, avoid a
global array reset, and keep to a supported port count and read-during-write mode so the BRAM template matches.
Engineering takeaway: block RAM inference needs a registered read and a BRAM-compatible style — an
asynchronous read or array-wide reset forces distributed RAM or flip-flops instead.
-- BUG: asynchronous read → cannot infer block RAM (distributed RAM / flops instead).
-- dout <= ram(to_integer(unsigned(addr))); -- concurrent, combinational read
-- FIX: register the read inside the clocked process → block RAM.
process (clk) begin if rising_edge(clk) then
dout <= ram(to_integer(unsigned(addr))); -- synchronous read → BRAM
end if; end process;7. Common mistakes & what to watch for
- Asynchronous read expecting block RAM. BRAM needs a registered read; a combinational read infers distributed/LUT RAM.
- Resetting the whole array. Block RAM cannot be reset cycle-by-cycle; use initial contents, not an array-wide synchronous reset.
- Ignoring read-during-write. Same-address read+write in one cycle returns old or new data per your ordering; choose read-first/write-first deliberately.
- Too many ports. Most block RAMs are single- or simple-dual-port; true multi-port memories cost extra logic or replication.
- Forgetting the read-latency. A synchronous read adds one cycle; account for it in surrounding timing (FIFOs, pipelines).
8. Engineering insight & continuity
Memory modeling is pattern-matching: an array of words plus the right access style infers the intended primitive — a constant array for ROM, a clocked write with a registered read for block RAM, an asynchronous read for distributed RAM — with read-during-write ordering and port count selecting the exact mode. The registered read (and its one-cycle latency) is the signature of block RAM. This is the synthesis payoff of arrays of words. Module 13 next turns to two simulation-only data constructs that round out the type system — Protected Types (the next lesson: shared, safely-accessed state for testbenches) and then access types.