This lesson has one job: teach the failure signatures so a lab symptom maps to a cause in minutes. A lost interrupt looks like a hang with healthy status bits. A doubled interrupt looks like corrupt counts. A storm looks like a system that does nothing but service one device. A spurious interrupt makes a handler run and find nothing to do. Each has a distinct mechanism, a distinct test that catches it, and a distinct fix; guessing between them wastes the bring-up week this section is trying to save.
A lost interrupt means the level fell before the controller acted on it, or the event never reached the status bit. Two mechanisms dominate. The first is the missed pulse across a clock domain: the source asserted for less than the synchroniser can see, so RAW never sets while the source believes it signalled. The second is the clear-versus-set race: software writes 1 to clear a status bit in the same cycle the hardware sets it for a new event, and if the clear wins, the new event vanishes with no trace. The fix is arbitration in the hardware: set-wins. The real DMA is set-wins, because a later done <= 1 overrides the W1C done <= 0 in the same clocked block. A third cause belongs to software: a read-modify-write on a write-1-to-clear register writes back a 1 for every bit that read as 1, including bits the handler never looked at, so an event that arrived between the read and the write is cleared silently. Write only the bits you mean to clear (for the DMA, 0x6 clears DONE and ERR and nothing else).
A doubled interrupt is the mirror image: the handler cleared the status the controller sees but not the cause, or the source re-asserts, so the level never dropped and the handler re-entered on exit. A storm is a level that never drops because the clear never took effect. On NanoSoC an APB write is not posted from software's point of view (each access completes before the next starts, as the bring-up lesson shows), so the storm comes from clearing the wrong bit (the DMA's ERR is sticky and a job start does not clear it, so 0x2 alone leaves irq high), from IRQ_EN being re-written, or from a source that really does re-assert. On a different bridge that does post writes, the clear can still be in flight when the handler returns, and a read-back after the clear flushes it. Distinguishing storm from chatter matters: a storm re-enters immediately with no new event, at handler-return speed; a chatty device re-enters at event speed with fresh status each time. Count the events per entry and the two are unmistakable.
A spurious interrupt is an entry with nothing to service. The main hardware cause is propagation: after the source is cleared or masked, irq_cpu stays high for roughly 135 to 182 ns while the fall travels through X5, the INTC output register and X6. A DMA register access is only 125 ns, so a handler that clears the source and returns quickly can be re-entered while the CPU input is still high, and TOP reads 0x1F. That is not an error. The handler must treat TOP = 0x1F as a normal exit, and the dispatch loop of the priority lesson must do the same on its re-read.
Signature | Mechanism | Caught by | Fix |
|---|---|---|---|
Never comes, source pin healthy | Pulse shorter than sync window; clear-versus-set race with clear winning; RMW on a W1C register writes back stale 1s and clears a new event | Ratio-sweep pulse tests; directed race tests, formal; a test that sets a second event between the read and the clear | Held levels; sticky status; set-wins arbitration; write only the bits you mean to clear. |
Comes twice per event | Handler clears the status but not the cause, or the source re-asserts | Count entries per event in a directed test | Clear the cause, not just the bit; hold-off the source until drained. |
Never stops, no new events | Wrong bit cleared (DMA ERR is sticky); IRQ_EN re-written; on a bridge that posts writes, the clear still in flight | Software storm tests | Write |
Entry with TOP = 0x1F | Clear or mask has not yet propagated through X5, the INTC output register and X6 (135.4 to 182.3 ns) | Back-to-back clear-and-return test | Treat TOP = 0x1F as a normal exit, never an error; no hardware fix needed. |
The script below models a W1C status bit cycle by cycle. A handler takes the interrupt when it sees the bit set and clears it three cycles later. Twenty events arrive; four of them land exactly on a clear cycle. The two arbitration orders give different entry counts, so the check can fail: a clear-wins bit enters 16 times and loses four events, while set-wins enters 20 times, once per event. A second check models the read-modify-write mistake on a two-bit status.
def run(arbitration, event_cycles):
status, clear_at, entries = 0, None, 0
for c in range(max(event_cycles) + 10):
hw_set = c in event_cycles
sw_clr = (clear_at == c)
if sw_clr:
clear_at = None
if hw_set and sw_clr: # same-cycle collision
status = 1 if arbitration == "set-wins" else 0
elif hw_set:
status = 1
elif sw_clr:
status = 0
if status and clear_at is None: # handler takes the interrupt
entries += 1
clear_at = c + 3 # and clears three cycles later
return entries
cycles, t = [], 0
for i in range(20):
cycles.append(t)
t += 3 if i % 5 == 3 else 10 # events 4, 9, 14, 19 hit a clear
cycles = set(cycles)
entries_set = run("set-wins", cycles)
entries_clr = run("clear-wins", cycles)
print(f"set-wins: {entries_set} entries for {len(cycles)} events")
print(f"clear-wins: {entries_clr} entries for {len(cycles)} events")
assert entries_set == 20 and entries_clr == 16
def rmw_clear(status_now, status_read, want):
# RMW: write back the value read, with the wanted bit forced on.
written = status_read | want
return status_now & ~written # W1C: 1s clear
DONE, ERR = 0x2, 0x4
# The handler only means to clear DONE. If ERR was already set when it read
# STATUS, the RMW writes that 1 back and clears ERR unexamined.
assert rmw_clear(DONE | ERR, DONE | ERR, DONE) == 0 # RMW wipes unexamined ERR
assert DONE | ERR == 0x6 and (DONE | ERR) & ~DONE == ERR # a targeted write of DONE keeps ERR
print("PASS: set-wins keeps all 20 events, clear-wins drops 4, RMW on W1C drops ERR")Every W1C status bit documents its simultaneous set-and-clear behaviour, and set-wins is the default.
Handlers clear exactly the bits they handled; no read-modify-write on a W1C register.
Every handler ends with clear, read-back, then return, and treats TOP = 0x1F on re-entry as a normal exit.
The storm test exists: force the source, mask nothing, and confirm the handler runs exactly once per event.
For a stuck interrupt, read the source status with the handler masked: still set means the clear path is broken (check ERR as well as DONE), cleared means the clear works but something re-asserts. For a missed interrupt, force the source and watch RAW: RAW never setting points at the synchroniser or the wire, RAW setting with no handler points at masking or CPU enables. For a doubled count, single-step the handler and watch whether the status bit survives its own clear; if it does, the clear is writing the wrong bit. For an entry with TOP = 0x1F, check the clear-to-return time against the 182.3 ns bound before calling it a bug.
Chatter: DONE is genuinely set again with new data, so a new event arrived. A storm would re-enter with DONE clear or with the same stale event; here the device is simply producing faster than the handler drains, which is a rate problem, not a clear problem.
No. The clear fell at the source, but the fall still has to cross X5, the INTC output register and X6, which takes 135.4 to 182.3 ns, longer than a 125 ns register access. The CPU took the interrupt on the old level. The handler should treat TOP = 0x1F as a normal exit.