On NanoSoC the dangerous pattern is concrete: firmware writes 0xFFFF to the GPIO data register with every pad set as an output, all 16 pads change on one clock edge, and the shared I/O supply bounces. If the bounce crosses an input threshold, a quiet input such as the UART receive pad misreads a bit, a framing error that appears on the board only and never in simulation. Estimation before tapeout is the only cheap place to catch it.
Switching noise follows one mechanism: a changing current through the inductance of the supply path makes a voltage, proportional to the number of outputs switching together, their drive current and the edge rate. No lesson can hand you the inductance or the allowance as standard numbers, since both belong to a specific package and cell library, so the integrator treats the result as an input: the package data gives the number of outputs that may switch together per I/O supply pair, and the check compares the worst pattern with it. For NanoSoC that input is an illustrative limit of 8 outputs per VDD_IO and VSS_IO pair at the default drive (4 mA, medium slew), halving to 4 per pair at a stronger, fast drive. NanoSoC has two pairs, so the capacity is 2 x 8 = 16.
The check is per supply pair, not per chip. Only the 16 GPIO pads can ever be outputs that matter: RESET_N, UART receive and SPI MISO are inputs, and the crystal pads are analog. The worst case is one APB write of 0xFFFF to the data register with the direction register at 0xFFFF, which flips all 16 pads on one clock edge; the peripheral functions use at most four outputs, a subset. Each GPIO pad is served by the nearest I/O pair along the ring: pair 0, on the east side, serves P0 to P7, and pair 1, on the south side, serves P8 to P15, with the farthest pad 5.5 slots (440 µm) from its pair. That gives 8 and 8 against a limit of 8: the check passes with zero margin. Any new output pad, or a fast drive, makes it fail, and the fix is a third pair.
The fixes, in order of preference when a pattern fails: stagger the switching in software, lower the drive, regroup outputs onto the quieter pair, and only then add a supply pair. Staggering and lower drive cost nothing in silicon; a new pair costs two pads, and on NanoSoC two of the 9 spare slots.
The script below builds the groups from the ring itself, so the groups follow the placement and not a hand-typed list. It checks the whole-port write at the default drive (8 and 8, pass), at the fast drive (limit 4, fail), and the same fast-drive write staggered into two phases (pass again). It then checks two layouts that must fail: both I/O pairs placed side by side, and a seventeenth output pad.
RING = ["VDD_CORE0", "VSS_CORE0", "XTAL_IN", "XTAL_OUT", "VSS_CORE1", "VDD_CORE1", "RESET_N", "GPIO0",
"GPIO1", "GPIO2", "GPIO3", "VDD_IO0", "VSS_IO0", "GPIO4", "GPIO5", "GPIO6",
"GPIO7", "VSS_CORE2", "VDD_CORE2", "GPIO8", "GPIO9", "GPIO10", "VDD_IO1", "VSS_IO1",
"GPIO11", "GPIO12", "GPIO13", "GPIO14", "GPIO15", "VSS_CORE3", "VDD_CORE3"]
LIMIT_DEFAULT, LIMIT_FAST = 8, 4 # illustrative inputs per VDD_IO/VSS_IO pair
def groups(ring):
n = len(ring)
centres = []
for k in ("0", "1"):
a, b = ring.index("VDD_IO" + k), ring.index("VSS_IO" + k)
centres.append((a + b) / 2)
out = {0: [], 1: []}
for i, p in enumerate(ring):
if p.startswith("GPIO"):
d = [min(abs(i - c), n - abs(i - c)) for c in centres]
out[d.index(min(d))].append(p)
return out
def worst(groups_, patterns, limit):
for pair, pads in groups_.items():
for firing in patterns:
assert sum(1 for p in pads if p in firing) <= limit, (pair, limit)
G = groups(RING)
assert sorted(len(v) for v in G.values()) == [8, 8], G
whole = [set(G[0]) | set(G[1])]
worst(G, whole, LIMIT_DEFAULT) # passes with zero margin
try:
worst(G, whole, LIMIT_FAST)
raise SystemExit("fast drive should fail")
except AssertionError:
pass
phases = [set(G[0][:4]) | set(G[1][:4]), set(G[0][4:]) | set(G[1][4:])]
worst(G, phases, LIMIT_FAST) # staggered: 4 per pair per phase
bad = list(RING) # P2: both I/O pairs side by side
bad[13], bad[14], bad[22], bad[23] = "VDD_IO1", "VSS_IO1", "GPIO4", "GPIO5"
GB = groups(bad)
assert sorted(len(v) for v in GB.values()) != [8, 8], GB
try:
worst(GB, [set(GB[0]) | set(GB[1])], LIMIT_DEFAULT)
raise SystemExit("a lopsided layout should fail")
except AssertionError:
pass
G[0].append("GPIO16") # a 17th output pad
try:
worst(G, [set(G[0]) | set(G[1])], LIMIT_DEFAULT)
raise SystemExit("a 17th output should fail")
except AssertionError:
pass
print("PASS: 8 and 8 at the default drive (zero margin); fast drive fails; staggered passes; lopsided pairs and a 17th output fail")The SSO limit quoted as a package input with its source, never as a textbook constant, and the drive it assumes.
The worst switching pattern per pair identified in words: which firmware write, which pads.
A failing pattern paired with its fix: stagger, lower drive, regroup, or add a pair, in that order.
The margin stated: here exactly zero, so the next output pad or drive change reopens the question.
Extract the real patterns from firmware and the test plan: GPIO port writes and the SPI and UART functions are the usual worst cases. On the bench, an SSO suspect shows as data-dependent errors on a quiet neighbour: repeat the pattern with the aggressors held still, then with them switching, and the difference is the noise. For NanoSoC the symptom is UART framing errors that appear only when the GPIO port is written while a frame is arriving.
Because the noise is local: the bounce appears on the supply path the switching current actually flows through. Sixteen outputs spread eight and eight over two pairs disturb each pair far less than sixteen on one pair, so only the per-pair worst count predicts the worst bounce.