Project 1: Bare-Metal Blinky (No SDK)

Build a blinky from first principles: no SDK, no startup files, just the datasheet, a linker script, and a scope.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: Assembly, Rust)
Alternative Programming Languages Assembly, Rust
Coolness Level Level 4: “I know exactly what the silicon is doing”
Business Potential 1. Foundational skill for any embedded product
Prerequisites C pointers/bitwise ops, reading datasheets, basic build tools
Key Topics Boot ROM, stage-2 boot, XIP, clock tree, GPIO/SIO, linker scripts

1. Learning Objectives

By completing this project, you will:

  1. Implement the RP2040/RP2350 boot path from ROM to main() without the Pico SDK.
  2. Configure XOSC + PLL clocks and verify timing on a GPIO pin.
  3. Build a minimal vector table, startup code, and linker script that place .text, .data, and .bss correctly.
  4. Toggle GPIO via SIO registers with deterministic timing.
  5. Debug early-boot failures using register inspection and waveform checks.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Boot ROM, Stage-2 Boot, XIP, and Memory Map

Fundamentals

The RP2040/RP2350 starts executing from a small on-chip Boot ROM that knows how to load and validate a tiny “stage-2” boot block from external QSPI flash. That stage-2 program configures the QSPI interface and enables XIP (execute-in-place), after which the CPU can fetch instructions directly from flash as if it were memory. This architecture makes the memory map the primary tool for understanding boot behavior: ROM at 0x00000000, XIP flash at 0x10000000, SRAM at 0x20000000, peripherals at 0x40000000, and the fast SIO region at 0xD0000000. Every register write you make is a memory access in this map.

Deep Dive into the concept

In the RP2040/RP2350, the boot pipeline is split across hardware and a tiny software stub. On reset, the CPU jumps into Boot ROM. The ROM reads a fixed-size header from flash, validates a checksum, and copies a small stage-2 program into SRAM. Stage-2 is responsible for configuring the QSPI flash interface and enabling the XIP mapping. Without stage-2, the ROM cannot safely enable XIP because different flash chips need different QSPI configuration. Once stage-2 runs, the CPU can jump to your reset handler in flash, and the standard C runtime initialization begins: initialize stack, copy .data from flash to SRAM, zero .bss, then jump to main().

The memory map is not just addresses; it implies latency and bus contention. XIP reads are slower than SRAM and depend on cache behavior. If you run time-critical code from flash, instruction fetches can stall on cache misses or flash erase operations. This is why many bare-metal examples place their ISR or tight loops in SRAM. The RP2040/RP2350 bus fabric also means cores, DMA, and peripherals contend for access to SRAM banks. Understanding which SRAM bank holds your buffers can be the difference between stable GPIO timing and jitter.

The vector table is another essential detail: it must live at a valid address and point to valid handlers. If the reset vector is wrong, the CPU will jump into an unmapped region and hard fault silently. That is why you must understand the memory map and boot flow together: the ROM and stage-2 are only the beginning; your vector table and startup code are the first things you control.

How this fits on projects

You will use this concept directly in §3.2 Functional Requirements and in §5.10 Phase 1 to create a working boot flow. It also underpins the OTA and secure boot projects. Also used in: P08-rp2350-secure-boot-implementation.md, P11-custom-bootloader-with-ota-updates.md.

Definitions & key terms

  • Boot ROM -> immutable code that runs at reset and loads stage-2
  • Stage-2 boot -> small SRAM-resident stub that configures QSPI/XIP
  • XIP -> execute-in-place from external flash via cache
  • Vector table -> initial stack pointer + exception/interrupt handlers
  • Memory map -> fixed address regions for ROM/flash/SRAM/peripherals

Mental model diagram (ASCII)

Reset -> Boot ROM -> Stage-2 (SRAM) -> XIP Flash -> C runtime -> main()
             |             |              |
             |             |              +-- .text in flash
             |             +-- QSPI/XIP init
             +-- CRC/checks

