Register code generation

There is a register code generation eco-system available in tsfpga which generates code from textual configuration files. To start using it simply create a file regs_<name>.toml in the root of a module (see module structure).

From the TOML definition the register generator can create a VHDL package with all registers and their fields. This VHDL package can then be used with the generic AXI-Lite register file in tsfpga. Apart from that a C header and a C++ class can be generated, as well as a HTML page with human-readable documentation. See Register examples for a real-world example of register definitions and the code that it generates.

The register generator is well-integrated in the tsfpga module work flow. It is fast enough that before each build and each simulation run, the modules will re-generate their VHDL register package so that it is always up-to-date. Creating documentation and headers, which are typically distributed as part of FPGA release artifacts, is simple and easy to integrate in a build script.

There is also a set of VHDL AXI components that enable the register bus: AXI-to-AXI-Lite converter, AXI/AXI-Lite interconnect, AXI-Lite mux (splitter), AXI-Lite clock domain crossing, AXI-Lite generic register file. These are found in the repo within the axi module.

Register TOML format

The register generator parses a TOML file in order to gather all register information. It is important the the TOML is formatted correctly and has the necessary fields. The register TOML parser will warn if there are any error in the TOML, such as missing fields, unknown fields, wrong data types for fields, etc.

Below is a compilation of all the TOML properties that are available. Comments describe what attributes are optional and which are required.

Register TOML format rules.
################################################################################
# This will allocate a register with the name "configuration".
[register.configuration]

# The "mode" property MUST be present for a register.
# The value specified must be a valid mode string value.
mode = "r_w"
# The "description" property is optional for a register. Will default to "" if not specified.
# The value specified must be a string.
description = """This is the description of my register.

Rudimentary RST formatting can be used, such as **boldface** and *italics*."""


# This will allocate a bit field named "enable" in the "configuration" register.
[register.configuration.bit.enable]

# The "description" property is optional for a bit field. Will default to "" if not specified.
# The value specified must be a string.
description = "Description of the **enable** bit field."
# The "default_value" property is optional for a bit field.
# Must hold either of the strings "1" or "0" if specified.
# Will default to "0" if not specified.
default_value = "1"


# This will allocate a bit vector field named "data_tag" in the "configuration" register.
[register.configuration.bit_vector.data_tag]

# The "width" property MUST be present for a bit vector field.
# The value specified must be an integer.
width = 4
# The "description" property is optional for a bit vector field. Will default to "" if not specified.
# The value specified must be a string.
description = "Description of my **data_tag** bit vector field."
# The "default_value" property is optional for a bit vector field.
# The value specified must be a string whose length is the same as the specified **width** property value.
# May only contain ones and zeros.
# Will default to all zeros if not specified.
default_value = "0101"


################################################################################
# This will allocate a register array with the name "base_addresses".
[register_array.base_addresses]

# The "array_length" property MUST be present for a register array.
# The value specified must be an integer.
# The registers within the array will be repeated this many times.
array_length = 3
# The "description" property is optional for a register array. Will default to "" if not specified.
# The value specified must be a string.
description = "One set of base addresses for each feature."


# ------------------------------------------------------------------------------
# This will allocate a register "read_address" in the "base_addresses" array.
[register_array.base_addresses.register.read_address]

# Registers in a register array follow the exact same rules as "plain" registers.
# The properties that may and must be set are the same.
# Fields (bits, bit vectors, ...) can be added to array registers in the same way.
mode = "w"

# This will allocate a bit vector field named "address" in the "read_address" register within the "base_addresses" array.
[register_array.base_addresses.register.read_address.bit_vector.address]

width = 28
description = "Read address for a 256 MB address space."


# ------------------------------------------------------------------------------
# This will allocate a register "write_address" in the "base_addresses" array.
[register_array.base_addresses.register.write_address]

mode = "w"

# This will allocate a bit vector field named "address" in the "write_address" register within the "base_addresses" array.
[register_array.base_addresses.register.write_address.bit_vector.address]

width = 28
description = "Write address for a 256 MB address space."

Default registers

