In larger FPGA designs, we often have a large group of related signals that make up some complex bus or protocol, like PCIe, AXI, DDR, etc. We often want to apply some operation to the entire group of signals, such as pipelining them, muxing them, putting them into a fifo, or using them in a port in some level of the design. This is when records really shine.

What are Records?

Records are a collection of signals in one named object, similar to structs in C.

architecture example_arch of example is type ex_record is record a : std_logic ; b : std_logic_vector ( 15 downto 0 ) ; c : unsigned ( 31 downto 0 ) ; end record ex_record ; signal rec is ex_record ; begin rec . a <= enable ; rec . b <= halfword_vec ; rec . c <= uint_a ;

Records Make Designs Easier to Understand and Less Error Prone

Records take advantage of VHDL's strict typing. Signals of a certain type can only be assigned to ports, signals, and variables of the same type or a parent type. When working with a known interface (like a bus) defined as a set of separate signals, assigning them all is tedious and prone to copy-paste errors. Using a record removes the possibility of these errors, and let the designer reason about abstract interfaces between components instead of individual signals.

This is what a component port assignment for an AXI bus looks like with just std_logic_vector :

example_inst : axi_example_peripheral port_map ( clk = > clk , data = > data , s_axi_gp0_aclk = > aclk , s_axi_gp0_arvalid = > periph_axi_arvalid , s_axi_gp0_awvalid = > periph_axi_awvalid , s_axi_gp0_bready = > periph_axi_bready , s_axi_gp0_rready = > periph_axi_rready , s_axi_gp0_wlast = > periph_axi_wlast , s_axi_gp0_wvalid = > periph_axi_wvalid , s_axi_gp0_arid = > periph_axi_arid , s_axi_gp0_awid = > periph_axi_awid , s_axi_gp0_wid = > periph_axi_wid , s_axi_gp0_arburst = > periph_axi_arburst , s_axi_gp0_arlock = > periph_axi_arlock , s_axi_gp0_arsize = > periph_axi_arsize , s_axi_gp0_awburst = > periph_axi_awburst , s_axi_gp0_awlock = > periph_axi_awlock , s_axi_gp0_awsize = > periph_axi_awsize , s_axi_gp0_arprot = > periph_axi_arprot , s_axi_gp0_awprot = > periph_axi_awprot , s_axi_gp0_araddr = > periph_axi_araddr , s_axi_gp0_awaddr = > periph_axi_awaddr , s_axi_gp0_wdata = > periph_axi_wdata , s_axi_gp0_arcache = > periph_axi_arcache , s_axi_gp0_arlen = > periph_axi_arlen , s_axi_gp0_arqos = > periph_axi_arqos , s_axi_gp0_awcache = > periph_axi_awcache , s_axi_gp0_awlen = > periph_axi_awlen , s_axi_gp0_awqos = > periph_axi_awqos , s_axi_gp0_wstrb = > periph_axi_wstrb , s_axi_gp0_arready = > periph_axi_arready , s_axi_gp0_awready = > periph_axi_awready , s_axi_gp0_bvalid = > periph_axi_bvalid , s_axi_gp0_rlast = > periph_axi_rlast , s_axi_gp0_rvalid = > periph_axi_rvalid , s_axi_gp0_wready = > periph_axi_wready , s_axi_gp0_bid = > periph_axi_bid , s_axi_gp0_rid = > periph_axi_rid , s_axi_gp0_bresp = > periph_axi_bresp , s_axi_gp0_rresp = > periph_axi_rresp , s_axi_gp0_rdata = > periph_axi_rdata , ) ;

This is what the same port assignment can look like using record types:

example_inst : axi_example_peripheral port_map ( clk = > clk , data = > data , s_axi_gp0_aclk = > aclk , s_axi_gp0_in = > periph_axi . from_master , s_axi_gp0_out = > periph_axi . to_master ) ;

The individual protocol signals are defined in the type, and they all get assigned at once instead of individually. Now imagine the data from this peripheral needs to be delayed by one clock cycle to line up correctly with another part of the design, or that there are two data sources into this peripheral that must be muxed. Which code example would be easier to change?

How to Use Records in a Design

The most effective way to define custom record types is to put these type definitions in a package that gets called into other files. This allows the use of these types in all parts of the design, including in entity definitions.

library ieee ; use ieee . std_logic_1164 . all ; package my_package is type from_AXI_master is record arvalid : std_logic ; awvalid : std_logic ; bready : std_logic ; rready : std_logic ; wlast : std_logic ; wvalid : std_logic ; arid : std_logic_vector ( 11 downto 0 ) ; awid : std_logic_vector ( 11 downto 0 ) ; wid : std_logic_vector ( 11 downto 0 ) ; arburst : std_logic_vector ( 1 downto 0 ) ; arlock : std_logic_vector ( 1 downto 0 ) ; arsize : std_logic_vector ( 2 downto 0 ) ; awburst : std_logic_vector ( 1 downto 0 ) ; awlock : std_logic_vector ( 1 downto 0 ) ; awsize : std_logic_vector ( 2 downto 0 ) ; arprot : std_logic_vector ( 2 downto 0 ) ; awprot : std_logic_vector ( 2 downto 0 ) ; araddr : std_logic_vector ( 31 downto 0 ) ; awaddr : std_logic_vector ( 31 downto 0 ) ; wdata : std_logic_vector ( 31 downto 0 ) ; arcache : std_logic_vector ( 3 downto 0 ) ; arlen : std_logic_vector ( 3 downto 0 ) ; arqos : std_logic_vector ( 3 downto 0 ) ; awcache : std_logic_vector ( 3 downto 0 ) ; awlen : std_logic_vector ( 3 downto 0 ) ; awqos : std_logic_vector ( 3 downto 0 ) ; wstrb : std_logic_vector ( 3 downto 0 ) ; end record from_AXI_master ; type to_AXI_master is record arready : std_logic ; awready : std_logic ; bvalid : std_logic ; rlast : std_logic ; rvalid : std_logic ; wready : std_logic ; bid : std_logic_vector ( 11 downto 0 ) ; rid : std_logic_vector ( 11 downto 0 ) ; bresp : std_logic_vector ( 1 downto 0 ) ; rresp : std_logic_vector ( 1 downto 0 ) ; rdata : std_logic_vector ( 31 downto 0 ) ; end record to_AXI_master ; type AXI_slave is record to_master : to_AXI_master ; from_master : from_AXI_master ; end record AXI_slave ; end my_package ;

Then in the AXI peripheral file:

library ieee ; use ieee . std_logic_1164 . all ; use ieee . numeric_std . all ; use work . my_package . all ; entity axi_example_peripheral is port ( clk : in std_logic ; data : in std_logic_vector ( 31 downto 0 ) ; s_axi_gp0_aclk : in std_logic ; s_axi_gp0_in : in from_AXI_master ; s_axi_gp0_out : out to_AXI_master ; ) ; end axi_example_peripheral ; architecture arch of ddr3_controller is signal AXI_i : AXI_slave ; signal axi_write_data : std_logic_vector ( 31 downto 0 ) ; begin AXI_i . from_master <= m_axi_gp0_in ; AXI_i . to_master <= m_axi_gp0_out ; axi_write_data <= AXI_i . from_master . wdata ; end arch ;

The internals of the AXI protocol have been abstracted away, and the designer can reason about it on a higher level. These new types can also be used as function inputs/outputs, and even combined into still more abstract types (for instance, a bus array).

Conclusion

Records are a great way of using VHDL's typing system to abstract away complex and verbose interfaces and protocols. They make designs easier to reason about, easier to change, and higher level.