How it works (step-by-step, with invariants and failure modes)

  1. Reset: CPU enters Boot ROM.
  2. ROM reads flash header and copies stage-2 to SRAM.
  3. Stage-2 configures QSPI and enables XIP mapping.
  4. CPU jumps to reset handler in flash.
  5. Startup code sets stack, copies .data, zeros .bss.

Invariants:

  • Vector table must be valid and aligned.
  • Stage-2 must match flash chip requirements.

Failure modes:

  • Bad CRC or header -> ROM refuses to boot.
  • Wrong vector table -> CPU hard faults silently.

Minimal concrete example

__attribute__((section(".vectors")))
const uint32_t vectors[] = {
  0x20042000,              // initial SP
  (uint32_t)Reset_Handler, // reset
};

Common misconceptions

  • “The CPU starts at main” -> it starts in ROM, not your code.
  • “XIP is as fast as SRAM” -> cache misses introduce stalls.
  • “Any flash chip will work” -> stage-2 must match chip timing.

Check-your-understanding questions

  1. Why does RP2040 need stage-2 boot code?
  2. What happens if the vector table points to 0x00000000?
  3. When should you copy code into SRAM?

Check-your-understanding answers

  1. Stage-2 configures QSPI/XIP because flash chips vary in requirements.
  2. The CPU jumps to invalid memory and faults before visible output.
  3. When deterministic timing or fast ISR response is required.

Real-world applications

  • Safe bootloaders for field updates
  • Deterministic low-latency firmware for sensors and control loops
  • Secure boot chains (RP2350)

Where you’ll apply it

References

  • RP2040 datasheet (boot & memory map sections)
  • RP2350 datasheet (secure boot + memory map)
  • Pico SDK docs (boot flow overview)

Key insights

  • Boot is a memory mapping problem before it is a programming problem.

Summary

The Boot ROM and stage-2 sequence decide whether your code ever runs. Mastering the memory map and vector table is the foundation of bare-metal firmware.

Homework/Exercises to practice the concept

  1. Draw the memory map and label ROM/XIP/SRAM/SIO.
  2. Write a minimal vector table by hand.
  3. Explain what stage-2 must do for a specific flash chip.

Solutions to the homework/exercises

  1. ROM at 0x00000000, XIP at 0x10000000, SRAM at 0x20000000, peripherals at 0x40000000, SIO at 0xD0000000.
  2. Vector table contains initial SP and reset handler address in flash.
  3. It configures QSPI timing, mode, and enables XIP mapping.

2.2 Clock Tree, Reset, and Glitchless Switching

Fundamentals

The RP2040/RP2350 has multiple clock sources (ROSC, XOSC, PLLs) and multiple clock domains (CLK_SYS, CLK_USB, CLK_PERI). Peripherals are held in reset until you explicitly enable them. If you switch clocks incorrectly or skip PLL lock checks, your UART baud rate drifts, USB fails, and timing-sensitive protocols break. Clock configuration is a dependency graph: stable source first, PLL second, then domain switching.

Deep Dive into the concept

The safest early-boot sequence is to start with the ROSC (free-running), enable the external crystal oscillator (XOSC), wait for stability, configure PLL_SYS, wait for lock, then switch CLK_SYS to the PLL output using a glitchless mux. USB requires a strict 48 MHz clock, which typically comes from a dedicated PLL or divider. The RP2040/RP2350 provides hardware that ensures safe switching, but you still have to set the correct bits and wait for ready signals. The reset controller allows per-peripheral resets, which must be released in the right order (IO and pads before using GPIO, for example).

Clock and reset are the first examples of “write, wait, verify.” This is the habit that makes bare-metal firmware reliable. You should also validate clock configuration on a real pin: route CLK_SYS to a GPIO via the clock output mux and measure with a scope. This feedback loop is invaluable when debugging timing.

How this fits on projects

