Skip to content

VHDL · Chapter 2.11 · Data Types

Record Types

An array repeats one element. A record does the opposite. It groups different fields, an address, a valid bit, and a data word, under a single name, exactly the way you think of a bus as one bundle of related wires. Records are how a real VHDL design keeps interfaces readable, because instead of threading eight loose ports through every level of hierarchy you pass one request record. This lesson shows how to declare and access a record, the difference between named and positional aggregates, and the two things engineers most need to know. A record costs nothing in hardware, since the netlist is identical to flattened signals, and every field of a record port shares one direction, which is why a bidirectional bus is split into a request record and a response record.

Foundation15 min readVHDLRecordsInterfacesBus BundleAggregatesTypes

1. Intuition — a type that bundles unlike fields

An array (lesson 2.10) repeats one element. But most interfaces are not repetition — they are a small set of related but different signals that always travel together. A write request on a simple bus is an address, a data word, and a valid flag: three different types, one logical thing. A record is the type for exactly that — a named bundle of fields, each with its own type:

record_declaration.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
 
type wr_req_t is record
  addr  : std_logic_vector(31 downto 0);   -- a bus
  data  : std_logic_vector(31 downto 0);   -- a bus
  valid : std_logic;                       -- a single bit
end record;
 
signal req : wr_req_t;
 
req.addr  <= x"4000_0010";                 -- access a field with a dot
req.valid <= '1';

Where an array index picks the n-th identical slot, a record field name picks a specific, named part. The two are complementary: arrays for repetition, records for composition.

2. Aggregates — building a whole record at once

You can assign every field in one shot with an aggregate. Prefer the named form (field => value) over the positional form, because named aggregates do not break when the record later grows or its fields are reordered:

record_aggregates.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- named (robust): each field is labelled, order is irrelevant
req <= ( addr  => x"4000_0010",
         data  => x"0000_00FF",
         valid => '1' );
 
-- a clean "idle" value with others
req <= ( valid => '0', others => (others => '0') );
 
-- positional (fragile): relies on declaration order — avoid in real code
req <= ( x"4000_0010", x"0000_00FF", '1' );

Records nest, too: a transaction record can contain a wr_req_t field, and you reach in with chained dots (txn.req.addr). That is how a complex interface stays one typed object instead of a loose pile of signals.

3. Where records belong — bundling an interface

The dominant use of a record is grouping a bus into one port. Compare an entity that lists every wire against one that passes a single record — the second is the same hardware, far more readable, and adds a field in one place instead of editing every port list in the hierarchy:

record_in_port.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- a request bundle and a response bundle (see Section 5 for why two)
type bus_req_t is record
  addr  : std_logic_vector(31 downto 0);
  wdata : std_logic_vector(31 downto 0);
  we    : std_logic;
  valid : std_logic;
end record;
 
type bus_resp_t is record
  rdata : std_logic_vector(31 downto 0);
  ready : std_logic;
end record;
 
entity peripheral is
  port ( clk  : in  std_logic;
         req  : in  bus_req_t;     -- one downstream bundle
         resp : out bus_resp_t );  -- one upstream bundle
end entity;

One req and one resp carry the whole interface. Wiring two blocks together is u1.resp -> u2 and u2.req, not a dozen hand-matched signal connections — fewer places to mistype a name.

4. Hardware — a record costs nothing

The single most important fact about records: they add no hardware. A record is a source-level grouping. Synthesis flattens it, and the resulting netlist is identical to declaring every field as a separate signal. req.addr is the same 32 wires whether it lives in a record or stands alone.

a record type flattening into its individual field wires with no added hardwareflattens toflattens toflattens tobus_req_tone record port (sourceview)addr32 wireswdata32 wireswe, valid2 wires12
A record is pure source-level grouping — it flattens away in synthesis. The bus_req_t bundle is, in the netlist, exactly its fields as individual wires: addr (32), wdata (32), we (1), valid (1) = 66 wires, the same 66 you would get by declaring them separately. The record buys readability, refactor-safety, and a self-documenting interface at zero gate, flip-flop, or wire cost. There is no 'record logic' in hardware.

Because the grouping is compile-time only, a record has no timing behaviour of its own — there is nothing about a record that a waveform would show. Each field behaves exactly as its own type does (a std_logic field is a wire, a std_logic_vector field is a bus); the record itself is just the name you filed them under. That is why this lesson uses a structural diagram and no waveform — the concept here is composition, not timing.

5. The real caveat — one record, one direction

