Introduction
Happy New Year! It’s 2026 and VHDL interfaces, a new feature added in the 2019 language standard, are finally ready for prime-time.
VHDL-2019 interfaces solve one of the most painful problems in large VHDL designs: cleanly connecting wide, repetitive interfaces like AXI-Stream, AXI-Lite, or AXI-Full. Until recently, two barriers prevented their use: non-existent synthesis tool support and expensive paid-only simulator support.
After spending December developing a set of general-purpose AXI-Stream modules built around interfaces, I can confidently say that both Vivado (synthesis / implementation) and NVC (open-source simulation) now have solid support. VHDL interfaces are ready for new designs in 2026.
Motivation
I’ll start by pointing to some existing articles that explain interfaces and why you might want to use them.
- Here’s an explanation from Sigasi
- Here’s one from OSVVM
- And finally, a short description from the Vivado documentation
Now here’s an overview:
One of the biggest annoyances with HDL design is wiring up large repetitive interfaces between modules while coding a large system. AXI is one of the best examples of this. Who wants to repeatedly and manually connect up 100 independent signals between modules? Its error-prone, time-consuming, and feels like the sort of problem that should be easily solvable, if only we had better tools.
Enter VHDL interfaces.
Historically, engineers addressed this problem using a two-record approach: One record for grouping common input signals, and another for output signals. This worked reasonably well, but VHDL-19 introduced an even better way to handle this, by adding a built-in way to group input and output signals via interface mode views.
While records only group signals; interfaces group direction, intent, and usage.
With interfaces, chaining AXI-Stream modules together becomes as simple as connecting one interface signal to the next. If you’ve ever used Vivado’s IP Integrator, you can think of VHDL interfaces as a more usable, text-based equivalent without the GUI overhead.
Here’s a quick example showing how compact a chain of modules becomes when using interfaces:
architecture rtl of intf_example is
signal clk : std_ulogic;
signal srst : std_ulogic;
signal int_axis : axis_arr_t(0 to 3) (
tdata(15 downto 0),
tkeep( 1 downto 0),
tuser( 0 downto 0)
);
begin
u_axis_0 : entity work.axis_function_0
port map(
clk => clk,
srst => srst,
s_axis => int_axis(0),
m_axis => int_axis(1)
);
u_axis_1 : entity work.axis_function_1
port map(
clk => clk,
srst => srst,
s_axis => int_axis(1),
m_axis => int_axis(2)
);
u_axis_2 : entity work.axis_function_2
port map(
clk => clk,
srst => srst,
s_axis => int_axis(2),
m_axis => int_axis(3)
);
end architecture;
It doesn’t get much better than that!
Usage
First, create a package with the common interface definition. For maximum flexibility, I’d recommend using unconstrained arrays and allowing the user of the module to specify vector sizes at the instantiation.
Here’s an example of an axi stream interface definition:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package axis_pkg is
-- AXI-Stream type
type axis_t is record
tready : std_ulogic;
tvalid : std_ulogic;
tlast : std_ulogic;
tdata : std_ulogic_vector;
tkeep : std_ulogic_vector;
tuser : std_ulogic_vector;
end record;
-- AXI-Stream array
type axis_arr_t is array(natural range <>) of axis_t;
-- Manager view
view m_axis_v of axis_t is
tready : in;
tvalid : out;
tlast : out;
tdata : out;
tkeep : out;
tuser : out;
end view;
-- Subordinate view
alias s_axis_v is m_axis_v'converse;
-- Monitor view
view monitor_axis_v of axis_t is
tready : in;
tvalid : in;
tlast : in;
tdata : in;
tkeep : in;
tuser : in;
end view;
end package;
Next, create an entity that uses the interface. The example below implements an AXI-Stream broadcaster because it’s a short, standalone example using an interface at the input port and an array of interfaces at the output port, so it covers most of the standard use cases.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.axis_pkg.all;
entity axis_broadcast is
port (
clk : in std_ulogic;
srst : in std_ulogic;
--
s_axis : view s_axis_v;
--
m_axis : view (m_axis_v) of axis_arr_t
);
end entity;
architecture rtl of axis_broadcast is
signal int_axis_tready : std_ulogic_vector(m_axis'range);
signal int_axis_tvalid : std_ulogic_vector(m_axis'range);
signal int_axis_tdata : std_ulogic_vector(s_axis.tdata'range);
signal int_axis_tuser : std_ulogic_vector(s_axis.tuser'range);
signal int_axis_tkeep : std_ulogic_vector(s_axis.tkeep'range);
signal int_axis_tlast : std_ulogic;
begin
s_axis.tready <= (and int_axis_tready) or not (or int_axis_tvalid);
prc_broadcast : process (clk) is begin
if rising_edge(clk) then
for i in m_axis'range loop
if int_axis_tready(i) then
int_axis_tvalid(i) <= '0';
end if;
end loop;
if s_axis.tvalid and s_axis.tready then
int_axis_tvalid <= (others=> '1');
int_axis_tlast <= s_axis.tlast;
int_axis_tdata <= s_axis.tdata;
int_axis_tkeep <= s_axis.tkeep;
int_axis_tuser <= s_axis.tuser;
end if;
if srst then
int_axis_tvalid <= (others=> '0');
end if;
end if;
end process;
gen_assign_outputs : for i in m_axis'range generate
int_axis_tready(i) <= m_axis(i).tready;
m_axis(i).tvalid <= int_axis_tvalid(i);
m_axis(i).tlast <= int_axis_tlast;
m_axis(i).tdata <= int_axis_tdata;
m_axis(i).tkeep <= int_axis_tkeep;
m_axis(i).tuser <= int_axis_tuser;
end generate;
end architecture;
Finally, instantiate the new entity in the top-level design. Here’s a snippet showing the broadcaster with three outputs:
architecture rtl of top is
constant DW : integer := 16;
constant KW : integer := 2;
constant UW : integer := 1;
signal clk : std_ulogic;
signal srst : std_ulogic;
signal s_axis : axis_t (
tdata(DW - 1 downto 0),
tkeep(KW - 1 downto 0),
tuser(UW - 1 downto 0)
);
signal m_axis : axis_arr_t(0 to 2)(
tdata(DW - 1 downto 0),
tkeep(KW - 1 downto 0),
tuser(UW - 1 downto 0)
);
begin
u_axis_broadcast : entity work.axis_broadcast
port map (
clk => clk,
srst => srst,
s_axis => s_axis,
m_axis => m_axis
);
end architecture;
Alternatively, you could break out individual interfaces instead of using an array. While this partially defeats the elegance of interfaces, it’s useful when you need fine-grained control or signal-level customization. This example also shows how you could use a subtype to reduce the lines of code when repeating a constrained record.
architecture rtl of top is
signal clk : std_ulogic;
signal srst : std_ulogic;
subtype axis16_t is axis_t(
tkeep( 1 downto 0),
tdata(15 downto 0),
tuser( 3 downto 0)
);
signal s_axis : axis16_t;
signal m0_axis : axis16_t;
signal m1_axis : axis16_t;
signal m2_axis : axis16_t;
begin
u_axis_broadcast : entity work.axis_broadcast
port map (
clk => clk,
srst => srst,
s_axis => s_axis,
m_axis(0).tready => m0_axis.tready,
m_axis(0).tvalid => m0_axis.tvalid,
m_axis(0).tlast => m0_axis.tlast,
m_axis(0).tkeep => m0_axis.tkeep,
m_axis(0).tdata(15 downto 8) => m0_axis.tdata(15 downto 8),
m_axis(0).tdata( 7 downto 0) => m0_axis.tdata( 7 downto 0),
m_axis(0).tuser => m0_axis.tuser,
m_axis(1) => m1_axis,
m_axis(2) => m2_axis
);
end architecture;
Sblib AXI Stream Modules
If you’d like to see more examples, I have a whole library of axi stream modules built around VHDL interfaces in the Shrikebyte open-source VHDL repo, complete with fully self-checking VUnit testbenches and Github Actions CI.
To my knowledge, there aren’t any other existing open-source VHDL libraries that make use of interfaces.
So far, the list of modules includes:
axis_arb.vhd- Priority arbiter.
axis_broadcast.vhd- Broadcast one source stream to several destinations.
axis_cat.vhd- Packet concatenate. Useful for adding a header or trailer to a payload.
axis_demux.vhd- De-multiplexer.
axis_fifo_async.vhd- Async FIFO with optional packet mode.
axis_fifo.vhd- Sync FIFO with optional packet mode.
axis_mux.vhd- Multiplexer.
axis_pack.vhd- Tkeep packer - Removes null bytes from an axi stream and re-packs it to guarantee that all tkeep bits are high on all beats, except for tlast.
axis_pipes.vhd- Cascaded version of axis_pipe, for use when multiple back-to-back pipeline stages are needed.
axis_pipe.vhd- Single pipeline stage to break combinatorial paths. AKA skid buffer.
axis_pkg.vhd- Package containing the common axi stream interface definition.
axis_resize.vhd- Upsize or downsize the width of a stream. Drops null tkeep bytes wherever it can to improve protocol efficiency.
axis_slice.vhd- Packet slice. Slices one packet into two at a user-specified byte boundary. Useful for stripping a header or trailer from a payload.
Why build yet another HDL library when so many already exist?
- As an FPGA programmer, I like having complete control over the low-level implementation details of my designs. Primarily because having a deep understanding each LUT and flip-flop makes debugging a breeze. I’m sure many other types of programmers would appreciate being able to write their own libraries as well, but almost always, the level of productivity that they have to operate at fully prevents them from doing this. While writing your own glibc would be completely out of the question in 99% of cases, FPGA programmers are in a different position, where it is actually quite feasible to write their own personal library within a reasonable timeframe since the level of abstraction is so low. Having such detailed control over design elements is part of what makes this type of work so rewarding for me. If I wanted to spend all of my time gluing together existing libraries, I’d just become a Javascript programmer /s.
- I wanted to adopt VHDL interfaces, and I knew of no existing libraries that already use them.
- Relying on vendor IP locks you into a vendor. It’s a good short-term stop-gap solution, but I typically try to limit vendor IP to high-speed transceiver interfaces and hard macro blocks, like the Zynq IP or PCIe DMA engine, for example.
- I’m starting work on an FPGA-based DVB-S2 transceiver for wireless video transport, and I’ll need these generic AXI stream blocks for this project. Along with that, I anticipate using these blocks in plenty of other future contracting projects, so I think this was an investment worth making.
- Its fun!
Comparison to SystemVerilog
One notable difference between VHDL and SystemVerilog interfaces is that SV allows constants to be defined directly as part of the the interface, whereas VHDL does not.
Here’s an example from the excellent taxi repo of SV components (I’d highly recommend this repo if your project needs ethernet):
// SPDX-License-Identifier: MIT
/*
Copyright (c) 2025 FPGA Ninja, LLC
Authors:
- Alex Forencich
*/
interface taxi_axis_if #(
// Width of AXI stream interfaces in bits
parameter DATA_W = 8,
// tkeep signal width (bytes per cycle)
parameter KEEP_W = ((DATA_W+7)/8),
// Use tkeep signal
parameter logic KEEP_EN = KEEP_W > 1,
// Use tstrb signal
parameter logic STRB_EN = 1'b0,
// Use tlast signal
parameter logic LAST_EN = 1'b1,
// Use tid signal
parameter logic ID_EN = 0,
// tid signal width
parameter ID_W = 8,
// Use tdest signal
parameter logic DEST_EN = 0,
// tdest signal width
parameter DEST_W = 8,
// Use tuser signal
parameter logic USER_EN = 0,
// tuser signal width
parameter USER_W = 1
)
();
logic [DATA_W-1:0] tdata;
logic [KEEP_W-1:0] tkeep;
logic [KEEP_W-1:0] tstrb;
logic [ID_W-1:0] tid;
logic [DEST_W-1:0] tdest;
logic [USER_W-1:0] tuser;
logic tlast;
logic tvalid;
logic tready;
modport src (
output tdata,
output tkeep,
output tstrb,
output tid,
output tdest,
output tuser,
output tlast,
output tvalid,
input tready
);
modport snk (
input tdata,
input tkeep,
input tstrb,
input tid,
input tdest,
input tuser,
input tlast,
input tvalid,
output tready
);
modport mon (
input tdata,
input tkeep,
input tstrb,
input tid,
input tdest,
input tuser,
input tlast,
input tvalid,
input tready
);
endinterface
In practice, I haven’t missed the constant declaration capability. Unconstrained vectors handle width flexibility cleanly. Optional signals (such as tlast or tuser) are more awkward, but I’ve found the best way to handle it is documenting expected behavior, having the user tie unused input pins to constants, leaving unused outputs disconnected, and allowing synthesis to optimize unused signals away.
If necessary, per-module generics (e.g., USE_TLAST) can be added, but in-general, I try to minimize generics to reduce verification complexity.
Issues
As expected, I ran into a few issues while trying out the new language feature. All were manageable, and none were deal-breakers.
I tend to disagree with the advice to “stick to VHDL-93 for maximum compatibility.” I’m of the belief that the language should work for you, not the other way around. I’m more than willing to tolerate occasional tool bugs in exchange for meaningful productivity (and enjoyment of coding) gains.
NVC Issue #1 - Cannot assign interface elements from a loop within in a process.
- This turned out to NOT be a simulator bug. Its actually just a weird edge-case with the VHDL 19 language that probably should have been fixed by the language standard committee.
- For examples of the issue and a deeper discussion, check out the NVC Github issue.
NVC Issue #2 - Crash during element-wise view port association.
- This turned out to be an actual simulator bug. The NVC author is incredibly responsive and had a fix pushed in less than a week.
- For examples of the issue and a deeper discussion, check out the NVC Github issue
- For anyone else using NVC, or any open-source tool for that matter, I’d highly encourage you to file an issue for any bugs that you encounter. If you’re used to dealing with large EDA companies, working with open-source authors can be a huge breath of fresh air.
Vivado Issue #1 - Modules with interfaces randomly removed from synthesis.
- I thought all of my VHDL 19 dreams were dead out of the gate the first time I tried synthesizing an example design composed of all of my new axi stream modules. I wanted to get a feel for the expected timing performance and make pipelining improvements where it made sense, but before you know how fast your design will run, you have to put it through synthesis and implementation. Anyways, I was using Vivado 2024.2, which officially supports VHDL 19 interfaces. I ran into an immediate bug, where some of my modules would completely disappear as if they were being synthesized away. The only reason this should ever happen is if signals are left disconnected, but this was not the case in my design. Everything was connected up to an IO pin.
- I tried upgrading to Vivado 2025.2 and that resolved the problem. So I would not trust anything older than Vivado 2025.2 with VHDL 19 interfaces, despite what Xilinx’s documentation might say. I’m no newbie to Vivado bugs, so its not at all surprising to me that it had trouble with new language features. The tool went through the same set of growing pains back when they started adding VHDL 08 support. I recall an old co-worker telling me that he experienced the same problem back when he tried passing regular records as entity IO, sometime back in the early 2010’s. Back then, the only safe bet was using
std_logicandstd_logic_vectorfor entity IO. I’m glad those days are behind us!
Vivado Issue #2 - Cannot assign independent elements of an interface array directly to an output port.
- For example, although the following example is perfectly legal VHDL 19 syntax (to the best of my knowledge) it causes Vivado elaboration to segfault with this message, even on version 2025.2.
[filemgmt 20-2001] Source scanning failed (heap error) while processing fileset "sources_1" due to unrecoverable syntax error or design hierarchy issues. Recovering last known analysis of the source files.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.axis_pkg.all;
entity vivado_bug is
port (
clk : in std_ulogic;
srst : in std_ulogic;
--
m_axis : view (m_axis_v) of axis_arr_t
);
end entity;
architecture rtl of vivado_bug is
signal int_axis : axis_arr_t(m_axis'range) (
tdata(m_axis(m_axis'low).tdata'range),
tkeep(m_axis(m_axis'low).tkeep'range),
tuser(m_axis(m_axis'low).tuser'range)
);
begin
gen_loop : for i in m_axis'range generate
int_axis(i).tvalid <= '1';
int_axis(i).tlast <= '1';
int_axis(i).tdata <= (others=>'1');
int_axis(i).tkeep <= (others=>'1');
int_axis(i).tuser <= (others=>'1');
u_axis_pipe : entity work.axis_pipe
port map(
clk => clk,
srst => srst,
s_axis => int_axis(i),
m_axis => m_axis(i) -- THIS LINE IS THE PROBLEM!!!
);
end generate;
end architecture;
For now, I found that the best workaround is to use an intermediate record. For example, this works without issue.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.axis_pkg.all;
entity vivado_bug_workaround is
port (
clk : in std_ulogic;
srst : in std_ulogic;
--
m_axis : view (m_axis_v) of axis_arr_t
);
end entity;
architecture rtl of vivado_bug_workaround is
signal int_axis : axis_arr_t(m_axis'range) (
tdata(m_axis(m_axis'low).tdata'range),
tkeep(m_axis(m_axis'low).tkeep'range),
tuser(m_axis(m_axis'low).tuser'range)
);
signal int0_axis : axis_arr_t(m_axis'range) (
tdata(m_axis(m_axis'low).tdata'range),
tkeep(m_axis(m_axis'low).tkeep'range),
tuser(m_axis(m_axis'low).tuser'range)
);
begin
gen_loop : for i in m_axis'range generate
int_axis(i).tvalid <= '1';
int_axis(i).tlast <= '1';
int_axis(i).tdata <= (others=>'1');
int_axis(i).tkeep <= (others=>'1');
int_axis(i).tuser <= (others=>'1');
u_axis_pipe : entity work.axis_pipe
port map(
clk => clk,
srst => srst,
s_axis => int_axis(i),
m_axis => int0_axis(i)
);
int0_axis(i).tready <= m_axis(i).tready;
m_axis(i).tvalid <= int0_axis(i).tvalid;
m_axis(i).tlast <= int0_axis(i).tlast;
m_axis(i).tdata <= int0_axis(i).tdata;
m_axis(i).tkeep <= int0_axis(i).tkeep;
m_axis(i).tuser <= int0_axis(i).tuser;
end generate;
end architecture;
I’m hoping this gets fixed in a future release, but its not a deal-breaker bug.
Conclusion
VHDL 19 interfaces are finally ready for real-world use. I’m hugely thankful for the great open-source projects that make their use accessible for every-day people, not just large companies that can afford expensive proprietary simulators.
The NVC simulator and Surfer waveform viewer have been a game-changing combination for my work. While GHDL and GTKWave were excellent, these new simulation tools take it a step further, truly enabling new possibilities.
I’m looking forward to continuing to use interfaces in my upcoming projects. I don’t expect to go back.