Learn RP2040/RP2350: From Zero to Embedded Systems Master

Goal: Deeply understand the RP2040 and RP2350 microcontrollers—from blinking LEDs to building custom USB devices, mastering PIO state machines, implementing bare-metal drivers, and leveraging dual-core architecture. You’ll learn how these chips work at the silicon level, why Raspberry Pi’s design choices make them unique, and how to build production-quality embedded systems that exploit every feature: DMA, PIO, multicore synchronization, and the RP2350’s ARM TrustZone security.


Why RP2040/RP2350 Matters

In January 2021, Raspberry Pi shocked the embedded world. They released the RP2040—a $1 microcontroller with features that cost $10+ from other vendors. Then in August 2024, they did it again with the RP2350, adding ARM Cortex-M33, RISC-V cores, and hardware security.

Why these chips are revolutionary:

  1. Programmable I/O (PIO): 8-12 independent state machines that can implement any digital protocol. Want to drive WS2812B LEDs, VGA displays, or custom interfaces? PIO handles it without CPU involvement.

  2. True Dual-Core: Two independent cores with hardware synchronization primitives (spinlocks, FIFOs). Not a gimmick—real parallel processing at $1.

  3. 12 DMA Channels: Move data at 100+ MB/sec without touching the CPU. Chain operations, control peripherals, stream audio—all in hardware.

  4. RP2350’s Dual Architecture: Choose between ARM Cortex-M33 (with DSP and floating-point) or RISC-V Hazard3 cores at runtime. Learn two architectures on one chip.

  5. Security That’s Actually Documented: Unlike “security through obscurity” vendors, Raspberry Pi publishes everything. ARM TrustZone, secure boot, OTP key storage—all open.

┌──────────────────────────────────────────────────────────────────────────────┐
│                        RP2040 vs RP2350 Comparison                            │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│   RP2040 (2021)                        RP2350 (2024)                         │
│   ═══════════════                      ═══════════════                        │
│                                                                               │
│   ┌─────────────────────┐              ┌─────────────────────────────────┐   │
│   │   Dual Cortex-M0+   │              │   Dual Cortex-M33 + DSP/FPU     │   │
│   │      @ 133 MHz      │              │        @ 150 MHz                │   │
│   └─────────────────────┘              │   ─ ─ ─ ─ OR ─ ─ ─ ─ ─          │   │
│                                        │   Dual RISC-V Hazard3           │   │
│                                        └─────────────────────────────────┘   │
│                                                                               │
│   264 KB SRAM                          520 KB SRAM                           │
│   ──────────────                       ──────────────                        │
│   8 PIO state machines                 12 PIO state machines                 │
│   30 GPIO pins                         30-48 GPIO pins                       │
│                                                                               │
│   No security features                 ┌─────────────────────────────────┐   │
│                                        │ • ARM TrustZone                 │   │
│                                        │ • Secure Boot                   │   │
│                                        │ • 8KB Anti-fuse OTP             │   │
│                                        │ • SHA-256 Acceleration          │   │
│                                        │ • Hardware TRNG                 │   │
│                                        │ • Glitch Detectors              │   │
│                                        └─────────────────────────────────┘   │
│                                                                               │
│   Power: ~180 µA dormant               Power: <18 µA dormant                 │
│   Price: $1                            Price: $0.80 (bulk)                   │
│                                                                               │
└──────────────────────────────────────────────────────────────────────────────┘

Real-World Impact

  • MicroPython & CircuitPython: The Pico became the reference platform for Python on microcontrollers
  • USB-PD Controllers: Hackers build USB Power Delivery analyzers with PIO
  • Retro Gaming: Emulators exploit dual-core and DMA for cycle-accurate NES/GB emulation
  • Logic Analyzers: $4 Pico + PIO = Saleae-quality logic capture
  • Audio Synthesizers: DMA + PIO = jitter-free I2S audio without interrupts

The RP2040/RP2350 Architecture

Memory Map

┌─────────────────────────────────────────────────────────────────────────────┐
│                         RP2040 Address Space (4GB)                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  0x0000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │                    ROM (16 KB)                              │ │
│              │    Boot code, floating-point routines, flash helpers       │ │
│  0x0000_4000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0x1000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │             XIP Flash (up to 16 MB)                         │ │
│              │      Execute-in-Place via QSPI, cacheable                   │ │
│  0x1100_0000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0x2000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │                   SRAM (264 KB)                             │ │
│              │   6 banks, striped for dual-core performance                │ │
│              │   Bank 0-3: 64 KB each (main memory)                        │ │
│              │   Bank 4-5: 4 KB each (for core-local stacks)               │ │
│  0x2004_2000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0x4000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │              APB Peripherals                                 │ │
│              │   SYSINFO, SYSCFG, Clocks, Resets, PSM, IO Bank,            │ │
│              │   Pads, Timers, Watchdog, RTC, Rosc, Vreg, etc.            │ │
│  0x4008_0000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0x5000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │              AHB-Lite Peripherals                            │ │
│              │   DMA, USB, PIO0, PIO1, XIP_CTRL, SIO                       │ │
│  0x5020_0000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0xD000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │           SIO (Single-cycle IO)                             │ │
│              │   GPIO, Spinlocks, Divider, Interpolators                   │ │
│              │   Core-local access (no bus arbitration)                    │ │
│  0xD000_0200 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  0xE000_0000 ┌────────────────────────────────────────────────────────────┐ │
│              │         Cortex-M0+ Private Peripheral Bus                   │ │
│              │   NVIC, SysTick, SCB (System Control Block)                 │ │
│  0xE010_0000 └────────────────────────────────────────────────────────────┘ │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

The Clock System

                    ┌─────────────────────────────────────────────────────────┐
                    │               RP2040 Clock Architecture                  │
                    └─────────────────────────────────────────────────────────┘
                                              │
          ┌───────────────────────────────────┼───────────────────────────────┐
          │                                   │                               │
          ▼                                   ▼                               ▼
┌─────────────────────┐           ┌─────────────────────┐         ┌───────────────────┐
│    ROSC (Ring       │           │     XOSC (Crystal   │         │   USB/ADC PLL     │
│   Oscillator)       │           │    Oscillator)      │         │                   │
│   ~6.5 MHz          │           │    12 MHz           │         │                   │
│   (unpredictable)   │           │    (precise)        │         │                   │
└─────────┬───────────┘           └──────────┬──────────┘         └─────────┬─────────┘
          │                                  │                              │
          │     ┌────────────────────────────┘                              │
          │     │                                                           │
          │     ▼                                                           │
          │   ┌─────────────────────────────────────────┐                   │
          │   │              PLL_SYS                    │                   │
          │   │   12 MHz × 125 = 1500 MHz               │                   │
          │   │   ÷ 6 ÷ 2 = 125 MHz (default)           │                   │
          │   └────────────────┬────────────────────────┘                   │
          │                    │                                            │
          ▼                    ▼                                            ▼
    ┌───────────────────────────────────────────────────────────────────────────┐
    │                          Clock Distribution                               │
    │                                                                           │
    │   CLK_SYS ──────────────────────────────────────────────────▶ CPU, Bus   │
    │              │                                                            │
    │              ├──▶ CLK_REF ───────────────────────────────▶ Watchdog      │
    │              │                                                            │
    │              ├──▶ CLK_PERI ──────────────────────────────▶ UART, SPI     │
    │              │                                                            │
    │              ├──▶ CLK_USB ───────────────────────────────▶ USB (48 MHz)  │
    │              │                                                            │
    │              ├──▶ CLK_ADC ───────────────────────────────▶ ADC (48 MHz)  │
    │              │                                                            │
    │              └──▶ CLK_RTC ───────────────────────────────▶ RTC (46875 Hz)│
    │                                                                           │
    └───────────────────────────────────────────────────────────────────────────┘

PIO (Programmable I/O) Architecture

The RP2040’s most unique feature. Each PIO block is essentially a mini-processor:

┌──────────────────────────────────────────────────────────────────────────────┐
│                         PIO Block Architecture                                │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                       Instruction Memory (32 × 16-bit)                   │ │
│  │   Shared by all 4 state machines in this PIO block                      │ │
│  └──────────────────────────────────┬──────────────────────────────────────┘ │
│                                     │                                        │
│         ┌───────────────────────────┼───────────────────────────┐            │
│         │                           │                           │            │
│         ▼                           ▼                           ▼            │
│  ┌─────────────┐             ┌─────────────┐             ┌─────────────┐     │
│  │    SM0      │             │    SM1      │             │  SM2, SM3   │     │
│  ├─────────────┤             ├─────────────┤             │   (same)    │     │
│  │ • PC        │             │ • PC        │             └─────────────┘     │
│  │ • ISR (32b) │             │ • ISR (32b) │                                 │
│  │ • OSR (32b) │             │ • OSR (32b) │  ◀── Output Shift Register      │
│  │ • X (32b)   │             │ • X (32b)   │  ◀── Scratch register           │
│  │ • Y (32b)   │             │ • Y (32b)   │  ◀── Scratch register           │
│  │ • Divider   │             │ • Divider   │  ◀── Clock divider (16.8 frac)  │
│  └──────┬──────┘             └──────┬──────┘                                 │
│         │                           │                                        │
│         ▼                           ▼                                        │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                           TX/RX FIFOs                                    │ │
│  │   • 4-deep TX FIFO per SM (32-bit entries)                              │ │
│  │   • 4-deep RX FIFO per SM (32-bit entries)                              │ │
│  │   • Can be combined: 8-deep TX or RX per SM                             │ │
│  └──────────────────────────────────┬──────────────────────────────────────┘ │
│                                     │                                        │
│                                     ▼                                        │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                        GPIO Mapping                                      │ │
│  │   Each SM can access any GPIO for input/output                          │ │
│  │   Side-set: up to 5 pins controlled as "side effect" of instruction     │ │
│  │   SET: up to 5 pins controlled by SET instruction                       │ │
│  │   OUT: up to 32 pins for parallel data output                           │ │
│  │   IN: up to 32 pins for parallel data input                             │ │
│  └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                               │
└──────────────────────────────────────────────────────────────────────────────┘

PIO Instruction Set (only 9 instructions):
═══════════════════════════════════════════

  JMP   ─── Jump (conditional: !X, X--, !Y, Y--, X!=Y, PIN, !OSRE)
  WAIT  ─── Stall until condition (GPIO, PIN, IRQ)
  IN    ─── Shift bits into ISR from source (PINS, X, Y, NULL, ISR, OSR)
  OUT   ─── Shift bits out of OSR to destination (PINS, X, Y, NULL, PINDIRS, PC, ISR, EXEC)
  PUSH  ─── Push ISR to RX FIFO
  PULL  ─── Pull from TX FIFO to OSR
  MOV   ─── Move/copy between registers
  IRQ   ─── Set/clear/wait IRQ flag
  SET   ─── Write immediate value to destination (PINS, X, Y, PINDIRS)

Each instruction can include:
  • Delay: 0-31 cycles after instruction
  • Side-set: Control up to 5 pins as side effect

DMA Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                         RP2040 DMA Controller                                 │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│   12 Independent DMA Channels                                                 │
│   ═══════════════════════════                                                 │
│                                                                               │
│   ┌─────────────────────────────────────────────────────────────────────────┐│
│   │  Channel Configuration:                                                  ││
│   │                                                                          ││
│   │   READ_ADDR   ────▶  Source address (auto-increment optional)           ││
│   │   WRITE_ADDR  ────▶  Destination address (auto-increment optional)      ││
│   │   TRANS_COUNT ────▶  Number of transfers                                ││
│   │   CTRL        ────▶  Transfer size, triggers, chaining                  ││
│   └─────────────────────────────────────────────────────────────────────────┘│
│                                                                               │
│   Transfer Sizes:                                                             │
│   • Byte (8-bit)                                                              │
│   • Halfword (16-bit)                                                         │
│   • Word (32-bit)                                                             │
│                                                                               │
│   Trigger Sources (DREQ):                                                     │
│   ┌─────────────────────────────────────────────────────────────────────────┐│
│   │  DREQ_PIO0_TX0-TX3    │  DREQ_PIO1_TX0-TX3    │  DREQ_SPI0_TX          ││
│   │  DREQ_PIO0_RX0-RX3    │  DREQ_PIO1_RX0-RX3    │  DREQ_SPI0_RX          ││
│   │  DREQ_UART0_TX        │  DREQ_UART1_TX        │  DREQ_SPI1_TX          ││
│   │  DREQ_UART0_RX        │  DREQ_UART1_RX        │  DREQ_SPI1_RX          ││
│   │  DREQ_ADC             │  DREQ_XIP_STREAM      │  DREQ_XIP_SSITX        ││
│   │  DREQ_I2C0_TX         │  DREQ_I2C1_TX         │  DREQ_PWM_WRAP0-7      ││
│   │  DREQ_I2C0_RX         │  DREQ_I2C1_RX         │  DREQ_TIMER0-3         ││
│   └─────────────────────────────────────────────────────────────────────────┘│
│                                                                               │
│   Advanced Features:                                                          │
│   ┌─────────────────────────────────────────────────────────────────────────┐│
│   │  • Channel Chaining: ch0 completes → triggers ch1                       ││
│   │  • Ring Buffer: Wrap address at power-of-2 boundary                     ││
│   │  • Sniff: CRC/checksum calculation during transfer                      ││
│   │  • Pacing: Rate-limit transfers via timer                               ││
│   │  • Control Block: One DMA writes to another's registers                 ││
│   └─────────────────────────────────────────────────────────────────────────┘│
│                                                                               │
│   Aggregate Bandwidth: >100 MB/sec (multiple channels)                        │
│                                                                               │
└──────────────────────────────────────────────────────────────────────────────┘

Multicore Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                     Dual-Core Synchronization Primitives                      │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  ┌─────────────────────┐                       ┌─────────────────────┐       │
│  │       Core 0        │                       │       Core 1        │       │
│  │   (Cortex-M0+)      │                       │   (Cortex-M0+)      │       │
│  │                     │                       │                     │       │
│  │  Startup: Active    │                       │  Startup: Sleeping  │       │
│  │  Vector Table: 0x0  │                       │  Woken by Core 0    │       │
│  └──────────┬──────────┘                       └──────────┬──────────┘       │
│             │                                             │                  │
│             │           ┌─────────────────┐               │                  │
│             └──────────▶│  Hardware FIFOs │◀──────────────┘                  │
│                         │                 │                                  │
│                         │  Core0→Core1:   │                                  │
│                         │  • 8 entries    │  (RP2040)                        │
│                         │  • 32-bit each  │  (4 entries on RP2350)           │
│                         │                 │                                  │
│                         │  Core1→Core0:   │                                  │
│                         │  • 8 entries    │                                  │
│                         │  • 32-bit each  │                                  │
│                         └─────────────────┘                                  │
│                                                                               │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                      32 Hardware Spinlocks                               │ │
│  │                                                                          │ │
│  │   Spinlock 0-15:  Reserved by SDK                                       │ │
│  │   Spinlock 16-31: Available for user code                               │ │
│  │                                                                          │ │
│  │   Behavior:                                                              │ │
│  │   • Read: Returns 0 if locked, non-zero if acquired                     │ │
│  │   • Write: Releases lock                                                │ │
│  │   • If both cores try same lock on same cycle: Core 0 wins              │ │
│  └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                               │
│  Memory: No per-CPU caches (coherent SRAM)                                    │
│  Atomics: Cortex-M0+ lacks atomic instructions → use spinlocks               │
│                                                                               │
└──────────────────────────────────────────────────────────────────────────────┘

