Skip to content

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

rom_and_ram.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

dual_port_and_rdw.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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

synchronous RAM: write port into array, registered read producing dout one cycle laterwriteregistered readwrite portwe, addr, din (clocked)word array (storage)DEPTH × DATA_Wread registersynchronous read → BRAMdoutone cycle after addr12
A synchronous-read RAM infers block RAM. The write port clocks din into the array at addr when we is high; the read port assigns the array output into a register, so dout appears one cycle after addr — exactly the structure of a block RAM with a registered read port. An asynchronous read (a concurrent dout <= ram(addr)) would instead infer distributed/LUT RAM with combinational output. The read-during-write ordering selects read-first vs write-first behavior on same-address collisions. This is a memory-inference structure; the waveform below shows the one-cycle registered read.

5. Simulation interpretation — the one-cycle synchronous read

Synchronous-read RAM: write address 5, then read it back one cycle late

8 cycles
Synchronous-read RAM: write address 5, then read it back one cycle latewe=1: write 0xC3 into address 5 (clocked write)we=1: write 0xC3 into …registered read: dout = 0xC3 — ONE cycle after addr is presentedregistered read: dout …clkwe11000000addr55555555dinC3C3------------dout----C3C3C3C3C3C3t0t1t2t3t4t5t6t7
The registered read is what makes this a block RAM: dout reflects the addressed word one clock after the address, not combinationally. That single cycle of read latency is the visible signature of synchronous-read memory — and exactly the timing a block RAM primitive provides. An asynchronous read would show dout following addr in the same cycle, at the cost of using LUT/distributed RAM.

6. 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.

register_the_read.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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.