A ballout drawn by convenience, with signals in netlist order and supplies wherever they fit, gives the board an unroutable array and the supplies long detours. The NanoSoC ballout follows four rules in strict order, and this lesson applies them to all 31 pads of the 6 x 6 array: supplies first for short paths, fast signals where they escape cleanly, quiet pins guarded, and everything else in the space left over. Its job is the assignment; proving the board can route it is the next lesson.
Rule one: supplies take the best balls. The four core supply pairs are spread one per quadrant of the array, each VDD ball beside its own VSS ball, on the ring of balls just inside the outer row where the logic they feed is closest. The two I/O pairs sit beside the outputs they feed. Rule two: fast signals take the outermost row or column, where they escape without vias, and within two balls of their I/O supply pair: the four SPI signals leave on the south and east edges beside pair 0. Rule three: quiet pins get quiet neighbours: the crystal balls sit in the north-west corner with one core ground and one I/O ground ball as guards and no fast signal near. Rule four: slow control fills what remains, with the sixteen GPIO groups matched to their I/O pairs. The array has 36 balls, 31 are assigned and 5 stay spare: the central 2 x 2 block, which a board could not escape anyway, and one edge ball.
Row \ column | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
A | XTAL_IN | XTAL_OUT | RESET_N | P8 | P9 | P10 |
B | P15 | VSS_CORE0 | VSS_IO1 | VDD_IO1 | VSS_CORE1 | P11 |
C | P13 | VDD_CORE0 | spare | spare | VDD_CORE1 | P12 |
D | P14 | VDD_CORE2 | spare | spare | VDD_CORE3 | P7 |
E | spare | VSS_CORE2 | VSS_IO0 | VDD_IO0 | VSS_CORE3 | P5 (CS_N) |
F | P0 (TX) | P1 (RX) | P2 (SCK) | P3 (MOSI) | P4 (MISO) | P6 |
The script below holds that array as data and checks it through one function: all 31 pads placed once and exactly 5 spare; one core pair in each quadrant with its VSS beside its VDD; each I/O pair adjacent; SPI pads on the outer ring and within 2 balls of their pair; every other output within 4; and the crystal with a core ground and an I/O ground beside it and no SPI pad within 3 balls. It then moves the SPI clock into the centre, and swaps the crystal guards for GPIO, and requires both to be rejected.
GRID = [
["XTAL_IN", "XTAL_OUT", "RESET_N", "GPIO8", "GPIO9", "GPIO10"],
["GPIO15", "VSS_CORE0", "VSS_IO1", "VDD_IO1", "VSS_CORE1", "GPIO11"],
["GPIO13", "VDD_CORE0", "NC", "NC", "VDD_CORE1", "GPIO12"],
["GPIO14", "VDD_CORE2", "NC", "NC", "VDD_CORE3", "GPIO7"],
["NC", "VSS_CORE2", "VSS_IO0", "VDD_IO0", "VSS_CORE3", "GPIO5"],
["GPIO0", "GPIO1", "GPIO2", "GPIO3", "GPIO4", "GPIO6"],
]
SPI = ["GPIO2", "GPIO3", "GPIO4", "GPIO5"] # fast signals: SCK, MOSI, MISO, CS_N
def where(grid):
return {n: (r, c) for r, row in enumerate(grid) for c, n in enumerate(row) if n != "NC"}
def dist(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def check(grid):
pos = where(grid)
nc = sum(row.count("NC") for row in grid)
assert len(pos) == 31 and nc == 5, (len(pos), nc)
quadrants = set()
for k in range(4):
v, g = pos["VDD_CORE%d" % k], pos["VSS_CORE%d" % k]
assert dist(v, g) == 1, "core pair %d is split" % k
quadrants.add((v[0] >= 3, v[1] >= 3))
assert len(quadrants) == 4, "core pairs are not one per quadrant"
for k in range(2):
assert dist(pos["VDD_IO%d" % k], pos["VSS_IO%d" % k]) == 1, "I/O pair split"
for i in range(16):
pad = "GPIO%d" % i
pair = 0 if i < 8 else 1
d = min(dist(pos[pad], pos["VDD_IO%d" % pair]), dist(pos[pad], pos["VSS_IO%d" % pair]))
assert d <= (2 if pad in SPI else 4), (pad, "too far from its I/O pair", d)
for pad in SPI:
r, c = pos[pad]
assert min(r, c, 5 - r, 5 - c) == 0, pad + " is not on the outer ring"
xo = pos["XTAL_OUT"]
near = [n for n, p in pos.items() if max(abs(p[0] - xo[0]), abs(p[1] - xo[1])) == 1]
assert any(n.startswith("VSS_CORE") for n in near) and any(n.startswith("VSS_IO") for n in near), "crystal has no guards"
for pad in SPI:
assert dist(pos[pad], pos["XTAL_IN"]) >= 3 and dist(pos[pad], xo) >= 3
check(GRID)
moved = [row[:] for row in GRID]
moved[5][2], moved[2][2] = "NC", "GPIO2" # SCK moved to the centre of the array
swapped = [row[:] for row in GRID]
swapped[1][1], swapped[1][0] = "GPIO15", "VSS_CORE0" # core ground moved away from the crystal
for bad in (moved, swapped):
try:
check(bad)
raise SystemExit("a bad assignment was accepted")
except AssertionError:
pass
print("PASS: 31 balls placed once, 5 spare; core pair per quadrant; SPI on the outer ring near its pair; crystal guarded; two bad ballouts rejected")Supplies placed before signals, with each pair's feed named.
Fast signals on the outer ring, quoted by coordinate, not by intent.
Quiet pins with named guards, and no fast neighbour within the stated distance.
The spare balls listed, so a later change knows what is free.
Diff the ballout against the pin database and the board netlist together: a ball the board team moved without telling the chip team is the classic divergence, and the diff catches it in seconds. If a late change moves a fast signal inward, the escape lesson re-prices the vias before the move is accepted.
Because the swap moves a fast signal away from the outer ring or from its supply pair: the board saves one trace while the chip spends escape vias and the noise budget the pairing lesson closed. Ball moves are priced on both sides before acceptance.