The hand-written decoder and the hand-written header agree on the day they are written and drift from the first engineering change order. Somebody widens a window in the RTL and forgets the header, or fixes a base in the header and forgets the RTL, and the classic failure writes itself: the register specification is out of sync with the RTL, drivers hit the wrong block, and the bug survives every review because each file looks right on its own. The cure is a single map table as data, with the decode, the headers and the documentation table all generated from it.
The map table is a list of rows, each naming the region, its base and the bit slice the select compares. Three small emitters read the same rows in the full flow: one writes the SystemVerilog assigns, one writes the C defines, one writes the documentation table. The miniature below shows two of the three. Nothing else is allowed to state a base address. The generation step then asserts what generation guarantees: every region appears in every output, every emitted compare matches the row it came from, and every base satisfies its own equation. A mismatch fails the build instead of becoming a silicon bug.
The script below is the whole flow in miniature for the four interconnect windows. It generates the decode text and the header text, then checks three things: each region has one select and one define from the same base, each compare names the slice from its own row, and each base really satisfies its equation. Add the second DMA and the script grows by one row, not by three coordinated edits.
REGIONS = [
("ROM", 0x0000_0000, 31, 14, 0x00000),
("SRAM0", 0x2000_0000, 31, 16, 0x2000),
("SRAM1", 0x2001_0000, 31, 16, 0x2001),
("APB", 0x4000_0000, 31, 16, 0x4000),
]
def gen_sv(regions):
lines = ["// generated from the map table; do not hand-edit"]
for name, base, hi, lo, val in regions:
lines.append(f"assign sel_{name.lower()} = (addr[{hi}:{lo}] == {(hi - lo + 1)}'h{val:X});")
lines.append("assign sel_default = ~(sel_rom | sel_sram0 | sel_sram1 | sel_apb);")
return "\n".join(lines) + "\n"
def gen_header(regions):
lines = ["/* generated from the map table; do not hand-edit */"]
for name, base, hi, lo, val in regions:
lines.append(f"#define {name}_BASE 0x{base:08X}")
return "\n".join(lines) + "\n"
sv_text = gen_sv(REGIONS)
h_text = gen_header(REGIONS)
for name, base, hi, lo, val in REGIONS:
assert f"sel_{name.lower()}" in sv_text
assert f"#define {name}_BASE 0x{base:08X}" in h_text
assert f"addr[{hi}:{lo}]" in sv_text
assert ((base >> lo) & ((1 << (hi - lo + 1)) - 1)) == val
assert "sel_default" in sv_text
print("PASS: 4 selects and 4 defines generated from one table and agree")A hand edit fixes the symptom in one file and leaves the other two stale, because nothing connects them except discipline, and discipline does not survive tapeout pressure. The generated flow inverts the burden: the table is the only file anyone edits, and the check that all outputs derive from it runs on every build. The review then reads the table, which fits on a slide, instead of diffing three files line by line. It is a habit worth carrying to every later project: state each address fact once, generate every copy, and let the build prove the copies agree.
The generated files carry a do-not-hand-edit banner and a map version stamp; any hand edit is a review finding.
The agreement check runs in the build, not on request, and its output is archived with the release.
The documentation table is generated too, so the document the software team reads cannot disagree with the RTL.
Change one base in the table and rebuild: all three outputs must change together and the agreement check must still pass. If the check passes but simulation disagrees, the RTL under test was not rebuilt from the generated file, which means the build, not the map, is broken. Keep one golden copy of the generated outputs per map version so any drift shows up as a diff.
Somebody hand-edited the RTL without regenerating, or regenerated the RTL without the header. In the generated flow this state cannot reach review: the table holds one value and the agreement check rebuilds both outputs from it.