Your register map should be generated, not maintained
Every FPGA IP block carries the same three artifacts: the RTL that implements its control/status registers, the C header its driver includes, and the documentation table humans read. Three files, one truth — and in most teams, three separate hand-maintained copies of it.
The failure mode is always the same and always expensive. Someone moves a
field from bit 3 to bit 4 in the RTL during bring-up. The header gets
updated next sprint; the wiki, never. Two months later a driver engineer
burns a day on a peripheral that "randomly" won't enable, because
CTRL_ENABLE_MASK still says 0x08. Nothing in CI could catch it: each
file was internally consistent. They just disagreed with each other.
The fix is simple and absolute: one source of truth, everything else generated. Big shops do this with SystemRDL or IP-XACT toolchains. Those are heavy — licensed tools, XML schemas, build-system surgery — which is why small teams keep hand-writing register files and paying the drift tax.
So we built a lightweight version: the register map generator. You describe the block in plain text:
CTRL RW 0x00 = 0x0
enable 0 Core enable
irq_en 1 Interrupt enable
mode 3:2 0=idle 1=run 2=loopback
STATUS RO 0x04
busy 0 Transfer in progress
IRQ W1C 0x08
done 0 Transfer complete
DATA WO 0x0C
and get all three artifacts: a synthesizable AXI4-Lite slave (per-field
ports, WSTRB honored, SLVERR on unmapped addresses, per-register write
strobes), a C header with offsets/masks/shifts, and a Markdown
register table for your docs.
A few design decisions worth explaining:
W1C done right. Interrupt-status registers follow the
hardware-sets/software-clears pattern: a pulse on i_irq_done_set latches
the bit; the driver writes 1 to clear it. Getting the set/clear collision
cycle right (set wins) is exactly the kind of subtle thing you want
generated once, not re-derived per project.
Write strobes. Every writable register has an o_<reg>_wr pulse.
That's the idiom for command registers and FIFO pushes — write the DATA
register, the strobe pushes it into your pipeline.
The RTL is tested like RTL. The generator's output compiles clean under
iverilog -Wall, and its AXI handshake, field masking, W1C semantics and
write-only behavior are exercised by a transaction-level simulation
testbench in our test suite. Generated code you can't trust is worse than
hand-written code.
It's regenerable in CI. Keep the text definition in your repo and pull the artifacts through the JSON API:
curl -sG 'https://libfpga.com/tools/register-map' \
--data-urlencode "block=mycore" \
--data-urlencode "definition@regs.txt" \
--data-urlencode "format=json" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["code"][0]["text"])' \
> rtl/mycore_regs.v
Now the header can't disagree with the RTL, because neither was written by a person. The register map lives in one reviewable diff, and "update the wiki" stops being a task anyone can forget — the doc is an artifact too.
If the tool is missing something your team needs (AXI-Lite byte addressing quirks, field enums, interrupt aggregation), tell us: hello@libfpga.com — the roadmap is ranked by real requests.