A lot of projects use a few default registers in standard locations that shall be present in all modules. In order to achieve this, without having to duplicate names and descriptions in many places, there is a default_registers flag to the get_modules() function. Passing a list of Register objects will insert them in the register list of all modules that use registers.

Bus layout

Below is a diagram of the typical layout for a register bus.

digraph my_graph {
graph [ dpi = 300 splines=ortho ];
rankdir="LR";

cpu [ label="AXI master\n(CPU)" shape=box ];
cpu -> axi_to_axi_lite [label="AXI"];

axi_to_axi_lite [ label="axi_to_axi_lite" shape=box ];
axi_to_axi_lite -> axi_lite_mux  [label="AXI-Lite" ];

axi_lite_mux [ label="axi_lite_mux" shape=box height=3.5 ];

axi_lite_mux -> axi_lite_reg_file0;
axi_lite_reg_file0 [ label="axi_lite_reg_file" shape=box ];

axi_lite_mux -> axi_lite_reg_file1;
axi_lite_reg_file1 [ label="axi_lite_reg_file" shape=box ];

axi_lite_mux -> axi_lite_cdc2;
axi_lite_cdc2 [ label="axi_lite_cdc" shape=box ];
axi_lite_cdc2 -> axi_lite_reg_file2;
axi_lite_reg_file2 [ label="axi_lite_reg_file" shape=box ];

axi_lite_mux -> axi_lite_cdc3;
axi_lite_cdc3 [ label="axi_lite_cdc" shape=box ];
axi_lite_cdc3 -> axi_lite_reg_file3;
axi_lite_reg_file3 [ label="axi_lite_reg_file" shape=box ];

dots [ shape=none label="..."];
axi_lite_mux -> dots;
}

In tsfpga, the register bus used is AXI-Lite. In cases where a module uses a different clock than the AXI master (CPU), the bus must be resynchronized. This makes sure that each module’s register values are always in the clock domain where they are used. This means that the module design does not have to worry about metastability, vector coherency, pulse resynchronization, etc.

  • axi_to_axi_lite is a simple protocol converter between AXI and AXI-Lite. It does not perform any burst splitting or handling of write strobes, but instead assumes the master to be well behaved. If this is not the case, AXI slave error (SLVERR) will be sent on the response channel (R/B).

  • axi_lite_mux is a 1-to-N AXI-Lite multiplexer that operates based on base addresses and address masks specified via a generic. If the address requested by the master does not match any slave, AXI decode error (DECERR) will be sent on the response channel (R/B). There will still be proper AXI handshaking done, so the master will not be stalled.

  • axi_lite_cdc is an asynchronous FIFO-based clock domain crossing (CDC) for AXI-Lite buses. It must be used in the cases where the axi_lite_reg_file (i.e. your module) is in a different clock domain than the CPU AXI master.

  • axi_lite_reg_file is a generic, parameterizable, register file for AXI-Lite register buses. It is parameterizable via a generic that sets the list of registers, with their modes and their default values. A constant with this generic is generated from TOML in the VHDL package. If the address requested by the master does not match any register, or there is a mode mismatch (e.g. write to a read-only register), AXI slave error (SLVERR) will be sent on the response channel (R/B).

All these entities are available in the repo in the axi and reg_file modules. Note that there is a convenience wrapper axi.axi_to_axi_lite_vec that instantiates axi_to_axi_lite, axi_lite_mux and any necessary axi_lite_cdc based on the appropriate generics.

Register examples

Example register code generation from the ddr_buffer example module.

TOML file

This is the source TOML file that defines the registers.

regs_ddr_buffer.toml
################################################################################
[register.status]

[register.status.bit.idle]

description = """'1' when the module is inactive and a new run can be launched.

'0' when the module is working."""


[register.status.bit_vector.counter]

description = "Number of AXI bursts that have finished."
width = 8


################################################################################
[register.command]

[register.command.bit.start]

description = "Start a read and write cycle."


################################################################################
[register.version]

mode = "r"
description = "Version number for this module"

[register.version.bit_vector.version]

width = 8
description = "Version field."


################################################################################
[register_array.addrs]

array_length = 2


# ------------------------------------------------------------------------------
[register_array.addrs.register.read_addr]

mode = "r_w"
description = "Where to read data from."


