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.