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
    • Interrupt Routing and Latency
    • DMA Integration
    • Peripheral Subsystem
      • UART Integration
      • SPI and GPIO Integration
      • Bridges, Clocks and Bring-Up Order
    • 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

←Peripheral Subsystem
  1. Topics
  2. SoC Design
  3. Interrupts, DMA and Peripherals
  4. Peripheral Subsystem
  5. SPI and GPIO Integration
On this page

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

    SPI and GPIO Integration

    The boot-image SPI master and the sixteen GPIO pins: clocks, chip-select, direction discipline and the level interrupt.

    Why it matters

    This lesson has one job: integrate the two peripherals the boot flow stands on. The SPI master loads the 8 KB second-stage image that the ROM cannot contain, and the GPIO block gives software sixteen pins to read and drive. NanoSoC has no boot straps and no GPIO read in its boot flow: the ROM reads the reset cause and then loads over SPI from the one boot source. Both are simple blocks with one dangerous habit each: the SPI chip-select is software-driven with no interlock, and the GPIO direction register decides whether the chip drives or listens on every pin. Get either wrong and the symptom is physical: contention on the bus or on the board.

    The core idea

    The SPI block is a mode-0 master with four registers. DATA carries one byte per touch: write to transmit, read for the last received byte. STATUS reports BUSY and RXNE. CLKDIV divides the 24 MHz clock as SPI clock equals 24 MHz over twice CLKDIV plus 1, so the reset value 1 gives 6 MHz. CTRL holds the enable, the IE interrupt enable for receive-full, and the CS bit that drives spi_cs_n low while set: software asserts CS, clocks out the 4-byte read command plus 8,192 image bytes, then releases CS, and nothing in hardware checks the framing. The load costs 8,196 bytes times 8 bits at 6 Mb/s, which is 10.928 ms, 86.9 percent of the entire boot time; the SPI clock is the boot clock in every sense that matters.

    The GPIO block is sixteen pins with four registers. DATA_IN reads the pins, DATA_OUT drives them, DIR selects direction with 1 for output, and INT_EN masks the level interrupt, which is the OR over all pins of DATA_IN AND INT_EN, active-high while any enabled pin reads high. DIR resets to all inputs, so the chip never drives at reset; DATA_OUT resets to 0, so the first configured output drives low. The interrupt follows the pin condition with no latch: configure an enabled pin as an output driving high and the block interrupts itself, which is correct behaviour for a level contract and a classic first-board surprise.

    Worked example: the image load and the pin mask

    The script below replays the boot ROM's SPI arithmetic and models the GPIO interrupt mask over a representative pin pattern, checking the load time against the boot ledger and showing how one mask bit decides between waking on a pin and ignoring it.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # SPI image load: (8192 + 4) bytes at 6 Mb/s, CLKDIV reset = 1.
    clk_per = 24e6
    clkdiv = 1
    spi_clk = clk_per / (2 * (clkdiv + 1))
    assert spi_clk == 6e6
    load_bits = (8192 + 4) * 8
    load_time = load_bits / spi_clk
    print(f"SPI load: {load_bits} bits at 6 Mb/s = {load_time * 1e3:.3f} ms")
    assert abs(load_time - 10.928e-3) < 1e-6
    
    # GPIO: irq follows enabled pins only; outputs driving high self-trigger.
    def gpio_irq(data_in, int_en):
        return 1 if (data_in & int_en) & 0xFFFF else 0
    
    pins = 0b0000_0010_0000_0100  # pins 2 and 9 read high
    assert gpio_irq(pins, 0xFFFF) == 1
    assert gpio_irq(pins, 0x0004) == 1   # pin 2 enabled: wakes
    assert gpio_irq(pins, 0x0200) == 1   # pin 9 enabled: wakes
    assert gpio_irq(pins, 0x0000) == 0   # masked: sleeps through it
    assert gpio_irq(pins, 0x00F8) == 0   # wrong nibble enabled: silent
    print("PASS: 10.928 ms image load; GPIO mask selects pins 2 and 9")

    What the review looks for

    • The SPI command framing, CS assertion window and 10.928 ms load budget are written in the boot plan with nothing left to the driver author's imagination.

    • Every GPIO pin has a direction, a reset state and an interrupt-mask decision in the pin table before first silicon; no pin is configured by trial on the bench.

    • The wake-up pins for sleep exit are named explicitly, with their INT_EN bits set by the sleep-entry code and cleared on wake.

    Verify and debug

    For SPI, read back the image checksum the ROM computes over 2,048 words: a single-bit wire fault fails it deterministically. If the load stalls with BUSY never clearing, CS was released early or the flash never saw the command; scope CS against the clock. For GPIO, drive each pin both ways with the direction set both ways and confirm DATA_IN follows the pin in input mode and the pin follows DATA_OUT in output mode; a pin that follows neither is muxed to another function, which is the next topic's conflict table.

    Floating input read as configuration (general discipline; NanoSoC has no straps, so this applies if your chip has them): a pin whose direction and pull were never set is read before it is valid, and the chip makes the wrong decision. Caught by reviewing every sampled pin for a defined pull and sample time; fixed with defined pulls and sampling at reset release. On NanoSoC the same habit applies to every GPIO input: the pull is fixed in the pad cell, so set DIR and read the pad table for the open-pin level before software trusts DATA_IN.

    If asked where boot time goes, answer the SPI load first, 86.9 percent, then the banner at 11 percent, then everything else. Examiners promote the candidate who prices the load from CLKDIV without being asked.

    Check yourself The ROM enables a GPIO interrupt on a pin, configures that pin as an output, and writes 1 to its DATA_OUT bit. What happens?

    The interrupt fires immediately and stays asserted: the output drives the pin high, DATA_IN reads high, and the enabled mask passes it through. The block is behaving correctly; the configuration asked for a self-trigger. Mask the pin or use it as an input.

    Related topics

    • UART Integration

    • Next: Bridges, Clocks and Bring-Up Order

    • RTL: Watchdog Timer Example

    Previous

    146 / 289

    Next: Bridges, Clocks and Bring-Up Order

    Up next

    Up nextBridges, Clocks and Bring-Up Order

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error