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.

tooling