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.
Standards & specifications
- Governing standard
- IEEE Std 1076 (VHDL)(opens IEEE in a new tab)
Defines the VHDL language — types, the simulation cycle, and the semantics a conforming analyser and simulator must implement. Synthesis restrictions and vendor coding rules are tool behaviour, not language rules.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the VHDL curriculum.