# ------------------------------------------------------------------------------
[register_array.addrs.register.write_addr]

mode = "r_w"
description = "Where to write data."


################################################################################
[constant.axi_data_width]

value = 64
description = "Data width of the AXI port used by this module."


################################################################################
[constant.burst_length_beats]

value = 16
description = """
The burst length, in number of beats, that will be used by this module.
This value, in conjunction with **axi_data_width** gives the size of the memory buffer that will be used.
"""

In this example module we use a set of default registers that include status and command. That is why these registers do not have a mode set in the TOML, which is otherwise required. For the other registers we have to explicitly set a mode.

For default registers, the register description is also inherited from the default specification. While a description is not strictly required it is used for all registers and bits in this example.

In this example we use a register array for the read and write addresses. The registers read_addr and write_addr will be repeated two times in the register list.

Manipulating registers from Pyhton

The ddr_buffer example module also showcases how to manipulate registers from Python via tsfpga’s module system. This method for manipulating registers can be very useful for information that is known in the Python realm, but is not convenient to add to the TOML file.

module_ddr_buffer.py
from tsfpga.module import BaseModule


class Module(BaseModule):

    # This will be much nicer when the register generator supports integer fields.
    # For now it is a bit vector field.
    version = "00000011"

    def registers_hook(self):
        self.registers.add_constant(
            "version", int(self.version, base=2), f"Version number for the {self.name} module."
        )
        self.registers.get_register("version").get_field("version").default_value = self.version

Using BaseModule.registers_hook() we add a constant as well as a read-only register for the module’s version number. The idea behind this example is that a software that uses this module will read the version register and compare to the static constant that shows up in the header. This will make sure that the software is running against the correct FPGA with expected module version.

VHDL package

The VHDL package file is designed to be used with the generic AXI-Lite register file available in tsfpga.

Since generation of VHDL packages is usually run in real time (e.g. before running a simulation) the speed of the tool is important. In order the save time, RegisterList.create_vhdl_package() maintains a hash of the register definitions, and will only generate the VHDL when necessary.

ddr_buffer_regs_pkg.vhd
-- This file is automatically generated by tsfpga.
-- Generated 2021-09-18 04:04 from file regs_ddr_buffer.toml at commit a2df45bca2daecbc.
-- Register hash 49f915a733d648eeeeaf2e5de68324355045117c, generator version 1.0.5.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

library reg_file;
use reg_file.reg_file_pkg.all;


package ddr_buffer_regs_pkg is

  constant ddr_buffer_config : natural := 0;
  constant ddr_buffer_command : natural := 1;
  constant ddr_buffer_status : natural := 2;
  constant ddr_buffer_irq_status : natural := 3;
  constant ddr_buffer_irq_mask : natural := 4;
  constant ddr_buffer_version : natural := 5;
  function ddr_buffer_addrs_read_addr(array_index : natural) return natural;
  function ddr_buffer_addrs_write_addr(array_index : natural) return natural;

  constant ddr_buffer_addrs_array_length : natural := 2;

  -- Declare register map constants here, but define them in body.
  -- This is done so that functions have been elaborated when they are called.
  subtype ddr_buffer_reg_range is natural range 0 to 9;
  constant ddr_buffer_reg_map : reg_definition_vec_t(ddr_buffer_reg_range);

  subtype ddr_buffer_regs_t is reg_vec_t(ddr_buffer_reg_range);
  constant ddr_buffer_regs_init : ddr_buffer_regs_t;

  subtype ddr_buffer_reg_was_accessed_t is std_logic_vector(ddr_buffer_reg_range);

  constant ddr_buffer_command_start : natural := 0;

  constant ddr_buffer_status_idle : natural := 0;
  subtype ddr_buffer_status_counter is natural range 8 downto 1;
  constant ddr_buffer_status_counter_width : positive := 8;

  subtype ddr_buffer_version_version is natural range 7 downto 0;
  constant ddr_buffer_version_version_width : positive := 8;

  constant ddr_buffer_constant_axi_data_width : integer := 64;
  constant ddr_buffer_constant_burst_length_beats : integer := 16;
  constant ddr_buffer_constant_version : integer := 3;

end package;

