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
interfacedeclarations bundle related signals into a named group - How
viewdeclarations assign direction (master, slave) to interface signals - How port declarations use
viewinstead 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 Signal | Port Name | Architecture Signal | Direction (master) | Direction (slave) |
|---|---|---|---|---|
data | bus | bus_data | out | in |
valid | bus | bus_valid | out | in |
ready | bus | bus_ready | in | out |
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:
| Protocol | Signals | Without interfaces | With interfaces |
|---|---|---|---|
| Simple handshake | 3 | 3 per entity | 1 view port |
| AXI-Lite | 20 | 20 per entity | 1 view port |
| AXI4 full | 50+ | 50+ per entity | 1 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 (
inorout). 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.
skalp VHDL-2019 Notes 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 port Direction in view VHDL-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
validmaster-to-slave andreadyslave-to-master). skalp handles this through separateinandoutstruct 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
| Tool | Interfaces/Views | Generic Types | Cost |
|---|---|---|---|
| skalp | Supported | Supported | Free |
| GHDL | Partial (experimental) | Partial | Free |
| Vivado Simulator | Not supported | Not supported | Free (with Vivado) |
| ModelSim/Questa | Supported (recent) | Supported (recent) | Commercial |
| Riviera-PRO | Supported | Supported | Commercial |
| NVC | Partial | Partial | Free |
If you are using free tools, skalp is currently the most complete option for VHDL-2019 features.
Quick Reference
| Feature | Syntax | Purpose |
|---|---|---|
| Interface declaration | interface name is signal ...; end interface; | Bundle related signals |
| View declaration | view name of iface is signal : dir; end view; | Assign directions to interface signals |
| View port | port_name : view view_name | Port with bundled, directed signals |
| Signal access | portname_signalname | Access individual signals from a view port |
| Generic type | generic (type T) | Type parameter on entity |
| Generic type map | generic map (T => actual_type) | Supply type at instantiation |
VHDL-2019 vs Earlier VHDL
| Task | Before VHDL-2019 | With VHDL-2019 |
|---|---|---|
| Group bus signals | Declare individually in every port list | Declare once in interface, use view in ports |
| Direction consistency | Manually ensure master/slave ports match | Views enforce correct directions at compile time |
| Parameterize data type | Integer generic + std_logic_vector(W-1 downto 0) | generic (type T) preserves the actual type |
| Add a signal to a bus | Edit every entity and every port map | Add 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.