Concept Summary Table

Concept Cluster What You Need to Internalize
Memory Map Everything is memory-mapped. Peripherals are just addresses. The XIP flash executes code directly via cache.
Clock System ROSC is fast but imprecise. XOSC + PLL gives you 125MHz. USB needs exactly 48MHz. Configure clocks before peripherals.
PIO 9 instructions, 32 cycles per instruction max, side-effects everywhere. PIO is deterministic—one instruction per clock when running.
DMA Don’t poll—let DMA move data. Chain channels for complex transfers. DMA can write to other DMA channel registers.
Multicore Core 1 sleeps at boot. Wake it, give it work, synchronize via FIFO or spinlocks. No cache coherency issues (no caches).
GPIO 30 pins, each with multiple functions. Pull-ups/downs configurable. Drive strength and slew rate matter for high-speed.
Interrupts NVIC handles priorities. DMA and PIO generate IRQs. Keep ISRs short—set flags, let main loop handle work.

Deep Dive Reading by Concept

This section maps each concept from above to specific book chapters for deeper understanding. Read these before or alongside the projects to build strong mental models.

Hardware Architecture & Memory

Concept Book & Chapter
ARM Cortex-M0+ Core “Introduction to Computer Organization: ARM Edition” by Robert G. Plantz — Ch. 1-5 (ARM basics)
Memory-Mapped I/O “The Secret Life of Programs” by Jonathan E. Steinhart — Ch. 8: “Memory”
XIP Flash Execution “RP2040 Datasheet” by Raspberry Pi — Section 2.6 (SSI/QSPI)
SRAM Banks “Dive Into Systems” by Matthews, Newhall, Webb — Ch. 6: “Memory Hierarchy”

PIO Programming

Concept Book & Chapter
State Machine Basics “RP2040 Assembly Language Programming” by Stephen Smith — Ch. 10-12 (PIO chapters)
PIO Instruction Set “RP2040 Datasheet” — Section 3 (PIO)
Timing & Side-sets “RP2040 PIO Docs” (official SDK) — All examples

DMA & Performance

Concept Book & Chapter
DMA Fundamentals “Making Embedded Systems, 2nd Edition” by Elecia White — Ch. 9: “Getting Into Trouble” (DMA section)
Channel Chaining “RP2040 Datasheet” — Section 2.5 (DMA)
Peripheral DREQs “The Linux Programming Interface” by Michael Kerrisk — Ch. 63 (for concepts, not RP2040-specific)

Multicore & Synchronization

Concept Book & Chapter
Spinlocks “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau — Ch. 28: “Locks”
Producer-Consumer “Operating Systems: Three Easy Pieces” — Ch. 30: “Condition Variables”
Memory Barriers “C++ Concurrency in Action” by Anthony Williams — Ch. 5

USB Programming

Concept Book & Chapter
USB Protocol Basics “USB Complete, 5th Edition” by Jan Axelson — Ch. 1-6
HID Devices “USB Complete” — Ch. 11: “Human Interface Devices”
TinyUSB Stack TinyUSB Documentation — GitHub examples

Essential Reading Order

For maximum comprehension, read in this order:

  1. Foundation (Week 1):
    • RP2040 Datasheet Section 1 (System overview)
    • Introduction to Computer Organization Ch. 1-3 (ARM basics)
  2. Peripherals (Week 2):
    • RP2040 Datasheet Sections 2.1-2.4 (GPIO, Clocks, Resets)
    • Making Embedded Systems Ch. 4 (I/O)
  3. PIO Deep Dive (Week 3):
    • RP2040 Assembly Language Programming Ch. 10-12
    • RP2040 Datasheet Section 3 (all of it)
  4. Advanced Topics (Week 4+):
    • DMA, multicore, USB as needed by projects

Project 1: Bare-Metal Blinky (No SDK)

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Assembly, Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Bare-Metal Programming / ARM Architecture
  • Software or Tool: GCC ARM Toolchain, RP2040 Datasheet
  • Main Book: “RP2040 Assembly Language Programming” by Stephen Smith

What you’ll build: A LED blinker that works without the Pico SDK—you’ll configure clocks, GPIO, and boot from scratch by reading only the datasheet.

Why it teaches RP2040: The SDK hides everything. To truly understand the RP2040, you must configure the XOSC, PLL, reset peripherals, and set up GPIO yourself. This is how you learn what “memory-mapped I/O” really means.

Core challenges you’ll face:

  • Understanding the boot sequence → maps to RP2040’s unique 2-stage boot
  • Configuring clocks without SDK helpers → maps to XOSC, PLL, clock glitchless mux
  • Writing a linker script → maps to where code lives in flash vs RAM
  • Computing the CRC32 for stage 2 → maps to boot ROM validation

Key Concepts:

  • Two-Stage Boot: RP2040 Datasheet Section 2.8 - Bootrom
  • Clock Configuration: RP2040 Datasheet Section 2.15 - Clocks
  • GPIO Registers: RP2040 Datasheet Section 2.19 - GPIO
  • Linker Scripts: “Bare Metal C” by Steve Oualline — Ch. 3

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: C programming, basic understanding of pointers and memory, familiarity with make or CMake, ARM assembly basics helpful but not required


Real World Outcome

You’ll have a binary that runs on the Pico without any SDK involvement. When you power on the board, the LED blinks at a predictable rate because you configured the system clock.

Example Output:

$ arm-none-eabi-objcopy -O binary blinky.elf blinky.bin
$ ./crc32_patch blinky.bin   # Add CRC32 to first 252 bytes
Patching blinky.bin: CRC32 = 0x5A34B72D

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

# LED blinks at exactly 1Hz because you configured:
#   XOSC: 12 MHz crystal
#   PLL:  12 MHz × 125 / 6 / 2 = 125 MHz
#   Timer: 125,000,000 cycles = 1 second

The Core Question You’re Answering

“What happens between power-on and main()? Where does the first instruction come from?”

Before you write any code, sit with this question. On a PC, the BIOS loads your OS. On RP2040, the boot ROM validates your flash image, copies 256 bytes into SRAM, and jumps to it. That 256-byte “stage 2” then configures flash for XIP and jumps to your real code.


Concepts You Must Understand First

Stop and research these before coding:

  1. Memory-Mapped I/O
    • How do you “talk” to hardware by reading/writing addresses?
    • What happens when you write *(volatile uint32_t*)0x400140CC = 1;?
    • Why is volatile crucial for hardware registers?
    • Book Reference: “The Secret Life of Programs” Ch. 8 - Jonathan Steinhart
  2. ARM Cortex-M0+ Startup
    • What are the first two words in the vector table?
    • How does the CPU know where to start executing?
    • What’s the difference between MSP and PSP?
    • Book Reference: “Introduction to Computer Organization: ARM Edition” Ch. 1-3 - Robert Plantz
  3. Reset and Clock Control
    • Why must you reset peripherals before using them?
    • What’s a PLL and why do you need it?
    • What’s the “glitchless mux” and why does it matter?
    • Book Reference: “Making Embedded Systems, 2nd Edition” Ch. 5 - Elecia White

Questions to Guide Your Design

Before implementing, think through these:

  1. Boot Sequence
    • Where does the boot ROM live? (Hint: 0x00000000)
    • What does the boot ROM check in your flash image?
    • What goes in the first 256 bytes of flash?
  2. Clocking
    • What frequency does the ring oscillator run at? Is it stable?
    • How do you start the crystal oscillator (XOSC)?
    • How do you configure PLL_SYS for 125 MHz?
  3. GPIO
    • How many GPIO pads does the RP2040 have?
    • What’s the difference between IO_BANK0 and PADS_BANK0?
    • How do you select which function a pin performs?

Thinking Exercise

Trace the Boot Sequence

Before coding, diagram what happens from power-on to your code:

Power On
    │
    ▼
Boot ROM (in mask ROM at 0x00000000)
    │
    ├── Check: Is USB button pressed? → Enter USB boot mode
    │
    ├── Check: Is flash present and valid?
    │         Read first 256 bytes, compute CRC32
    │         If CRC matches word at offset 252...
    │
    ▼
Copy 256 bytes to SRAM at 0x20000000
Jump to 0x20000001 (thumb mode)
    │
    ▼
Your Stage 2 Boot Code
    ├── Configure QSPI flash for faster reads
    ├── Enable XIP (execute-in-place)
    │
    ▼
Jump to real application in flash
(Your main() finally runs!)

Questions while tracing:

  • Why does the jump address end in 1 (0x20000001)?
  • Why only 256 bytes for stage 2?
  • What happens if the CRC doesn’t match?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain what happens when an ARM Cortex-M processor comes out of reset.”
  2. “What is the vector table and what does the first entry contain?”
  3. “Why do embedded systems use PLLs for clock generation?”
  4. “What’s the difference between volatile and non-volatile in C? When would you use volatile?”
  5. “How do you configure a GPIO pin for output on a bare-metal system?”

Hints in Layers

Hint 1: Start with the datasheet Read RP2040 Datasheet Section 2.8 (Boot) completely. Draw the boot flowchart.

Hint 2: Study the SDK’s boot2 Look at pico-sdk/src/rp2_common/boot_stage2/. See how they configure QSPI for XIP.

Hint 3: Clock configuration sequence

  1. Enable XOSC, wait for stable
  2. Configure PLL_SYS (FBDIV, POSTDIV1, POSTDIV2)
  3. Wait for PLL lock
  4. Switch clk_sys to PLL via glitchless mux

Hint 4: Use existing bare-metal repos Check out vxj9800/bareMetalRP2040 for a complete working example. Don’t copy—understand, then implement.


Books That Will Help

Topic Book Chapter
ARM Startup Code “Introduction to Computer Organization: ARM Edition” by Robert Plantz Ch. 1-5
Bare Metal Basics “Bare Metal C” by Steve Oualline All
RP2040 Specifics “RP2040 Assembly Language Programming” by Stephen Smith Ch. 1-4
Linker Scripts “The Art of Assembly Language” by Randall Hyde Ch. 3
Makefile Mastery “The GNU Make Book” by John Graham-Cumming Ch. 1-4

Implementation Hints

Your project structure should look like:

bare-metal-blinky/
├── Makefile
├── linker.ld         # Where things go in memory
├── startup.c         # Vector table, reset handler
├── boot2.S           # Stage 2 bootloader (256 bytes)
├── clocks.c          # XOSC and PLL configuration
├── gpio.c            # GPIO control
└── main.c            # Your blinky code

The vector table lives at the start of your code. First word = initial SP, second word = reset handler address.

To configure clocks:

  1. Write to XOSC_CTRL to start crystal
  2. Wait for XOSC_STATUS.STABLE
  3. Configure PLL_SYS_CS, PLL_SYS_PWR, PLL_SYS_FBDIV_INT, PLL_SYS_PRIM
  4. Switch CLK_SYS_CTRL to PLL source

GPIO output requires:

  1. Take GPIO block out of reset (RESETS)
  2. Configure pad (PADS_BANK0_GPIO25)
  3. Set function to SIO (IO_BANK0_GPIO25_CTRL)
  4. Set direction (SIO_GPIO_OE_SET)
  5. Toggle output (SIO_GPIO_OUT_XOR)

Learning Milestones

  1. Stage 2 boots correctly → You understand the CRC32 requirement and boot sequence
  2. Clock runs at 125 MHz → You understand PLLs and clock distribution
  3. LED blinks at exact 1Hz → You understand GPIO and timer configuration from registers

Project 2: PIO LED Strip Controller (WS2812B)

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C (with PIO assembly)
  • Alternative Programming Languages: MicroPython, Rust
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: PIO Programming / Protocol Implementation
  • Software or Tool: Pico SDK, WS2812B LED strip
  • Main Book: “RP2040 Assembly Language Programming” by Stephen Smith

What you’ll build: A WS2812B LED strip controller using PIO—no CPU cycles spent on precise timing. Rainbow animations, color chasing, and reactive effects, all offloaded to PIO state machines.

Why it teaches RP2040: WS2812B requires sub-microsecond timing precision. On most microcontrollers, you’d bit-bang with interrupts disabled. On RP2040, PIO handles it perfectly while your CPU does other work. This is why PIO exists.

Core challenges you’ll face:

  • Writing your first PIO program → maps to PIO instruction set and timing
  • Meeting WS2812B timing requirements → maps to clock dividers and cycle counting
  • Managing the TX FIFO → maps to PIO data streaming with DMA
  • Color encoding (GRB format) → maps to bit ordering and data packing

Key Concepts:

  • PIO State Machines: RP2040 Datasheet Section 3 - PIO
  • WS2812B Protocol: Datasheet (800 kHz, 0.35µs/0.9µs timing)
  • PIO Assembly: “RP2040 Assembly Language Programming” Ch. 10-12
  • DMA with PIO: Cornell ECE4760 DMA tutorial

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Project 1 or basic Pico SDK experience, understanding of binary/hex, a WS2812B LED strip (8-60 LEDs)


Real World Outcome

Connect a WS2812B strip to GPIO pin (e.g., GP2), run your code, and watch the LEDs display a smooth rainbow that cycles endlessly—all driven by PIO + DMA while your CPU could be doing literally anything else.

Example Output:

$ cmake --build build
$ picotool load build/ws2812.uf2 -f

# Console output:
WS2812B Controller initialized
  PIO: 0, SM: 0, GPIO: 2
  Frequency: 800 kHz
  LEDs: 30

Starting rainbow animation...
Frame 0: [R:255 G:0 B:0] [R:247 G:32 B:0] [R:239 G:64 B:0] ...
Frame 1: [R:253 G:8 B:0] [R:245 G:40 B:0] [R:237 G:72 B:0] ...
DMA transfers: 30/sec
CPU usage: 0.3% (DMA handles everything!)

# The LED strip shows a continuously rotating rainbow pattern
# Each frame transition is butter-smooth at 30 FPS

The Core Question You’re Answering

“How can I generate precise timing signals without wasting CPU cycles or disabling interrupts?”

Before you write any code, sit with this question. Traditional bit-banging requires disabling interrupts for the entire LED update (1.25µs × 24 bits × LEDs). For 100 LEDs, that’s 3ms with interrupts disabled! PIO runs independently—it’s like having a tiny dedicated processor.


Concepts You Must Understand First

Stop and research these before coding:

  1. WS2812B Protocol
    • What’s the timing for a ‘0’ bit vs a ‘1’ bit?
    • What’s the reset/latch pulse duration?
    • Why is the color order GRB instead of RGB?
    • Reference: WS2812B Datasheet
  2. PIO Instruction Set
    • What does each of the 9 PIO instructions do?
    • How do you count cycles in PIO assembly?
    • What are side-sets and why are they useful?
    • Book Reference: “RP2040 Assembly Language Programming” Ch. 10 - Stephen Smith
  3. Clock Dividers
    • How do you calculate the PIO clock divider for 800 kHz?
    • What’s the relationship between system clock, divider, and output frequency?
    • What resolution does the fractional divider provide?
    • Book Reference: RP2040 Datasheet Section 3.5.4

