A driver starts a DMA job with a write of 0x1 to CTRL, meaning GO without the interrupt enable, and from then on every completion is silent: bit 1 of every CTRL write copies into IRQ_EN, so writing 0x1 clears it. The next job reuses a stale KEY because LEN and KEY writes while BUSY are silently ignored. And after one failed job, the interrupt never drops, because GO clears DONE but not ERR. Three driver bugs, zero hardware bugs, one missing contract. This lesson introduces the hardware-software contract and the errata process; the register-map detail lives with the memory-maps section. Write the contract first and all three disappear.
The hardware-software contract has four parts. The reset-value table: every register's reset value, and which bits read back differently from what was written, like CTRL bit 0 always reading 0. The access-type table: read-only, read-write, write-1-to-clear, write-only with side effects, and what happens on access widths the block does not support. The sequencing rules: wait for BUSY equals 0 before touching LEN or KEY, clear both DONE and ERR after every job, always start with 0x3 when interrupts are wanted, write one variable to both LEN registers, and clear the NS bit before the stream bridge touches secure memory. And the bring-up order: reset values first, then one polled job, then the interrupt path, then the error-injection cases, and only then performance. Each part is generated from the same map source as the RTL, so the contract cannot drift from the hardware it describes.
Driver rule on NanoSoC | Hardware truth behind it | Symptom of breaking it |
|---|---|---|
Start with CTRL 0x3, never 0x1 | Every CTRL write copies bit 1 into IRQ_EN | Silent completions; polling works, interrupts never fire |
Touch LEN and KEY only when BUSY is 0 | Writes while BUSY ignored with no error | Second job uses the stale KEY; data wrong, COUNT right |
Clear DONE and ERR after every job | GO clears DONE but never ERR | irq stuck high after a failed job followed by a good one |
Write one length to SBR LEN and DMA LEN | Two separate registers, one stream | ERR with COUNT 32, or SBR stuck BUSY forever |
Clear SBR CTRL.NS for secure buffers | NS resets to 1, non-secure | First beat SLVERR from secure SRAM; DMA stays BUSY |
The script below models the DMA register behaviour exactly as the NanoSoC specification specifies it, then demonstrates the three classic driver slips against the model: the 0x1 start that kills the interrupt, the KEY written while BUSY that never lands, and the sticky ERR that survives a good job. The passing driver at the end obeys the three DMA-side rules (the LEN-pair and NS rules belong to the stream bridge and are outside this model) and completes two jobs cleanly. Keep a model like this beside the driver: it runs in milliseconds and it encodes the contract better than prose.
class DMA: # behavioural model of the DMA register rules
def __init__(self):
self.busy = False
self.done = False
self.err = False
self.irq_en = False
self.len = 0
self.key = 0
self.count = 0
def write_len(self, v):
if not self.busy:
self.len = v & 0xFFFF # ignored while BUSY, no error
def write_key(self, v):
if not self.busy:
self.key = v # ignored while BUSY, no error
def write_ctrl(self, v):
self.irq_en = bool(v & 0x2) # copied on EVERY write
if (v & 0x1) and not self.busy:
if self.len == 0:
self.done = True
else:
self.busy, self.count, self.done = True, 0, False
# GO clears DONE; ERR is NOT cleared
def write_status(self, v): # write-1-to-clear bits 1 and 2
if v & 0x2:
self.done = False
if v & 0x4:
self.err = False
@property
def irq(self):
return self.irq_en and (self.done or self.err)
d = DMA()
d.write_len(64)
d.write_ctrl(0x1) # slip 1: GO without IRQ_EN
d.busy = False
d.done = True # job completes
assert d.irq is False # silent completion: interrupt never fires
d = DMA()
d.write_len(64)
d.write_ctrl(0x3)
d.write_key(0xDEAD) # slip 2: KEY while BUSY, silently ignored
assert d.key == 0
d = DMA()
d.err = True # a failed job left ERR set
d.write_len(64)
d.write_ctrl(0x3) # slip 3: GO clears DONE, not ERR
d.busy, d.done = False, True # good job completes
assert d.irq is True # DONE and the stale ERR both hold irq
d.write_status(0x2) # software clears DONE, as a naive driver does
assert d.irq is True # stuck: the stale ERR still holds irq high
d.write_status(0x4) # the rule: clear DONE and ERR after every job
assert d.irq is False
d = DMA() # a driver obeying the three DMA-side rules: two clean jobs
for _ in (1, 2):
d.write_len(64)
d.write_key(0xA5A5) # BUSY is 0, so both land
assert (d.len, d.key) == (64, 0xA5A5)
d.write_ctrl(0x3)
assert d.busy and d.irq is False
d.busy, d.done, d.count = False, True, 64 # stream completes
assert d.irq is True
d.write_status(0x6)
assert d.irq is False
print("PASS: 3 driver slips shown; DMA-side driver completes clean")Some hardware behaviour escapes into silicon, and then the contract grows an errata list instead of an RTL fix. Each entry carries five fields: the symptom with a reproducer, the root cause, the software workaround with its cost, the affected revisions, and the planned fix with its spin. A typical erratum candidate is behaviour nobody specified: an SBR that stays BUSY forever when the DMA LEN is shorter than the SBR LEN, say. The workaround is one length written to both registers plus a timeout on SBR BUSY, the cost is a poll in every job, and the entry closes when a spin lets the job end cleanly. The DMA's silently-ignored LEN write is different: it is specified behaviour, so it belongs in the contract and the driver rules, not on the errata list, unless the specification had promised an error response. Errata are reviewed like waivers: each has an owner, each is re-read at every release, and software never works around an erratum that is not written down, because an undocumented workaround is a bug waiting for the next driver author.
The contract's four parts generate from the same source as the RTL and headers, with matching revision stamps.
The bring-up order is a test plan, not advice: each stage gates the next, and error injection is explicit.
Every erratum names its workaround cost, so software can budget for hardware honesty.
When a driver misbehaves, replay its register sequence against the model before touching hardware: most slips show as a diverged flag in three steps. When hardware misbehaves against the model, the model is wrong and the handoff's register map is the next witness. The register-side detail, banks, access types and generation flow, is taught in full by the memory-maps section of this course.
Not on NanoSoC: STATUS reads have no side effects and the bits clear only by a write of 1. Look for a stray write of 1 to STATUS bit 1. On a chip with read-to-clear registers the answer would be yes, which is why the access-type table must say which registers are read-to-clear and the debug procedure must avoid reading them.