package body ddr_buffer_regs_pkg is

  function ddr_buffer_addrs_read_addr(array_index : natural) return natural is
  begin
    assert array_index < ddr_buffer_addrs_array_length
      report "Array index out of bounds: " & natural'image(array_index)
      severity failure;
    return 6 + array_index * 2 + 0;
  end function;

  function ddr_buffer_addrs_write_addr(array_index : natural) return natural is
  begin
    assert array_index < ddr_buffer_addrs_array_length
      report "Array index out of bounds: " & natural'image(array_index)
      severity failure;
    return 6 + array_index * 2 + 1;
  end function;

  constant ddr_buffer_reg_map : reg_definition_vec_t(ddr_buffer_reg_range) := (
    0 => (idx => ddr_buffer_config, reg_type => r_w),
    1 => (idx => ddr_buffer_command, reg_type => wpulse),
    2 => (idx => ddr_buffer_status, reg_type => r),
    3 => (idx => ddr_buffer_irq_status, reg_type => r_wpulse),
    4 => (idx => ddr_buffer_irq_mask, reg_type => r_w),
    5 => (idx => ddr_buffer_version, reg_type => r),
    6 => (idx => ddr_buffer_addrs_read_addr(0), reg_type => r_w),
    7 => (idx => ddr_buffer_addrs_write_addr(0), reg_type => r_w),
    8 => (idx => ddr_buffer_addrs_read_addr(1), reg_type => r_w),
    9 => (idx => ddr_buffer_addrs_write_addr(1), reg_type => r_w)
  );

  constant ddr_buffer_regs_init : ddr_buffer_regs_t := (
    0 => std_logic_vector(to_signed(0, 32)),
    1 => std_logic_vector(to_signed(0, 32)),
    2 => std_logic_vector(to_signed(0, 32)),
    3 => std_logic_vector(to_signed(0, 32)),
    4 => std_logic_vector(to_signed(0, 32)),
    5 => std_logic_vector(to_signed(3, 32)),
    6 => std_logic_vector(to_signed(0, 32)),
    7 => std_logic_vector(to_signed(0, 32)),
    8 => std_logic_vector(to_signed(0, 32)),
    9 => std_logic_vector(to_signed(0, 32))
  );

end package body;

For the plain registers the register index is simply an integer, e.g. ddr_buffer_config, but for the register arrays is is a function, e.g. ddr_buffer_addrs_read_addr(). The function takes an array index argument and will assert if it is out of bounds of the array.

Note that there is a large eco-system of register related components in tsfpga. Firstly there are wrappers available for easier working with VUnit verification components. See the bfm library and reg_operations_pkg. Furthermore there is a large number of synthesizable AXI components available that enable the register bus: AXI-to-AXI-Lite converter, AXI/AXI-Lite interconnect, AXI-Lite mux (splitter), AXI-Lite clock domain crossing, etc. See the axi library for more details.

HTML page

A complete HTML page can be generated, with register details as well as textual description of the different register modes.

Note

Markdown/reStructuredText syntax can be used in register and bit descriptions, which will be converted to appropriate HTML tags. Text can be set bold with double asterisks, and italicised with a single asterisk. A paragraph break can be inserted with consecutive newlines.

Generated HTML file here: ddr_buffer_regs.html

HTML tables

Optionally, only the tables with register and constant descriptions can be generated to HTML. These can then be included in a separate documentation flow.

Generated HTML file here: ddr_buffer_register_table.html

Generated HTML file here: ddr_buffer_constant_table.html

C++ class

A complete C++ class can be generated with methods that read or write the registers. There is an abstract interface header available that can be used for mocking in a unit test environment.

DdrBuffer interface header
// This file is automatically generated by tsfpga.
// Generated 2021-09-18 04:04 from file regs_ddr_buffer.toml at commit a2df45bca2daecbc.

#pragma once

#include <cassert>
#include <cstdint>
#include <cstdlib>

namespace fpga_regs
{

class IDdrBuffer
{
public:
  // Register constant.
  // Data width of the AXI port used by this module.
  static const int axi_data_width = 64L;
  // Register constant.
  // The burst length, in number of beats, that will be used by this module.
  // This value, in conjunction with **axi_data_width** gives the size of the memory buffer that will be used.
  static const int burst_length_beats = 16L;
  // Register constant.
  // Version number for the ddr_buffer module.
  static const int version = 3L;

