The sweep proves the expected answers; this lesson hunts the unexpected ones. An undocumented alias means two addresses own the same storage, a hole that answers means a responder exists where the map says none, and a select that reaches no slave hangs the first wild pointer that finds it. Each is invisible to well-behaved software and fatal to debugging, so each gets a deliberate, controlled hunt in a test environment where probing is safe.
Alias detection uses write-read asymmetry: write a distinctive pattern through the canonical address, then read through every candidate alias and compare. The register must be one a write-then-read experiment can use, so the DMA STATUS register is out (BUSY is read-only and DONE and ERR are write-1-to-clear: a written pattern would clear bits and read back something else). KEY is the right choice: 32 bits, plain read-write, writes ignored only while BUSY, so written while the DMA is idle the pattern 0xA5A5_5A5A reads back unchanged through the canonical address, and through any alias. On NanoSoC the expected result is known: the DMA's sixteen images all return the canonical value, because the slot decoder passes twelve offset bits and the block consumes eight. The check proves the count is exactly sixteen, no more and no fewer; a seventeenth answering address would mean a second, undocumented alias, and fewer would mean the documented one changed. The script below is a model of the silicon, a prediction written from the specification and not a measurement: its value is that the same experiment, run unchanged against the real chip, is compared with it. The model keeps real state (a write must land somewhere to be read back), and a variant with a fully decoded slot shows the check failing as it should.
class Silicon:
"""Model of the APB slots 0 to 6 plus holes. dma_mask: address bits the DMA decodes."""
def __init__(self, dma_mask):
self.dma_mask = dma_mask
self.dma_key = 0 # DMA KEY, reset 0
self.sbr_ctrl = 0x4 # SBR CTRL, reset 0x0000_0004
def _dma_off(self, addr):
return addr & self.dma_mask
def write(self, addr, value):
if 0x4000_0000 <= addr < 0x4000_1000 and self._dma_off(addr) == 0x0C:
self.dma_key = value # KEY: plain read-write while the DMA is idle
def read(self, addr):
if 0x4000_0000 <= addr < 0x4000_1000: # DMA slot, all images
off = self._dma_off(addr)
if off == 0x0C:
return ("DATA", self.dma_key)
return ("DATA", 0) if off in (0x00, 0x04, 0x08, 0x10) else ("SLVERR", None)
if addr == 0x4000_100C:
return ("DATA", self.sbr_ctrl)
if 0x4000_7000 <= addr < 0x4001_0000:
return ("SLVERR", None) # H3: APB default inside the window
return ("DECERR", None) # H1, H2, H4 (the other slots are not modelled)
CANON = 0x4000_000C # DMA KEY through the canonical first image
PATTERN = 0xA5A5_5A5A
def alias_images(chip):
chip.write(CANON, PATTERN) # the DMA is idle, so the write lands
return [i for i in range(16) if chip.read(0x4000_0000 + i * 0x100 + 0x0C) == ("DATA", PATTERN)]
chip = Silicon(dma_mask=0xFF)
assert alias_images(chip) == list(range(16)) # exactly the 16 documented images
assert chip.read(0x4000_100C) == ("DATA", 0x4) # SBR CTRL keeps its own value
for hole in (0x0000_4000, 0x2002_0000, 0x8000_0000):
assert chip.read(hole)[0] == "DECERR"
assert chip.read(0x4000_0014)[0] == "SLVERR" # unmapped offset inside image 0
full = Silicon(dma_mask=0xFFF) # a revision that wires all 12 offset bits
assert alias_images(full) == [0] # only the canonical image answers: count 1, not 16
print("PASS: exactly 16 alias images through KEY; a fully decoded slot is caught")Connectivity is the structural twin of probing: instead of asking the silicon, ask the netlist whether the map and the wiring agree. Every decoder select must fan into exactly one slave or responder, every slave input bit must be driven by the address bit the specification assigns (the DMA's paddr bits 7 down to 0 from apb_paddr 7 down to 0, the fact whose violation is planted bug 4), and every responder must exist for every decoder that can produce an unmapped result. Probing and connectivity check the same contract from opposite ends; running both means a failure is not just detected but pre-localised, since the two disagreeing views name the level between them.
Probing is a test-mode activity with rules, because the same accesses in production are how reserved space turns into field failures. Probe from a test master or under a debugger with interrupts masked, one access at a time with a timeout so a hang is data rather than a stuck bench, and never from a driver. Record every unexpected answer as a defect against the map or the wiring, not as a discovery to program against. And keep the probe list generated from the map model, so a newly added region automatically acquires boundary probes instead of waiting for somebody to remember them.
The alias list is proven exact: the documented images answer, their neighbours do not, and the count is asserted, not eyeballed.
Connectivity output is reviewed with the map table side by side; every mismatch is resolved in the wiring or the table, with the table winning on addresses.
Production software contains no probes; the probe suite lives in the test environment with timeouts on every access.
Either a second block is decoding part of the slot (a select drawn wider than its slot, the overlap family), or the slot decoder passes more offset bits than the specification says and the neighbour slot mirrors. The neighbour read at the seventeenth address names the intruding block.