What This Chapter Teaches

VHDL-2019 (IEEE 1076-2019) is the most significant revision of the language since VHDL-2008. It introduced features that directly address the verbosity problems that have plagued VHDL designs for decades: interfaces that bundle related signals into a single declaration, views that assign direction to those bundles, and generic type parameters that enable truly reusable components.

The problem is tool support. As of 2026, most free VHDL tools do not implement these features. GHDL has partial support. Vivado’s simulator ignores them. ModelSim/Questa requires a recent and expensive license. skalp is one of the few free tools that compiles VHDL-2019 interfaces and views today.

By the end of this chapter you will understand:

  • How interface declarations bundle related signals into a named group
  • How view declarations assign direction (master, slave) to interface signals
  • How port declarations use view instead of listing individual signals
  • How generic type parameters create components that work with any type
  • Why these features matter for real-world bus protocols like AXI, Wishbone, and Avalon

Interfaces and Views

The headline feature of VHDL-2019 is the interface / view system. It solves a problem every VHDL designer has encountered: the 50-signal port map.

Consider an AXI-Lite bus. The write channel alone has awaddr, awvalid, awready, wdata, wstrb, wvalid, wready, bresp, bvalid, bready. Add the read channel and you are at 20+ signals. Every entity that touches the bus must declare all of them. Every instantiation must map all of them. Miss one, and you get a cryptic error or a silent default.

Interfaces fix this. You declare the signals once, name the bundle, and use it everywhere.

A Simple Bus Interface

Start with a minimal example. Create src/bus_system.vhd with the interface and view declarations at the top:

library ieee;
use ieee.std_logic_1164.all;

interface simple_bus is
    signal data  : std_logic_vector(7 downto 0);
    signal valid : std_logic;
    signal ready : std_logic;
end interface simple_bus;

view bus_master of simple_bus is
    data  : out;
    valid : out;
    ready : in;
end view bus_master;

view bus_slave of simple_bus is
    data  : in;
    valid : in;
    ready : out;
end view bus_slave;

The interface declares three signals with types but no directions. The view declarations assign directions from each side’s perspective: the master drives data and valid, reads ready; the slave does the opposite. You cannot accidentally give two masters write access to the same signal — the view enforces it at compile time.

Using Views in Entities

With the interface and views defined, entities declare a single port instead of three:

entity sender is
    port (
        clk     : in  std_logic;
        rst     : in  std_logic;
        trigger : in  std_logic;
        tx_data : in  std_logic_vector(7 downto 0);
        bus     : view bus_master
    );
end entity sender;

architecture rtl of sender is
begin
    bus_data  <= tx_data when trigger = '1' else (others => '0');
    bus_valid <= trigger;
end architecture rtl;

The bus port carries all three signals with the directions defined by bus_master. Inside the architecture, signals are accessed by prefixing the port name: bus_data, bus_valid, bus_ready. This is a simple name-mangling rule — <port>_<signal> — with no special accessor syntax.

The receiver uses bus_slave — the same interface, opposite directions:

entity receiver is
    port (
        clk      : in  std_logic;
        rst      : in  std_logic;
        bus      : view bus_slave;
        rx_data  : out std_logic_vector(7 downto 0);
        rx_valid : out std_logic
    );
end entity receiver;

architecture rtl of receiver is
begin
    bus_ready <= '1';

    process(clk)
    begin
        if rising_edge(clk) then
            if rst = '1' then
                rx_data  <= (others => '0');
                rx_valid <= '0';
            elsif bus_valid = '1' then
                rx_data  <= bus_data;
                rx_valid <= '1';
            else
                rx_valid <= '0';
            end if;
        end if;
    end process;
end architecture rtl;

If you tried to assign to bus_data inside the receiver, skalp would report an error — the slave view declares data as in.


How Views Work

The system has three layers: the interface declares signal names and types without direction; the view assigns a direction to each signal from one participant’s perspective; the port uses view instead of in/out, and the compiler expands it into individual signals. You can define as many views as you need for one interface — master, slave, monitor, debug.

Inside the architecture, signals are flattened with the port name as prefix:

Interface SignalPort NameArchitecture SignalDirection (master)Direction (slave)
databusbus_dataoutin
validbusbus_validoutin
readybusbus_readyinout

At instantiation, the view port maps to individual signals in the parent:

entity bus_system is
    port (
        clk      : in  std_logic;
        rst      : in  std_logic;
        trigger  : in  std_logic;
        tx_data  : in  std_logic_vector(7 downto 0);
        rx_data  : out std_logic_vector(7 downto 0);
        rx_valid : out std_logic
    );
end entity bus_system;

