Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • The NanoSoC Tour
  • Reading Path
  • SoC Glossary10
  • SoC Metrics Reference
  • SoC Cheatsheet
  • What SoC Integration Is12
  • Architecture and Budgets21
  • Memory Maps and Address Decode26
  • Interconnect and Bus Fabrics26
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
    • Interrupt Controller Integration
      • Levels, Polarity and Synchronisation
      • Priority, Masking and the TOP Register
      • The Wiring Table and the Controller Sketch
    • Interrupt Routing and Latency
    • DMA Integration
    • Peripheral Subsystem
    • Pad Muxing and GPIO
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
Loading lesson

←Interrupt Controller Integration
  1. Topics
  2. SoC Design
  3. Interrupts, DMA and Peripherals
  4. Interrupt Controller Integration
  5. Levels, Polarity and Synchronisation
On this page

    Part 7 of 14, lesson 1 of 15Part 7 of 14/Interrupts, DMA and Peripherals/Lesson 1 of 15All levels

    Levels, Polarity and Synchronisation

    Why every NanoSoC interrupt is an active-high level, what polarity bugs look like, and what the synchroniser costs.

    Why it matters

    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.

    The core idea

    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.

    nano_intc_sync.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 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;
    endmodule

    Polarity 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.

    nano_irq_cond.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 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;
    endmodule

    Worked example: how long must a level hold

    The 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.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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")

    What the review looks for

    • 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.

    Verify and debug

    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.

    Interrupt polarity and sensitivity mismatch: a level-high source wired to an active-low or edge-triggered input, so the interrupt never fires or fires continuously. Caught by a connectivity review and one software test per interrupt; fixed by tabulating polarity and type in the integration spec and inserting explicit inverters where a block disagrees.

    If asked level or edge, answer level for any controller slower than its sources, quote the 2 T_per hold rule, and say where the synchroniser lives. Then volunteer the cost: two destination cycles of latency, priced in the routing topic.

    Check yourself The SPI block raises its interrupt for exactly one 24 MHz cycle. Is that safe on NanoSoC, and why?

    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.

    Related topics

    • Next: Priority, Masking and the TOP Register

    • Protocol First Principles: Interrupts and Notification

    • CDC and RDC: Synchronizer Selection Guide

    Previous

    133 / 289

    Next: Priority, Masking and the TOP Register

    Up next

    Up nextPriority, Masking and the TOP Register

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • The Wiring Table and the Controller Sketch