The trap that bites everyone: when a record is used as a port, every field inherits the port's mode. A record port declared in makes all its fields inputs; declared out, all outputs. So a bidirectional bus — where the request travels downstream and the response upstream — cannot be one record port:

split by direction, not by signal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- WRONG: one record cannot carry both directions through a port
-- type bus_t is record
--   addr  : std_logic_vector(31 downto 0);  -- wants to be IN at the slave
--   rdata : std_logic_vector(31 downto 0);  -- wants to be OUT at the slave
-- end record;
-- port ( bus : in bus_t );   -- forces rdata to be 'in' too — broken
 
-- RIGHT: group BY direction — a request bundle and a response bundle
port ( req  : in  bus_req_t;     -- everything flowing into the slave
       resp : out bus_resp_t );  -- everything flowing out of the slave

This is the design rule for record interfaces: group fields by who drives them. A clean bus is a downstream record (master → slave) and an upstream record (slave → master). (VHDL-2019 adds interface views / mode view to mix directions in one record, but the portable, universally-supported pattern is two records — use it unless your whole toolchain is on 2019.)

6. Debugging example — the positional-aggregate trap

The classic record bug is a positional aggregate that silently rots. You initialise a record by position, then a colleague adds or reorders a field — and every positional aggregate now assigns the wrong values, with no error because the types still happen to match:

how a positional aggregate goes wrong
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
type ctrl_t is record
  enable : std_logic;
  mode   : std_logic_vector(1 downto 0);
end record;
 
signal c : ctrl_t;
c <= ('1', "10");          -- positional: enable='1', mode="10"   (fine, today)
 
-- Later, someone inserts a field at the front:
--   type ctrl_t is record
--     irq_en : std_logic;       <-- new field, same type as 'enable'
--     enable : std_logic;
--     mode   : std_logic_vector(1 downto 0);
--   end record;
-- The aggregate ('1', "10") now needs THREE elements and fails to compile —
-- or, if the new field had matched, it would silently shift every value by one.

The fix is to always use named aggregates so each value is bound to a field by name, immune to order changes:

named aggregates are refactor-proof
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
c <= ( enable => '1', mode => "10" );           -- order-independent, self-checking
-- after the refactor, just add: irq_en => '0' — nothing else moves

The second classic is an uninitialised field: a record signal with no reset leaves every field at its type's default (a std_logic field at 'U'), so a forgotten req.valid shows up as 'U' propagating downstream. Reset the whole record to a known idle value with one aggregate.

7. Common mistakes & what to watch for

  • Mixing directions in one record port. Every field inherits the port's mode. Split into a request record and a response record, grouped by who drives them.
  • Positional aggregates. They break (or silently misassign) when fields are added or reordered. Use named aggregates (field => value) everywhere.
  • Assuming a record adds hardware. It does not — it flattens to its fields. There is no area, delay, or wire cost to grouping.
  • Forgetting to reset a field. An unassigned field sits at its type default ('U' for std_logic). Reset the whole record to a known idle aggregate.
  • Comparing whole records carelessly. req1 = req2 compares all fields, including ones you did not mean to (a stale valid). Compare the specific fields you care about.
  • Over-bundling. A record is for signals that genuinely travel together. Don't bundle unrelated signals just to shorten a port list — it couples things that should stay separate.

8. Engineering insight

A record separates the logical interface from the physical wires. Logically you have one req — an address, data, and a valid bit that mean nothing apart; physically you have 65 wires that synthesis lays down exactly as if you had typed them out. That separation is pure leverage: an interface becomes self-documenting (the field names are the protocol), refactor-safe (add a field in one type, not in every port list down the hierarchy), and connectable in one line — all at zero gate cost. The one discipline the abstraction demands is honesty about direction: because a port has a single mode, you model a real bus as the request bundle and the response bundle it physically is. Pair this with arrays — array for repetition, record for composition — and you can describe any structured interface in the type system itself.

9. Summary & next step

A record groups unlike fields under one name — the type of a bus bundle. Access fields with a dot (req.addr), build whole records with named aggregates (field => value) that survive refactors, and nest records for layered interfaces. A record costs nothing in hardware: it flattens to its fields, the netlist identical to separate signals, which is why it carries no timing of its own and needs no waveform. The one rule that matters is direction — every field of a record port shares the port's mode, so a bidirectional bus is a request record plus a response record, grouped by who drives each field.

You can now describe values that are names (enums), values that repeat (arrays), and values that compose (records). The next lesson sharpens all of them with subtypes and range constraints — how to take a base type and pin down exactly which values are legal, so the compiler enforces your intent and synthesis sizes the hardware to fit.