A block with no domain gets no clock tree, no reset synchroniser and no crossing rule; a block in two domains gets wired to the wrong one somewhere. The domain table is the one page that prevents both, and the crossing inventory is generated from it. This lesson writes the NanoSoC table in full and gives the script that audits it. Its one job is the table; the crossings it implies belong to the next topic.
Each row names a block, its clock and its power domain, because clock and power travel together in the checks. Bridges appear twice, once per side: the AXI bridge is clk_cpu on the CPU side and clk_sys on the system side, the APB bridge is clk_sys on one side and clk_per on the other. Both bridges sit in PD_SW, which is why the APB bridge's clk_per-side outputs need isolation cells into PD_AON. The table below follows the NanoSoC specification; clk_cpu is 192 MHz, clk_sys 96 MHz and clk_per 24 MHz:
Block (side) | Clock | Power |
|---|---|---|
nano_cpu core and I-cache | clk_cpu | PD_SW |
nano_axi_cdc_m (CPU side) | clk_cpu | PD_SW |
nano_axi_cdc_m (system side) | clk_sys | PD_SW |
nano_xbar, nano_dma, nano_sbr, nano_sram1, nano_axi2apb, nano_apb_dec | clk_sys | PD_SW |
nano_apb_cdc (system side) | clk_sys | PD_SW |
nano_sram0 (arrays; its wrapper logic is in PD_SW), nano_rom | clk_sys | PD_AON |
nano_apb_cdc (peripheral side) | clk_per | PD_SW |
nano_uart, nano_spi, nano_gpio, nano_intc, nano_rstpwr | clk_per | PD_AON |
Ownership means the table has one writer: the integrator. Block owners may request a domain change, but the table changes only with the crossing inventory updated in the same edit, because every moved block adds crossings on one side and removes them on the other. The audit below checks the two invariants a reviewer cannot check by eye across revisions: every block appears exactly once per side, and every bridge side has a partner side in a different domain with a named mechanism.
# (block-side, clock, power): the table above, one row per block-side.
ROWS = [
("nano_cpu", "clk_cpu", "PD_SW"), ("axi_cdc_m.cpu", "clk_cpu", "PD_SW"),
("axi_cdc_m.sys", "clk_sys", "PD_SW"), ("nano_xbar", "clk_sys", "PD_SW"),
("nano_sram0", "clk_sys", "PD_AON"), ("nano_sram1", "clk_sys", "PD_SW"),
("nano_rom", "clk_sys", "PD_AON"), ("nano_dma", "clk_sys", "PD_SW"),
("nano_sbr", "clk_sys", "PD_SW"), ("nano_axi2apb", "clk_sys", "PD_SW"),
("nano_apb_dec", "clk_sys", "PD_SW"), ("nano_apb_cdc.sys", "clk_sys", "PD_SW"),
("nano_apb_cdc.per", "clk_per", "PD_SW"), ("nano_uart", "clk_per", "PD_AON"),
("nano_spi", "clk_per", "PD_AON"), ("nano_gpio", "clk_per", "PD_AON"),
("nano_intc", "clk_per", "PD_AON"), ("nano_rstpwr", "clk_per", "PD_AON"),
]
BRIDGES = [("axi_cdc_m.cpu", "axi_cdc_m.sys", "async FIFO"),
("nano_apb_cdc.sys", "nano_apb_cdc.per", "req/ack handshake")]
ALWAYS_ON = {"nano_rstpwr", "nano_intc", "nano_uart", "nano_spi", "nano_gpio",
"nano_sram0", "nano_rom"} # power domain list (PLL is a macro)
def audit(rows, bridges):
errors = []
names = [r[0] for r in rows]
if len(names) != len(set(names)):
errors.append("a block sits in two rows")
clock = {r[0]: r[1] for r in rows}
power = {r[0]: r[2] for r in rows}
for a, b, mech in bridges:
if a not in clock or b not in clock:
errors.append("bridge side missing: " + a + "/" + b)
elif clock[a] == clock[b]:
errors.append("bridge sides share a clock: " + a)
elif mech == "none":
errors.append("bridge without mechanism: " + a)
for blk in ALWAYS_ON:
if power.get(blk) != "PD_AON":
errors.append(blk + ": not PD_AON")
if blk != "nano_sram0" and blk != "nano_rom" and clock.get(blk) != "clk_per":
errors.append(blk + ": always-on control must be on clk_per")
return errors
assert len(ROWS) == 18
assert audit(ROWS, BRIDGES) == []
# The audit can fail: each deliberately wrong input is rejected.
assert audit(ROWS + [("nano_uart", "clk_sys", "PD_AON")], BRIDGES) != [] # two rows
assert audit([r for r in ROWS if r[0] != "nano_apb_cdc.per"], BRIDGES) != [] # lost side
assert audit(ROWS, [("axi_cdc_m.cpu", "axi_cdc_m.sys", "none")]) != [] # no mechanism
assert audit([("nano_gpio", "clk_sys", "PD_SW") if r[0] == "nano_gpio" else r
for r in ROWS], BRIDGES) != [] # moved GPIO
print("PASS: 18 block-sides placed once; 2 bridges have mechanisms; 4 bad tables rejected")The table above, current, with a version shared by the crossing inventory and the constraint modes.
Bridge halves listed as separate rows; no bridge drawn as living in one domain.
The audit output attached to the review, rerun after every hierarchy change.
Diff the table against the netlist clock pins with a connectivity checker: every flop's clock must trace to its row's domain, and any pin traced elsewhere is either a table bug or a wiring bug, both fixed in the table first. A crossing that appears in silicon but not in the inventory always traces to a row that moved without its inventory update.
Its interrupt path into nano_intc changes domains, its APB segment moves across the APB bridge boundary, and its wake role dies: clk_sys stops in sleep while clk_per runs. Faster sampling is bought with a lost wake source.