Questions to Guide Your Design

Before implementing, think through these:

  1. Timing Analysis
    • At 125 MHz system clock, how many cycles is 1.25µs?
    • How do you use delays and side-sets to hit exact timing?
    • How do you handle the 50µs+ reset pulse?
  2. Data Flow
    • How does data get from your LED buffer to PIO?
    • Should you use CPU push or DMA?
    • How many bits shift out per PIO cycle?
  3. Animation Architecture
    • How do you double-buffer to avoid tearing?
    • How do you synchronize frame updates with DMA completion?
    • How do you implement smooth HSV-to-RGB color transitions?

Thinking Exercise

Analyze the PIO Program

Study this PIO program for WS2812B and trace its execution:

.program ws2812
.side_set 1

.wrap_target
bitloop:
    out x, 1        side 0 [T3-1]  ; Shift 1 bit out to X, drive low
    jmp !x do_zero  side 1 [T1-1]  ; If bit is 0, jump. Drive high.
do_one:
    jmp bitloop     side 1 [T2-1]  ; Stay high for '1' bit
do_zero:
    nop             side 0 [T2-1]  ; Stay low for '0' bit
.wrap

Questions while tracing:

  • What’s the total cycle count for a ‘0’ bit? A ‘1’ bit?
  • If T1=3, T2=3, T3=4, and the clock runs at 10× bit rate, what’s T0+T1+T2?
  • How does side_set control the output pin without explicit instructions?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What is PIO on the RP2040 and why would you use it?”
  2. “How do you implement a custom protocol using PIO?”
  3. “Explain the difference between bit-banging and hardware peripherals for timing-critical protocols.”
  4. “How does DMA reduce CPU overhead in embedded systems?”
  5. “What’s the WS2812B protocol and why is its timing critical?”

Hints in Layers

Hint 1: Start with the SDK example Run pico-examples/pio/ws2812/ first. See it work, then study the .pio file.

Hint 2: Timing math WS2812B bit period = 1.25µs. At 125 MHz, that’s 156.25 cycles. With clock divider, aim for 10 PIO cycles per bit period. Divider = 125MHz / (800kHz × 10) = 15.625

Hint 3: Side-set is your friend Side-set changes the output pin as a “side effect” of any instruction. This lets you control timing precisely while doing other work.

Hint 4: Use DMA for continuous streaming Configure DMA to feed the TX FIFO automatically. Chain DMA channels for continuous animation without CPU intervention.


Books That Will Help

Topic Book Chapter
PIO Deep Dive “RP2040 Assembly Language Programming” by Stephen Smith Ch. 10-12
PIO Reference “RP2040 Datasheet” by Raspberry Pi Section 3 (all)
DMA Basics “Making Embedded Systems, 2nd Edition” by Elecia White Ch. 9
LED Protocols WS2812B Datasheet All

Implementation Hints

The WS2812B timing window is:

  • T0H (0-bit high): 0.35µs ± 0.15µs
  • T0L (0-bit low): 0.9µs ± 0.15µs
  • T1H (1-bit high): 0.9µs ± 0.15µs
  • T1L (1-bit low): 0.35µs ± 0.15µs
  • Reset: >50µs low

Your PIO program should:

  1. Pull 32 bits from FIFO (or autopull 24 bits)
  2. For each bit, output HIGH, then conditionally stay high/go low based on bit value
  3. End with LOW

For DMA setup:

  1. Configure channel to read from your LED buffer
  2. Write to PIO TX FIFO (sm_txf[0] register)
  3. Set DREQ to PIO0_TX0 or similar
  4. On completion, trigger next frame

Learning Milestones

  1. Single LED lights up → You can push data to PIO TX FIFO
  2. 8 LEDs show correct colors → Your timing and bit ordering are correct
  3. DMA drives animation → You understand autonomous DMA transfers

Project 3: USB HID Keyboard Emulator

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, CircuitPython
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: USB Protocol / HID Devices
  • Software or Tool: Pico SDK, TinyUSB
  • Main Book: “USB Complete, 5th Edition” by Jan Axelson

What you’ll build: A device that appears to your computer as a USB keyboard. Press buttons on the Pico, and it types text. Add macros, key combinations, and even a rubber ducky-style script interpreter.

Why it teaches RP2040: USB is everywhere but rarely understood. The RP2040’s native USB 1.1 controller and TinyUSB stack let you implement custom HID devices. You’ll understand USB descriptors, endpoints, and the HID report protocol.

Core challenges you’ll face:

  • USB descriptors and enumeration → maps to how the host discovers your device
  • HID report format → maps to encoding keystrokes as data packets
  • Endpoint configuration → maps to USB data transfer mechanisms
  • TinyUSB stack integration → maps to using professional USB middleware

Key Concepts:

  • USB Enumeration: “USB Complete” Ch. 4 - Jan Axelson
  • HID Protocol: “USB Complete” Ch. 11 - Jan Axelson
  • TinyUSB Architecture: TinyUSB GitHub documentation
  • RP2040 USB Controller: RP2040 Datasheet Section 4.1

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: C programming, basic understanding of USB (host/device/endpoint concepts), Pico SDK experience


Real World Outcome

Plug your Pico into any computer. It appears as a keyboard named “Pico Macropad”. Press a button, and it types your email signature, opens applications, or performs complex macros—all without installing drivers.

Example Output:

$ cmake --build build
$ picotool load build/usb_keyboard.uf2 -f

# On your computer (dmesg or Device Manager):
[USB] New device: Pico Macropad (VID:2E8A PID:000A)
[USB] HID Keyboard detected
[USB] Device ready

# When you press buttons on the Pico:
Button 0 pressed → Types: "Hello from RP2040!"
Button 1 pressed → Types: Ctrl+Alt+T (opens terminal on Linux)
Button 2 pressed → Types: Your email signature (multi-line)
Button 3 pressed → Types: "#!/bin/bash\necho 'Automated!'"

# The Pico acts as a USB keyboard—no drivers needed!

The Core Question You’re Answering

“How does a USB device tell the host what it is and what it can do?”

Before you write any code, sit with this question. When you plug in a keyboard, how does your computer know it’s a keyboard? The answer is USB enumeration—a dialog where the device sends descriptors (device, configuration, interface, endpoint, HID report) that fully describe its capabilities.


Concepts You Must Understand First

Stop and research these before coding:

  1. USB Architecture
    • What’s the difference between USB host and device?
    • What are endpoints and why do you need IN and OUT directions?
    • What’s the difference between Control, Bulk, Interrupt, and Isochronous transfers?
    • Book Reference: “USB Complete” Ch. 1-4 - Jan Axelson
  2. USB Descriptors
    • What information is in a Device Descriptor?
    • What’s the hierarchy: Device → Configuration → Interface → Endpoint?
    • How do String Descriptors work?
    • Book Reference: “USB Complete” Ch. 5-6 - Jan Axelson
  3. HID Protocol
    • What’s a HID Report Descriptor and why is it complex?
    • How are keyboard scan codes different from ASCII?
    • What’s the modifier byte and how do you send Ctrl+C?
    • Book Reference: “USB Complete” Ch. 11 - Jan Axelson

Questions to Guide Your Design

Before implementing, think through these:

  1. Descriptor Design
    • What VID/PID will you use? (Raspberry Pi provides test PIDs)
    • How many interfaces do you need? (HID keyboard + maybe CDC serial for debug?)
    • What endpoint address and polling interval for keyboard?
  2. HID Reports
    • How do you encode “press ‘A’” vs “press Shift+A”?
    • How do you handle key release?
    • How do you send a string of characters?
  3. Host Interaction
    • How do you handle USB suspend/resume?
    • What happens if the host sends a SET_IDLE request?
    • How do you handle LED status (Caps Lock, Num Lock)?

Thinking Exercise

Decode a HID Keyboard Report

USB HID keyboards send 8-byte reports:

Byte 0: Modifier keys (bit field)
        Bit 0: Left Ctrl
        Bit 1: Left Shift
        Bit 2: Left Alt
        Bit 3: Left GUI (Windows/Command key)
        Bits 4-7: Right modifiers

Byte 1: Reserved (0x00)

Bytes 2-7: Up to 6 simultaneous key scan codes (0x00 = no key)

Decode these reports:

  • [0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00] → What key?
  • [0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00] → What key?
  • [0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00] → What key combination?

Questions:

  • What’s scan code 0x04? (Hint: it’s not ‘D’)
  • How would you send “Hello” as 5 separate key reports?
  • What report do you send for “key release”?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the USB enumeration process from device connection to ready.”
  2. “What’s the difference between USB HID and CDC device classes?”
  3. “How do USB endpoints work and why are they unidirectional?”
  4. “What’s a HID report descriptor and why is it so complex?”
  5. “How would you implement a composite USB device (keyboard + mouse)?”

Hints in Layers

Hint 1: Start with pico-examples Run pico-examples/usb/device/dev_hid_composite/. It’s a working keyboard+mouse.

Hint 2: Understand the descriptor hierarchy Study tusb_config.h and usb_descriptors.c. Every byte has meaning.

Hint 3: HID scan codes Scan codes are NOT ASCII. 0x04 = ‘a’, 0x05 = ‘b’, etc. Look up “HID Usage Tables” PDF.

Hint 4: Character-to-scancode mapping Create a lookup table: 'a' → (0x04, 0x00), 'A' → (0x04, 0x02) (shift modifier).


Books That Will Help

Topic Book Chapter
USB Fundamentals “USB Complete, 5th Edition” by Jan Axelson Ch. 1-6
HID Devices “USB Complete” by Jan Axelson Ch. 11
TinyUSB Usage TinyUSB GitHub Documentation Examples
RP2040 USB “RP2040 Datasheet” by Raspberry Pi Section 4.1

Implementation Hints

TinyUSB callback structure:

  • tud_hid_report_complete_cb() - Report sent successfully
  • tud_hid_get_report_cb() - Host requests a report (GET_REPORT)
  • tud_hid_set_report_cb() - Host sends a report (SET_REPORT for LED status)

To send a keystroke:

1. Call tud_hid_keyboard_report() with modifier and keycode
2. Wait for tud_hid_report_complete_cb()
3. Call tud_hid_keyboard_report() with all zeros (key release)
4. Wait for completion

For typing strings:

  1. Convert ASCII to HID scan code
  2. Determine if shift is needed
  3. Send press, wait, send release, wait
  4. Repeat for each character

USB descriptors are byte arrays. Use the HID report descriptor tool to generate them.


Learning Milestones

  1. Device enumerates → Host recognizes your keyboard
  2. Single keypress works → You understand HID reports
  3. Full string typing works → You’ve mastered the ASCII-to-scancode conversion

Project 4: Logic Analyzer with PIO

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Python (host software)
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: PIO / DMA / Signal Capture
  • Software or Tool: Pico SDK, sigrok/PulseView (host), SUMP protocol
  • Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White

What you’ll build: A USB logic analyzer that captures digital signals at up to 100+ MHz on multiple channels. Compatible with sigrok/PulseView for professional waveform analysis.

Why it teaches RP2040: This project combines PIO (for precise signal capture), DMA (for high-speed data transfer), USB CDC (for host communication), and multicore (capture on core 1, communicate on core 0). It’s the ultimate RP2040 integration project.

Core challenges you’ll face:

  • High-speed PIO sampling → maps to PIO as a parallel input shift register
  • DMA ring buffer capture → maps to continuous data acquisition
  • SUMP protocol implementation → maps to standard logic analyzer communication
  • Trigger detection → maps to PIO pattern matching

Key Concepts:

  • PIO Input Capture: RP2040 Datasheet Section 3.5.6 (IN instruction)
  • DMA Ring Buffers: RP2040 Datasheet Section 2.5.3
  • SUMP Protocol: sigrok wiki / original SUMP documentation
  • USB CDC: TinyUSB CDC examples

Difficulty: Advanced Time estimate: 2-4 weeks Prerequisites: PIO experience (Project 2), USB experience (Project 3), understanding of digital signals and timing


Real World Outcome

Open PulseView, connect to your Pico, and capture SPI, I2C, UART, or any digital signals. See waveforms, decode protocols, and debug hardware—using a $4 board instead of a $100+ commercial logic analyzer.

Example Output:

$ cmake --build build
$ picotool load build/logic_analyzer.uf2 -f

# In PulseView:
# 1. Select "Openbench Logic Sniffer & SUMP compatible"
# 2. Choose your Pico's serial port
# 3. Scan for devices

Device found: RP2040 Logic Analyzer
  Channels: 8
  Max sample rate: 125 MHz
  Buffer size: 200 KB

# Capture settings:
Sample rate: 24 MHz
Channels: D0-D7
Trigger: D0 rising edge
Pre-trigger: 10%
Samples: 50000

# Hit "Run" and see your I2C traffic:
┌──────────┬──────────────────────────────────────────────────────┐
│ Time     │ I2C Decoder Output                                   │
├──────────┼──────────────────────────────────────────────────────┤
│ 0.000 ms │ START                                                │
│ 0.012 ms │ Address: 0x50 (Write)                                │
│ 0.024 ms │ ACK                                                  │
│ 0.036 ms │ Data: 0x00                                           │
│ 0.048 ms │ ACK                                                  │
│ 0.060 ms │ RESTART                                              │
│ 0.072 ms │ Address: 0x50 (Read)                                 │
│ 0.084 ms │ ACK                                                  │
│ 0.096 ms │ Data: 0x42                                           │
│ 0.108 ms │ NACK                                                 │
│ 0.120 ms │ STOP                                                 │
└──────────┴──────────────────────────────────────────────────────┘

The Core Question You’re Answering

“How do you capture external digital signals at rates faster than you can process them?”

Before you write any code, sit with this question. At 100 MHz, you’re sampling every 10 nanoseconds. The CPU can’t possibly handle each sample individually. The answer: PIO shifts samples in at wire speed, DMA moves them to memory without CPU involvement, and you process the captured buffer later.


Concepts You Must Understand First

Stop and research these before coding:

  1. Parallel Input Sampling
    • How does PIO’s IN instruction work with multiple pins?
    • What determines the maximum sample rate?
    • How do you synchronize sampling to a clock?
    • Book Reference: RP2040 Datasheet Section 3 (PIO)
  2. DMA Ring Buffers
    • What’s a ring buffer and why is it useful for continuous capture?
    • How does the DMA RING_SIZE control work?
    • How do you know where the write pointer is?
    • Book Reference: RP2040 Datasheet Section 2.5.3
  3. SUMP Protocol
    • What commands does SUMP use? (SHORT, LONG, META)
    • How do you report device capabilities?
    • How do triggers work in SUMP?
    • Reference: sigrok wiki “SUMP protocol”

Questions to Guide Your Design

