Serialize VHDL Records

1. Advantages of VHDL Records
2. Issue with Serialization
3. Improved Records Serialization

1. Advantages of VHDL Records

The use of records in VHDL has, in addition to the very well-known advantages such as:

  1. Improvement of access to signal groups by name binding
  2. Easy possibility to realize registers (FlipFlops) for complete signal groups by assigning record-signals in clocked processes
  3. Possibility to access a newly inserted element (signal) in the record across all hierarchies of the VHDL design in which the record is used

also other advanced uses like:

Processing Pipeline

It is possible to build a processing pipeline with records in a simple and straightforward way. The record type contains all data that is needed per pipeline stage and a declared array over this record type can be used to realize the depth of the processing pipeline.

The following code 1 shows the architecture of an entity that implements such a simple processing pipeline:

-- Code 1
architecture Behavioral of processing is

  constant PIPELINE_STAGE_NUMBER : integer := 4;

  type stage_data is record
    value : std_logic_vector(15 downto 0);
    property_1 : integer;
    property_2 : std_logic;
  end record;

  type pipeline_stage_type is array(PIPELINE_STAGE_NUMBER-1 downto 0) of stage_data;
    
  signal pipeline_reg : pipeline_stage_type;

begin
  pipeline_stage_gen0 : for it_stage in 0 to PIPELINE_STAGE_NUMBER-2 generate
      
    pipeline_stage_reg0 : process(clk)
    begin
      if rising_edge(clk) then
      
        pipeline_reg(it_stage+1) <= pipeline_reg(it_stage);
      
        if pipeline_reg(it_stage).property_2='1' then
          pipeline_reg(it_stage+1).value <= pipeline_reg(it_stage).value + pipeline_reg(it_stage).property_1;
        else
          pipeline_reg(it_stage+1).value <= pipeline_reg(it_stage).value + 2*pipeline_reg(it_stage).property_1;
        end if;
      end if;
    end process;
      
  end generate;
end Behavioral;

Code 1: Processing pipeline register by records

In Code 1, all pipeline registers are implemented by a single signal called pipeline_reg (see line 14). A generate over all pipeline stages (see line 17) allows a separate process to be created for each stage of the processing pipeline. The process could even use the iterator of the generate loop it_stage to adapt the behavior of the process to the pipeline stage. Since all properties and data are passed from one pipeline stage to the next by default (see line 23), the register-record elements in each pipeline stage can be used with corresponding clock delay (see line 25).

Interfaces by Records

Starting from VHDL version 2019 it is possible to realize an interface for an entity with only one record. This was previously only possible with restrictions, since a record as a port of an entity could only be either Input, Output or InOutput. With the introduction of the keyword view in VHDL 2019, each signal of a record can be declared individually as input or output. A detailed description of this can be found here.

2. Issue with serialization

To use records in FPGA designs it is sometimes necessary to pass them to ports implemented by regular vectors of type std_logic_vector. For example, this is usually necessary in a case when the data of registers shall be passed from one clock domain to another clock domain and FIFOs are used for this purpose.

Code 2 shows this situation of synchronizing register data between two clock domains:

-- Code 2
architecture Structural of register_synchronizer is

  constant C_REG1_WIDTH : integer := 3;
  constant C_REG2_WIDTH : integer := 8;
  constant C_REG3_WIDTH : integer := 32;
  constant FIFO_DATA_LENGTH : integer := C_REG1_WIDTH + C_REG2_WIDTH + C_REG3_WIDTH;

  type registers_type is record -- register for each TDC channel
    flags : std_logic_vector(C_REG1_WIDTH-1 downto 0);
    reload : std_logic_vector(C_REG2_WIDTH-1 downto 0);
    data : std_logic_vector(C_REG3_WIDTH-1 downto 0);
  end record;
  
  signal registers_src, registers_dest : registers_type;
  signal fifo_data_in, fifo_data_dout : std_logic_vector(FIFO_DATA_LENGTH-1 downto 0);

begin

  fifo_inst : fifo_async -- asynchronous FIFO
  generic map
  (
    READ_DATA_WIDTH => FIFO_DATA_LENGTH,
    WRITE_DATA_WIDTH => FIFO_DATA_LENGTH
  )
  port map
  (
    rd_clk => clk_dest,
    wr_clk => clk_src,
    data_in => fifo_data_in,
    data_out => fifo_data_dout
  );
  
  fifo_data_in <= registers_src.flags & registers_src.reload & registers_src.data;

  registers_dest.flags <= fifo_data_dout(42 downto 40);
  registers_dest.reload <= fifo_data_dout(39 downto 32);
  registers_dest.data <= fifo_data_dout(31 downto 0);

end Structural;

Code 2: Shortcomings in the standard serialization of records

The structural description of code 2 shows that the data of the record registers_src (see line 15) are transferred from the clock domain of the clock clk_src into the data memories of the record registers_dest and thus into the clock domain of the clock clk_dest. Thereby this usual method proves to be complicated, because there is no possibility to convert a record signal automated and deterministic into a vector of the type std_logic_vector. This is shown by code 2 starting at line 34, where all record elements are concatenated and passed to the FIFO input port via the signal fifo_data_in. If a new record element or a new register, so to say, has been added, this line must be adapted.

The same applies for reading the synchronization FIFO and assigning the data to the record registers_dest within the target clock domain (see from line 36). For this purpose, the corresponding vector indices of the FIFO output signal fifo_data_out must be selected for each individual record element and assigned to the record element. It is easy to see that this approach can lead to costly changes, especially when an element is to be added to a large register record and the serialization of the record still has to be performed at various locations all over the FPGA design.