This concept defines the implementation in §5.10 Phase 1 and is required for the USB and PIO projects. Also used in: P02-pio-led-strip-controller-ws2812b.md, P03-usb-hid-keyboard-emulator.md.

Definitions & key terms

  • XOSC -> crystal oscillator, accurate reference
  • ROSC -> ring oscillator, fast but imprecise
  • PLL -> phase-locked loop that multiplies a reference
  • CLK_SYS -> main CPU clock domain
  • Glitchless mux -> safe clock switch without runt pulses

Mental model diagram (ASCII)

ROSC ----\
         \--> CLK_SYS (safe source) ---> CPU
XOSC --> PLL_SYS ----/

How it works (step-by-step)

  1. Enable XOSC, wait for stable bit.
  2. Configure PLL_SYS (dividers, feedback).
  3. Wait for PLL lock.
  4. Switch CLK_SYS to PLL output using glitchless mux.
  5. Release resets for IO/PAD/SIO before GPIO use.

Failure modes:

  • Switching before PLL lock -> unstable clock.
  • Skipping reset release -> GPIO appears dead.

Minimal concrete example

// Pseudocode for clock switch
enable_xosc(); wait_xosc_stable();
setup_pll_sys(12, 1, 6); wait_pll_lock();
switch_clk_sys_to_pll();

Common misconceptions

  • “ROSC is fine for USB” -> USB needs 48 MHz accuracy.
  • “Clocks are global” -> many peripherals have independent dividers.

Check-your-understanding questions

  1. Why do you wait for PLL lock?
  2. Which clock domain does USB require?
  3. How would you verify CLK_SYS frequency?

Check-your-understanding answers

  1. Without lock, the output frequency is unstable and may glitch.
  2. USB requires a precise 48 MHz domain.
  3. Route the clock to a GPIO and measure with a scope.

Real-world applications

  • Deterministic timing for motor control and PWM
  • USB device/host reliability

Where you’ll apply it

References

  • RP2040 datasheet: Clock and Reset chapters
  • RP2350 datasheet: PLL and power sections

Key insights

  • Clock stability is the hidden dependency of every peripheral.

Summary

Correct clock and reset sequencing is the difference between a booting device and a silent board.

Homework/Exercises to practice the concept

  1. Compute PLL settings for 125 MHz from a 12 MHz crystal.
  2. Sketch a safe clock-switch sequence.

Solutions to the homework/exercises

  1. Example: refdiv=1, fbdiv=125, postdiv=6 and 2 (check datasheet).
  2. XOSC -> wait -> PLL config -> lock -> glitchless switch.

2.3 GPIO, Pad Control, and SIO Fast I/O

Fundamentals

GPIO on RP2040/RP2350 is split between IO_BANK (function select, pad control) and SIO (fast, single-cycle output). You must configure a pin’s function (SIO) and pad attributes (pull-up/down, drive strength) before toggling it. SIO provides deterministic single-cycle access, which is crucial for precise timing and fast bit manipulation.

Deep Dive into the concept

Each GPIO has a function select register that chooses between SIO, peripheral functions, or PIO. The pad control registers define input enable, output disable, pull resistors, and drive strength. The SIO block provides direct GPIO_OUT and GPIO_OUT_XOR registers that allow atomic toggles without read-modify-write hazards. This architecture is why bit-banging is reliable on RP2040 when you use SIO correctly.

In bare-metal code, most errors come from skipping pad configuration or using the wrong function select. Another common bug is using the slow peripheral bus GPIO registers instead of SIO. For tight timing, the difference matters: SIO is a single-cycle interface on the fast bus, while IO_BANK accesses can add latency.

How this fits on projects

This is the core of §3.2 Functional Requirements and §5.10 Phase 2. It also appears in the PIO projects. Also used in: P02-pio-led-strip-controller-ws2812b.md, P04-logic-analyzer-with-pio.md.