architecture rtl of bus_system is
    signal bus_data  : std_logic_vector(7 downto 0);
    signal bus_valid : std_logic;
    signal bus_ready : std_logic;
begin
    u_sender: entity work.sender
        port map (
            clk       => clk,
            rst       => rst,
            trigger   => trigger,
            tx_data   => tx_data,
            bus_data  => bus_data,
            bus_valid => bus_valid,
            bus_ready => bus_ready
        );

    u_receiver: entity work.receiver
        port map (
            clk       => clk,
            rst       => rst,
            bus_data  => bus_data,
            bus_valid => bus_valid,
            bus_ready => bus_ready,
            rx_data   => rx_data,
            rx_valid  => rx_valid
        );
end architecture rtl;

The sender drives bus_data and bus_valid; the receiver drives bus_ready. The view system guarantees there is no conflict.


Scaling Up: AXI-Lite

The simple bus has three signals. Real protocols have many more. The same pattern scales directly. Create src/axi_peripheral.vhd:

library ieee;
use ieee.std_logic_1164.all;

interface axi_lite is
    signal awaddr  : std_logic_vector(31 downto 0);
    signal awvalid : std_logic;
    signal awready : std_logic;
    signal wdata   : std_logic_vector(31 downto 0);
    signal wvalid  : std_logic;
    signal wready  : std_logic;
end interface axi_lite;

view axi_master of axi_lite is
    awaddr  : out;
    awvalid : out;
    awready : in;
    wdata   : out;
    wvalid  : out;
    wready  : in;
end view axi_master;

entity axi_reg is
    port (
        clk : in std_logic;
        rst : in std_logic;
        bus : view axi_master
    );
end entity axi_reg;

architecture rtl of axi_reg is
    signal reg_data : std_logic_vector(31 downto 0);
begin
    bus_awready <= '1';
    bus_wready  <= '1';

    process(clk)
    begin
        if rising_edge(clk) then
            if rst = '1' then
                reg_data <= (others => '0');
            elsif bus_awvalid = '1' and bus_wvalid = '1' then
                reg_data <= bus_wdata;
            end if;
        end if;
    end process;
end architecture rtl;

Without interfaces, axi_reg would need eight ports instead of three. Add the read channels for a full AXI-Lite and you save 20+ port declarations per entity. Add a signal like wstrb to the interface, and every entity that uses it sees the change automatically.

Before and After

Compare the sender from Chapter 5 (without interfaces) to this chapter’s version. The port list drops from seven entries to five — and for AXI-scale protocols, the savings are far larger:

ProtocolSignalsWithout interfacesWith interfaces
Simple handshake33 per entity1 view port
AXI-Lite2020 per entity1 view port
AXI4 full50+50+ per entity1 view port

Generic Type Parameters

VHDL-2019 also introduces generic type parameters — the ability to parameterize an entity on actual types, not just integer constants:

entity generic_fifo is
    generic (
        type element_t;
        DEPTH : positive := 16
    );
    port (
        clk     : in  std_logic;
        wr_en   : in  std_logic;
        wr_data : in  element_t;
        rd_en   : in  std_logic;
        rd_data : out element_t
    );
end entity;

The type element_t generic declares a type parameter. The FIFO does not know or care whether element_t is an 8-bit vector, a 32-bit unsigned, or a record. The caller supplies the type at instantiation:

u_fifo: entity work.generic_fifo
    generic map (
        element_t => std_logic_vector(31 downto 0),
        DEPTH     => 32
    )
    port map (
        clk     => clk,
        wr_en   => fifo_wr,
        wr_data => fifo_din,
        rd_en   => fifo_rd,
        rd_data => fifo_dout
    );

Before VHDL-2019, the only way to parameterize data width was with integer generics and std_logic_vector(WIDTH-1 downto 0), which forces everything into untyped bit vectors. Generic type parameters preserve the actual type through the hierarchy. A FIFO of unsigned(15 downto 0) stays unsigned; a FIFO of a record stays a record.

skalp supports generic type parameters in entity declarations and instantiations.


Coming from skalp?

VHDL-2019 interfaces map closely to skalp struct types used as ports:

// skalp equivalent of a simple_bus interface
struct SimpleBus {
    data:  bit[8],
    valid: bit[1],
    ready: bit[1],
}

entity Sender {
    clk:     in  clock,
    rst:     in  bit[1],
    trigger: in  bit[1],
    tx_data: in  bit[8],
    bus:     out SimpleBus,
}

The key difference: skalp uses struct types directly in port declarations, with direction determined by the port direction (in or out). VHDL-2019 separates signal declaration (interface) from direction assignment (view), which gives more flexibility — the same interface can have master, slave, and monitor views without separate struct types.