Before implementing, think through these:

  1. Sample Rate vs Resolution
    • How do you configure PIO clock for exact sample rates?
    • What’s the tradeoff between sample rate and channel count?
    • How do you handle sample rates the PIO can’t hit exactly?
  2. Buffer Management
    • How much SRAM can you dedicate to capture buffer?
    • How do you handle pre-trigger data (circular buffer)?
    • When do you stop capturing after trigger?
  3. Trigger Implementation
    • How can PIO detect edge or level triggers?
    • How do you arm/disarm the trigger?
    • How do you handle complex triggers (patterns, delays)?

Thinking Exercise

Design the PIO Capture Program

The simplest logic analyzer PIO program:

.program logic_capture
    in pins, 8   ; Shift 8 GPIO pins into ISR

Think through:

  • At what rate does this sample? (Every PIO cycle)
  • How does autopush help? (Push to FIFO when ISR full)
  • What’s the maximum sample rate with 16 channels?

Now consider triggers:

.program logic_capture_with_trigger
wait_trigger:
    jmp pin, capture  ; Jump if trigger pin high
    jmp wait_trigger  ; Keep waiting
capture:
    in pins, 8        ; Sample and push

Questions:

  • What’s the trigger latency? (2 cycles)
  • How do you implement rising edge trigger?
  • How do you implement pattern triggers?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you implement a high-speed data capture system on an embedded device?”
  2. “Explain how DMA can be used for continuous data acquisition.”
  3. “What’s a ring buffer and when would you use one?”
  4. “How do you handle real-time data that arrives faster than you can process it?”
  5. “What’s the SUMP protocol and why would you implement it?”

Hints in Layers

Hint 1: Start with picoprobe/SUMP examples Look at pico-examples/pio/logic_analyser/ for a basic implementation.

Hint 2: DMA ring buffer setup Set RING_SIZE to log2 of your buffer size. DMA wraps automatically. Use TRANS_COUNT for total samples, let it underflow to capture continuously.

Hint 3: Trigger on core 1 Run capture on core 1. When trigger fires, signal core 0 via FIFO. Core 0 handles USB communication.

Hint 4: Study sigrok’s sump driver Look at how sigrok parses SUMP metadata. Match the protocol exactly.


Books That Will Help

Topic Book Chapter
DMA Fundamentals “Making Embedded Systems, 2nd Edition” by Elecia White Ch. 9
PIO Capture “RP2040 Assembly Language Programming” by Stephen Smith Ch. 11
Ring Buffers “The Art of Embedded Systems” Ch. 5
Logic Analyzer Theory “The Art of Electronics” by Horowitz & Hill Ch. 10

Implementation Hints

Architecture overview:

┌─────────────────────────────────────────────────────────────────┐
│                       Logic Analyzer                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  GPIO Pins ──▶ PIO (sampling) ──▶ DMA ──▶ SRAM Buffer           │
│                     │                         │                  │
│                     ▼                         ▼                  │
│              Trigger PIO              USB CDC to Host            │
│                                                                  │
│  Core 0: USB handling, SUMP protocol, configuration             │
│  Core 1: Capture control, trigger monitoring                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

PIO autopush configuration:

  • sm_config_set_in_pins() - First GPIO for sampling
  • sm_config_set_in_shift() - Shift direction, autopush threshold
  • sm_config_set_fifo_join() - Combine FIFOs for 8-entry depth

DMA configuration:

  • Read from PIO RX FIFO
  • Write to capture buffer
  • Ring-wrap on write address
  • Chain to second channel for continuous operation

Learning Milestones

  1. PIO captures data to FIFO → You understand PIO input
  2. DMA fills a buffer → You understand DMA with PIO
  3. PulseView displays waveforms → You’ve implemented SUMP protocol

Project 5: Dual-Core Audio Synthesizer

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, C++
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: DSP / Multicore / I2S Audio
  • Software or Tool: Pico SDK, I2S DAC (PCM5102), MIDI
  • Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White

What you’ll build: A polyphonic synthesizer with wavetable oscillators, ADSR envelopes, and filters—Core 0 handles MIDI input and UI, Core 1 generates audio samples in real-time. DMA streams audio to an I2S DAC.

Why it teaches RP2040: Real-time audio is unforgiving. Miss a sample deadline and you hear clicks. This project forces you to master multicore synchronization, DMA double-buffering, and fixed-point DSP. You’ll understand why the RP2040’s deterministic timing matters.

Core challenges you’ll face:

  • I2S protocol via PIO → maps to implementing audio protocols with PIO
  • DMA double-buffering → maps to glitch-free continuous streaming
  • Multicore audio pipeline → maps to real-time producer-consumer patterns
  • Fixed-point DSP → maps to efficient audio processing without FPU (RP2040)

Key Concepts:

  • I2S Protocol: I2S Specification by Philips (original)
  • DMA Double Buffering: RP2040 Datasheet Section 2.5
  • Multicore Sync: “Operating Systems: Three Easy Pieces” Ch. 28-30
  • DSP Fundamentals: “The Scientist and Engineer’s Guide to DSP” by Steven Smith (free online)

Difficulty: Advanced Time estimate: 2-4 weeks Prerequisites: DMA experience (Project 2), multicore concepts, basic understanding of audio and waveforms, I2S DAC hardware


Real World Outcome

Connect a MIDI keyboard and I2S DAC to your Pico. Play notes and hear synthesized audio—multiple voices with independent envelopes, all generated in real-time on a $4 chip. No audio dropouts, no clicks.

Example Output:

$ cmake --build build
$ picotool load build/synth.uf2 -f

# Serial monitor:
RP2040 Synthesizer v1.0
  Sample rate: 44100 Hz
  Voices: 8
  Waveforms: Sine, Saw, Square, Triangle
  Core 0: MIDI, UI
  Core 1: DSP engine

Audio DMA initialized:
  Buffer A: 0x20010000 (2048 samples)
  Buffer B: 0x20011000 (2048 samples)
  I2S PIO: pio0, SM0, GPIO 18-20

MIDI input detected: USB MIDI
[Note On] Channel 1, Note 60 (C4), Velocity 100
  Voice 0 triggered: Saw wave, 261.63 Hz
  ADSR: Attack 10ms, Decay 100ms, Sustain 0.7, Release 200ms

[Note On] Channel 1, Note 64 (E4), Velocity 80
  Voice 1 triggered: Saw wave, 329.63 Hz

[Note On] Channel 1, Note 67 (G4), Velocity 90
  Voice 2 triggered: Saw wave, 392.00 Hz

# C major chord plays through speakers!
# Core 1 utilization: 45%
# DMA buffer underruns: 0

The Core Question You’re Answering

“How do you guarantee sample-accurate timing when your CPU has other work to do?”

Before you write any code, sit with this question. At 44.1 kHz, you need a new sample every 22.7 microseconds. If Core 1 is interrupted for 50µs, you’ve already missed two samples. The answer: dedicated core for audio, DMA for output, and careful buffer sizing to hide latency.


Concepts You Must Understand First

Stop and research these before coding:

  1. I2S Protocol
    • What are BCLK, LRCK, and DATA lines?
    • What’s the relationship between sample rate, bit depth, and BCLK frequency?
    • What’s the difference between I2S, Left-Justified, and Right-Justified formats?
    • Reference: Philips I2S Specification
  2. Real-Time Audio Constraints
    • What’s audio latency and why does it matter?
    • What’s a buffer underrun and what causes it?
    • Why use fixed-point instead of floating-point on RP2040?
    • Book Reference: “Making Embedded Systems” Ch. 9 - Elecia White
  3. DMA Double Buffering
    • Why can’t you use a single buffer for audio?
    • How do you switch buffers without glitches?
    • How do you know when to fill the next buffer?
    • Book Reference: RP2040 Datasheet Section 2.5

Questions to Guide Your Design

Before implementing, think through these:

  1. Audio Pipeline
    • What’s your sample rate and bit depth?
    • How many samples per DMA buffer? (Trade-off: latency vs CPU overhead)
    • How does Core 0 communicate voice parameters to Core 1?
  2. Synthesis Architecture
    • How do you implement wavetable lookup?
    • How do you handle multiple simultaneous voices?
    • How do you implement ADSR envelopes efficiently?
  3. Fixed-Point Math
    • What fixed-point format will you use? (Q16.16? Q1.15?)
    • How do you handle multiplication overflow?
    • How do you convert between integer samples and DAC output?

Thinking Exercise

Design the Audio Pipeline

Diagram the data flow for your synthesizer:

Core 0 (MIDI/UI)                    Core 1 (Audio DSP)
═══════════════                     ══════════════════

MIDI In ──▶ Parse ──▶ Voice        ┌──────────────────┐
                      Parameters ──▶│ Shared Voice     │
                          │         │ State (atomic)   │
UI Knobs ─────────────────┘         └────────┬─────────┘
                                             │
                                             ▼
                                    ┌────────────────┐
                                    │ For each voice │
                                    │ • Oscillator   │
                                    │ • Envelope     │
                                    │ • Mix          │
                                    └───────┬────────┘
                                            │
                                            ▼
                                    ┌────────────────┐
                                    │ Sum all voices │
                                    │ Apply effects  │
                                    └───────┬────────┘
                                            │
                                            ▼
                         ┌─────────────────────────────────────┐
                         │          DMA Double Buffer          │
                         │  Buffer A ◄───┐   ┌───► Buffer B    │
                         │     ▲         │   │       ▲         │
                         │     │ Fill    │   │ Play  │         │
                         │     └─────────┴───┴───────┘         │
                         └─────────────────┬───────────────────┘
                                           │
                                           ▼
                                    PIO I2S Output
                                           │
                                           ▼
                                       I2S DAC
                                           │
                                           ▼
                                      🔊 Speakers

Questions while designing:

  • What happens if Core 1 takes too long to fill a buffer?
  • How do you detect buffer underruns?
  • What’s the maximum latency with 256-sample buffers at 44.1 kHz?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How do you handle real-time audio on a resource-constrained microcontroller?”
  2. “Explain DMA double-buffering and why it’s important for audio.”
  3. “What’s the difference between multicore and multithreaded programming?”
  4. “How do you safely share data between cores without locks?”
  5. “Why use fixed-point math instead of floating-point in embedded systems?”

Hints in Layers

Hint 1: Start with I2S output Get a sine wave playing through the DAC before adding synthesis.

Hint 2: Use SDK’s audio example Look at pico-examples/pio/i2s_audio/ for a working I2S PIO setup.

Hint 3: Voice parameters in lock-free structure Use atomic operations or a lock-free queue for Core 0 → Core 1 communication.

Hint 4: Fixed-point format Use Q16.16 (16 integer bits, 16 fractional bits). Multiply, then shift right 16.


Books That Will Help

Topic Book Chapter
Real-time Audio “Making Embedded Systems, 2nd Edition” by Elecia White Ch. 9
DSP Fundamentals “The Scientist and Engineer’s Guide to DSP” by Steven Smith Ch. 1-4 (free online)
Multicore Sync “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau Ch. 28-30
Fixed-Point Math “Fixed-Point Arithmetic” (ARM Application Note) All

Implementation Hints

I2S PIO program (simplified):

.program i2s_out
.side_set 2            ; BCLK and LRCK

    ; Send left channel
    set x, 15          side 0b01  ; LRCK high
left_loop:
    out pins, 1        side 0b00  ; Data out, BCLK low
    jmp x-- left_loop  side 0b01  ; BCLK high

    ; Send right channel
    set x, 15          side 0b00  ; LRCK low
right_loop:
    out pins, 1        side 0b10  ; Data out, BCLK low
    jmp x-- right_loop side 0b00  ; BCLK high

DMA double buffer setup:

  1. Create two buffers (A and B)
  2. Configure DMA channel 0 to transfer buffer A
  3. Chain to DMA channel 1, which transfers buffer B
  4. Channel 1 chains back to channel 0
  5. On each transfer complete interrupt, fill the just-played buffer

Voice allocation:

  1. Keep array of voice states (frequency, phase, envelope)
  2. On Note On: Find free voice, set parameters
  3. On Note Off: Start release phase, mark voice free when envelope reaches zero

Learning Milestones

  1. I2S outputs audio → PIO and DMA work for audio streaming
  2. Sine wave plays → Basic oscillator generates samples correctly
  3. Polyphonic playback → Multiple voices mix without artifacts

Project 6: Multicore Real-Time Scheduler

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, C++
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: RTOS / Scheduling / Multicore
  • Software or Tool: Pico SDK (no RTOS library)
  • Main Book: “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau

What you’ll build: A minimal cooperative scheduler that runs tasks on both cores with priority support, inter-task communication via queues, and timing guarantees. Basically, build a tiny RTOS from scratch.

Why it teaches RP2040: Most embedded systems use FreeRTOS without understanding it. Building your own scheduler teaches context switching, synchronization primitives, and the unique challenges of multicore scheduling. You’ll understand why spinlocks exist.

Core challenges you’ll face:

  • Context switching on Cortex-M0+ → maps to saving/restoring CPU registers
  • Priority-based scheduling → maps to ready queues and preemption
  • Multicore work distribution → maps to load balancing and core affinity
  • Inter-task communication → maps to lock-free queues and synchronization

Key Concepts:

  • Context Switching: “Operating Systems: Three Easy Pieces” Ch. 6-7
  • Scheduling Algorithms: “Operating Systems: Three Easy Pieces” Ch. 7-9
  • Synchronization: “Operating Systems: Three Easy Pieces” Ch. 26-32
  • Cortex-M0+ Architecture: ARM Cortex-M0+ Technical Reference Manual

Difficulty: Expert Time estimate: 3-6 weeks Prerequisites: Strong C programming, assembly basics, understanding of CPU architecture, multicore experience


Real World Outcome

Create tasks that run “simultaneously” on both cores with timing guarantees. See how your scheduler distributes work, handles priority inversion, and maintains real-time deadlines.

Example Output:

$ cmake --build build
$ picotool load build/rtos.uf2 -f

# Serial monitor:
PicoRTOS v1.0 initialized
  Cores: 2
  Tick rate: 1000 Hz
  Max tasks: 16
  Schedulers: Round-robin with priority

Creating tasks:
  Task 0: "LED_Blink"     Priority 1, Core 0, Period 500ms
  Task 1: "Serial_Log"    Priority 2, Core 0, Period 100ms
  Task 2: "Sensor_Read"   Priority 3, Core 1, Period 10ms
  Task 3: "Control_Loop"  Priority 4, Core 1, Period 1ms

Scheduler started...

[0001] Core0: LED_Blink running (ctx switch: 12 cycles)
[0001] Core1: Control_Loop running (ctx switch: 12 cycles)
[0002] Core1: Control_Loop running
[0003] Core1: Control_Loop running
...
[0010] Core1: Sensor_Read running (preempted Control_Loop)
[0011] Core1: Control_Loop resumed

Statistics (after 10 seconds):
  Task          | Runs  | Avg Time | Max Time | Deadline Miss
  LED_Blink     | 20    | 50 µs    | 63 µs    | 0
  Serial_Log    | 100   | 230 µs   | 312 µs   | 0
  Sensor_Read   | 1000  | 45 µs    | 67 µs    | 0
  Control_Loop  | 10000 | 180 µs   | 220 µs   | 0

Context switches: 11,120 | Idle time: 45%

The Core Question You’re Answering

“How does an operating system make one CPU appear to run multiple programs simultaneously?”