Definitions & key terms

  • IO_BANK -> function select for each GPIO
  • PAD -> electrical configuration (pulls, drive)
  • SIO -> single-cycle I/O block

Mental model diagram (ASCII)

CPU -> SIO (fast) -> GPIO output
CPU -> IO_BANK (slow) -> function select/pad

How it works (step-by-step)

  1. Release IO_BANK and PADS from reset.
  2. Set GPIO function to SIO.
  3. Configure pad drive and pulls.
  4. Write to SIO GPIO_OUT or GPIO_OUT_XOR.

Minimal concrete example

sio_hw->gpio_oe_set = 1u << 25;
sio_hw->gpio_out_xor = 1u << 25; // toggle

Common misconceptions

  • “Setting GPIO_OUT is enough” -> function select must be SIO.
  • “Pad config is optional” -> floating pins cause unstable output.

Check-your-understanding questions

  1. Why use GPIO_OUT_XOR to toggle?
  2. What happens if you leave input enabled on an output-only pin?

Check-your-understanding answers

  1. It avoids read-modify-write hazards and is atomic.
  2. It can increase power or cause undefined reads.

Real-world applications

  • Fast digital I/O, bit-banged protocols
  • Deterministic toggles for timing verification

Where you’ll apply it

References

  • RP2040 datasheet: IO_BANK0, PADS_BANK0, SIO chapters

Key insights

  • GPIO is not a single register; it’s a multi-block configuration pipeline.

Summary

Correct GPIO setup requires IO_BANK function select, pad config, and SIO writes. Skip any step and the LED will not blink.

Homework/Exercises to practice the concept

  1. Configure a second GPIO and toggle both in opposite phase.
  2. Measure the difference between SIO and IO_BANK toggling.

Solutions to the homework/exercises

  1. Use two bits in GPIO_OUT_XOR with alternating delays.
  2. SIO toggles faster and with lower jitter.

2.4 Linker Script, Vector Table, and Startup Code

Fundamentals

Bare-metal firmware needs a linker script to decide where code and data live, and a startup file to initialize .data and .bss. Without these, global variables will be garbage, interrupts won’t work, and main() may never run. The vector table and initial stack pointer are the only contract the CPU requires to enter your code.

Deep Dive into the concept

The linker script defines memory regions: FLASH (XIP), RAM (SRAM), and sometimes SCRATCH for specialized regions. It places .text and .rodata in flash, .data and .bss in RAM, and defines symbols like _sidata, _sdata, _edata, _sbss, _ebss. The startup routine uses these symbols to copy initialized data from flash to SRAM and zero .bss.

Startup code also sets the vector table location (VTOR) if needed and may configure the stack pointer to the top of SRAM. On Cortex-M0+ (RP2040), the vector table is fixed at address 0x00000000 (ROM alias), but on RP2350 you may configure the secure/non-secure vector tables. When debugging boot failures, verifying linker symbols and startup code is a key diagnostic step.

How this fits on projects

You will write the linker script and startup file in §5.10 Phase 1. This concept is reused in P09 (RISC-V) and P11 (Bootloader). Also used in: P09-rp2350-risc-v-bare-metal-programming.md, P11-custom-bootloader-with-ota-updates.md.

Definitions & key terms

  • Linker script -> defines memory layout and section placement
  • .data -> initialized globals copied to RAM
  • .bss -> zero-initialized globals in RAM
  • VTOR -> vector table offset register

Mental model diagram (ASCII)

FLASH: [.text][.rodata][.data image]
RAM:   [.data][.bss][stack]

How it works (step-by-step)

  1. Linker script places sections and defines symbols.
  2. Reset handler sets stack and copies .data.
  3. Reset handler zeros .bss.
  4. main() runs with initialized globals.

Minimal concrete example

MEMORY { FLASH(rx) : ORIGIN=0x10000000, LENGTH=2M
         RAM(rwx)  : ORIGIN=0x20000000, LENGTH=264K }

