This lesson has one job: teach the register sequence that moves 64 words without surprises. The DMA register block looks ordinary, five offsets in a 256-byte window, but its contract has four quirks that read as bugs until they are taught as rules: writes ignored while busy, a sticky error the start command does not clear, a control write that silently changes the interrupt enable, and a go-while-busy that fails without any error. Every one of them is quoted from the DMA's own RTL, and the second job a driver ever runs trips at least one of them.
CTRL at 0x00 holds GO in bit 0 and IRQ_EN in bit 1. The write copies bit 1 into IRQ_EN every time, so starting with 0x1 clears the interrupt enable as a side effect; the correct start value with interrupts is 0x3. GO acts only when BUSY is 0, and a GO while busy is ignored with no error, not queued and not faulted. STATUS at 0x04 holds BUSY read-only, DONE and ERR as write-1-to-clear: writing 1 clears, writing 0 does nothing, and BUSY ignores writes. LEN at 0x08 and KEY at 0x0C are read-write but silently ignore writes while BUSY, again with no error response. COUNT at 0x10 counts accepted input words and is cleared by GO. A zero-length GO sets DONE at once with BUSY never rising; a normal GO with nonzero LEN raises BUSY, zeroes COUNT and clears DONE, but leaves ERR exactly as it was.
Step | Access | Value | Why |
|---|---|---|---|
1 | Write KEY (0x0C) | any 32-bit key | Must land while idle; busy writes are ignored. |
2 | Write LEN (0x08) | 64 | Word count; same value goes to the bridge. |
3 | Write CTRL (0x00) | 0x3 | GO with IRQ_EN kept on; 0x1 would mute the interrupt. |
4 | Poll STATUS (0x04) | BUSY bit | Or wait for the irq; the next lesson prices the wait. |
5 | Write STATUS (0x04) | 0x6 | Clear both DONE and ERR; clearing DONE alone leaves a stale ERR holding irq. |
The model below implements the four quirks exactly as the RTL behaves, then runs the canonical 64-word job plus one probe per quirk: a busy LEN write that must not stick, a redundant GO that must not fault, a 0x1 start that mutes the interrupt, and a stale ERR that survives GO and holds irq until cleared. A last probe covers the same-edge race: a W1C of ERR landing on the edge where a new error is set loses, because the hardware set is the later assignment in the clocked block (set-wins). The UVM bench clocks the same RTL at 100 MHz; inside NanoSoC it runs at clk_sys = 96 MHz.
class DMA:
def __init__(self):
self.irq_en = False
self.busy = False
self.done = False
self.err = False
self.len = 0
self.key = 0
self.count = 0
@property
def irq(self):
return self.irq_en and (self.done or self.err)
def write(self, addr, data):
if addr == 0x00: # CTRL: bit 1 copies into IRQ_EN every write
self.irq_en = bool(data & 0x2)
if (data & 0x1) and not self.busy: # GO acts only when idle
self.busy = self.len != 0
self.count = 0
self.done = (self.len == 0)
elif addr == 0x04: # STATUS: W1C on bits 1 and 2
if data & 0x2:
self.done = False
if data & 0x4:
self.err = False
elif addr == 0x08 and not self.busy: # LEN ignored while busy
self.len = data & 0xFFFF
elif addr == 0x0C and not self.busy: # KEY ignored while busy
self.key = data & 0xFFFFFFFF
def finish(self, early_tlast=False, clear=0): # LEN-th beat, or early tlast
# a STATUS write on the same edge is applied first; the hardware set
# comes later in the same clocked block, so set wins
if clear & 0x2:
self.done = False
if clear & 0x4:
self.err = False
if early_tlast:
self.err, self.busy = True, False
else:
self.count, self.done, self.busy = self.len, True, False
d = DMA()
d.write(0x0C, 0xA5A5A5A5)
d.write(0x08, 64)
d.write(0x00, 0x3) # GO with IRQ_EN on
assert (d.busy, d.count, d.done) == (True, 0, False)
d.write(0x08, 32) # quirk 1: busy LEN write ignored, no error
assert d.len == 64
d.write(0x00, 0x3) # quirk 2: GO while busy ignored, no error
assert d.busy and d.count == 0
d.finish()
assert d.done and d.irq # irq = IRQ_EN & (DONE | ERR)
d.write(0x04, 0x2) # clear DONE only
assert not d.irq
e = DMA()
e.write(0x08, 64)
e.write(0x00, 0x1) # quirk 3: CTRL write copies IRQ_EN -> muted
e.finish()
assert e.done and not e.irq
f = DMA()
f.write(0x08, 64)
f.write(0x00, 0x3)
f.finish(early_tlast=True) # quirk 4: ERR set, GO never clears it
f.write(0x00, 0x3)
assert f.err and f.irq # stale ERR holds irq across the new job
f.write(0x04, 0x6)
assert not f.irq
# set wins: a new early-tlast error on the same edge as a W1C of ERR survives
g = DMA()
g.write(0x08, 64)
g.write(0x00, 0x3)
g.finish(early_tlast=True)
g.write(0x00, 0x3) # second job, stale ERR still set
g.finish(early_tlast=True, clear=0x4)
assert g.err and g.irq # the clear lost to the new event
print("PASS: 64-word job, all four contract quirks and set-wins behave")The driver writes one length variable to both LEN registers, always writes 0x3 to start with interrupts, and always clears STATUS with 0x6.
No driver writes LEN or KEY without first reading BUSY as 0; the rule is in the coding standard because the hardware gives no error to catch violations.
The APB address wiring passes paddr bits 7 down to 0 as byte offsets, because the DMA decodes byte offsets 0x04 to 0x10, not word indices.
Read back the reset values first: five reads that must all return zero, which is the first step of the boot ROM's DMA self-test. Then run one job of length 1 and confirm COUNT reads 1 with DONE set: the shortest job exercises the whole contract. If STATUS, LEN and KEY reads return PSLVERR, a COUNT read returns the STATUS value with no error, and only CTRL works, the integrator wired word addresses to a block that decodes byte offsets, the width-and-addressing bug: reconnect paddr bits 7 down to 0 and check every other narrow block the same way.
Whatever KEY held before, usually the reset value 0: the KEY write landed while BUSY was already 1 and was silently ignored. Program KEY before GO, never after.