VHDL · Chapter 13.2 · Advanced Data Structures
Multidimensional and Nested Arrays
When a table needs more than one index, VHDL gives you two distinct tools, and choosing correctly matters. A true two-dimensional array is indexed with a comma, like m of i and j, and models a genuine matrix or grid. An array of arrays, also called nested, has an element type that is itself an array, so it is indexed by chaining, and it is the natural model for a memory as an array of words. The key practical difference is that only the nested form lets you grab an entire row as a unit, slice it, or pass it around, which is why RTL memories almost always use it. This lesson contrasts the two forms, shows indexing and row access, and notes what each synthesizes to.
Foundation14 min readVHDLArrays2D ArraysMemoryData StructuresRTL
1. Engineering intuition — a matrix versus a stack of words
Two-dimensional data comes in two flavours, and they want different shapes. Sometimes it is a genuine grid —
a matrix of coefficients, a 2-D pixel block — where (row, column) is one coordinate; a true 2-D array captures
that. Other times it is a stack of words — a memory where row i is a complete word you read, write, and
slice as a unit; an array of arrays captures that. The deciding question is: do you ever need a whole row at
once? If yes — and for memories you always do — you want the nested form, because mem(i) is itself a vector
you can manipulate. If you only ever touch individual cells of a fixed grid, a true 2-D array is fine.
2. Formal explanation — true 2-D vs array of arrays
library ieee; use ieee.std_logic_1164.all;
-- (1) TRUE 2-D array: one type, comma-indexed. Good for matrices / grids.
type matrix_t is array (0 to 3, 0 to 7) of std_logic; -- 4 × 8 grid of bits
signal m : matrix_t;
-- m(i, j) -- access ONE cell; you canNOT take a whole row m(i) as a value
-- (2) ARRAY OF ARRAYS (nested): element type is itself an array. Chained index.
subtype word_t is std_logic_vector(7 downto 0);
type mem_t is array (0 to 15) of word_t; -- 16 words of 8 bits
signal mem : mem_t;
-- mem(i) -- a WHOLE WORD (row) as a value → sliceable, assignable
-- mem(i)(j) -- bit j of word i
-- mem(i)(3 downto 0) -- slice of a rowA true 2-D array is a single type indexed m(i, j); its elements are scalars and you cannot address a
whole row as one object. An array of arrays has an array element type, indexed mem(i)(j); crucially mem(i)
is itself a vector — so you can read, write, slice, and pass a whole row.
3. Production usage — memory as an array of words
-- RTL memories use the NESTED form so a word is a first-class value.
process (clk) begin
if rising_edge(clk) then
if we = '1' then
mem(to_integer(unsigned(addr))) <= din; -- write a WHOLE word (row) at once
end if;
dout <= mem(to_integer(unsigned(addr))); -- read a whole word
end if;
end process;
-- Row-level operations the nested form enables:
-- tag <= mem(i)(7 downto 4); -- slice a field out of a word
-- mem(i) <= (others => '0'); -- clear a whole word
-- A true 2-D array suits a coefficient grid accessed cell-by-cell:
-- y <= coeff(r, c); -- one element of a fixed matrixWhat hardware does this become? The nested mem is a memory of words — 16 × 8-bit storage, written and
read a word at a time — which a synthesizer maps to a register bank or inferred RAM. Because mem(i) is a vector,
din/dout are whole words and slicing a field is free wiring. A true 2-D coeff(r, c) becomes a grid of
registers/constants addressed per cell. Both are real storage; the nested form is preferred for memories precisely
because the word is the unit you operate on.
4. Structural interpretation — 2-D grid vs array of words
5. Why this is structural, not timing
The choice between a true 2-D array and an array of arrays is a data-layout decision — how the dimensions are typed and indexed, and whether a row is a first-class value — so a structure diagram (above), not a waveform, captures it. The run-time behaviour is just that of the storage it produces (a memory of words reads and writes a word per access, with timing already covered in the sequential/memory material). What this lesson buys you is correct shape: pick nested for memories so word-level read/write/slice is natural, true 2-D for fixed grids accessed cell-by-cell.
6. Debugging example — the 2-D array that would not give a row
Expected: read or write a whole word/row at once. Observed: a compile error trying to use m(i) on a
true 2-D array, or awkward per-cell loops to move a row, or a din/word assignment that will not type-check.
Root cause: the storage was declared as a true 2-D array (array (R, C)), which has no notion of a row as
a value, when the design actually needs word-level access. Fix: declare it as an array of arrays
(array (0 to R-1) of word_t) so mem(i) is a vector you can read, write, and slice as a unit. Engineering
takeaway: if you ever need a whole row/word as one value — and memories always do — use an array of arrays, not
a true 2-D array; the comma-indexed form only addresses individual cells.
-- BUG: true 2-D array → no whole-row value, can't write a word at once.
-- type m_t is array (0 to 15, 0 to 7) of std_logic; signal m : m_t;
-- m(addr) <= din; -- illegal: m needs two indices; no row object
-- FIX: array of arrays → a word is a first-class value.
type mem_t is array (0 to 15) of std_logic_vector(7 downto 0);
mem(to_integer(unsigned(addr))) <= din; -- write a whole word7. Common mistakes & what to watch for
- True 2-D array for a memory. It has no row value; use an array of arrays so words are first-class (read/write/slice a whole word).
- Mixing the two index styles.
m(i, j)(true 2-D) vsarr(i)(j)(nested) are different types; use the syntax that matches the declaration. - Assuming whole-row ops on true 2-D. You cannot slice or assign a row of a comma-indexed array; only the nested form supports it.
- Forgetting inference rules for big arrays. Large word memories infer RAM with read latency; check the synthesis result for deep arrays.
- Ragged data in a true 2-D array. Fixed rectangular grids only; varying-length rows need nesting (and care), not a 2-D array.
8. Engineering insight & continuity
Two indices, two tools: a true 2-D array (m(i,j)) for genuine matrices and grids accessed cell-by-cell, and
an array of arrays (mem(i)(j)) for memories and any case where a whole row/word must be a first-class,
sliceable value — the form RTL memories use. Match the shape to whether you need row-level access. So far the
arrays have had fixed bounds; the next lesson removes that constraint — Unconstrained Array Types — letting
a type leave its size open so functions, ports, and generic blocks adapt to whatever width the caller supplies.