Serialisieren von VHDL Records

1. Vorteile von VHDL Records
2. Problem bei Serialisierung
3. Bessere Serialisierung von Records

1. Vorteile von VHDL Records

Die Verwendung von Records in VHDL hat neben den sehr bekannten Vorteilen wie:

  1. Verbesserung des Zugriffs auf Signalgruppen durch Name Binding
  2. Einfache Möglichkeit zur Realisierung von Registern (FlipFlops) für ganze Signalgruppen durch Zuweisung von einzelnen Record-Signalen in getakteten Prozessen
  3. Möglichkeit zum Zugriff auf ein in den Record neu eingefügtes Element (Signal) über alle Hierarchien des VHDL Designs hinweg, in denen der Record verwendet wird

auch weitere fortgeschrittene Verwendungen wie:

Verarbeitungs-Pipeline

Es ist auf einfache und übersichtliche Weise möglich mit Records eine Verarbeitungs-Pipeline aufzubauen. Dabei beinhaltet der Record-Typ alle Daten, welche pro Pipeline-Stufe benötigt werden und ein deklariertes Array über diesen Record-Typ kann genutzt werden, um die Tiefe der Verarbeitungs-Pipeline zu realisieren.

Der folgende Code 1 zeigt die Architektur einer Entity, welche eine solche einfache Verarbeitungs-Pipeline implementiert:

-- 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: Verarbeitungs-Pipeline-Register durch Records

In Code 1 werden alle Pipeline-Register durch ein einziges Signal namens pipeline_reg (siehe Zeile 14) implementiert. Ein generate über alle Pipeline-Stufen (siehe Zeile 17) ermöglicht es für jeden einzelnen Stage der Verarbeitungs-Pipeline einen eigenen Prozess zu erzeugen. Der Prozess könnte sogar den Iterator der generate Schleife it_stage nutzen, um das Verhalten des Prozesses an die Pipeline-Stufe anzupassen. Da alle Eigenschaften und Daten standardmäßig von einer Pipeline-Stufe an die nächste weitergegeben werden (siehe Zeile 23), können die Register-Record-Elemente in jeder Pipeline-Stufe entsprechend taktverzögert genutzt werden (siehe Zeile 25).

Interfaces mit Records

Ab VHDL Version 2019 ist es möglich ein Interface für eine Entity mit nur einem einzigen Record zu realisieren. Dies war zuvor nur beschränkt möglich, da ein Record als Port einer Entity nur entweder Input, Output oder InOutput sein konnte. Mit der Einführung des Keywords view ab VHDL 2019 kann jedes Signal eines Records einzeln als Input oder Output deklariert werden. Eine ausführliche Darstellung dieses Sachverhaltes findet sich hier.

2. Problem bei der Serialisierung

Zur Nutzung von Records in FPGA-Designs ist es manchmal notwendig diese an Ports zu übergeben, welche durch gewöhnliche Vektoren des Typs std_logic_vector implementiert sind. Dies ist zum Beispiel in einem Fall üblicherweise notwendig, wenn die Daten von Registern aus einer Taktbereich in eine andere Taktbereich übergeben werden sollen und dazu FIFOs genutzt werden.

Code 2 zeigt diesen Sachverhalt des Synchronisierens von Register-Daten zwischen zwei Taktbereichen:

-- 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: Defizite bei der herkömmlichen Serialisierung von Records

Die strukturelle Beschreibung des Code 2 zeigt, dass die Daten des Records registers_src (siehe Zeile 15) aus dem Taktbereich des Taktes clk_src in die Datenspeicher des Records registers_dest und damit in den Taktbereich des Taktes clk_dest übertragen werden. Dabei erweist sich diese übliche Methode als umständlich, da es keine Möglichkeit gibt ein Record-Signal automatisierte und deterministisch in einen Vektor des Typs std_logic_vector zu konvertieren. Dies zeigt Code 2 ab der Zeile 34. In dieser Zeile werden alle Record-Elemente konkateniert und über das Signal fifo_data_in an den FIFO-Eingangs-Port übergeben. Falls ein neues Record-Element oder sozusagen ein neues Register hinzugefügt wurde, so muss diese Zeile angepasst werden.

