A workaround that works by accident fails at temperature, at a different clock ratio, or under the next compiler. The driver rules must be proven against a model of the actual hardware behaviour, including the races, or they are just another bug with a comment. This lesson proves the NanoSoC workaround rules against a cycle-level model of the real DMA status behaviour, then shows the hardware register pattern, set-wins write-1-to-clear, that removes one race class by construction. Its one job is trustworthy workarounds; turning them into next-chip rules is the last lesson.
The DMA status behaviour has three sharp edges: length and key writes are silently ignored while busy, a control write copies bit 1 into the interrupt enable every time so writing 0x1 to start silently disables the interrupt, and starting a job clears DONE but never ERR. The driver rules follow each edge: wait for busy clear before touching length or key, always start with 0x3 so the enable survives, and always clear both DONE and ERR with 0x6 after every job. The model below executes the real behaviour, including a stuck ERR from a failed job and a key write attempted while busy, and runs all eight combinations of the three rules: exactly the full rule set gives a clean result. Then the register pattern: a write-1-to-clear status bit with set-wins arbitration, so a status event arriving in the same cycle as the clear is not lost, which is the clear-versus-set race fixed in RTL for the next chip.
// Set-wins write-1-to-clear status bit. A hardware event in the same
// cycle as a software clear sets the bit: the event is never lost.
// Fix for the clear-versus-set race in new NanoSoC blocks.
module nano_w1c_status (
input logic clk,
input logic rst_n,
input logic hw_set, // event from the block, level or pulse
input logic sw_clear, // software writes 1 to clear this bit
output logic status // sticky status until cleared
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
status <= 1'b0;
else if (hw_set)
status <= 1'b1; // set wins over a simultaneous clear
else if (sw_clear)
status <= 1'b0;
end
endmodule# Model the real DMA status behaviour and prove the driver rules.
# Behaviour from the DMA register specification: GO clears DONE not ERR,
# a CTRL write copies bit 1 into IRQ_EN every time, LEN and KEY writes
# are ignored while BUSY. Eight rule combinations are run.
from itertools import product
def run(wait_busy, start3, clear_both):
s = {"busy": False, "done": False, "err": False, "irq_en": False,
"len": 0, "key": 0}
def write_ctrl(v):
s["irq_en"] = bool(v & 2) # copied on every write
if (v & 1) and not s["busy"]:
s["busy"] = True
s["done"] = False # ERR deliberately kept
def write_key(v):
if not s["busy"]:
s["key"] = v
def write_len(v):
if not s["busy"]:
s["len"] = v
def finish(ok):
s["busy"] = False
s["done" if ok else "err"] = True
def clear(v):
if v & 2:
s["done"] = False
if v & 4:
s["err"] = False
def irq():
return s["irq_en"] and (s["done"] or s["err"])
start = 0x3 if start3 else 0x1
clr = 0x6 if clear_both else 0x2
write_len(4)
write_key(0x11)
write_ctrl(start) # job 1 starts
assert not s["done"] # a second GO while busy is ignored
write_ctrl(start)
assert s["busy"]
if wait_busy:
finish(False) # job 1 fails: ERR
write_key(0x22) # key written once idle
else:
write_key(0x22) # ignored: still busy
finish(False)
clear(clr)
write_ctrl(start) # job 2 uses the new key
finish(True)
seen = irq() # was the interrupt delivered?
clear(clr)
return {"irq_seen": seen, "irq_after_clear": irq(),
"key_ok": s["key"] == 0x22}
def good(r):
return r["irq_seen"] and not r["irq_after_clear"] and r["key_ok"]
passing = [c for c in product([True, False], repeat=3) if good(run(*c))]
assert passing == [(True, True, True)], passing
assert run(True, False, True)["irq_seen"] is False # 0x1: enable lost
assert run(True, True, False)["irq_after_clear"] is True # DONE alone: stuck
assert run(False, True, True)["key_ok"] is False # key write while busy
print("PASS: of 8 rule combinations only the full rule set gives a clean job")Every erratum workaround names the hardware behaviour it relies on, with the register-specification reference, not just the code.
The model or bench proof covers the race sequences: error then success, clear against event, start while busy.
New blocks use set-wins status registers, so the workaround class shrinks with each chip.
The script already runs the model with the rules deliberately broken, one rule at a time: starting with 0x1 loses the interrupt, clearing DONE alone sticks it, writing key or length while busy is ignored. Each broken run must fail; a model that passes with broken rules proves nothing. On silicon, the same sequences run as driver tests with the interrupt line observed, bounded by the 135.4 to 182.3 ns latency so a stuck line is distinguished from a slow one.
Read the DMA STATUS. If DONE or ERR is still set, the clear did not land: check for a clear written to the wrong offset (an unmapped offset answers PSLVERR) or a clear issued before BUSY dropped. If STATUS is clear, read the interrupt controller PENDING and RAW bits for the other sources (irq_sbr on input 2, irq_uart on 3, irq_spi on 4, irq_gpio on 5): the stuck line is a different source.