Skip to content

VHDL · Chapter 2.10 · Data Types

Array Types

A bus repeats one bit a fixed number of times, and a memory repeats one word. An array type is how VHDL expresses that repetition, one element type indexed over a range. You already use arrays, since the standard logic vector, string, and bit vector types are all predefined arrays, and your own arrays become register files, RAMs, and lookup tables. This lesson shows the difference between constrained and unconstrained arrays, how to index one with an integer and why a logic-vector address needs a numeric cast first, and how an aggregate initializes a whole array at once. It also covers the part that matters in silicon, namely how the element type and range fix the footprint while the read style decides whether the tool builds block RAM, distributed RAM, or plain flip-flops.

Foundation16 min readVHDLArraysMemoryRegister Filenumeric_stdTypes

1. Intuition — one element, repeated over an index

Hardware is full of regular structures: a register file is sixteen identical 32-bit registers; a RAM is a thousand identical words; a coefficient ROM is a fixed table of constants. An array type describes exactly that — a single element type repeated over an index range:

array_declaration.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 16 registers, each 32 bits wide, indexed 0 .. 15
type regfile_t is array (0 to 15) of std_logic_vector(31 downto 0);
signal regs : regfile_t;
 
regs(3) <= x"DEAD_BEEF";   -- index one element; the rest are untouched

The type carries the shape: the element type (std_logic_vector(31 downto 0)) says how wide each slot is, and the index range (0 to 15) says how many there are. One declaration, a whole memory.

2. You already use arrays

The bus type you have used since lesson 2.4 is itself an array — std_logic_vector is defined as an array of std_logic, and string is an array of character:

predefined_arrays.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- from std_logic_1164: a vector is an array of std_logic
type std_logic_vector is array (natural range <>) of std_logic;
 
-- from the language: a string is an array of characters
type string is array (positive range <>) of character;

The natural range <> is the key detail: these types are unconstrained — they declare the element type but leave the range open (<>, the box). You fix the range only when you make a signal or port: std_logic_vector(7 downto 0) is std_logic_vector constrained to 8 bits. So "a bus" is simply "an array, constrained at the point of use."

3. Where arrays belong — memories, register files, tables

The dominant use of a custom array is a memory or register file: a block of identical words you address with an index. A single-port synchronous RAM is the canonical example, and it is where the numeric_std cast from lesson 2.8 earns its keep — the address arrives as a std_logic_vector, but an array index must be an integer:

sync_ram.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;                     -- for unsigned / to_integer
 
entity sync_ram is
  generic ( AW : natural := 4;                 -- address width  → depth = 2**AW
            DW : natural := 8 );               -- data width
  port ( clk  : in  std_logic;
         we   : in  std_logic;
         addr : in  std_logic_vector(AW-1 downto 0);
         din  : in  std_logic_vector(DW-1 downto 0);
         dout : out std_logic_vector(DW-1 downto 0) );
end entity;
 
architecture rtl of sync_ram is
  type mem_t is array (0 to 2**AW - 1) of std_logic_vector(DW-1 downto 0);
  signal mem : mem_t := (others => (others => '0'));   -- aggregate: all words cleared
begin
  process (clk)
  begin
    if rising_edge(clk) then
      if we = '1' then
        mem(to_integer(unsigned(addr))) <= din;        -- write: index by integer
      end if;
      dout <= mem(to_integer(unsigned(addr)));          -- registered (synchronous) read
    end if;
  end process;
end architecture;

The address is cast to_integer(unsigned(addr)): read the bits as an unsigned number, then convert that number to the integer the index expects. Skipping the unsigned step does not compile — an array index is an integer, and a std_logic_vector is not a number (lesson 2.9).

4. Hardware — the shape is the footprint, the read style is the structure

An array's element type and range fix a precise hardware footprint: mem_t above is 2**AW words of DW bits, so an 8-bit, 16-deep RAM is exactly 128 bits of storage. But the type does not decide what kind of storage the tool builds — the access pattern does.

array type mapping to memory storage, with the read style selecting block RAM, distributed RAM, or flip-flopssync 1-port readasync / many portssmall, read infullmem_tarray (0 to 15) ofstd_logic_vector(7 downto0)block RAMclocked single port →dedicated RAM blockdistributed RAMasync / multi-read →LUT-RAMflip-flopssmall / parallel-read →registers12
An array type is element-type × index-range — here 16 words of 8 bits. Synthesis maps it to a storage structure chosen by how it is accessed: a clocked, single read/write port (as in sync_ram) infers a block RAM; an asynchronous (combinational) read or several read ports infers distributed RAM or LUT-RAM; a tiny array read in parallel becomes ordinary flip-flops. The array names the storage; the read/write style picks the silicon.