An approach, which offers a better comfort despite a missed direct possibility for the serialization of records in VHDL, is presented in section 3.

3. Records serialization better

Record conversion function in the package

In the approach presented here for the serialization and deserialization of a record, specific conversion functions are introduced, which minimize the effort of the conversions. These functions can be located in a package, just like the record definition itself. Code 3 shows the VHDL package with the corresponding contents:

-- Code 3
package registers_pkg is

  constant C_REG1_WIDTH : integer := 3;
  constant C_REG2_WIDTH : integer := 8;
  constant C_REG3_WIDTH : integer := 32;

  type sub_registers_type is record
    x : std_logic_vector(C_REG1_WIDTH-1 downto 0);
    y : std_logic_vector(C_REG1_WIDTH-1 downto 0);
  end record;
  
  function get_length(r : sub_registers_type) return integer;
  function to_slv(r : sub_registers_type) return std_logic_vector;
  function to_record(v : std_logic_vector) return sub_registers_type;
  
  type registers_type is record -- register for each TDC channel
    flags : std_logic_vector(C_REG2_WIDTH-1 downto 0);
    reload : std_logic_vector(C_REG3_WIDTH-1 downto 0);
    data : sub_registers_type;
  end record;

  function get_length(r : registers_type) return integer;
  function to_slv(r : registers_type) return std_logic_vector;
  function to_record( v : std_logic_vector) return registers_type;

end registers_pkg; 

package body registers_pkg is

  -- Conversion functions sub_registers_type
  
  function get_length(r : sub_registers_type) return integer is
  begin
    return r.x'length + r.y'length;
  end;

  function to_slv(r : sub_registers_type) return std_logic_vector is
    variable tmp : std_logic_vector(get_length(r)-1 downto 0);
  begin
    tmp := r.y & r.x;
    return tmp;
  end;
  
  function to_record(v : std_logic_vector) return sub_registers_type is
    variable tmp : sub_registers_type;
  begin
    (tmp.y, tmp.x) := v;
    return tmp;
  end;
  
  -- Conversion functions sub_registers_type
  
  function get_length(r : registers_type) return integer is
  begin
    return r.flags'length + r.reload'length + get_length(r.data);
  end;

  function to_slv(r : record_type) return std_logic_vector is
    variable tmp : std_logic_vector(C_LENGTH_record_type-1 downto 0);
  begin
    tmp := to_slv(r.data) & r.reload & r.flags;
    return tmp;
  end;
  
  function to_record(v : std_logic_vector) return record_type is
    variable tmp : record_type;
  begin
    (tmp.data.y, tmp.data.x, tmp.reload, tmp.flags) := v;
    return tmp;
  end;

end registers_pkg; 

Code 3: Definition of record conversion functions from record type to std_logic_vector ( to_slv() ) and vice versa ( to_record() )

In code 3 the register records are defined with the lines 8 and 17. The type registers_type is now even a nested record. This means that the original register record contains an additional record type as a sub-record. Lines 13-15 or 23-25 show the declaration of the conversion functions, which can be implemented for each record type to serialize it according to the described approach.

The functions are get_length(), for determining the total record length in bits, to_slv() for converting to a std_logic_vector and to_record() for converting a std_logic_vector of appropriate length to a record. It is good that the functions can always be named equally, although they are implemented differently for each record type. The function get_length() is optional, because its function can also be implemented by using constants.

The advantage of the conversion functions now is that they are defined only once in the FPGA design and must be maintained also only there with the record itself. The function to_slv() works with the simple concatenation & (see line 62). The big benefit is with the presented approach the function to_record() (see line 66), which implementation is possible in this way starting from VHDL version 2008. Only the package itself must be interpreted with this VHDL version. According to this approach, a mapping of a right-sided std_logic_vector to the individual elements of the record (left-sided) is implemented without addressing the vector via indices. This makes an extension of the corresponding record and the maintenance of its conversion functions easier. It should be noted at this point the inverse order of the right-sided record elements in comparison to their definition (see line 69). This is recommended to cause a correct order in the implementation.

Using the record conversion functions

The final use of the record conversion functions in the FPGA design is now shown in code 4:

-- Code 4
LIBRARY work;
USE work.registers_pkg.ALL;

entity register_synchronizer is
  port
  (
    ...
  );
end top;

architecture Structural of register_synchronizer is
  
  signal registers_src, registers_dest : registers_type;
  signal fifo_data_in, fifo_data_dout : std_logic_vector(get_length(registers_src)-1 downto 0);

begin

  fifo_inst : fifo_async -- asynchronous FIFO
  generic map
  (
    READ_DATA_WIDTH => fifo_data_in'length,
    WRITE_DATA_WIDTH => fifo_data_in'length
  )
  port map
  (
    rd_clk => clk_dest,
    wr_clk => clk_src,
    data_in => fifo_data_in,
    data_out => fifo_data_dout
  );
  
  fifo_data_in <= to_slv(registers_src);

  registers_dest <= to_record(fifo_data_dout);

end Structural;

Code 4: Using the record conversion functions

In code 4, which is now a simplified version of code 2, you can see that all record definitions have now been moved to the package. More importantly, the area of record conversions (see code 4 from line 33) is now much shorter and no longer requires adjustments when the record type has changed.