Common misconceptions

  • “The compiler does startup” -> it does not; you must provide it.
  • “Globals are initialized automatically” -> only if startup code runs.

Check-your-understanding questions

  1. Why is .data stored twice (flash + RAM)?
  2. What symbol tells startup code where .bss ends?

Check-your-understanding answers

  1. Flash is non-volatile; RAM is writable and used at runtime.
  2. _ebss (or equivalent) marks the end of .bss.

Real-world applications

  • Custom bootloaders
  • Multi-slot firmware layouts

Where you’ll apply it

References

  • “Making Embedded Systems” (startup and memory chapters)
  • GNU ld manual (linker scripts)

Key insights

  • Startup code is the smallest, most important program in your firmware.

Summary

The linker script and startup code define your entire runtime environment. If they’re wrong, nothing else matters.

Homework/Exercises to practice the concept

  1. Modify the linker script to move .data into a different SRAM bank.
  2. Add a custom section for a time-critical function in RAM.

Solutions to the homework/exercises

  1. Change ORIGIN or add a new memory region and place .data there.
  2. Use __attribute__((section(".ramfunc"))) and map .ramfunc to RAM.

3. Project Specification

3.1 What You Will Build

A bare-metal firmware image that boots on RP2040/RP2350, configures clocks, initializes GPIO25, and toggles it at a precise 1 Hz rate without using the Pico SDK or any prebuilt startup code.

Included:

  • Minimal stage-2 header compatibility
  • Custom linker script and startup code
  • Clock configuration and GPIO init
  • GPIO toggling loop with deterministic delay

Excluded:

  • RTOS, USB, PIO, or higher-level drivers
  • SDK helpers or bootloader frameworks

3.2 Functional Requirements

  1. Boot without SDK: Provide your own vector table and reset handler.
  2. Clock configuration: Use XOSC and PLL to achieve a stable system clock.
  3. GPIO configuration: Set GPIO25 to SIO output with correct pad settings.
  4. Deterministic blink: 1 Hz ±1% measured on a scope.
  5. Basic debug output: Optional UART log at 115200 baud (optional but recommended).

3.3 Non-Functional Requirements

  • Performance: Stable timing; jitter < 1%.
  • Reliability: Runs continuously for 24 hours without lockup.
  • Usability: Buildable with a single cmake/make command.

3.4 Example Usage / Output

$ mkdir build && cd build
$ cmake .. && make
[100%] Built target blinky
$ picotool load blinky.uf2 -f
Loading into flash: [==============================] 100%
The device was rebooted to start the application.

3.5 Data Formats / Schemas / Protocols

  • Linker symbols: _sidata, _sdata, _edata, _sbss, _ebss
  • Register writes: IO_BANK0 and SIO register offsets per datasheet

3.6 Edge Cases

  • XOSC fails to start (fallback to ROSC).
  • PLL lock never asserted (stay on safe clock).
  • GPIO pin muxed to wrong function.
  • Vector table alignment wrong (hard fault).

3.7 Real World Outcome

You can flash a raw binary and watch GPIO25 output a clean 1 Hz square wave. The UART log confirms clock configuration and GPIO setup. On a scope, you see a stable period with no long-term drift.

3.7.1 How to Run (Copy/Paste)

mkdir -p build
cd build
cmake ..
make -j
picotool load blinky.uf2 -f

3.7.2 Golden Path Demo (Deterministic)

  1. Flash the UF2.
  2. Observe GPIO25 on a scope: 0.5 s high, 0.5 s low.
  3. UART output prints clock frequency and a tick counter every second.

3.7.3 If CLI: exact terminal transcript

$ picotool load blinky.uf2 -f
Loading into flash: [==============================] 100%
The device was rebooted to start the application.

$ screen /dev/tty.usbmodem14101 115200
[Blinky] CLK_SYS=125000000 Hz
[Blinky] GPIO25 configured
[Blinky] tick=1
[Blinky] tick=2

Failure Demo (Expected)

