Two jobs fail with no error flag to blame. With SBR LEN at 32 and DMA LEN at 64 the DMA sets ERR at word 32 with COUNT at 32; reversed, the DMA finishes at 32 words while the SBR waits forever. And a second job silently reuses the old KEY, or irq stays high across a good job, or enabling the interrupt disables it. The DMA and SBR share no length register, ignore writes while BUSY, keep a sticky ERR, and rewrite IRQ_EN on every CTRL write. The driver must carry what the hardware does not.
Goal: an ordered driver sequence, DMA first and the SBR START last, plus the post-job check that proves both LENs agreed.
Inputs: DMA offsets 0x00 to 0x10, SBR SRC/DST/LEN/CTRL, the BUSY and W1C rules.
Steps: write the sequence; run both mismatch cases on the DMA model and confirm the driver check names them; run a naive driver and a careful driver against the three traps and confirm only the careful one is clean; file the sequence.
The sequence: wait for BUSY = 0 on both blocks; write KEY, then LEN, then CTRL = 0x3 (GO with IRQ_EN) to the DMA, so it is BUSY and ready to accept; then write SRC, DST and the same n_words to the SBR and set CTRL.START last; on the interrupt read STATUS, demand COUNT == SBR LEN, then clear both DONE and ERR with 0x6. That is seven writes in the order the boot self-test uses. If the SBR started first, its first word would meet a DMA whose s_tready is still low, and the stream would wait for a GO that software might never send. Case A of bug 8, SBR shorter, ends the job early with ERR set by the real early-tlast behaviour; case B, DMA shorter, leaves the SBR BUSY because the DMA stops accepting. Bug 9 has three traps: LEN and KEY writes while BUSY are silently ignored, GO clears DONE but not ERR so a stale ERR holds irq high, and any CTRL write copies bit 1 into IRQ_EN so 0x1 disables the interrupt. One variable, n_words, is written to both LENs, and the driver never touches LEN or KEY while BUSY.
MAP = [
("BOOTROM", 0x00000000, 0x4000),
("SRAM0", 0x20000000, 0x10000),
("SRAM1", 0x20010000, 0x10000),
("DMA", 0x40000000, 0x1000),
("SBR", 0x40001000, 0x1000),
("UART", 0x40002000, 0x1000),
("SPI", 0x40003000, 0x1000),
("GPIO", 0x40004000, 0x1000),
("INTC", 0x40005000, 0x1000),
("RSTPWR", 0x40006000, 0x1000),
]
SECURE_ONLY = {"SRAM1", "RSTPWR"}
SLOTS = ["DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR"]
T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
def end_of(base, size):
return base + size - 1
def overlaps(entries=MAP):
bad = []
xs = sorted(entries, key=lambda e: e[1])
for a, b in zip(xs, xs[1:]):
if b[1] < a[1] + a[2]:
bad.append((a[0], b[0]))
return bad
def misaligned(entries=MAP):
return [n for n, b, s in entries if b % s != 0]
def decode(addr, slots=SLOTS):
if addr >> 14 == 0:
return "BOOTROM"
if addr >> 16 == 0x2000:
return "SRAM0"
if addr >> 16 == 0x2001:
return "SRAM1"
if addr >> 16 == 0x4000:
slot = (addr >> 12) & 0xF
return slots[slot] if slot < 7 else "H3"
if addr < 0x20000000:
return "H1"
if addr < 0x40000000:
return "H2"
return "H4"
def response(addr, secure, slots=SLOTS):
t = decode(addr, slots)
if t in ("H1", "H2", "H4"):
return "DECERR"
if t == "H3":
return "SLVERR"
if t in SECURE_ONLY and not secure:
return "SLVERR"
return "OK"
def sustained(peak, beats):
return peak * beats / (beats + 1)
# ---- this lesson starts here ----
class Dma: # the register behaviour of the DMA, one rule per line
def __init__(self):
self.busy = self.done = self.err = self.irq_en = False
self.key = self.len = self.count = self.stalls = 0
def irq(self):
return self.irq_en and (self.done or self.err) # a level, held until software clears it
def write_key(self, v):
self.key = self.key if self.busy else v # ignored while BUSY, no PSLVERR
def write_len(self, v):
self.len = self.len if self.busy else v & 0xFFFF # ignored while BUSY
def ctrl_write(self, v):
self.irq_en = bool(v & 2) # every CTRL write copies bit 1 into IRQ_EN
if v & 1 and not self.busy: # GO while BUSY is ignored
if self.len == 0:
self.done = True
else:
self.busy, self.done, self.count = True, False, 0 # GO clears DONE but not ERR
def status_write(self, v):
self.done = self.done and not v & 2 # W1C
self.err = self.err and not v & 4
def accept(self, last): # one stream word offered
if not self.busy:
self.stalls += 1 # s_tready is low when not BUSY
return False
self.count += 1
if last and self.count < self.len:
self.err, self.busy = True, False # early tlast: ERR, never DONE
elif self.count == self.len:
self.done, self.busy = True, False
return True
def stream(dma, n): # the SBR read engine; True means it is stuck BUSY
return not all(dma.accept(i == n - 1) for i in range(n))
def job(n_dma, n_sbr, key=0x5A):
d = Dma()
d.write_key(key)
d.write_len(n_dma)
d.ctrl_write(0x3) # DMA first
return d, stream(d, n_sbr) # SBR START last
def driver_ok(count, sbr_len, dma_len):
return count == sbr_len == dma_len and dma_len > 0
ok, stuck = job(64, 64)
assert ok.done and not ok.err and not stuck and ok.stalls == 0 and driver_ok(ok.count, 64, 64)
a, a_stuck = job(64, 32) # case A: SBR LEN 32, DMA LEN 64
assert a.err and not a.done and a.count == 32 and not a_stuck and not driver_ok(a.count, 32, 64)
b, b_stuck = job(32, 64) # case B: SBR LEN 64, DMA LEN 32
assert b.done and not b.err and b.count == 32 and b_stuck and not driver_ok(b.count, 64, 32)
early = Dma()
assert early.accept(False) is False and early.stalls == 1 # a word offered before GO is refused
class Good:
def start(self, d): d.ctrl_write(0x3)
def rekey(self, d, key, finish):
if d.busy:
finish(d) # wait for BUSY = 0 first
d.write_key(key)
def clear(self, d): d.status_write(0x6) # DONE and ERR together
class Naive:
def start(self, d): d.ctrl_write(0x1)
def rekey(self, d, key, finish): d.write_key(key)
def clear(self, d): d.status_write(0x2)
def contract_problems(drv):
found = []
a = Dma(); a.write_key(0xAA); a.write_len(4); a.ctrl_write(0x3); a.accept(False) # a job is in flight
drv.rekey(a, 0x55, lambda x: [x.accept(i == 2) for i in range(3)])
if a.key != 0x55:
found.append("stale_key")
b = Dma(); b.write_len(8); b.ctrl_write(0x3); b.accept(True) # a failed job leaves ERR set
drv.clear(b)
if b.irq():
found.append("irq_stuck")
c = Dma(); c.write_len(1); drv.start(c)
if not c.irq_en:
found.append("irq_disabled")
return found
assert contract_problems(Good()) == []
assert contract_problems(Naive()) == ["stale_key", "irq_stuck", "irq_disabled"]
print("PASS: length mismatch named both ways; careful driver clean, naive driver trips all 3 traps")One PASS line. The two job calls with unequal lengths are the bug-8 demonstration, and driver_ok is the post-job check that refuses both. The naive driver is the bug-9 demonstration: it rewrites a key while BUSY, clears only DONE after a failed job, and starts with 0x1, and the scenario reports exactly the three symptoms.
A case-A failure means the early-tlast ERR behaviour was modelled as DONE: the real RTL sets ERR, never DONE, on an early last word. A case-B failure means the model let the SBR finish without the DMA accepting: s_tready is 0 when not BUSY, so the read engine stalls. A clean report from the naive driver means a trap was coded as a refusal that the RTL does not make: the writes are ignored silently, not rejected.
Hint 1: the two LEN registers are separate fields in separate blocks; nothing ties them. Hint 2: read the STATUS table row for what GO clears and what it does not. Hint 3: bit 1 of every CTRL write becomes the new IRQ_EN, including pure GO writes. Answer: one n_words variable to both LENs, wait BUSY = 0 before LEN or KEY, clear DONE and ERR together with 0x6, always start with 0x3, and check COUNT against SBR LEN after DONE.
The COUNT check: COUNT stays 0, which equals a zero SBR LEN, so the check passes vacuously and only an explicit non-zero-length rule keeps the job meaningful.