Once in a few thousand DMA jobs the interrupt never arrives, and INTC RAW flickers for one cycle. Every simulation passes. The irq_dma line at 96 MHz feeds a flop in the 24 MHz controller with no synchroniser, so setup or hold fails just often enough to ruin a week. Structural crossing inventory, not simulation, is the catcher.
Goal: the clock derivation plus a ten-row crossing table where every row names its mechanism.
Inputs: 24 MHz crystal, R=1, N=32, dividers /4 and /8, the crossing list.
Steps: derive the three frequencies; file each crossing with its width and mechanism; remove one synchroniser, then give a 32-bit bus a 2-flop synchroniser, and confirm the audit flags each; restore.
Crossing | From to to | Mechanism |
|---|---|---|
X1/X2 | clk_cpu to/from clk_sys | Async AXI bridge, 4-deep FIFO per channel, gray pointers |
X3/X4 | clk_sys to/from clk_per | APB req/ack handshake, data held stable |
X5 | clk_sys to clk_per | 2-flop level sync on irq_dma, irq_sbr |
X6 | clk_per to clk_cpu | 2-flop level sync on irq_cpu |
X7/X8 | clk_per to clk_sys/cpu | Reset sync release per domain; pll_lock via 2 flops |
X9 | pads to clk_per | 2-flop sync per external input |
X10 | clk_per to clk_sys | 2-flop sync on pwr_up_done, pgood |
MAP = [
("BOOTROM", 0x00000000, 0x4000),
("SRAM0", 0x20000000, 0x10000),
("SRAM1", 0x20010000, 0x10000),
("DMA", 0x40000000, 0x1000),
("SBR", 0x40001000, 0x1000),
("UART", 0x40002000, 0x1000),
("SPI", 0x40003000, 0x1000),
("GPIO", 0x40004000, 0x1000),
("INTC", 0x40005000, 0x1000),
("RSTPWR", 0x40006000, 0x1000),
]
SECURE_ONLY = {"SRAM1", "RSTPWR"}
SLOTS = ["DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR"]
T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
def end_of(base, size):
return base + size - 1
def overlaps(entries=MAP):
bad = []
xs = sorted(entries, key=lambda e: e[1])
for a, b in zip(xs, xs[1:]):
if b[1] < a[1] + a[2]:
bad.append((a[0], b[0]))
return bad
def misaligned(entries=MAP):
return [n for n, b, s in entries if b % s != 0]
def decode(addr, slots=SLOTS):
if addr >> 14 == 0:
return "BOOTROM"
if addr >> 16 == 0x2000:
return "SRAM0"
if addr >> 16 == 0x2001:
return "SRAM1"
if addr >> 16 == 0x4000:
slot = (addr >> 12) & 0xF
return slots[slot] if slot < 7 else "H3"
if addr < 0x20000000:
return "H1"
if addr < 0x40000000:
return "H2"
return "H4"
def response(addr, secure, slots=SLOTS):
t = decode(addr, slots)
if t in ("H1", "H2", "H4"):
return "DECERR"
if t == "H3":
return "SLVERR"
if t in SECURE_ONLY and not secure:
return "SLVERR"
return "OK"
def sustained(peak, beats):
return peak * beats / (beats + 1)
# ---- this lesson starts here ----
def chain(xtal=24, r=1, n=32, d_cpu=4, d_sys=8):
vco = xtal * n / r
assert 600 <= vco <= 1200 # the teaching VCO range
return vco, vco / d_cpu, vco / d_sys, xtal
assert chain() == (768.0, 192.0, 96.0, 24) and chain(n=30) != chain()
assert abs(T_PER - 41.6667) < 0.001 and abs(256 * T_PER / 1000 - 10.667) < 0.01
# id, from, to, signal, width in bits, mechanism, flops on the receiving side
XING = [("X1", "clk_cpu", "clk_sys", "axi_req", 32, "fifo", 2), ("X2", "clk_sys", "clk_cpu", "axi_resp", 32, "fifo", 2),
("X3", "clk_sys", "clk_per", "apb_req", 32, "reqack", 2), ("X4", "clk_per", "clk_sys", "apb_resp", 32, "reqack", 2),
("X5", "clk_sys", "clk_per", "irq_dma", 1, "2ff", 2), ("X6", "clk_per", "clk_cpu", "irq_cpu", 1, "2ff", 2),
("X7", "clk_per", "clk_sys", "rst_sys_n", 1, "rstsync", 2), ("X8", "clk_per", "clk_cpu", "rst_cpu_n", 1, "rstsync", 2),
("X9", "pad", "clk_per", "gpio_in", 1, "2ff", 2), ("X10", "clk_per", "clk_sys", "pgood", 1, "2ff", 2)]
def audit(rows):
bad = []
for x, src, dst, sig, width, mech, flops in rows:
if sig.startswith("rst"):
good = mech == "rstsync" and flops >= 2
elif width == 1:
good = mech == "2ff" and flops >= 2 # one bit: two flops, no fewer
else:
good = mech in ("fifo", "reqack") # many bits: a FIFO or a handshake, never a synchroniser per bit
if not good:
bad.append(x)
return bad
assert len(XING) == 10 and audit(XING) == []
def with_row(i, row):
return XING[:i] + [row] + XING[i + 1:]
assert audit(with_row(4, ("X5", "clk_sys", "clk_per", "irq_dma", 1, "direct", 0))) == ["X5"] # bug 5
assert audit(with_row(2, ("X3", "clk_sys", "clk_per", "apb_req", 32, "2ff", 2))) == ["X3"] # a bus with a bit synchroniser
assert audit(with_row(5, ("X6", "clk_per", "clk_cpu", "irq_cpu", 1, "2ff", 1))) == ["X6"] # one flop is not a synchroniser
print("PASS: 768/192/96/24 MHz chain; 10 crossings audited by width; bare X5, bus on 2ff and 1-flop X6 all flagged")One PASS line. The audit of the clean list is the deliverable; the three modified rows are the failing-state demonstrations. The bare irq_dma row is bug 5 exactly; the 32-bit bus and the single flop show the audit judges the mechanism against the width and depth, not against a vocabulary word.
A chain failure means R or N was misapplied: VCO is 24 times 32 over 1, and clk_per bypasses the PLL entirely. A count failure means a crossing was merged or split: X1/X2 and X3/X4 are pairs but count as four rows. An audit hit on clean data means a row has the wrong mechanism for its width: one bit needs two flops, a bus needs a FIFO or a handshake, and a reset needs a reset synchroniser.
Hint 1: single-bit levels need two flops, multi-bit needs a FIFO or a handshake, resets need per-domain sync release. Hint 2: the failing path is a level held long, so a pulse synchroniser is the wrong fix. Hint 3: the flop in clk_per samples a signal launched by an unrelated clock, so some edges violate setup or hold; the second flop gives the first a full period to settle. Answer: add nano_sync_2ff on every INTC input, require inputs held at least 2 T_per, waive nothing in CDC lint.
SLEEP with the PLL off would stop the reset controller, the wake logic and the slow APB segment, so the chip could never wake.