$ picotool load blinky_bad.uf2 -f
Loading into flash: [==============================] 100%
The device was rebooted to start the application.

# UART stays silent; LED does not blink.
# Exit code: 0 (programmer succeeded, firmware failed)

4. Solution Architecture

4.1 High-Level Design

+------------+     +--------------+     +-----------------+
| Boot ROM   | --> | Stage-2 Stub | --> | Reset Handler   |
+------------+     +--------------+     +-----------------+
                                        | Clock + GPIO    |
                                        | Blink Loop      |
                                        +-----------------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |———-|—————–|—————| | Stage-2 header | Satisfy ROM checks | Match flash chip timing | | Startup code | Init data/bss, stack | Keep minimal, no libc | | Clock init | Configure XOSC/PLL | Validate with GPIO clock output | | GPIO init | Function select + pads | Use SIO for toggling | | Blink loop | Timing via cycles | Use busy wait or timer |

4.3 Data Structures (No Full Code)

struct clock_state {
  uint32_t clk_sys_hz;
  uint32_t pll_sys_fbdiv;
};

4.4 Algorithm Overview

Key Algorithm: Deterministic Delay Loop

  1. Read clk_sys_hz from configuration.
  2. Compute cycles for 0.5 seconds.
  3. Toggle GPIO with GPIO_OUT_XOR.
  4. Busy-wait using cycle counter or timer.

Complexity Analysis:

  • Time: O(1) per toggle
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

# Example for macOS or Linux
brew install cmake ninja arm-none-eabi-gcc
# or use pico-sdk Docker image / prebuilt toolchain

5.2 Project Structure

blinky-no-sdk/
├── CMakeLists.txt
├── linker.ld
├── src/
│   ├── startup.S
│   ├── main.c
│   ├── clock.c
│   └── gpio.c
└── README.md

5.3 The Core Question You’re Answering

“What exactly happens between power-on and the first instruction in my firmware?”

This project forces you to map theory to hardware. Every register you touch is visible in the datasheet.

5.4 Concepts You Must Understand First

  1. Boot ROM and stage-2 flow (see §2.1)
  2. Clock tree and PLL lock (see §2.2)
  3. GPIO SIO vs IO_BANK (see §2.3)
  4. Linker script + startup (see §2.4)

5.5 Questions to Guide Your Design

  1. What minimal vector table entries are required?
  2. Which registers configure the XOSC and PLL_SYS?
  3. How will you verify clock frequency on a pin?
  4. What happens if .data is not copied to SRAM?

5.6 Thinking Exercise

Manually compute the delay loop count for 0.5 seconds at 125 MHz. Then compute the same for 100 MHz and compare.

5.7 The Interview Questions They’ll Ask

  1. Why is stage-2 boot required on RP2040?
  2. How do you ensure deterministic timing without an RTOS?
  3. Why is volatile essential for MMIO?
  4. How do you debug a firmware that never reaches main()?

5.8 Hints in Layers

  • Hint 1: Toggle a GPIO as early as possible to confirm boot progress.
  • Hint 2: Output CLK_SYS to a pin to measure real frequency.
  • Hint 3: Use SIO GPIO_OUT_XOR to avoid read-modify-write.
  • Hint 4: Validate .data by setting a global variable with nonzero value.

5.9 Books That Will Help

| Topic | Book | Chapter | |——|——|———| | Boot and memory | Computer Systems: A Programmer’s Perspective | Ch. 1-3 | | Embedded startup | Making Embedded Systems (2nd Ed.) | Ch. 2-4 | | Bare-metal C | Effective C (2nd Ed.) | Ch. 8 |

5.10 Implementation Phases

Phase 1: Boot & Memory ([2-3 days])

Goals:

  • Vector table and reset handler working
  • Linker script places .text and .data

Tasks:

  1. Create a minimal linker.ld and verify symbols.
  2. Implement Reset_Handler that zeros .bss.

