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
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
    • SoC Verification Strategy
    • Integration and Connectivity Tests
    • Firmware Co-Simulation
      • Running Boot Code Against RTL
      • Bench Checkpoints and the ROM Self-Test
      • Cost, Speed and What to Log
    • Performance Verification
    • Gate-Level and Power-Aware Simulation
    • Emulation and FPGA Prototyping
    • Bring-Up Readiness
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
Loading lesson

←Firmware Co-Simulation
  1. Topics
  2. SoC Design
  3. Top-Level Verification and Bring-up
  4. Firmware Co-Simulation
  5. Running Boot Code Against RTL
On this page

    Part 10 of 14, lesson 7 of 21Part 10 of 14/Top-Level Verification and Bring-up/Lesson 7 of 21All levels

    Running Boot Code Against RTL

    The ROM flow in simulation: blocking model, image, drivers, and what only this run can show.

    Why it matters

    The first fetch from 0x0000_0000, which completes 54.073 us after the supply is good, is the first moment the whole chip does anything together: the PLL path, the release order, the fabric, the ROM and the CPU all have to agree. No sweep reaches it, because no sweep fetches. Its one job is the setup: what code runs against what RTL, under what model, proving what the sweeps cannot.

    The core idea

    The run loads the real ROM image at 0x0000_0000, puts the second-stage image in a flash model behind the SPI pins, and releases reset exactly as silicon does: oscillator counter, peripheral release, lock, gates, system release, CPU release, 53.948 us of hardware before the CPU leaves reset. (The oscillator-ready counter is 1,024 clk_per cycles; a bench may shorten it for speed, but the full-length run must exist and must be the one that signs off.) The firmware model is blocking — one access in flight at a time — which matches the ROM code and keeps the run deterministic: same image, same latencies, the same 12.581 ms total for a fixed clock phase, and a spread of at most the 47 ns interrupt window (135.4 to 182.3 ns) across phases. Determinism is a feature, not a limitation; a boot that varies by more than that from run to run is hiding a synchroniser or arbitration race.

    What this run proves that nothing else does: the reset vector is readable and executable with a cold instruction cache, the stack top at 0x2001_0000 (one past the end of SRAM0, and the address of the secure-only SRAM1) is used correctly, so the first push lands at 0x2000_FFFC, RST_CAUSE reads through the slow APB segment in 281.25 ns, the UART banner prints at 115,384.6 baud, the DMA self-test moves 64 words with the checksum compared, the SPI loader pulls the 8,192-byte image (8,196 bytes on the wire with the 4-byte command) out of the flash model at 6 Mb/s, and the jump lands on the loaded image. That last claim holds only if the image really arrives through the loader: a run that preloads SRAM0 skips the 10,928 us stage and proves nothing about the loader. A preload variant is useful for iterating on the second-stage image, but it must be labelled as bypassing the loader and never be the run that signs off. The boot table is the pass criterion, stage by stage; any stage that overruns names its wait.

    Diagram
    # not-run: what the firmware run needs from the bench, NanoSoC teaching values
    # ROM image at 0x0000_0000; a flash model behind the SPI pins that holds the
    # 8,192-byte image and answers the 4-byte read command at 6 Mb/s, so the image
    # reaches SRAM0 through the real loader; UART output captured to a log; pads
    # modelled with their reset pulls (the RTL has none).
    # Bench monitors (zero cost to the ROM): timestamp the first fetch, the first
    # UART DATA write, the first DMA register access, SPI chip-select falling and
    # rising, the first fetch at 0x2000_0000, and irq_dma rising to irq_cpu at the
    # core input (135.4 to 182.3 ns; the DMA STATUS write is the clear, not the start).
    # Blocking model: one access in flight, latencies from the latency table.
    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    # The bench monitor as a checker: expected stage events (boot flow) against the
    # events a run reports, with a tolerance. NanoSoC teaching values; the 10 us
    # tolerance is ILLUSTRATIVE (the ROM polls TXE between characters: at most
    # 16 characters x two clk_per accesses of 281.25 ns = 9 us, plus the three SPI
    # register writes, 0.84 us, before chip-select falls).
    EXPECT_US = {
        "cpu out of reset, first fetch 0x0": 53.948,
        "first UART DATA write (start bit)": 65.865,
        "banner complete": 1452.531,
        "first DMA register access": 1452.531,
        "SPI chip-select falls": 1460.917,
        "SPI chip-select rises": 12388.917,
        "first fetch at 0x2000_0000": 12580.917,
    }
    TOL_US = 10.0
    def check(run):
        return [name for name, want in EXPECT_US.items()
                if name not in run or abs(run[name] - want) > TOL_US]
    good = {k: v + (3.0 if "banner" in k or "SPI" in k or "0x2000" in k else 0.0)
            for k, v in EXPECT_US.items()} # real polling makes later events a few us late
    assert check(good) == []
    # the check can fail: 64 extra ROM-printed characters at 86.667 us each push every later event out by 5.55 ms
    CHAR_US = 10 / 115384.6 * 1e6
    shift = 64 * CHAR_US
    late = {k: v + (shift if v >= 1452.5 else 0.0) for k, v in EXPECT_US.items()}
    assert abs(shift - 5546.7) < 0.1
    assert "SPI chip-select falls" in check(late) and "first fetch at 0x2000_0000" in check(late)
    # a preloaded image skips the loader: chip-select never moves, and the check names it
    bypass = dict(good)
    del bypass["SPI chip-select falls"], bypass["SPI chip-select rises"]
    assert check(bypass) == ["SPI chip-select falls", "SPI chip-select rises"]
    print(f"PASS: 7 events within {TOL_US:.0f} us; extra prints ({shift / 1000:.2f} ms) and a bypassed loader are caught")

    What the review looks for

    • The image under test is the real ROM build, not a test-only fork; test hooks are compiled out by a flag the review can see.

    • The bench models only what silicon has not yet: the flash behind the SPI pins, the UART terminal, the pad pulls — never the chip internals — and the image reaches SRAM0 through the SPI loader, or the run says it bypasses it.

    • Pass means every bench event matches the stage table within the stated tolerance (a few microseconds, from the TXE polling and the three SPI register writes), not merely that the prompt appears.

    Verify and debug

    A boot that hangs points at the last bench event that fired, which the next lesson places; a boot that is slow points at the slowest stage, usually the SPI load at 86.9 percent. If the run is nondeterministic across seeds, suspect the interrupt timing or the arbitration: the blocking model should make everything else repeatable. Never fix a nondeterministic boot by pinning the seed; fix the race it found.

    A wake treated as a cold boot: the ROM reads RST_CAUSE first and skips the SRAM0 clear when WAKE is set, because SRAM0 is retained and holds the saved image. A boot that clears first and reads the cause afterwards destroys that image and every wake looks like a fresh start. The check is a directed wake run: preload SRAM0 with a known pattern, power-cycle PD_SW through the sequencer, and require the pattern intact and the jump to 0x2000_0000 taken without an SPI load.

    If asked what firmware co-simulation adds over sweeps, answer sequence: reset order, cache-cold fetch, driver order, and the full 12.581 ms budget proved end to end. Sweeps prove the pieces answer; firmware proves they answer in order.

    Check yourself Why must the test image be the real ROM build rather than a simplified test program?

    A simplified program skips the driver order, the self-test and the loader — exactly the sequence bugs the run exists to find. Testing other code proves other code.

    Related topics

    • Next: Bench Checkpoints and the ROM Self-Test

    • Memories and Boot: Boot ROM and Boot Modes

    • Verification First Principles: Hardware-Software Co-Verification

    • Verification First Principles: SoC and Software-Driven Tests

    Previous

    199 / 289

    Next: Bench Checkpoints and the ROM Self-Test

    Up next

    Up nextBench Checkpoints and the ROM Self-Test

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Cost, Speed and What to Log