This is why the dout <= mem(...) line being inside the clocked process matters: a registered read is what lets the tool use a dedicated block-RAM primitive, which has a one-cycle read latency. Move the read outside the process (a combinational dout <= mem(addr_int);) and you ask for an asynchronous read, which most block RAMs cannot do — so the tool falls back to slower, larger distributed RAM. The array type is identical; the read style changed the silicon.

5. Array in action — addressed write, then registered read

Because a memory is accessed over time, its behaviour is best read on a waveform. Here a word is written to address 3, then the same address is read back — and the registered read shows up one cycle later, the latency the block-RAM mapping buys:

sync_ram — write to address 3, then read it back one cycle later

10 cycles
sync_ram — write to address 3, then read it back one cycle laterwe=1, addr=3, din=A5 → mem(3) captured this edgewe=1, addr=3, din=A5 →…we=0, read addr 3 → dout shows A5 (registered, 1-cycle latency)we=0, read addr 3 → do…clkwe0110000000addr0333333333din0A5A5A5A5A5A5A5A5A5dout000000A5A5A5A5A5A5A5t0t1t2t3t4t5t6t7t8t9
A memory is observed over the clock, not as a static value — which is what justifies a waveform here. The write commits mem(3)=A5 on the rising edge with we=1; the synchronous read presents that word on dout one cycle after the address is applied. The single-cycle gap between addr and dout is the read latency of the registered (block-RAM) read in Section 4.

6. Debugging example — index out of range

The most common array bug is an index that leaves the range. VHDL is strongly typed about indices too: mem(20) on a 16-deep array is illegal, and at simulation it is a hard error, not a wrap-around:

the out-of-range index
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
type mem_t is array (0 to 15) of std_logic_vector(7 downto 0);   -- legal index 0..15
signal mem : mem_t;
signal addr : std_logic_vector(4 downto 0);                      -- 5 bits → 0..31
 
-- mem(to_integer(unsigned(addr)))  -- if addr = 20, this is OUT OF RANGE
-- Simulation aborts: "index value 20 out of range 0 to 15".

The cause is almost always a width mismatch: the address bus is wider than the memory is deep. The fix is to size the index to the array, using the array's own bounds so the two can never drift apart:

size the address to the array
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- depth and address width derived from ONE generic, so they cannot disagree
type mem_t is array (0 to 2**AW - 1) of std_logic_vector(DW-1 downto 0);
-- addr is AW bits → unsigned range is exactly 0 .. 2**AW-1 = the array's range.
-- Use the array attribute to bound a loop, never a hard-coded number:
for i in mem'range loop                       -- 'range tracks the declaration
  mem(i) <= (others => '0');
end loop;

Using mem'range (and mem'length, mem'high, mem'low) instead of literal bounds means the loop and the declaration can never disagree — change the depth in one place and every 'range follows.

7. Common mistakes & what to watch for

  • Indexing with a std_logic_vector directly. An index is an integer. Cast the bus first: mem(to_integer(unsigned(addr))), never mem(addr).
  • Address wider than the array is deep. A 5-bit address into a 16-word array goes out of range. Derive depth and address width from one generic (2**AW).
  • Assuming an array is always block RAM. A combinational read, multiple read ports, or a small array read in full will not map to a block RAM — you get distributed RAM or flip-flops instead.
  • Hard-coding bounds. for i in 0 to 15 rots when the depth changes; write for i in mem'range.
  • Confusing to and downto in the index. array (0 to 15) ascends; array (15 downto 0) descends. It changes what 'left, 'high, and a positional aggregate mean — pick one and be consistent.
  • Read-during-write semantics. Reading and writing the same address on the same edge returns the old word here (the read sees mem before the scheduled write lands). If you need the new value, forward din explicitly. Know which your RAM does.

8. Engineering insight

An array is how VHDL expresses regular hardware. The type captures the shape exactly — element type for width, index range for depth — so a memory's footprint is visible in its declaration. But the deeper lesson is that the array type and the physical memory are decoupled: the same mem_t becomes a block RAM, a distributed RAM, or a bank of flip-flops depending on how you read it. That is leverage. You describe storage once and choose its implementation by where you put the read — a one-line move from registered to combinational changes latency, area, and which primitive the tool reaches for. Arrays repeat one type; the next lesson covers the complementary structure — grouping different types together.

9. Summary & next step

An array type is one element type indexed over a range — the type of memories, register files, and tables. std_logic_vector, string, and bit_vector are predefined unconstrained arrays; constrain the range at the signal, port, or generic. Index with an integer, casting a bus address through to_integer(unsigned(...)), and initialise with an aggregate ((others => (others => '0'))). The element type and range fix the footprint, while the read style (synchronous vs combinational, one port vs many) decides whether the tool builds block RAM, distributed RAM, or flops. Bound loops and indices with 'range and 'length so a depth change never strands a literal.

Arrays repeat the same element. The next lesson turns to the opposite need — bundling different fields (an address, a valid bit, a payload) under one name: the record, VHDL's tool for grouping a whole bus interface into a single, self-documenting type.