  // Number of registers within this register map.
  static const size_t num_registers = 10uL;

  // Length of the "addrs" register array
  static const size_t addrs_array_length = 2uL;

  virtual ~IDdrBuffer() { }

  // Register "config". Mode "Read, Write".
  // Configuration register.
  virtual uint32_t get_config() const = 0;
  virtual void set_config(uint32_t value) const = 0;

  // Register "command". Mode "Write-pulse".
  virtual void set_command(uint32_t value) const = 0;
  // Bitmask for the "start" field in the "command" register.
  // Start a read and write cycle.
  static const uint32_t command_start_shift = 0uL;
  static const uint32_t command_start_mask = 0b1uL << 0uL;

  // Register "status". Mode "Read".
  virtual uint32_t get_status() const = 0;
  // Bitmask for the "idle" field in the "status" register.
  // '1' when the module is inactive and a new run can be launched.
  // 
  // '0' when the module is working.
  static const uint32_t status_idle_shift = 0uL;
  static const uint32_t status_idle_mask = 0b1uL << 0uL;
  // Bitmask for the "counter" field in the "status" register.
  // Number of AXI bursts that have finished.
  static const uint32_t status_counter_shift = 1uL;
  static const uint32_t status_counter_mask = 0b11111111uL << 1uL;

  // Register "irq_status". Mode "Read, Write-pulse".
  // Reading a '1' in this register means the corresponding interrupt has triggered.
  // Writing to this register will clear the interrupts where there is a '1' in the written word.
  virtual uint32_t get_irq_status() const = 0;
  virtual void set_irq_status(uint32_t value) const = 0;

  // Register "irq_mask". Mode "Read, Write".
  // A '1' in this register means that the corresponding interrupt is enabled.
  virtual uint32_t get_irq_mask() const = 0;
  virtual void set_irq_mask(uint32_t value) const = 0;

  // Register "version". Mode "Read".
  // Version number for this module
  virtual uint32_t get_version() const = 0;
  // Bitmask for the "version" field in the "version" register.
  // Version field.
  static const uint32_t version_version_shift = 0uL;
  static const uint32_t version_version_mask = 0b11111111uL << 0uL;

  // Register "read_addr" within the "addrs" register array. Mode "Read, Write".
  // Where to read data from.
  virtual uint32_t get_addrs_read_addr(size_t array_index) const = 0;
  virtual void set_addrs_read_addr(size_t array_index, uint32_t value) const = 0;

  // Register "write_addr" within the "addrs" register array. Mode "Read, Write".
  // Where to write data.
  virtual uint32_t get_addrs_write_addr(size_t array_index) const = 0;
  virtual void set_addrs_write_addr(size_t array_index, uint32_t value) const = 0;
};

} /* namespace fpga_regs */
DdrBuffer class header
// This file is automatically generated by tsfpga.
// Generated 2021-09-18 04:04 from file regs_ddr_buffer.toml at commit a2df45bca2daecbc.

#pragma once

#include "i_ddr_buffer.h"

namespace fpga_regs
{

class DdrBuffer : public IDdrBuffer
{
private:
  volatile uint32_t *m_registers;

public:
  DdrBuffer(volatile uint8_t *base_address);

  virtual ~DdrBuffer() { }

  virtual uint32_t get_config() const override;
  virtual void set_config(uint32_t value) const override;

  virtual void set_command(uint32_t value) const override;

  virtual uint32_t get_status() const override;

  virtual uint32_t get_irq_status() const override;
  virtual void set_irq_status(uint32_t value) const override;

  virtual uint32_t get_irq_mask() const override;
  virtual void set_irq_mask(uint32_t value) const override;

  virtual uint32_t get_version() const override;

  virtual uint32_t get_addrs_read_addr(size_t array_index) const override;
  virtual void set_addrs_read_addr(size_t array_index, uint32_t value) const override;

