The value of a description is realised by the generator that consumes it: top-level wiring that connects every master to the fabric, decode logic emitted from the address assignments, and headers emitted from the same facts. When one description drives all three, an address change is a one-line edit and a rebuild instead of a treasure hunt. This lesson runs that flow in miniature on a written description of NanoSoC's core, small enough to audit by eye.
The description below states, as data, the instances, who masters and who slaves, and the slave windows with bases and sizes. Three emitters read it: one lists the top-level connections, one emits the decode assigns, and one emits the header defines. The check function then does the integration work, and the script shows it can fail: every slave has exactly one window, appearing exactly once in both the decode and the header with the same base, and every instance appears in the generated wiring, so a block with no window, a window with no block, or a forgotten connection fails the build instead of floating in the chip. This is the map-table generator from the decode topic grown by one dimension: the same discipline, now covering wiring as well as addresses.
DESC = {
"instances": [
("u_cpu", "nano_cpu", "master"), ("u_xbar", "nano_xbar", "fabric"),
("u_rom", "nano_rom", "slave"), ("u_sram0", "nano_sram0", "slave"),
("u_sram1", "nano_sram1", "slave"), ("u_axi2apb", "nano_axi2apb", "slave"),
],
"windows": [ # slave instance, base, size (power of two, aligned)
("u_rom", 0x0000_0000, 0x4000), ("u_sram0", 0x2000_0000, 0x1_0000),
("u_sram1", 0x2001_0000, 0x1_0000), ("u_axi2apb", 0x4000_0000, 0x1_0000),
],
}
def emit_wiring(d):
lines = []
for inst, _typ, role in d["instances"]:
if role == "master":
lines.append(f"connect {inst}.axi -> u_xbar.m0")
elif role == "slave":
lines.append(f"connect u_xbar.{inst[2:]} -> {inst}.axi")
return lines
def emit_decode(d):
lines = []
for inst, base, size in d["windows"]:
lo = size.bit_length() - 1
lines.append(f"assign sel_{inst[2:]} = (addr[31:{lo}] == {(base >> lo)});")
lines.append("assign sel_default = ~(sel_rom | sel_sram0 | sel_sram1 | sel_axi2apb);")
return lines
def emit_header(d):
return [f"#define {inst[2:].upper()}_BASE 0x{base:08X}u" for inst, base, _s in d["windows"]]
def check(d):
problems = []
wiring, dec, hdr = emit_wiring(d), emit_decode(d), emit_header(d)
insts = {i for i, _t, _r in d["instances"]}
slaves = {i for i, _t, r in d["instances"] if r == "slave"}
connected = set()
for w in wiring:
left, right = w[len("connect "):].split(" -> ")
connected |= {left.split(".")[0], right.split(".")[0]}
for i in sorted(insts - connected):
problems.append(f"{i}: instance never connected")
win = [w[0] for w in d["windows"]]
for i in sorted(slaves - set(win)):
problems.append(f"{i}: slave has no window")
for i in sorted(set(win) - slaves):
problems.append(f"{i}: window for a non-slave")
for inst, base, _size in d["windows"]:
n = inst[2:]
if win.count(inst) != 1 or sum(1 for l in dec if l.startswith(f"assign sel_{n} ")) != 1:
problems.append(f"{inst}: not decoded exactly once")
if sum(1 for l in hdr if l.startswith(f"#define {n.upper()}_BASE 0x{base:08X}u")) != 1:
problems.append(f"{inst}: header base missing or different")
if "sel_default" not in dec[-1]:
problems.append("no default select")
return problems
assert check(DESC) == []
forgot_window = {**DESC, "instances": DESC["instances"] + [("u_sram2", "nano_sram2", "slave")]}
assert check(forgot_window) == ["u_sram2: slave has no window"]
orphan = {**DESC, "instances": DESC["instances"] + [("u_gpio", "nano_gpio", "peripheral")]}
assert check(orphan) == ["u_gpio: instance never connected"]
twice = {**DESC, "windows": DESC["windows"] + [("u_rom", 0x0000_0000, 0x4000)]}
assert any("not decoded exactly once" in p for p in check(twice))
print("PASS: wiring, decode and header generated from one description and agree")Read the generated decode line for the ROM carefully: a 16 KB window puts lo at 14, so the compare is addr bits 31 down to 14 against zero, which is exactly the hand-derived equation from the decode topic. That agreement is the point of the exercise: the generator does not invent a style, it mechanises the derivation the earlier lessons did by hand, so the review burden moves from arithmetic to the description itself. Four windows fit in a lesson; the same emitters hold for forty, which is where hand derivation stops being reviewable at all.
The description is the only edited artifact; generated wiring, decode and headers carry do-not-edit banners and version stamps.
Completeness assertions run in the build: every instance connected, every window decoded and defined, no extras.
A sample of generated output is reviewed by eye each release, especially ties and defaults, which assertions alone cannot judge.
Add a fifth slave to the description and regenerate: wiring, decode and header must all grow by exactly their share, and the assertions must still pass. Then remove one connection and confirm the build fails. If generated output surprises a reviewer, fix the description or the emitter, never the output; an output file with a manual patch is a fork that the next regeneration silently reverts.
The slave-versus-window check at generation time: the instance is connected, but no window names it, so the check reports a slave with no window and the build fails before any simulation runs. The script above injects exactly that case and shows it is caught.