Before you write any code, sit with this question. A CPU can only execute one instruction at a time. The magic is context switching—saving one task’s state, loading another’s, and continuing as if nothing happened. On RP2040, you have two cores doing this dance.


Concepts You Must Understand First

Stop and research these before coding:

  1. CPU Context
    • What registers must be saved during a context switch?
    • What’s the difference between caller-saved and callee-saved registers?
    • What’s in the Cortex-M0+ stack frame during an exception?
    • Book Reference: ARM Cortex-M0+ Technical Reference Manual
  2. Scheduling Algorithms
    • What’s round-robin scheduling and its limitations?
    • How does priority scheduling work?
    • What’s a ready queue and how is it organized?
    • Book Reference: “Operating Systems: Three Easy Pieces” Ch. 7-9
  3. Multicore Scheduling
    • What’s the difference between SMP and AMP scheduling?
    • How do you avoid two cores picking the same task?
    • What’s core affinity and when would you use it?
    • Book Reference: “Operating Systems: Three Easy Pieces” Ch. 48

Questions to Guide Your Design

Before implementing, think through these:

  1. Context Switching
    • How do you trigger a context switch? (SysTick? PendSV?)
    • What state do you save? Stack pointer? All registers?
    • How do you switch between tasks?
  2. Task Management
    • What data structure holds task information (TCB)?
    • How do you track ready, blocked, and running tasks?
    • How do you handle task creation and deletion?
  3. Multicore Considerations
    • Does each core have its own scheduler or one shared?
    • How do you synchronize access to shared data structures?
    • How do you handle tasks that must run on a specific core?

Thinking Exercise

Design the Task Control Block (TCB)

What information does each task need?

typedef struct {
    // Stack management
    uint32_t *stack_pointer;     // Current SP (saved during context switch)
    uint32_t *stack_base;        // Bottom of allocated stack
    uint32_t stack_size;         // Stack size in bytes

    // Scheduling
    uint8_t priority;            // Task priority (0 = highest)
    uint8_t state;               // READY, RUNNING, BLOCKED, SUSPENDED
    uint8_t core_affinity;       // Which core? (0, 1, or ANY)

    // Timing
    uint32_t period_ticks;       // For periodic tasks
    uint32_t next_run_tick;      // When to run next
    uint32_t deadline_ticks;     // Hard deadline (for EDF)

    // Statistics
    uint32_t total_runs;
    uint32_t total_cycles;
    uint32_t max_cycles;

    // Linkage
    struct Task *next;           // For linked list traversal
    void (*entry)(void *);       // Task entry point
    void *arg;                   // Argument to task
} Task;

Questions:

  • Where do you store this structure?
  • How do you find the highest-priority ready task quickly?
  • What happens when a task’s stack overflows?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Walk me through what happens during a context switch.”
  2. “How does priority inversion occur and how do you prevent it?”
  3. “What’s the difference between cooperative and preemptive scheduling?”
  4. “How do you implement a mutex on a multicore system?”
  5. “Explain the tradeoffs between global and per-core run queues.”

Hints in Layers

Hint 1: Start cooperative Build a cooperative scheduler first (tasks yield explicitly). Add preemption later.

Hint 2: Use PendSV for context switching The Cortex-M0+ PendSV exception is designed for context switching. Set PendSV pending, let it fire at lowest priority.

Hint 3: Spinlocks for critical sections Use RP2040’s hardware spinlocks to protect shared scheduler data structures.

Hint 4: Study FreeRTOS port Look at FreeRTOS’s RP2040 port (portable/ThirdParty/GCC/RP2040/) for inspiration.


Books That Will Help

Topic Book Chapter
Context Switching “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau Ch. 6-7
Scheduling “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau Ch. 7-9
Multicore “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau Ch. 48
ARM Exception Model ARM Cortex-M0+ Technical Reference Manual Ch. 4

Implementation Hints

Minimal context switch (pseudocode):

PendSV_Handler:
    ; Save current context
    mrs r0, psp                    ; Get current PSP
    subs r0, r0, #32               ; Make room for r4-r11
    stmia r0!, {r4-r7}             ; Save low registers
    mov r4, r8
    mov r5, r9
    mov r6, r10
    mov r7, r11
    stmia r0!, {r4-r7}             ; Save high registers
    subs r0, r0, #32               ; Adjust back to start

    ; Save SP to current TCB
    ldr r1, =current_task
    ldr r1, [r1]
    str r0, [r1]                   ; Save SP to TCB

    ; Get next task (call scheduler)
    bl scheduler_get_next_task

    ; Switch to new task
    ldr r1, =current_task
    str r0, [r1]                   ; Update current_task pointer
    ldr r0, [r0]                   ; Load new SP from TCB

    ; Restore context
    ldmia r0!, {r4-r7}             ; Restore low registers
    ldmia r0!, {r1-r3, r0}         ; Load into temp registers
    mov r8, r1
    mov r9, r2
    mov r10, r3
    mov r11, r0
    msr psp, r0                    ; Set new PSP

    ; Return from exception
    ldr r0, =0xFFFFFFFD            ; Return to thread mode, PSP
    bx r0

Scheduler data structures:

  • Per-priority ready queue (linked list of tasks)
  • Global spinlock for accessing queues
  • Per-core current_task pointer
  • System tick counter

Learning Milestones

  1. Tasks switch cooperatively → You understand context saving/restoring
  2. Priority scheduling works → Higher priority tasks preempt lower
  3. Both cores run tasks → Multicore scheduling with synchronization

Project 7: VGA Display via PIO

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: PIO / Video Signals / Timing-Critical Systems
  • Software or Tool: Pico SDK, VGA connector, resistor DAC
  • Main Book: “RP2040 Assembly Language Programming” by Stephen Smith

What you’ll build: A VGA display driver that outputs 640x480 video using only PIO, DMA, and resistor-ladder DACs. No video chip—just software-defined video.

Why it teaches RP2040: VGA requires microsecond-precise timing for horizontal/vertical sync and pixel output. This project pushes PIO to its limits, requiring multiple state machines synchronized together. It’s the ultimate PIO mastery project.

Core challenges you’ll face:

  • VGA timing generation → maps to precise horizontal and vertical sync
  • Multi-SM synchronization → maps to coordinating PIO state machines
  • High-bandwidth pixel streaming → maps to DMA at maximum speed
  • Resistor DAC design → maps to analog output from digital pins

Key Concepts:

  • VGA Signal Specifications: VESA timing standards
  • PIO SM Synchronization: RP2040 Datasheet Section 3.5.7
  • DMA at Max Speed: RP2040 Datasheet Section 2.5
  • Resistor-Ladder DACs: Basic electronics / “The Art of Electronics”

Difficulty: Expert Time estimate: 3-6 weeks Prerequisites: Strong PIO experience (Projects 2, 4), DMA mastery, understanding of video signals, basic electronics for building DAC


Real World Outcome

Connect a VGA monitor to your Pico through resistors. Boot your code and see a colorful test pattern, text console, or even simple graphics—all generated by a $4 microcontroller with no GPU.

Example Output:

$ cmake --build build
$ picotool load build/vga.uf2 -f

# Serial monitor:
VGA Driver initialized
  Resolution: 640x480 @ 60 Hz
  Pixel clock: 25.175 MHz
  Color depth: 8 bits (3-3-2 RGB)

  PIO Configuration:
    SM0: Horizontal timing (sync, back porch, pixels, front porch)
    SM1: Vertical timing (sync, back porch, lines, front porch)
    SM2: Pixel output (8-bit color via DAC)

  DMA: 2 channels chained for continuous line streaming

  Memory:
    Framebuffer: 0x20010000 (307,200 bytes)
    Line buffer: 0x20000000 (640 bytes × 2 double buffer)

Video output active:
  Frame 0: Rendering test pattern
  Frame 1: Color bars
  Frame 2: Text console "Hello from RP2040!"

# VGA monitor shows:
# ┌────────────────────────────────────────────────────────────┐
# │                                                            │
# │   ██████  ██████  ██████  ██████  ██████  ██████  ██████   │
# │   WHITE   RED     GREEN   BLUE    CYAN    MAGENTA YELLOW   │
# │                                                            │
# │                   Hello from RP2040!                       │
# │                                                            │
# │   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   │
# │        Gradient bar (testing DAC linearity)                │
# │                                                            │
# └────────────────────────────────────────────────────────────┘

The Core Question You’re Answering

“How do you generate a video signal that requires sub-microsecond timing accuracy from software?”

Before you write any code, sit with this question. VGA expects pixels at 25 MHz—one every 40 nanoseconds. The CPU can’t possibly handle each pixel individually. PIO runs independently at system clock speed, and with careful programming, can output pixels while generating sync signals.


Concepts You Must Understand First

Stop and research these before coding:

  1. VGA Signal Timing
    • What are horizontal sync, back porch, active video, and front porch?
    • What are the timing requirements for 640x480 @ 60 Hz?
    • How do vertical blanking intervals work?
    • Reference: VESA Monitor Timing Specifications
  2. PIO Multi-SM Coordination
    • How do you synchronize multiple state machines?
    • How can one SM signal another?
    • How do you start multiple SMs simultaneously?
    • Book Reference: RP2040 Datasheet Section 3.5.7
  3. Resistor-Ladder DAC
    • How does an R-2R ladder convert bits to voltage?
    • What resistor values do you need for 3-3-2 RGB?
    • How do you match VGA input impedance (75Ω)?
    • Book Reference: “The Art of Electronics” by Horowitz & Hill

Questions to Guide Your Design

Before implementing, think through these:

  1. Timing Budget
    • How many PIO cycles per pixel at 25 MHz?
    • How do you configure PIO clock divider for exact pixel clock?
    • What happens if timing is slightly off?
  2. State Machine Allocation
    • Which SM generates horizontal sync?
    • Which SM generates vertical sync?
    • Which SM outputs pixel data?
    • How do they communicate?
  3. Memory Architecture
    • How big is the framebuffer? (640 × 480 × bytes per pixel)
    • Can you fit it in 264 KB SRAM?
    • How do you handle memory bandwidth constraints?

Thinking Exercise

Map the VGA Timing

VGA 640x480 @ 60 Hz timing:

Horizontal Line (31.777 µs total):
═══════════════════════════════════════════════════════════════════════

          ←── 3.813 µs ──→←─ 1.907 µs ─→←── 25.422 µs ──→←─ 0.635 µs ─→
          ┌──────────────┐              ┌────────────────┐
          │  Sync Pulse  │ Back Porch  │  Active Video  │ Front Porch
HSYNC ────┘              └──────────────────────────────────────────────

          │<── 96 px ───>│<── 48 px ──>│<──── 640 px ──>│<── 16 px ──>│
                                       │                │
                                       Pixels output here


Vertical Frame (16.683 ms total):
═══════════════════════════════════════════════════════════════════════

          ←── 2 lines ──→←── 33 lines ─→←── 480 lines ──→←─ 10 lines ─→
          ┌─────────────┐                ┌──────────────┐
          │ Sync Pulse  │  Back Porch   │ Active Video │ Front Porch
VSYNC ────┘              └───────────────────────────────────────────────

Calculate:

  • Pixel clock = 25.175 MHz (one pixel every 39.7 ns)
  • At 125 MHz system clock, how many cycles per pixel?
  • How many cycles per horizontal line?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you implement video output on a microcontroller without a GPU?”
  2. “Explain VGA signal timing and why precision matters.”
  3. “How do you synchronize multiple hardware state machines?”
  4. “What’s a resistor DAC and how does it work?”
  5. “How do you handle real-time constraints when memory bandwidth is limited?”

Hints in Layers

Hint 1: Start with sync signals only Generate HSYNC and VSYNC first. Verify timing with a scope or monitor’s “no signal” message.

Hint 2: Use the Pico VGA reference Study pico-extras/src/rp2_common/pico_scanvideo_dpi/ for a working implementation.

Hint 3: Pixel clock from PIO divider 125 MHz / 5 = 25 MHz (close enough to 25.175 MHz for most monitors)

Hint 4: Use DMA chaining for lines One DMA channel per line, chained together. On line complete, update next line pointer.


Books That Will Help

Topic Book Chapter
PIO Mastery “RP2040 Assembly Language Programming” by Stephen Smith Ch. 10-12
Video Signals “The Art of Electronics” by Horowitz & Hill Ch. 1, 10
VGA Timing VESA Monitor Timing Specifications Standard timings
PIO Reference “RP2040 Datasheet” by Raspberry Pi Section 3 (all)

Implementation Hints

VGA cable pinout (minimal):

  • Pin 1: RED (through resistor DAC)
  • Pin 2: GREEN (through resistor DAC)
  • Pin 3: BLUE (through resistor DAC)
  • Pin 13: HSYNC (direct from GPIO)
  • Pin 14: VSYNC (direct from GPIO)
  • Pins 5, 6, 7, 8, 10: Ground

Simple 3-3-2 color DAC (8 bits total):

  • R: 3 bits → 3 resistors to pin 1
  • G: 3 bits → 3 resistors to pin 2
  • B: 2 bits → 2 resistors to pin 3

PIO architecture:

SM0 (Timing): Count pixels/lines, generate sync pulses
             Side-set controls HSYNC/VSYNC
             IRQ signals other SMs

SM1 (Pixels): Pull pixel data from FIFO
             OUT to pins (RGB)
             Wait for sync from SM0

Memory optimization:

  • Consider lower resolution (320x240) if memory-constrained
  • Use chunky pixel format (packed bytes)
  • Consider text mode with character ROM for less memory

Learning Milestones

  1. Sync signals generated → Monitor locks to your signal
  2. Single color output → Pixels appear on screen
  3. Full color test pattern → DAC and framebuffer work correctly

Project 8: RP2350 Secure Boot Implementation

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 5: Master
  • Knowledge Area: Security / Cryptography / Secure Boot
  • Software or Tool: Pico SDK 2.0+, RP2350 board, OpenSSL
  • Main Book: “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson

What you’ll build: A complete secure boot chain for RP2350—sign your firmware with your private key, burn the public key hash to OTP, and verify signature on every boot. Implement rollback protection and secure key storage.

Why it teaches RP2350: The RP2350’s security features (TrustZone, OTP, secure boot) are professionally documented but rarely used by hobbyists. Understanding secure boot is essential for commercial products. This project bridges theory and practice.

Core challenges you’ll face:

  • OTP programming → maps to one-time programmable fuse memory
  • Cryptographic signing → maps to ECDSA signatures and verification
  • Boot chain design → maps to chain of trust from ROM to application
  • Key management → maps to protecting secrets in embedded systems

Key Concepts:

  • Secure Boot: RP2350 Datasheet Section 5.9
  • ARM TrustZone: ARM Cortex-M33 Technical Reference Manual
  • ECDSA Signatures: “Serious Cryptography” Ch. 11-12
  • OTP Memory: RP2350 Datasheet Section 5.3

Difficulty: Master Time estimate: 2-4 weeks Prerequisites: Strong C programming, cryptography basics, RP2350 hardware, understanding of security concepts


Real World Outcome

Build a firmware image that’s cryptographically signed. Program the RP2350 to only boot firmware signed by your key. Tampered firmware refuses to run, protecting your intellectual property and users.