  virtual uint32_t get_addrs_write_addr(size_t array_index) const override;
  virtual void set_addrs_write_addr(size_t array_index, uint32_t value) const override;
};

} /* namespace fpga_regs */
DdrBuffer class implementation
// This file is automatically generated by tsfpga.
// Generated 2021-09-18 04:04 from file regs_ddr_buffer.toml at commit a2df45bca2daecbc.

#include "include/ddr_buffer.h"

namespace fpga_regs
{

DdrBuffer::DdrBuffer(volatile uint8_t *base_address)
    : m_registers(reinterpret_cast<volatile uint32_t *>(base_address))
{
  // Empty
}

uint32_t DdrBuffer::get_config() const
{
  return m_registers[0];
}

void DdrBuffer::set_config(uint32_t value) const
{
  m_registers[0] = value;
}

void DdrBuffer::set_command(uint32_t value) const
{
  m_registers[1] = value;
}

uint32_t DdrBuffer::get_status() const
{
  return m_registers[2];
}

uint32_t DdrBuffer::get_irq_status() const
{
  return m_registers[3];
}

void DdrBuffer::set_irq_status(uint32_t value) const
{
  m_registers[3] = value;
}

uint32_t DdrBuffer::get_irq_mask() const
{
  return m_registers[4];
}

void DdrBuffer::set_irq_mask(uint32_t value) const
{
  m_registers[4] = value;
}

uint32_t DdrBuffer::get_version() const
{
  return m_registers[5];
}

uint32_t DdrBuffer::get_addrs_read_addr(size_t array_index) const
{
  assert(array_index < addrs_array_length);
  const size_t index = 6 + array_index * 2 + 0;
  return m_registers[index];
}

void DdrBuffer::set_addrs_read_addr(size_t array_index, uint32_t value) const
{
  assert(array_index < addrs_array_length);
  const size_t index = 6 + array_index * 2 + 0;
  m_registers[index] = value;
}

uint32_t DdrBuffer::get_addrs_write_addr(size_t array_index) const
{
  assert(array_index < addrs_array_length);
  const size_t index = 6 + array_index * 2 + 1;
  return m_registers[index];
}

void DdrBuffer::set_addrs_write_addr(size_t array_index, uint32_t value) const
{
  assert(array_index < addrs_array_length);
  const size_t index = 6 + array_index * 2 + 1;
  m_registers[index] = value;
}


} /* namespace fpga_regs */

Note that when the register is part of an array, the setter/getter takes a second argument array_index. There is an assert that the user-provided array index is within the bounds of the array.

C header

A C header with register and field definitions can be generated.

ddr_buffer header
// This file is automatically generated by tsfpga.
// Generated 2021-09-18 04:04 from file regs_ddr_buffer.toml at commit a2df45bca2daecbc.

#ifndef DDR_BUFFER_REGS_H
#define DDR_BUFFER_REGS_H

// Register constant "axi_data_width".
// Data width of the AXI port used by this module.
#define DDR_BUFFER_AXI_DATA_WIDTH (64)
// Register constant "burst_length_beats".
// The burst length, in number of beats, that will be used by this module.
// This value, in conjunction with **axi_data_width** gives the size of the memory buffer that will be used.
#define DDR_BUFFER_BURST_LENGTH_BEATS (16)
// Register constant "version".
// Version number for the ddr_buffer module.
#define DDR_BUFFER_VERSION (3)

// Number of registers within this register map.
#define DDR_BUFFER_NUM_REGS (10u)

// Type for the "addrs" register array.
typedef struct ddr_buffer_addrs_t
{
  // Where to read data from.
  // Mode "Read, Write".
  uint32_t read_addr;
  // Where to write data.
  // Mode "Read, Write".
  uint32_t write_addr;
} ddr_buffer_addrs_t;

// Type for this register map.
typedef struct ddr_buffer_regs_t
{
  // Configuration register.
  // Mode "Read, Write".
  uint32_t config;
  // Mode "Write-pulse".
  uint32_t command;
  // Mode "Read".
  uint32_t status;
  // Reading a '1' in this register means the corresponding interrupt has triggered.
  // Writing to this register will clear the interrupts where there is a '1' in the written word.
  // Mode "Read, Write-pulse".
  uint32_t irq_status;
  // A '1' in this register means that the corresponding interrupt is enabled.
  // Mode "Read, Write".
  uint32_t irq_mask;
  // Version number for this module
  // Mode "Read".
  uint32_t version;
  ddr_buffer_addrs_t addrs[2];
} ddr_buffer_regs_t;

