The DMA finished a job at 3 a.m. in the lab: its irq pin reads high, yet the controller's RAW register shows 0 on that input and the handler never runs. Nothing is broken; the wire is simply connected to the wrong input, which this lesson's review table would have caught, or the pulse the source sends never survives the controller clock, which the rest of this lesson explains. Polarity and sensitivity mismatches are the classic first interrupt bug, and they are silent: no error response, no hang, just an event that never arrives or an interrupt that never stops.
All eight NanoSoC controller inputs are active-high levels held by the source until software clears the cause: the DMA holds irq until its DONE or ERR bit is cleared, the UART holds its line until the data register is touched, the GPIO holds its line while the pin condition lasts. A level waits; a pulse does not. The controller lives on clk_per at 24 MHz, a 41.667 ns period, while sources like the DMA run at 96 MHz. A one-cycle 96 MHz pulse is 10.4 ns wide, far shorter than one controller period, so a plain flop in the controller clock can sample either side of it and miss it. The NanoSoC rule is absolute: inputs must be levels held at least 2 T_per, which is 83.3 ns, and anything shorter must be stretched to a level or a toggle before it crosses.
Each input therefore passes through a two-flop synchroniser inside the controller before it becomes a RAW bit. The synchroniser costs latency, which the routing topic bounds precisely, and it buys down the metastability risk: one extra flop per input turns a rare wrong sample into a negligible one. Even so, a level held only just over the 83.3 ns minimum is needlessly fragile; the DMA's hold of about 2 microseconds (illustrative) exceeds it about 24 times. For the synchroniser theory see Synchronizer Selection Guide; the SoC decision here is only where the synchronisers sit, one per input, inside the controller, never in the source block.
// NanoSoC teaching values: one controller input slice.
// irq_src is an active-high level from any clock domain; irq_raw is the
// RAW bit in clk_per. Two flops, no pulse stretching: the source promises
// to hold the level (at least 2 T_per, 83.3 ns on NanoSoC).
module nano_intc_sync (
input logic clk_per,
input logic rst_per_n,
input logic irq_src,
output logic irq_raw
);
logic s1, s2;
always_ff @(posedge clk_per or negedge rst_per_n) begin
if (!rst_per_n) begin
s1 <= 1'b0;
s2 <= 1'b0;
end else begin
s1 <= irq_src;
s2 <= s1;
end
end
assign irq_raw = s2;
endmodulePolarity is fixed at the source boundary, not inside the controller. A block with an active-low interrupt gets one inverter before the wire is named; a block that can only produce an edge gets a stretcher that holds a level until software clears it. Both fragments below are teaching sketches.
// snippet: polarity fix and edge-to-level stretcher at a source boundary.
// irq_n is an active-low source; evt is a one-cycle event pulse.
module nano_irq_cond (
input logic clk,
input logic rst_n,
input logic irq_n, // active-low source: invert once, here
input logic evt, // one-cycle event pulse
input logic clr, // software clear (W1C decoded)
output logic irq_a, // active-high level from the polarity fix
output logic irq_held // level held until clr
);
assign irq_a = ~irq_n;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) irq_held <= 1'b0;
else if (evt) irq_held <= 1'b1; // set wins over a same-cycle clear
else if (clr) irq_held <= 1'b0;
endmoduleThe hold requirement is two destination periods plus margin for the source edge alignment. The script below prices it for every NanoSoC source domain, and shows why the DMA level, which holds for microseconds until software clears STATUS, satisfies the rule by roughly 24 times (2 us against 83.3 ns) while every single-cycle pulse fails it.
T_PER = 1 / 24e6
hold_min = 2 * T_PER
print(f"minimum level hold: {hold_min * 1e9:.1f} ns")
domains = {"DMA/sys (96 MHz)": 1 / 96e6, "CPU (192 MHz)": 1 / 192e6,
"GPIO/UART (24 MHz)": T_PER}
for name, period in domains.items():
pulse = period # one source cycle, the worst pulse a block could send
ok = pulse >= hold_min
print(f"{name}: one-cycle pulse {pulse * 1e9:6.2f} ns -> {'HELD' if ok else 'MISSED'}")
assert not ok # a single source cycle never meets 2 T_per, in any domain
# The DMA holds irq until software clears it: worst-case service is ~2 us.
dma_hold = 2e-6
ratio = dma_hold / hold_min
assert 23 < ratio < 25
print(f"DMA hold is {ratio:.1f}x the rule")
print("PASS: every one-cycle pulse is shorter than the 83.3 ns rule; only held levels qualify")The interrupt table names polarity and sensitivity per input, and every row says active-high level.
Every input that arrives from another clock domain has its two-flop synchroniser drawn inside the controller, not assumed inside the source.
No source offers a pulse: each block's documentation names the status bit whose clearing drops the level.
Drive each source, read the matching RAW bit, then remove the cause and read it again: set means the path works, cleared means the clear path works. Then hold the scope on the wire, not the register: a level that flickers is a source that re-asserts, often a status bit cleared before its cause drained. If RAW never sets while the source pin is high, the table is wrong about which input the wire reaches, which is the wiring lesson's bug.
No. One 24 MHz cycle is 41.667 ns, below the 2 T_per = 83.3 ns rule, and the input synchroniser sits on the same clock, so the pulse can be missed. Stretch it to a held level, as NanoSoC's SPI does: its irq is IE & RXNE, held until DATA is read.