Example Output:

# Generate signing keypair
$ openssl ecparam -genkey -name secp256k1 -out private.pem
$ openssl ec -in private.pem -pubout -out public.pem

# Build and sign firmware
$ cmake --build build
$ picotool sign build/app.bin private.pem -o build/app_signed.bin

# First-time provisioning (burns OTP - IRREVERSIBLE!)
$ picotool otp set BOOT_FLAGS0 0x00000001  # Enable secure boot
$ picotool otp load public.pem --hash       # Burn public key hash
WARNING: OTP programming is PERMANENT. Continue? [y/N] y
OTP programmed successfully:
  BOOT_FLAGS0: 0x00000001 (Secure boot enabled)
  BOOTKEY0-3: SHA-256 hash of public key

# Load signed firmware
$ picotool load build/app_signed.bin -f

# Serial monitor:
RP2350 Secure Boot Demo
  Boot mode: SECURE
  Signature verification: PASSED
  Rollback counter: 5
  TrustZone: Secure world active

Application running in secure mode...
Secure storage test:
  Writing secret key to OTP... OK
  Reading secret key... OK (matches)
  Attempting read from non-secure code... DENIED (SAU fault)

# Try loading unsigned firmware:
$ picotool load build/unsigned_app.bin -f
Error: Signature verification failed
Boot aborted by secure boot ROM

The Core Question You’re Answering

“How do you ensure that only YOUR code runs on the hardware, even if someone has physical access?”

Before you write any code, sit with this question. Secure boot creates a “chain of trust”—the ROM (which can’t be modified) verifies the next stage, which verifies the next, and so on. If any link fails verification, boot stops.


Concepts You Must Understand First

Stop and research these before coding:

  1. Chain of Trust
    • What’s a root of trust and why is it important?
    • How does each stage verify the next?
    • What happens if verification fails at any stage?
    • Book Reference: “Serious Cryptography” Ch. 13 - Jean-Philippe Aumasson
  2. ECDSA Signatures
    • How does public key cryptography enable signature verification?
    • What curve does RP2350 use? (secp256k1 or similar)
    • What’s a message digest and why do you sign the hash?
    • Book Reference: “Serious Cryptography” Ch. 11-12
  3. OTP Memory
    • What is OTP and why can it only be written once?
    • What happens if you try to flip a bit back?
    • Why store a hash instead of the public key itself?
    • Book Reference: RP2350 Datasheet Section 5.3

Questions to Guide Your Design

Before implementing, think through these:

  1. Key Management
    • Where do you store your private key? (NEVER on the device!)
    • How do you handle key rotation for future firmware?
    • What’s your recovery strategy if the key is compromised?
  2. Boot Chain Architecture
    • What does the ROM verify?
    • What does your bootloader verify?
    • How do you update firmware securely?
  3. Rollback Protection
    • How do you prevent someone installing old (vulnerable) firmware?
    • How do you increment the rollback counter?
    • What happens when OTP space for rollback runs out?

Thinking Exercise

Design the Secure Boot Chain

Diagram the trust chain from power-on to your application:

┌─────────────────────────────────────────────────────────────────────────────┐
│                        RP2350 Secure Boot Chain                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  POWER ON                                                                    │
│      │                                                                       │
│      ▼                                                                       │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                           BOOT ROM                                     │  │
│  │  • Immutable (mask ROM in silicon)                                    │  │
│  │  • Reads OTP flags: Is secure boot enabled?                           │  │
│  │  • Reads public key hash from OTP                                     │  │
│  │  • Loads Stage 2 from flash                                           │  │
│  │  • Computes SHA-256 of Stage 2                                        │  │
│  │  • Verifies ECDSA signature using OTP key hash                        │  │
│  │  • PASS → Jump to Stage 2 | FAIL → Halt                               │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│      │                                                                       │
│      ▼ (only if signature valid)                                            │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                          STAGE 2 (Bootloader)                          │  │
│  │  • Contains public key for application verification                   │  │
│  │  • Checks rollback counter in OTP vs. firmware header                 │  │
│  │  • Loads application from flash                                       │  │
│  │  • Verifies application signature                                     │  │
│  │  • Configures TrustZone secure/non-secure regions                     │  │
│  │  • PASS → Jump to App | FAIL → Enter recovery mode                    │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│      │                                                                       │
│      ▼ (only if signature valid AND rollback OK)                            │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                           APPLICATION                                  │  │
│  │  • Runs in secure or non-secure world (your choice)                   │  │
│  │  • Can access secure peripherals, secrets in OTP                      │  │
│  │  • Cannot modify secure boot configuration                            │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Questions:

  • What happens if an attacker modifies flash contents?
  • What if they try to use an old firmware version?
  • What if they desolder the flash chip and put it on another RP2350?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the concept of a chain of trust in secure boot.”
  2. “Why do you store a hash of the public key instead of the key itself?”
  3. “What is rollback protection and why is it important?”
  4. “How does ARM TrustZone provide isolation between secure and non-secure code?”
  5. “What’s the difference between signing firmware and encrypting it?”

Hints in Layers

Hint 1: Start with unsigned boot Understand the boot process without security first. Read RP2350 datasheet Section 2.8.

Hint 2: Use picotool for signing picotool sign handles the signature format. Study the output binary structure.

Hint 3: Test with development keys first Never burn production keys until you’ve verified the process works. OTP is permanent!

Hint 4: Read the DEF CON challenge writeups Raspberry Pi’s hacking challenge at DEF CON 32 exposed the security model. Study it.


Books That Will Help

Topic Book Chapter
Cryptography “Serious Cryptography, 2nd Edition” by Jean-Philippe Aumasson Ch. 10-13
Secure Boot “Practical IoT Hacking” by Fotios Chantzis Ch. 8
ARM TrustZone ARM Cortex-M33 Technical Reference Manual Ch. 6
RP2350 Security RP2350 Datasheet by Raspberry Pi Section 5 (all)

Implementation Hints

OTP organization on RP2350:

  • BOOT_FLAGS0-3: Boot configuration (secure boot enable, key selection)
  • BOOTKEY0-7: Public key hash (SHA-256 of your public key)
  • ROLLBACK: Monotonic counter pages for rollback protection

Signature verification flow:

  1. Load firmware from flash
  2. Parse header (contains signature, version, length)
  3. Compute SHA-256 of firmware contents
  4. Verify ECDSA signature using public key derived from OTP hash
  5. Check rollback counter
  6. If all pass, jump to firmware; else halt or recovery mode

TrustZone configuration:

  • SAU (Security Attribution Unit): Define secure/non-secure regions
  • Secure peripherals: OTP read/write, crypto accelerator
  • Non-secure: Most application code

⚠️ WARNING:

  • OTP programming is PERMANENT and IRREVERSIBLE
  • Test everything with development boards before production
  • Keep your private keys OFFLINE and BACKED UP
  • Document your key management procedures

Learning Milestones

  1. Understand OTP structure → Can read boot flags and key locations
  2. Sign and verify firmware → picotool sign/verify works correctly
  3. Secure boot active → Unsigned firmware refuses to boot

Project 9: RP2350 RISC-V Bare Metal Programming

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C with RISC-V Assembly
  • Alternative Programming Languages: Rust, Assembly only
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: RISC-V Architecture / Bare Metal / Dual Architecture
  • Software or Tool: Pico SDK 2.0+, RISC-V GCC Toolchain
  • Main Book: “Computer Organization and Design RISC-V Edition” by Patterson & Hennessy

What you’ll build: A bare-metal application that runs on RP2350’s RISC-V Hazard3 cores instead of ARM Cortex-M33. Switch between architectures at runtime and understand the fundamental differences.

Why it teaches RP2350: The RP2350 is the first affordable chip offering both ARM and RISC-V on the same silicon. Understanding both architectures gives you insight into CPU design, instruction encoding, and why ARM dominates embedded while RISC-V is rising.

Core challenges you’ll face:

  • RISC-V instruction set → maps to RV32I base ISA and extensions
  • Switching architectures at boot → maps to OTP configuration and boot process
  • Toolchain setup → maps to cross-compilation for RISC-V targets
  • Debugging RISC-V → maps to different debug register conventions

Key Concepts:

  • RISC-V ISA: “Computer Organization and Design RISC-V Edition” Ch. 2
  • Hazard3 Core: RP2350 Datasheet Section 3 (Hazard3 implementation)
  • Architecture Switching: RP2350 Datasheet Section 2.8
  • RISC-V Toolchain: RISC-V GNU Toolchain documentation

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: ARM assembly experience (Project 1), strong C skills, understanding of CPU architecture


Real World Outcome

Boot your RP2350 into RISC-V mode, run your code, and understand the differences from ARM at the instruction level. Compare performance, code size, and ease of programming.

Example Output:

# Configure for RISC-V boot (development, not permanent)
$ PICO_PLATFORM=rp2350-riscv cmake -B build
$ cmake --build build
$ picotool load build/riscv_demo.uf2 -f

# Serial monitor:
RP2350 RISC-V Demo
  Architecture: Hazard3 RISC-V (RV32IMAC)
  Frequency: 150 MHz
  Extensions: I (Integer), M (Multiply), A (Atomic), C (Compressed)

Instruction set demo:
  ADD:  x1 = x2 + x3  →  Result: 42
  ADDI: x1 = x2 + 10  →  Result: 15
  LW:   Load word from 0x20000000 → 0xDEADBEEF
  SW:   Store word 0xCAFEBABE to 0x20000004 → OK

Comparison: ARM vs RISC-V (same code)
  Fibonacci(20):
    ARM Cortex-M33: 1,247 cycles (with DSP)
    RISC-V Hazard3: 1,389 cycles

  LED Toggle loop (1M iterations):
    ARM: 8,000,012 cycles
    RISC-V: 8,000,003 cycles

Code size comparison:
  Blinky program:
    ARM: 1,284 bytes
    RISC-V: 1,156 bytes (Compressed ISA advantage)

The Core Question You’re Answering

“What is RISC-V and why does having both ARM and RISC-V on one chip matter?”

Before you write any code, sit with this question. ARM is proprietary—you pay licensing fees. RISC-V is open—anyone can implement it royalty-free. The RP2350 lets you experience both paradigms and understand why this architectural choice matters for the future of computing.


Concepts You Must Understand First

Stop and research these before coding:

  1. RISC-V Base ISA (RV32I)
    • What are the 32 general-purpose registers?
    • What’s the difference between R-type, I-type, S-type, B-type instructions?
    • How does RISC-V handle function calls (calling convention)?
    • Book Reference: “Computer Organization and Design RISC-V Edition” Ch. 2
  2. Hazard3 Implementation
    • What extensions does Hazard3 support? (M, A, C, Zba, Zbb, Zbs)
    • What’s the 3-stage pipeline (fetch, decode, execute)?
    • How does Hazard3 handle branch prediction?
    • Book Reference: Hazard3 GitHub documentation
  3. Architecture Switching
    • How do you tell the RP2350 to boot RISC-V instead of ARM?
    • Can you switch at runtime?
    • What’s the performance difference?
    • Book Reference: RP2350 Datasheet Section 2.8

Questions to Guide Your Design

Before implementing, think through these:

  1. Toolchain Setup
    • Where do you get a RISC-V cross-compiler?
    • How do you tell CMake to target RISC-V?
    • What linker script differences exist?
  2. Register Conventions
    • What’s ra, sp, gp, tp in RISC-V?
    • How does this compare to ARM’s lr, sp, etc.?
    • What’s the calling convention for function arguments?
  3. Memory-Mapped I/O
    • Are peripheral addresses the same in RISC-V mode?
    • What about the SIO (single-cycle I/O)?
    • How do you access GPIO?

Thinking Exercise

Compare ARM and RISC-V Instruction Encoding

Study how both architectures encode the same operation:

ADD r0, r1, r2 (ARM Thumb-2):

1110 1011 000 0 0001 0 000 0000 0000 0010
│    │    │   │ │    │ │   │    │
│    │    │   │ │    │ │   │    └── Rm (r2)
│    │    │   │ │    │ │   └─────── Rd (r0)
│    │    │   │ │    │ └─────────── shift
│    │    │   │ │    └───────────── S flag
│    │    │   │ └────────────────── Rn (r1)
│    │    │   └──────────────────── 0
│    │    └──────────────────────── opcode
└────└───────────────────────────── condition/prefix

add x0, x1, x2 (RISC-V RV32I):

0000000 00010 00001 000 00000 0110011
│       │     │     │   │     │
│       │     │     │   │     └── opcode (0110011 = OP)
│       │     │     │   └──────── rd (x0)
│       │     │     └──────────── funct3 (000 = ADD)
│       │     └────────────────── rs1 (x1)
│       └──────────────────────── rs2 (x2)
└──────────────────────────────── funct7 (0000000 = ADD)

Questions:

  • Which encoding is more regular (easier to decode)?
  • Why does ARM need different encoding for Thumb vs ARM mode?
  • What’s the advantage of RISC-V’s fixed 32-bit instructions?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What is RISC-V and how does it differ from ARM?”
  2. “Explain the RISC-V register file and calling convention.”
  3. “What are RISC-V extensions and which are essential?”
  4. “Why is RISC-V gaining traction in embedded systems?”
  5. “Compare code density between ARM Thumb and RISC-V Compressed.”

Hints in Layers

Hint 1: Use the Pico SDK for RISC-V Set PICO_PLATFORM=rp2350-riscv when configuring CMake. The SDK handles toolchain setup.

Hint 2: Study the Hazard3 repo github.com/Wren6991/Hazard3 has excellent documentation on the core.

Hint 3: Compare generated assembly Compile the same C code for both ARM and RISC-V. Use objdump to compare.

Hint 4: Start with blinky Get LED blinking on RISC-V first, then explore more complex code.


Books That Will Help

Topic Book Chapter
RISC-V ISA “Computer Organization and Design RISC-V Edition” by Patterson & Hennessy Ch. 2
RISC-V Assembly “RISC-V Assembly Language Programming” by Stephen Smith All
CPU Architecture “Computer Architecture” by Hennessy & Patterson Ch. 1, Appendix A
Hazard3 Core Hazard3 GitHub Documentation All

Implementation Hints

RISC-V register naming: | Number | ABI Name | Purpose | |——–|———-|———| | x0 | zero | Always zero | | x1 | ra | Return address | | x2 | sp | Stack pointer | | x3 | gp | Global pointer | | x4 | tp | Thread pointer | | x5-7 | t0-t2 | Temporaries | | x8 | s0/fp | Saved/frame pointer | | x9 | s1 | Saved register | | x10-11 | a0-a1 | Function args/return | | x12-17 | a2-a7 | Function arguments | | x18-27 | s2-s11 | Saved registers | | x28-31 | t3-t6 | Temporaries |

Simple blinky in RISC-V assembly:

.section .text
.global main
main:
    # Load GPIO base address
    lui   t0, 0xd0000       # SIO base
    li    t1, (1 << 25)     # LED pin mask

    # Set GPIO direction
    sw    t1, 0x24(t0)      # GPIO_OE_SET

loop:
    # Toggle LED
    sw    t1, 0x1c(t0)      # GPIO_OUT_XOR

    # Delay loop
    li    t2, 1000000