Checkpoint: LED toggles once at startup (even without proper timing).

Phase 2: Clocks & GPIO ([3-4 days])

Goals:

  • XOSC + PLL configured
  • GPIO25 toggling via SIO

Tasks:

  1. Configure XOSC and PLL_SYS.
  2. Set GPIO25 to SIO output and toggle.

Checkpoint: 1 Hz blinking measured on scope.

Phase 3: Debug & Polish ([2-3 days])

Goals:

  • Optional UART logs
  • Clean build system

Tasks:

  1. Add UART output with fixed baud rate.
  2. Add compile-time checks for clock settings.

Checkpoint: Logs print correctly and blink rate is stable.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———|———|—————-|———–| | Delay method | Busy loop vs timer | Timer if available | More stable timing | | Clock speed | 125 MHz vs lower | 125 MHz | Matches Pico defaults | | Code placement | Flash vs RAM | Flash for size | RAM only for timing-critical sections |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———|———|———-| | Unit Tests | Verify register helpers | GPIO config functions | | Integration Tests | Validate boot sequence | Boot + blink test | | Edge Case Tests | Failure handling | XOSC fail, PLL unlock |

6.2 Critical Test Cases

  1. Boot to blink: LED toggles within 1 second of reset.
  2. Clock validation: CLK_SYS output measures expected frequency.
  3. GPIO config: GPIO reads back as output-enabled.

6.3 Test Data

Expected blink period: 1.000 s ± 0.01 s
Expected CLK_SYS: 125 MHz ± 0.5%

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Wrong vector table | No boot, no UART | Verify linker symbol and alignment | | PLL not locked | Blink rate wrong | Wait for lock bit before switching | | GPIO not set to SIO | LED never toggles | Check IO_BANK function select |

7.2 Debugging Strategies

  • Early GPIO toggle: set a pin high in the reset handler.
  • Clock output: route CLK_SYS to a GPIO for scope validation.

7.3 Performance Traps

  • Running tight loops from flash can stall on cache misses.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Blink two LEDs with phase offset.
  • Add a configurable blink rate via UART input.

8.2 Intermediate Extensions

  • Move the delay loop to SRAM and compare jitter.
  • Add a watchdog reset and verify recovery.

8.3 Advanced Extensions

  • Build a minimal bootloader that jumps to two firmware slots.
  • Implement a HardFault handler that blinks an error code.

9. Real-World Connections

9.1 Industry Applications

  • Factory test firmware: minimal, deterministic bring-up code.
  • Safety-critical devices: tight control of boot flow and clocks.
  • Pico SDK: provides reference startup code you can compare against.
  • TinyUF2 bootloaders: shows stage-2 and flash handling.

9.3 Interview Relevance

  • Boot flow, memory map, and clock tree are common embedded interview topics.

10. Resources

10.1 Essential Reading

  • RP2040 datasheet, Boot ROM and Clocks sections
  • RP2350 datasheet, Memory Map and Boot sections

10.2 Video Resources

  • Raspberry Pi Pico SDK boot flow walkthroughs

10.3 Tools & Documentation

  • picotool for flashing and inspection
  • OpenOCD + GDB for stepping reset handlers

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain the stage-2 boot sequence without notes.
  • I can draw the RP2040 memory map accurately.
  • I can justify my PLL settings and clock routing.

11.2 Implementation

  • Blink timing matches scope measurement.
  • Boot always reaches main().
  • UART output is stable (if implemented).

11.3 Growth

  • I documented at least one hard bug and fix.
  • I can explain this project in an interview.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Boot without SDK and blink GPIO25 at 1 Hz.
  • Provide linker script and startup code.
  • Provide a short write-up with scope capture.

Full Completion:

  • All MVC criteria plus UART logs and clock verification.
  • 24-hour stability test passed.

Excellence (Going Above & Beyond):

  • Deterministic timing verified across two clock configurations.
  • Boot diagnostics via an early GPIO trace pin.