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
memoryblock is one element (: uint32), or several (: uint16[4], orcount 80), at the address after@. - Its element is its type's size, or
sizebytes; a record of fields whosebitsare all given is 4 bytes. rw,r(ro) orw(wo) after the type is who may read or write it on the bus. No access saysrw.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"(orbits "3", one bit). A field with nobitsis the bytes after the field before it: itssize, else its type's size. So a record laid out byte by byte — kacao's shared memory — needs nobits. - 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
WSTRBwrites 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/_weand<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_aresetnlow) 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_awaddris 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.