delay:
    addi  t2, t2, -1
    bnez  t2, delay

    j     loop

Learning Milestones

  1. Toolchain works → Can compile and run “Hello World” on RISC-V
  2. GPIO works → LED blinks using RISC-V instructions
  3. Understand differences → Can explain ARM vs RISC-V tradeoffs

Project 10: USB Host Mode (Keyboard/Mouse Reader)

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 4: Expert
  • Knowledge Area: USB Host / Protocol Analysis
  • Software or Tool: Pico SDK, USB-A connector, TinyUSB
  • Main Book: “USB Complete, 5th Edition” by Jan Axelson

What you’ll build: A USB host that reads keyboards and mice connected to it. Display keypresses and mouse movements. Build a USB protocol analyzer that logs all traffic.

Why it teaches RP2040: Most projects use USB device mode. Host mode is harder—you control the bus, enumerate devices, and manage power. This teaches the “other side” of USB and completes your understanding.

Core challenges you’ll face:

  • USB host enumeration → maps to discovering and configuring devices
  • HID report parsing → maps to decoding keyboard/mouse data
  • Power management → maps to 5V supply and current limiting
  • Multiple device handling → maps to hub support and device addressing

Key Concepts:

  • USB Host Controller: “USB Complete” Ch. 14-15
  • HID Protocol (Host Side): “USB Complete” Ch. 11
  • TinyUSB Host Stack: TinyUSB documentation
  • RP2040 USB Host: RP2040 Datasheet Section 4.1

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: USB device experience (Project 3), understanding of USB protocol, hardware setup for 5V supply


Real World Outcome

Connect a USB keyboard or mouse to your Pico. See keypresses logged, mouse movements tracked, and understand exactly how USB hosts enumerate and communicate with devices.

Example Output:

$ cmake --build build
$ picotool load build/usb_host.uf2 -f

# Connect a USB keyboard to Pico (via USB-A port with 5V supply)

# Serial monitor:
USB Host initialized
  Mode: Full-Speed Host
  Port power: 5V @ 500mA max

[USB] Device connected on port 0
[USB] Resetting device...
[USB] Device reset complete, speed: Full-Speed

[USB] GET_DESCRIPTOR (Device)
  bLength: 18
  bDescriptorType: DEVICE
  bcdUSB: 2.00
  bDeviceClass: 0 (Defined by Interface)
  idVendor: 0x046D (Logitech)
  idProduct: 0xC31C
  iProduct: "USB Keyboard"

[USB] SET_ADDRESS: 1
[USB] GET_DESCRIPTOR (Configuration)
  Interface 0: HID (Keyboard)
    Endpoint 0x81: Interrupt IN, 8 bytes, 10ms

[USB] SET_CONFIGURATION: 1
[USB] HID SET_PROTOCOL: Boot Protocol
[USB] Device enumerated: Logitech USB Keyboard @ Address 1

Keyboard events:
  [KEY DOWN] A (0x04)
  [KEY UP] A
  [KEY DOWN] Left Shift (0xE1) + B (0x05)'B'
  [KEY UP] Left Shift, B
  [KEY DOWN] Enter (0x28)
  [KEY UP] Enter

# You're seeing exactly what the keyboard sends!

The Core Question You’re Answering

“What does your computer do when you plug in a USB keyboard?”

Before you write any code, sit with this question. When you’re the host, you control everything—reset the device, assign its address, read its descriptors, and poll for data. Understanding host mode completes the USB picture.


Concepts You Must Understand First

Stop and research these before coding:

  1. USB Host Responsibilities
    • What’s the difference between host and device mode?
    • How does the host detect device connection?
    • What’s the enumeration sequence from host perspective?
    • Book Reference: “USB Complete” Ch. 14-15 - Jan Axelson
  2. USB Power Delivery
    • How much current can USB provide? (500mA standard, 900mA USB3)
    • How does the host negotiate power with devices?
    • What happens on overcurrent?
    • Book Reference: “USB Complete” Ch. 3
  3. HID Boot Protocol
    • What’s the difference between Boot and Report protocol?
    • Why do BIOSes need Boot protocol?
    • What’s in a boot keyboard report?
    • Book Reference: “USB Complete” Ch. 11

Questions to Guide Your Design

Before implementing, think through these:

  1. Hardware Setup
    • How do you provide 5V to connected devices?
    • What about current limiting for safety?
    • Do you need a USB-A connector or can you wire directly?
  2. Enumeration Implementation
    • How do you detect device connect/disconnect?
    • What’s the minimum enumeration sequence?
    • How do you handle enumeration failures?
  3. HID Polling
    • How often do you poll the interrupt endpoint?
    • How do you parse HID reports?
    • How do you handle multiple devices?

Thinking Exercise

Trace USB Enumeration

What messages does the host send during enumeration?

Host                              Device
═════                             ══════

    ─────── Reset Bus ───────────────▶
    ◀─────── (waits 10ms) ─────────────

    ─── GET_DESCRIPTOR (Device) ────▶
    ◀─── Device Descriptor (18 bytes) ─

    ─────── SET_ADDRESS (1) ────────▶
    ◀─────── ACK ─────────────────────

    (Device now responds to address 1)

    ── GET_DESCRIPTOR (Config) ─────▶
    ◀── Configuration Descriptor ────
        (includes interfaces, endpoints)

    ────── SET_CONFIGURATION (1) ───▶
    ◀─────── ACK ─────────────────────

    (Device is now configured and ready)

For HID keyboard:
    ───── HID SET_PROTOCOL (Boot) ─▶
    ◀─────── ACK ─────────────────────

    ─────── Interrupt IN poll ──────▶
    ◀─── 8-byte keyboard report ─────
         (modifier, reserved, key0-key5)

Questions:

  • Why does enumeration happen at default address (0) first?
  • What’s in the Configuration Descriptor besides the configuration?
  • Why use Boot Protocol instead of Report Protocol?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain USB enumeration from the host’s perspective.”
  2. “What’s the difference between USB host and device mode?”
  3. “How does USB power delivery work?”
  4. “What’s an interrupt endpoint and how is it polled?”
  5. “How would you add support for USB hubs to your host?”

Hints in Layers

Hint 1: Start with TinyUSB host examples Look at pico-examples/usb/host/host_cdc_msc_hid/ for a working example.

Hint 2: Hardware: 5V supply is critical You need external 5V to VBUS for connected devices. The Pico’s 3.3V won’t work.

Hint 3: Use PIO-USB for additional ports Pico-PIO-USB adds extra USB ports via PIO.

Hint 4: Debug with a USB analyzer A hardware USB analyzer (or software like Wireshark + usbmon) helps debug issues.


Books That Will Help

Topic Book Chapter
USB Host “USB Complete, 5th Edition” by Jan Axelson Ch. 14-15
HID Protocol “USB Complete” by Jan Axelson Ch. 11
TinyUSB Host TinyUSB GitHub Documentation Host examples
RP2040 USB “RP2040 Datasheet” by Raspberry Pi Section 4.1

Implementation Hints

Hardware setup for USB host:

                 Pico
                  │
    VBUS (5V) ────┼──── External 5V supply
         │        │       (with current limiting)
    D+ ───────────┼──── GPIO (USB_DP)
    D- ───────────┼──── GPIO (USB_DM)
    GND ──────────┼──── GND
                  │
            USB-A Connector
                  │
         (Keyboard/Mouse plugs here)

TinyUSB host callbacks:

  • tuh_hid_mount_cb() - Device connected and configured
  • tuh_hid_umount_cb() - Device disconnected
  • tuh_hid_report_received_cb() - Data received from device

Keyboard boot protocol report:

Byte 0: Modifier keys (Ctrl, Shift, Alt, GUI)
Byte 1: Reserved
Bytes 2-7: Up to 6 simultaneous key codes

Mouse boot protocol report:

Byte 0: Buttons (bit 0 = left, bit 1 = right, bit 2 = middle)
Byte 1: X movement (signed, -127 to +127)
Byte 2: Y movement (signed, -127 to +127)

Learning Milestones

  1. Device enumerated → You can read descriptors from connected device
  2. Keyboard input works → Keypresses are logged correctly
  3. Multiple devices → Can handle keyboard AND mouse simultaneously

Project 11: Custom Bootloader with OTA Updates

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Bootloaders / Flash Management / OTA Updates
  • Software or Tool: Pico SDK, WiFi module (optional), SD card (optional)
  • Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White

What you’ll build: A custom bootloader that lives in the first 64KB of flash, with A/B partitioning, integrity checking, and the ability to update firmware from USB, SD card, or WiFi. Includes factory reset and brick protection.

Why it teaches RP2040: Professional products need safe firmware updates. Building a bootloader teaches flash management, integrity checking, and the critical boot process. Brick-proofing is an art.

Core challenges you’ll face:

  • Flash layout design → maps to partitioning for bootloader + A/B images
  • Integrity verification → maps to CRC32, SHA-256, digital signatures
  • Atomic updates → maps to preventing bricks during update
  • Boot decisions → maps to choosing which partition to boot

Key Concepts:

  • Flash Programming: RP2040 Datasheet Section 2.6 (SSI/XIP)
  • A/B Partitioning: Android’s bootloader architecture
  • Integrity Checking: “Making Embedded Systems” Ch. 12
  • Safe Updates: MCUboot documentation

Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: Bare-metal experience (Project 1), understanding of flash memory, linker scripts


Real World Outcome

Build a system where firmware updates “just work” without bricking the device. The bootloader validates images, handles partial updates gracefully, and always has a fallback.

Example Output:

# Initial boot
Pico Custom Bootloader v1.0
  Flash layout:
    0x10000000 - 0x1000FFFF: Bootloader (64 KB)
    0x10010000 - 0x1008FFFF: Slot A (512 KB) [ACTIVE]
    0x10090000 - 0x1010FFFF: Slot B (512 KB) [BACKUP]
    0x10110000 - 0x101FFFFF: User data (960 KB)

Checking Slot A:
  Header: Valid
  Version: 1.2.3
  CRC32: 0xABCD1234 (OK)
  Signature: VERIFIED

Booting Slot A...
────────────────────────────────
Application v1.2.3 running!

# Firmware update via USB
$ picotool load new_firmware.uf2

Bootloader: Update received (Slot B)
  Erasing Slot B... OK
  Writing 256 KB... ████████████████████ 100%
  Verifying CRC32... OK
  Marking Slot B as pending...
  Rebooting...

# After reboot
Bootloader v1.0
  Slot A: v1.2.3 [BACKUP]
  Slot B: v1.3.0 [PENDING]

Booting Slot B for testing...
Application v1.3.0 running!

# If app calls "confirm_update()":
  Slot B marked as ACTIVE
  Slot A marked as BACKUP

# If app crashes or fails health check:
  Watchdog timeout detected
  Reverting to Slot A (v1.2.3)
  Application v1.2.3 running! (safe fallback)

The Core Question You’re Answering

“How do you update firmware in the field without ever bricking the device?”

Before you write any code, sit with this question. Power can fail mid-update. Flash can have bad sectors. The new firmware might be buggy. Your bootloader must handle ALL of these cases and still boot something.


Concepts You Must Understand First

Stop and research these before coding:

  1. Flash Memory Characteristics
    • What’s the erase block size on RP2040’s flash?
    • Why can you only flip bits from 1 to 0 (not 0 to 1)?
    • What’s flash wear leveling and do you need it?
    • Book Reference: RP2040 Datasheet Section 2.6
  2. A/B Partition Scheme
    • Why have two firmware slots?
    • How do you know which slot is “active”?
    • What triggers a switch between slots?
    • Reference: Android A/B OTA documentation
  3. Atomic Updates
    • What makes an update “atomic”?
    • Where do you store the “which slot is active” flag?
    • How do you handle power loss during update?
    • Book Reference: “Making Embedded Systems” Ch. 12

Questions to Guide Your Design

Before implementing, think through these:

  1. Flash Layout
    • How big should each slot be?
    • Where do you store boot metadata?
    • How do you handle flash wear on metadata pages?
  2. Image Format
    • What’s in your image header? (version, size, CRC, signature)
    • How do you differentiate your images from raw binaries?
    • What magic number identifies a valid image?
  3. Boot Decision Tree
    • What happens on fresh boot? Cold boot? Watchdog reset?
    • How do you detect a failed update?
    • How does “confirm update” work?

Thinking Exercise

Design the Boot State Machine

Draw the bootloader decision tree:

                    ┌───────────────────────┐
                    │        POWER ON       │
                    └───────────┬───────────┘
                                │
                    ┌───────────▼───────────┐
                    │ Read boot metadata    │
                    │ from flash            │
                    └───────────┬───────────┘
                                │
              ┌─────────────────┴─────────────────┐
              ▼                                   ▼
    ┌─────────────────┐                 ┌─────────────────┐
    │ Slot A active?  │                 │ Slot B active?  │
    │ Validate image  │                 │ Validate image  │
    └────────┬────────┘                 └────────┬────────┘
             │                                   │
    ┌────────┴─────────┐               ┌────────┴─────────┐
    │Valid?            │               │Valid?            │
    ├──▶ Yes: Boot A   │               ├──▶ Yes: Boot B   │
    └──▶ No: Try B     │               └──▶ No: Try A     │
             │                                   │
             └───────────────┬───────────────────┘
                            │
              ┌─────────────▼─────────────┐
              │ Both invalid?             │
              │ ──▶ Enter recovery mode   │
              │     (USB boot or factory) │
              └───────────────────────────┘

Edge cases to consider:

  • Power fail during flash erase
  • Power fail during flash write
  • New firmware crashes immediately
  • New firmware works but fails after 10 minutes

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you design an OTA update system for an embedded device?”
  2. “What is A/B partitioning and why is it used?”
  3. “How do you prevent bricking during firmware updates?”
  4. “What integrity checks should a bootloader perform?”
  5. “How do you handle rollback to previous firmware versions?”

Hints in Layers

Hint 1: Start with flash read/write Before building the bootloader, master flash programming with the SDK’s flash API.

Hint 2: Use the last page for metadata Store boot flags in the last 4KB page of each slot. This survives image updates.

Hint 3: Golden rule: Write new, verify, then update flags Never erase the old image until the new one is verified. Never update flags until verified.

Hint 4: Study MCUboot MCUboot is a professional bootloader. Study its design.


Books That Will Help

Topic Book Chapter
Bootloader Design “Making Embedded Systems, 2nd Edition” by Elecia White Ch. 12
Flash Memory “RP2040 Datasheet” by Raspberry Pi Section 2.6
Safe Updates MCUboot Documentation All
Integrity Checking “Serious Cryptography” by Jean-Philippe Aumasson Ch. 7

Implementation Hints

Flash layout (16 MB flash):

0x10000000 ┌───────────────────────────┐
           │ Bootloader (64 KB)        │
0x10010000 ├───────────────────────────┤
           │ Slot A Header (4 KB)      │
           │ Slot A Code (508 KB)      │
0x10090000 ├───────────────────────────┤
           │ Slot B Header (4 KB)      │
           │ Slot B Code (508 KB)      │
0x10110000 ├───────────────────────────┤
           │ Boot Metadata (4 KB)      │
           │ User Data (remaining)     │