skalpVHDL-2019Notes
struct SimpleBus { ... }interface simple_bus is ... end interface;Bundle of typed fields/signals
bus: out SimpleBusbus : view bus_masterPort with bundled signals
self.bus.databus_dataSignal access syntax differs
Generic types via <T>generic (type element_t)Both support type parameterization
Direction on portDirection in viewVHDL-2019 assigns direction per signal in a view

VHDL-2019’s view system is more expressive for protocols where signals flow in both directions (like valid master-to-slave and ready slave-to-master). skalp handles this through separate in and out struct ports or through its own interface mechanism.


Build and Test

Place the complete bus_system.vhd (interfaces, views, sender, receiver, and bus_system entities) in src/bus_system.vhd. Set top = "bus_system" in skalp.toml and run skalp build. skalp parses the interface and view declarations, verifies that every view port is connected correctly, and checks that no signal has multiple drivers.

Testing with a Rust Testbench

Create tests/bus_system_test.rs:

use skalp_testing::Testbench;

#[tokio::test]
async fn test_bus_transfer() {
    let mut tb = Testbench::new("src/bus_system.vhd").await.unwrap();
    tb.reset(2).await;
    tb.expect("rx_valid", 0u8).await;

    // Send a byte
    tb.set("tx_data", 0xA5u32);
    tb.set("trigger", 1u8);
    tb.clock(1).await;
    tb.set("trigger", 0u8);

    // Receiver captures on the next clock
    tb.clock(1).await;
    tb.expect("rx_data", 0xA5u32).await;
    tb.expect("rx_valid", 1u8).await;

    // rx_valid deasserts when no new data arrives
    tb.clock(1).await;
    tb.expect("rx_valid", 0u8).await;
}

#[tokio::test]
async fn test_bus_back_to_back() {
    let mut tb = Testbench::new("src/bus_system.vhd").await.unwrap();
    tb.reset(2).await;

    tb.set("tx_data", 0x11u32);
    tb.set("trigger", 1u8);
    tb.clock(1).await;

    tb.set("tx_data", 0x22u32);
    tb.clock(1).await;
    tb.expect("rx_data", 0x11u32).await;
    tb.expect("rx_valid", 1u8).await;

    tb.set("trigger", 0u8);
    tb.clock(1).await;
    tb.expect("rx_data", 0x22u32).await;
    tb.expect("rx_valid", 1u8).await;
}

Run the tests:

cargo test

The first test verifies a single transfer: assert trigger for one cycle, then check that the receiver captures it. The second test verifies back-to-back transfers without dropping data.


Tool Support Comparison

ToolInterfaces/ViewsGeneric TypesCost
skalpSupportedSupportedFree
GHDLPartial (experimental)PartialFree
Vivado SimulatorNot supportedNot supportedFree (with Vivado)
ModelSim/QuestaSupported (recent)Supported (recent)Commercial
Riviera-PROSupportedSupportedCommercial
NVCPartialPartialFree

If you are using free tools, skalp is currently the most complete option for VHDL-2019 features.


Quick Reference

FeatureSyntaxPurpose
Interface declarationinterface name is signal ...; end interface;Bundle related signals
View declarationview name of iface is signal : dir; end view;Assign directions to interface signals
View portport_name : view view_namePort with bundled, directed signals
Signal accessportname_signalnameAccess individual signals from a view port
Generic typegeneric (type T)Type parameter on entity
Generic type mapgeneric map (T => actual_type)Supply type at instantiation

VHDL-2019 vs Earlier VHDL

TaskBefore VHDL-2019With VHDL-2019
Group bus signalsDeclare individually in every port listDeclare once in interface, use view in ports
Direction consistencyManually ensure master/slave ports matchViews enforce correct directions at compile time
Parameterize data typeInteger generic + std_logic_vector(W-1 downto 0)generic (type T) preserves the actual type
Add a signal to a busEdit every entity and every port mapAdd to interface and views; entities update automatically

Next: Real-World Project

You now have the full set of VHDL features that skalp supports — from basic combinational logic through hierarchical design, testing, skalp integration pragmas, and VHDL-2019 interfaces. The next chapter puts it all together.

In Chapter 9, you will build a parameterized SPI master from scratch:

  • Generic parameters for clock divider, data width, and SPI mode
  • A multi-state FSM that manages the SPI protocol
  • Generate statements for configurable shift register width
  • VHDL-2019 interfaces for the SPI bus signals
  • A complete Rust test suite that verifies all SPI modes and edge cases

This is the capstone project for the tutorial — a real peripheral that you could drop into an FPGA design.

Continue to Chapter 9: Real-World Project.