Ähnliches gilt für das Auslesen der Synchronisations-FIFO und die Zuweisung der Daten an das Record registers_dest innerhalb der Ziel-Taktbereich (siehe ab Zeile 36). Dazu müssen für jedes einzelne Record-Element die entsprechenden Vektor-Indizes des FIFO-Ausgangssignals fifo_data_out angewählt werden und dem Record-Element zugewiesen werden. Es ist leicht zu erkennen, dass dieser Ansatz zu aufwändigen Änderungen führen kann, vor allem, wenn einem großen Register-Record ein Element hinzugefügt werden soll und die Serialisierung des Records noch an verschiedenen Stellen innerhalb des gesamten FPGA-Designs ausgeführt werden muss.

Ein Ansatz, welcher trotz einer fehlenden direkten Möglichkeit zur Serialisierung von Records in VHDL einen besseren Komfort bietet, wird in Punkt 3 vorgestellt

3. Serialisierung von Records

Record-Konvertierungsfunktion im Package

Bei dem hier vorgestellten Ansatz zur Serialisierung und Deserialisierung eines Records werden dedizierte Konvertierungsfunktionen eingeführt, welche den Aufwand der Konvertierungen minimieren. Diese Funktionen können dabei, wie die Record-Definition selbst, in einem Package abgelegt sein. Code 3 zeigt das VHDL-Package mit den entsprechenden Inhalten:

-- 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 der Record-Konvertierungsfunktionen von Record-Typ nach std_logic_vector ( to_slv() ) und umgekehrt ( to_record() )

In Code 3 sind mit den Zeilen 8 und 17 die Register-Records definiert. Beim type registers_type handelt es sich nun sogar um einen verschachtelten Record. Dies bedeutet, dass der ursprüngliche Register-Record noch einen zusätzlichen anderen Record-Type als Unter-Record beinhaltet. Die Zeilen 13-15 oder 23-25 zeigen die Deklaration der Konvertierungsfunktionen, welche jeweils für einen einzelnen Record-Typ implementiert werden können, um diesen nach dem beschriebenen Ansatz serialisierbar zu machen.

Es handelt sich bei den Funktionen um get_length(), zur Bestimmung der gesamten Record-Länge in Bits, to_slv() zur Konvertierung in einen std_logic_vector und um to_record() zur Konvertierung eines std_logic_vector entsprechender Länge in einen Record. Gut ist, dass die Funktionen immer gleich benannt sein können, obwohl sie für jeden Record-Typ anders implementiert sind. Die Funktion get_length() ist optional, denn ihre Funktion kann auch über den Einsatz von Konstanten realisiert werden.

Der Vorteil der Konvertierungsfunktionen ist nun, dass Sie nur einmal im FPGA-Design definiert sind und auch nur dort mit dem Record selbst gepflegt werden müssen. Die Funktion to_slv() arbeitet mit der einfachen Konkatenation & (siehe Zeile 62). Das Nützliche ist beim vorgestellten Ansatz die Funktion to_record() (siehe Zeile 66), deren Implementierung auf diese Weise ab VHDL Version 2008 möglich ist. Nur das Package an sich muss dabei mit diesem VHDL Version interpretiert werden. Nach diesem Ansatz wird eine Abbildung eines rechtsseitigen std_logic_vector auf die einzelnen Elemente des Records (linksseitig) umgesetzt, ohne den Vektor über Indizes anzusprechen. Dies macht eine Erweiterung des entsprechenden Records und die Pflege seiner Konvertierungsfunktionen einfacher. Es sei an dieser Stelle auf die invertierte Reihenfolge der rechtsseitigen Record-Elemente im Vergleich zu deren Definition hingewiesen (siehe Zeile 69). Diese ist empfehlenswert, um eine korrekte Reihenfolge in der Implementierung zu bewirken.

Nutzung der Record-Konvertierungsfunktionen

Die letztendliche Nutzung der Record-Konvertierungsfunktionen im FPGA-Design zeig nun 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: Nutzung der Record-Konvertierungsfunktionen

Anhand des Code 4, welcher eine nun vereinfachte Version des Code 2 ist, kann man erkennen, dass alle Record-Definitionen nun in das Package ausgelagert wurden. Wichtiger ist allerdings, dass der Bereich der Record-Konvertierungen (siehe Code 4 ab Zeile 33) nun deutlich kürzer ausfällt und keiner Anpassungen mehr benötigt, wenn sich der Record-Typ geändert hat.