0x11000000 └───────────────────────────┘

Image header format:

typedef struct {
    uint32_t magic;          // 0x50495250 ("PIRP" - Pico Image)
    uint32_t version;        // Semantic version packed
    uint32_t image_size;     // Size in bytes
    uint32_t crc32;          // CRC32 of image data
    uint8_t  signature[64];  // ECDSA signature (optional)
    uint8_t  reserved[44];   // Pad to 128 bytes
} image_header_t;

Boot metadata:

typedef struct {
    uint8_t  active_slot;    // 0 = A, 1 = B
    uint8_t  pending_slot;   // Slot waiting for confirmation
    uint8_t  boot_count;     // Consecutive boots without confirm
    uint8_t  flags;          // Recovery requested, etc.
    uint32_t crc32;          // Metadata integrity
} boot_metadata_t;

Learning Milestones

  1. Bootloader boots app → Can jump to application in Slot A
  2. A/B switching works → Can update and switch between slots
  3. Brick protection works → Failed update rolls back correctly

Project 12: PIO-Based I2C/SPI Analyzer

View Detailed Guide

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Python (host software)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: PIO / Protocol Analysis / Hardware Debugging
  • Software or Tool: Pico SDK, Serial Terminal, I2C/SPI devices to analyze
  • Main Book: “The I2C Bus Specification” (NXP Official)

What you’ll build: A protocol analyzer that passively monitors I2C or SPI buses and decodes all traffic in real-time. See addresses, data, ACK/NAK, and timing—essential for debugging hardware.

Why it teaches RP2040: Unlike the logic analyzer (Project 4) which captures raw bits, this project uses PIO to understand protocols at the semantic level. It’s a practical tool you’ll use for years.

Core challenges you’ll face:

  • Protocol-aware PIO programs → maps to state machines that understand I2C timing
  • Real-time decoding → maps to processing data as fast as it arrives
  • Non-intrusive monitoring → maps to passive observation without affecting the bus
  • Error detection → maps to identifying protocol violations

Key Concepts:

  • I2C Protocol: NXP I2C Bus Specification
  • SPI Protocol: Various modes (CPOL/CPHA)
  • PIO State Detection: RP2040 Datasheet Section 3
  • Real-Time Processing: Producer-consumer patterns

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: PIO experience, understanding of I2C/SPI protocols, logic analyzer experience helpful


Real World Outcome

Connect your analyzer to an I2C bus between a microcontroller and sensor. See every transaction decoded: addresses, read/write, data bytes, ACK/NAK. Debug timing issues and protocol errors.

Example Output:

$ cmake --build build
$ picotool load build/i2c_analyzer.uf2 -f

# Serial monitor (connected to I2C bus):
I2C Analyzer v1.0
  SDA: GPIO4, SCL: GPIO5
  Mode: Standard (100 kHz detected)
  Capture: Active

[0.000000] ════════════════════════════════════════════════
[0.001234] START
[0.001245] Address: 0x68 (WRITE) ← MPU6050
[0.001267] ACK
[0.001289] Data: 0x3B ← ACCEL_XOUT_H register
[0.001312] ACK
[0.001334] REPEATED START
[0.001356] Address: 0x68 (READ)
[0.001378] ACK
[0.001400] Data: 0xFC ← Accel X high byte
[0.001422] ACK
[0.001444] Data: 0x80 ← Accel X low byte
[0.001466] ACK
[0.001488] Data: 0x01 ← Accel Y high byte
[0.001510] ACK
[0.001532] Data: 0x20 ← Accel Y low byte
[0.001554] NAK ← Master signals end
[0.001576] STOP

Decoded: MPU6050 read 4 bytes from register 0x3B
         Accel X: -896 (-0.055g), Accel Y: 288 (0.018g)

[0.001600] ════════════════════════════════════════════════
[0.010000] Warning: Clock stretching detected (15 µs)
[0.020000] Error: Missing ACK from slave!

The Core Question You’re Answering

“How do you debug communication between two chips when you can’t modify their code?”

Before you write any code, sit with this question. When hardware doesn’t work, you need visibility into what’s actually happening on the wire. A protocol analyzer provides this visibility without changing the system under test.


Concepts You Must Understand First

Stop and research these before coding:

  1. I2C Protocol Details
    • What defines a START and STOP condition?
    • How does addressing work (7-bit vs 10-bit)?
    • What’s clock stretching and how do you detect it?
    • Reference: NXP I2C Bus Specification
  2. SPI Protocol Modes
    • What are CPOL and CPHA?
    • How do you know when a transaction starts/ends?
    • What’s the difference between modes 0, 1, 2, 3?
    • Reference: SPI specification (various sources)
  3. Non-Intrusive Monitoring
    • Why must your analyzer be high-impedance?
    • How do you synchronize to an external clock?
    • What happens if your sampling is too slow?
    • Book Reference: “The Art of Electronics” Ch. 10

Questions to Guide Your Design

Before implementing, think through these:

  1. I2C Detection
    • How do you detect START (SDA falling while SCL high)?
    • How do you sample data (on SCL rising edge)?
    • How do you handle repeated START vs STOP?
  2. Timing Analysis
    • How do you timestamp events precisely?
    • How do you measure clock period for speed detection?
    • How do you detect timing violations?
  3. Data Presentation
    • How do you group bytes into transactions?
    • How do you identify known device addresses?
    • How do you display errors clearly?

Thinking Exercise

Design the I2C State Machine

                              SCL=H, SDA↓
                ┌──────────────────────────────────────┐
                │                                      │
                ▼                                      │
    ┌───────────────────┐                              │
    │       IDLE        │◀─────────────────────────────┤
    │   (wait for START)│       SCL=H, SDA↑ (STOP)    │
    └─────────┬─────────┘                              │
              │ START detected                         │
              │ (SCL=H, SDA↓)                          │
              ▼                                        │
    ┌───────────────────┐                              │
    │   ADDRESS (8 bits)│                              │
    │   Sample on SCL↑  │                              │
    └─────────┬─────────┘                              │
              │ 8 bits received                        │
              ▼                                        │
    ┌───────────────────┐                              │
    │    ACK/NAK bit    │                              │
    │   Sample on SCL↑  │                              │
    └─────────┬─────────┘                              │
              │                                        │
    ┌─────────┴─────────┐                              │
    │                   │                              │
    ▼                   ▼                              │
┌───────┐         ┌───────────┐                        │
│ DATA  │◀───────▶│  REPEATED │────────────────────────┤
│ BYTES │ (more)  │   START   │ (new transaction)      │
└───────┘         └───────────┘                        │
    │                                                  │
    │ STOP detected                                    │
    └──────────────────────────────────────────────────┘

Implementation questions:

  • What PIO program captures this state machine?
  • How do you pass captured data to CPU efficiently?
  • How do you handle bus speeds from 100 kHz to 3.4 MHz?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the I2C protocol and its addressing scheme.”
  2. “How would you implement a passive protocol analyzer?”
  3. “What’s clock stretching in I2C and why does it matter?”
  4. “Compare I2C and SPI in terms of complexity and use cases.”
  5. “How do you detect protocol violations programmatically?”

Hints in Layers

Hint 1: Use PIO’s wait instruction wait 0 pin SCL and wait 1 pin SCL synchronize to the bus clock.

Hint 2: Detect START with edge sensitivity Configure PIO to interrupt on SDA falling edge, then check if SCL is high.

Hint 3: Use DMA for high-speed capture At 3.4 MHz Fast-mode Plus, you have only ~300ns per bit. DMA helps.

Hint 4: Parse in phases Capture raw bits first, then parse into frames in CPU.


Books That Will Help

Topic Book Chapter
I2C Protocol NXP I2C Bus Specification All
SPI Protocol Various vendor datasheets -
PIO Programming “RP2040 Assembly Language Programming” by Stephen Smith Ch. 10-12
Protocol Analysis “Debugging Embedded Systems” Ch. 5

Implementation Hints

I2C analyzer PIO program (simplified):

.program i2c_analyzer

wait_idle:
    wait 1 pin SDA        ; Wait for SDA high
    wait 1 pin SCL        ; Wait for SCL high

detect_start:
    wait 0 pin SDA        ; SDA falls
    jmp pin start_found   ; If SCL still high, it's START
    jmp wait_idle

start_found:
    irq set 0             ; Signal START to CPU

sample_bits:
    set x, 7              ; 8 bits per byte
bit_loop:
    wait 0 pin SCL        ; Wait for SCL low
    wait 1 pin SCL        ; Wait for SCL high
    in pins, 1            ; Sample SDA
    jmp x-- bit_loop      ; Next bit

    push                  ; Send byte to FIFO
    jmp sample_bits       ; Next byte

Data structure for decoded transactions:

typedef struct {
    uint32_t timestamp_us;
    enum { START, STOP, RESTART, DATA, ACK, NAK, ERROR } type;
    uint8_t data;
    uint8_t address;  // If applicable
    uint8_t rw;       // 0=write, 1=read
} i2c_event_t;

Learning Milestones

  1. START/STOP detected → PIO recognizes I2C conditions
  2. Bytes decoded → Address and data bytes appear correctly
  3. Full transactions logged → Complete read/write sequences visible

Project Comparison Table

# Project Difficulty Time Coolness Key Learning
1 Bare-Metal Blinky Advanced 1-2 weeks Level 4 Boot process, clocks, memory map
2 PIO LED Controller Intermediate Weekend Level 5 PIO basics, timing, DMA
3 USB HID Keyboard Advanced 1-2 weeks Level 4 USB protocol, TinyUSB
4 Logic Analyzer Advanced 2-4 weeks Level 5 PIO capture, SUMP, multicore
5 Audio Synthesizer Advanced 2-4 weeks Level 5 I2S, DMA double-buffer, DSP
6 Real-Time Scheduler Expert 3-6 weeks Level 4 Context switching, RTOS
7 VGA Display Expert 3-6 weeks Level 5 Video timing, multi-SM sync
8 Secure Boot (RP2350) Master 2-4 weeks Level 4 Cryptography, OTP, TrustZone
9 RISC-V Programming Expert 2-3 weeks Level 5 Dual architecture, ISA comparison
10 USB Host Expert 2-3 weeks Level 4 USB host mode, enumeration
11 Custom Bootloader Expert 3-4 weeks Level 4 Flash, A/B updates, brick protection
12 I2C/SPI Analyzer Advanced 2-3 weeks Level 4 Protocol decoding, debugging tools

Recommendation

If you’re new to RP2040/RP2350: Start with Project 1 (Bare-Metal Blinky) and Project 2 (PIO LED Controller). These build foundational understanding of the architecture and PIO—the RP2040’s killer feature.

If you have microcontroller experience but are new to RP2040: Start with Project 2 (PIO LED Controller) and Project 4 (Logic Analyzer). These showcase what makes RP2040 unique.

If you want practical tools: Project 4 (Logic Analyzer) and Project 12 (I2C/SPI Analyzer) create tools you’ll use for years.

If you want the most impressive projects: Project 5 (Audio Synthesizer), Project 7 (VGA Display), and Project 9 (RISC-V) are showstoppers.

If you’re preparing for embedded systems interviews: Project 1 (Bare-Metal), Project 3 (USB), Project 6 (Scheduler), and Project 11 (Bootloader) cover the most common interview topics.


Final Overall Project: RP2040/RP2350 Development Board from Scratch

  • File: LEARN_RP2040_RP2350_DEEP_DIVE.md
  • Main Programming Language: C, KiCad (schematic/PCB)
  • Alternative Programming Languages: Rust
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 5: Master
  • Knowledge Area: Hardware Design / PCB / Full-Stack Embedded
  • Software or Tool: KiCad, JLCPCB/PCBWay, Pico SDK
  • Main Book: “Designing Electronics That Work” by Hunter Scott

What you’ll build: Your own RP2040 or RP2350 development board from scratch—schematic, PCB layout, assembly, and custom firmware. Include USB-C, external flash, LED indicators, and your choice of peripherals (WiFi module, display, sensors).

Why it’s the ultimate project: This combines everything: understanding the silicon at the deepest level (power sequencing, crystal oscillator, flash interface), PCB design (high-speed USB, power integrity), manufacturing (DFM, assembly), and all the firmware from previous projects.

Core challenges you’ll face:

  • Power supply design → maps to LDO/switching, sequencing, decoupling
  • Crystal oscillator layout → maps to high-frequency PCB design
  • Flash interface → maps to QSPI routing, impedance control
  • USB-C implementation → maps to CC lines, power negotiation
  • Debugging when it doesn’t work → maps to systematic fault isolation

Real World Outcome: You’ll hold a custom PCB with your own RP2040/RP2350 board. It boots, runs USB, and executes your firmware. You designed it, ordered it, assembled it, and programmed it.

Learning Path:

  1. Study the Pico/Pico 2 hardware design files (open source)
  2. Create minimal schematic (RP2040 + flash + USB + power)
  3. Layout PCB following reference design guidelines
  4. Order boards and components
  5. Assemble (hand solder or JLCPCB assembly)
  6. Debug and bring up
  7. Port your favorite projects to your custom board

Difficulty: Master Time estimate: 2-3 months Prerequisites: All previous projects, basic electronics, KiCad experience helpful


Summary

This learning path covers RP2040 and RP2350 microcontrollers through 12 hands-on projects plus one capstone. Here’s the complete list:

# Project Name Main Language Difficulty Time Estimate
1 Bare-Metal Blinky (No SDK) C Advanced 1-2 weeks
2 PIO LED Strip Controller (WS2812B) C Intermediate Weekend
3 USB HID Keyboard Emulator C Advanced 1-2 weeks
4 Logic Analyzer with PIO C Advanced 2-4 weeks
5 Dual-Core Audio Synthesizer C Advanced 2-4 weeks
6 Multicore Real-Time Scheduler C Expert 3-6 weeks
7 VGA Display via PIO C Expert 3-6 weeks
8 RP2350 Secure Boot Implementation C Master 2-4 weeks
9 RP2350 RISC-V Bare Metal Programming C/RISC-V ASM Expert 2-3 weeks
10 USB Host Mode (Keyboard/Mouse Reader) C Expert 2-3 weeks
11 Custom Bootloader with OTA Updates C Expert 3-4 weeks
12 PIO-Based I2C/SPI Analyzer C Advanced 2-3 weeks
Final RP2040/RP2350 Development Board C/KiCad Master 2-3 months

For beginners: Start with projects #1, #2, #3 For intermediate: Jump to projects #4, #5, #12 For advanced: Focus on projects #6, #7, #8, #9

Expected Outcomes

After completing these projects, you will:

  • Understand RP2040/RP2350 architecture from silicon to software
  • Master PIO—the RP2040’s unique programmable I/O system
  • Implement USB device and host applications
  • Build real-time systems with multicore and DMA
  • Design secure embedded systems with cryptographic boot
  • Create practical debugging tools you’ll use for years
  • Be prepared for embedded systems interviews at any level
  • Have built 12+ working projects demonstrating deep microcontroller expertise

You’ll have built 13 working projects that demonstrate deep understanding of RP2040 and RP2350 from first principles.


Sources

Research for this learning guide included: