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.