// Address of the "config" register. Mode "Read, Write".
// Configuration register.
#define DDR_BUFFER_CONFIG_INDEX (0u)
#define DDR_BUFFER_CONFIG_ADDR (4u * DDR_BUFFER_CONFIG_INDEX)

// Address of the "command" register. Mode "Write-pulse".
#define DDR_BUFFER_COMMAND_INDEX (1u)
#define DDR_BUFFER_COMMAND_ADDR (4u * DDR_BUFFER_COMMAND_INDEX)
// Mask and shift for the "start" field in the "command" register.
// Start a read and write cycle.
#define DDR_BUFFER_COMMAND_START_SHIFT (0u)
#define DDR_BUFFER_COMMAND_START_MASK (0b1u << 0u)

// Address of the "status" register. Mode "Read".
#define DDR_BUFFER_STATUS_INDEX (2u)
#define DDR_BUFFER_STATUS_ADDR (4u * DDR_BUFFER_STATUS_INDEX)
// Mask and shift for the "idle" field in the "status" register.
// '1' when the module is inactive and a new run can be launched.
// 
// '0' when the module is working.
#define DDR_BUFFER_STATUS_IDLE_SHIFT (0u)
#define DDR_BUFFER_STATUS_IDLE_MASK (0b1u << 0u)
// Mask and shift for the "counter" field in the "status" register.
// Number of AXI bursts that have finished.
#define DDR_BUFFER_STATUS_COUNTER_SHIFT (1u)
#define DDR_BUFFER_STATUS_COUNTER_MASK (0b11111111u << 1u)

// Address of the "irq_status" register. Mode "Read, Write-pulse".
// Reading a '1' in this register means the corresponding interrupt has triggered.
// Writing to this register will clear the interrupts where there is a '1' in the written word.
#define DDR_BUFFER_IRQ_STATUS_INDEX (3u)
#define DDR_BUFFER_IRQ_STATUS_ADDR (4u * DDR_BUFFER_IRQ_STATUS_INDEX)

// Address of the "irq_mask" register. Mode "Read, Write".
// A '1' in this register means that the corresponding interrupt is enabled.
#define DDR_BUFFER_IRQ_MASK_INDEX (4u)
#define DDR_BUFFER_IRQ_MASK_ADDR (4u * DDR_BUFFER_IRQ_MASK_INDEX)

// Address of the "version" register. Mode "Read".
// Version number for this module
#define DDR_BUFFER_VERSION_INDEX (5u)
#define DDR_BUFFER_VERSION_ADDR (4u * DDR_BUFFER_VERSION_INDEX)
// Mask and shift for the "version" field in the "version" register.
// Version field.
#define DDR_BUFFER_VERSION_VERSION_SHIFT (0u)
#define DDR_BUFFER_VERSION_VERSION_MASK (0b11111111u << 0u)

// Address of the "read_addr" register within the "addrs" register array (array_index < 2). Mode "Read, Write".
// Where to read data from.
#define DDR_BUFFER_ADDRS_READ_ADDR_INDEX(array_index) (6u + (array_index) * 2u + 0u)
#define DDR_BUFFER_ADDRS_READ_ADDR_ADDR(array_index) (4u * DDR_BUFFER_ADDRS_READ_ADDR_INDEX(array_index))

// Address of the "write_addr" register within the "addrs" register array (array_index < 2). Mode "Read, Write".
// Where to write data.
#define DDR_BUFFER_ADDRS_WRITE_ADDR_INDEX(array_index) (6u + (array_index) * 2u + 1u)
#define DDR_BUFFER_ADDRS_WRITE_ADDR_ADDR(array_index) (4u * DDR_BUFFER_ADDRS_WRITE_ADDR_INDEX(array_index))

#endif // DDR_BUFFER_REGS_H

It provides two methods for usage: A struct that can be memory mapped, or address definitions that can be offset a base address. For the addresses, array registers use a macro with an array index argument.