Code generation › Register maps

Code generation

Register maps

archec gen main.arche --target regmap writes the register map of every part that has memory blocks with an address, once for each side that has to agree on it: the software that reads and writes the registers, the hardware that holds them, and the tools and documents in between. They all come from the same blocks, so a change in the model is a change everywhere at once.

enum TimerMode { ONE_SHOT, PERIODIC, PWM = 3 }

component Timer "Timer peripheral" {
  /// Control: start, mode and the interrupt enable.
  memory CTRL @ 0x4300_0000 : uint32 rw {
    enable: bool bits "0"
    mode: TimerMode = PERIODIC bits "2:1"
    prescale: uint8 = 4 bits "23:16"
  }
  memory STATUS @ 0x4300_0004 : uint32 r { running: bool bits "0" }
  memory COMPARE @ 0x4300_0010 : uint16[4] rw
  memory BUFFER @ 0x4300_0100 : uint32[128] rw
}

For each map, in regmap/<part>/:

File What it is
<part>_regs.h C (C11 and C++): the base (TIMER_BASE, which you can define before including it), each block's offset, address, element size, count and reset, each field's _SHIFT, _WIDTH, _MASK, _GET(v) and _SET(v, x), an enum field's values, access macros that take a base pointer (TIMER_CTRL(base), TIMER_COMPARE(base, i); a read-only block is const volatile), a struct of the whole map to lay over the base, and static asserts that put each block of that struct at its offset. A record of whole-byte fields (kacao's TRACKING) is a struct of its own, its size asserted.
<part>_regs.vhd VHDL-93: a package of the offsets and field positions, and the register bank, an AXI4-Lite slave.
<part>_regs.sv SystemVerilog: the same bank, port for port.
<part>.xml IP-XACT (IEEE 1685-2014): the memory map — the registers with their fields, resets, access and enumerated values, the memories as address blocks of their own.
<part>.rdl SystemRDL 2.0: the address map, for PeakRDL and the other RDL tools.
<part>.md The memory map as a document: a table of the blocks, then each block's fields.

A map is named after its part, and after the parts around it when two parts have the same name (kria_shm, kriakr260_shm). Its base is the first block's address on a 4 KiB page; offsets are from there.

The model#

  • A memory block is one element (: uint32), or several (: uint16[4], or count 80), at the address after @.
  • Its element is its type's size, or size bytes; a record of fields whose bits are all given is 4 bytes.
  • rw, r (ro) or w (wo) after the type is who may read or write it on the bus. No access says rw.
  • reset 0x… is the element's value at reset; a field's own value (mode: TimerMode = PERIODIC) goes over it.
  • A field is at bits "hi:lo" (or bits "3", one bit). A field with no bits is the bytes after the field before it: its size, else its type's size. So a record laid out byte by byte — kacao's shared memory — needs no bits.
  • A block with no address is not in the map (a note says so).

archec check warns of what would make two sides disagree:

Code When
memory-overlap two blocks of a part overlap
memory-misaligned a block of 2-, 4- or 8-byte elements not on a multiple of its element: a bus reads or writes it in two accesses, not at once
memory-field-width a field past the bits of one element, wider than its type (uint8 bits "15:4"), or bits that do not read as "hi:lo"
memory-field-overlap two fields share a bit

The bank#

Why AXI4-Lite rather than a simple generic bus: the parts that have register maps in practice sit on a Zynq or a Kria, where the PS reaches the PL over AXI, and Vivado's IP packager, its interconnects and every other FPGA vendor's speak it. A generic bus would need a bridge written by hand on each side. AXI4-Lite is also small: one read and one write at a time, 32-bit data, byte strobes.

  • Each byte of each element is a byte lane of a 32-bit word, so 8-, 16-, 32- and 64-bit registers, packed together or not, are written and read the same way, and WSTRB writes only the bytes it says.
  • rw: a register the bus writes and reads back, on <block>_o.
  • w: a register the bus writes and reads as 0, on <block>_o.
  • r: a value the hardware drives on <block>_i; writes are ignored.
  • A writable block has a write strobe per element, <block>_wr, high for one cycle after a write to it.
  • A block of more than 16 elements, or of elements past 8 bytes, is a memory, not registers: the bank decodes its range and hands each access to a port of its own — <block>_waddr/_wdata/_wstrb/_we and <block>_raddr/_re/_rdata — as a synchronous dual-port RAM takes them (the read data one cycle after _re). The addresses are byte offsets in the memory.
  • Registers come out of reset (s_axi_aresetn low) at their reset values.
  • An address the map does not have answers SLVERR.
  • Addresses are from the map's base: the interconnect strips it, so s_axi_awaddr is as wide as the map needs.

Verified#

The tests (test/regmap.test.js) compile the headers of the fixture, examples/kacao and examples/autonomous-car as C11 and C++17 with -Wall -Wextra -Werror (so the static asserts hold), build every SystemVerilog bank with iverilog and simulate the fixture's against AXI4-Lite reads and writes (resets, byte strobes, a read-only input in the upper lanes, a memory port, SLVERR), analyse the VHDL with ghdl when it is installed, and check the IP-XACT with xmllint. The SystemRDL and the IP-XACT of these maps were also read back by systemrdl-compiler and PeakRDL's IP-XACT importer, which found the blocks, the fields and the resets the model gives.

Not done#

  • Field-level access (a read-only field in a read-write register), write-1-to-clear and read-to-clear fields: the model has no way to say them yet.
  • The bank's bus is AXI4-Lite only, 32-bit; no APB, Avalon or Wishbone.
  • A part's map has no base of its own in the model: the base is where its first block is.