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:
-
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.
-
True Dual-Core: Two independent cores with hardware synchronization primitives (spinlocks, FIFOs). Not a gimmick—real parallel processing at $1.
-
12 DMA Channels: Move data at 100+ MB/sec without touching the CPU. Chain operations, control peripherals, stream audio—all in hardware.
-
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.
-
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:
- Foundation (Week 1):
- RP2040 Datasheet Section 1 (System overview)
- Introduction to Computer Organization Ch. 1-3 (ARM basics)
- Peripherals (Week 2):
- RP2040 Datasheet Sections 2.1-2.4 (GPIO, Clocks, Resets)
- Making Embedded Systems Ch. 4 (I/O)
- PIO Deep Dive (Week 3):
- RP2040 Assembly Language Programming Ch. 10-12
- RP2040 Datasheet Section 3 (all of it)
- Advanced Topics (Week 4+):
- DMA, multicore, USB as needed by projects
Project 1: Bare-Metal Blinky (No SDK)
- 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:
- 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
volatilecrucial for hardware registers? - Book Reference: “The Secret Life of Programs” Ch. 8 - Jonathan Steinhart
- 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
- 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:
- 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?
- 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?
- 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:
- “Explain what happens when an ARM Cortex-M processor comes out of reset.”
- “What is the vector table and what does the first entry contain?”
- “Why do embedded systems use PLLs for clock generation?”
- “What’s the difference between volatile and non-volatile in C? When would you use volatile?”
- “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
- Enable XOSC, wait for stable
- Configure PLL_SYS (FBDIV, POSTDIV1, POSTDIV2)
- Wait for PLL lock
- 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:
- Write to
XOSC_CTRLto start crystal - Wait for
XOSC_STATUS.STABLE - Configure
PLL_SYS_CS,PLL_SYS_PWR,PLL_SYS_FBDIV_INT,PLL_SYS_PRIM - Switch
CLK_SYS_CTRLto PLL source
GPIO output requires:
- Take GPIO block out of reset (
RESETS) - Configure pad (
PADS_BANK0_GPIO25) - Set function to SIO (
IO_BANK0_GPIO25_CTRL) - Set direction (
SIO_GPIO_OE_SET) - Toggle output (
SIO_GPIO_OUT_XOR)
Learning Milestones
- Stage 2 boots correctly → You understand the CRC32 requirement and boot sequence
- Clock runs at 125 MHz → You understand PLLs and clock distribution
- LED blinks at exact 1Hz → You understand GPIO and timer configuration from registers
Project 2: PIO LED Strip Controller (WS2812B)
- 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:
- 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
- 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
- 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:
- 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?
- 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?
- 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_setcontrol the output pin without explicit instructions?
The Interview Questions They’ll Ask
Prepare to answer these:
- “What is PIO on the RP2040 and why would you use it?”
- “How do you implement a custom protocol using PIO?”
- “Explain the difference between bit-banging and hardware peripherals for timing-critical protocols.”
- “How does DMA reduce CPU overhead in embedded systems?”
- “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:
- Pull 32 bits from FIFO (or autopull 24 bits)
- For each bit, output HIGH, then conditionally stay high/go low based on bit value
- End with LOW
For DMA setup:
- Configure channel to read from your LED buffer
- Write to PIO TX FIFO (sm_txf[0] register)
- Set DREQ to PIO0_TX0 or similar
- On completion, trigger next frame
Learning Milestones
- Single LED lights up → You can push data to PIO TX FIFO
- 8 LEDs show correct colors → Your timing and bit ordering are correct
- DMA drives animation → You understand autonomous DMA transfers
Project 3: USB HID Keyboard Emulator
- 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:
- 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
- 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
- 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:
- 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?
- 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?
- 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:
- “Explain the USB enumeration process from device connection to ready.”
- “What’s the difference between USB HID and CDC device classes?”
- “How do USB endpoints work and why are they unidirectional?”
- “What’s a HID report descriptor and why is it so complex?”
- “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 successfullytud_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:
- Convert ASCII to HID scan code
- Determine if shift is needed
- Send press, wait, send release, wait
- Repeat for each character
USB descriptors are byte arrays. Use the HID report descriptor tool to generate them.
Learning Milestones
- Device enumerates → Host recognizes your keyboard
- Single keypress works → You understand HID reports
- Full string typing works → You’ve mastered the ASCII-to-scancode conversion
Project 4: Logic Analyzer with PIO
- 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:
- 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)
- 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
- 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:
- 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?
- 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?
- 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:
- “How would you implement a high-speed data capture system on an embedded device?”
- “Explain how DMA can be used for continuous data acquisition.”
- “What’s a ring buffer and when would you use one?”
- “How do you handle real-time data that arrives faster than you can process it?”
- “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 samplingsm_config_set_in_shift()- Shift direction, autopush thresholdsm_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
- PIO captures data to FIFO → You understand PIO input
- DMA fills a buffer → You understand DMA with PIO
- PulseView displays waveforms → You’ve implemented SUMP protocol
Project 5: Dual-Core Audio Synthesizer
- 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:
- 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
- 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
- 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:
- 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?
- Synthesis Architecture
- How do you implement wavetable lookup?
- How do you handle multiple simultaneous voices?
- How do you implement ADSR envelopes efficiently?
- 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:
- “How do you handle real-time audio on a resource-constrained microcontroller?”
- “Explain DMA double-buffering and why it’s important for audio.”
- “What’s the difference between multicore and multithreaded programming?”
- “How do you safely share data between cores without locks?”
- “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:
- Create two buffers (A and B)
- Configure DMA channel 0 to transfer buffer A
- Chain to DMA channel 1, which transfers buffer B
- Channel 1 chains back to channel 0
- On each transfer complete interrupt, fill the just-played buffer
Voice allocation:
- Keep array of voice states (frequency, phase, envelope)
- On Note On: Find free voice, set parameters
- On Note Off: Start release phase, mark voice free when envelope reaches zero
Learning Milestones
- I2S outputs audio → PIO and DMA work for audio streaming
- Sine wave plays → Basic oscillator generates samples correctly
- Polyphonic playback → Multiple voices mix without artifacts
Project 6: Multicore Real-Time Scheduler
- 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:
- 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
- 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
- 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:
- 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?
- 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?
- 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:
- “Walk me through what happens during a context switch.”
- “How does priority inversion occur and how do you prevent it?”
- “What’s the difference between cooperative and preemptive scheduling?”
- “How do you implement a mutex on a multicore system?”
- “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
- Tasks switch cooperatively → You understand context saving/restoring
- Priority scheduling works → Higher priority tasks preempt lower
- Both cores run tasks → Multicore scheduling with synchronization
Project 7: VGA Display via PIO
- 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:
- 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
- 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
- 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:
- 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?
- State Machine Allocation
- Which SM generates horizontal sync?
- Which SM generates vertical sync?
- Which SM outputs pixel data?
- How do they communicate?
- 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:
- “How would you implement video output on a microcontroller without a GPU?”
- “Explain VGA signal timing and why precision matters.”
- “How do you synchronize multiple hardware state machines?”
- “What’s a resistor DAC and how does it work?”
- “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
- Sync signals generated → Monitor locks to your signal
- Single color output → Pixels appear on screen
- Full color test pattern → DAC and framebuffer work correctly
Project 8: RP2350 Secure Boot Implementation
- 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:
- 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
- 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
- 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:
- 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?
- Boot Chain Architecture
- What does the ROM verify?
- What does your bootloader verify?
- How do you update firmware securely?
- 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:
- “Explain the concept of a chain of trust in secure boot.”
- “Why do you store a hash of the public key instead of the key itself?”
- “What is rollback protection and why is it important?”
- “How does ARM TrustZone provide isolation between secure and non-secure code?”
- “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:
- Load firmware from flash
- Parse header (contains signature, version, length)
- Compute SHA-256 of firmware contents
- Verify ECDSA signature using public key derived from OTP hash
- Check rollback counter
- 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
- Understand OTP structure → Can read boot flags and key locations
- Sign and verify firmware → picotool sign/verify works correctly
- Secure boot active → Unsigned firmware refuses to boot
Project 9: RP2350 RISC-V Bare Metal Programming
- 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:
- 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
- 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
- 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:
- 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?
- Register Conventions
- What’s
ra,sp,gp,tpin RISC-V? - How does this compare to ARM’s
lr,sp, etc.? - What’s the calling convention for function arguments?
- What’s
- 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:
- “What is RISC-V and how does it differ from ARM?”
- “Explain the RISC-V register file and calling convention.”
- “What are RISC-V extensions and which are essential?”
- “Why is RISC-V gaining traction in embedded systems?”
- “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
- Toolchain works → Can compile and run “Hello World” on RISC-V
- GPIO works → LED blinks using RISC-V instructions
- Understand differences → Can explain ARM vs RISC-V tradeoffs
Project 10: USB Host Mode (Keyboard/Mouse Reader)
- 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:
- 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
- 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
- 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:
- 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?
- Enumeration Implementation
- How do you detect device connect/disconnect?
- What’s the minimum enumeration sequence?
- How do you handle enumeration failures?
- 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:
- “Explain USB enumeration from the host’s perspective.”
- “What’s the difference between USB host and device mode?”
- “How does USB power delivery work?”
- “What’s an interrupt endpoint and how is it polled?”
- “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 configuredtuh_hid_umount_cb()- Device disconnectedtuh_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
- Device enumerated → You can read descriptors from connected device
- Keyboard input works → Keypresses are logged correctly
- Multiple devices → Can handle keyboard AND mouse simultaneously
Project 11: Custom Bootloader with OTA Updates
- 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:
- 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
- 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
- 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:
- Flash Layout
- How big should each slot be?
- Where do you store boot metadata?
- How do you handle flash wear on metadata pages?
- 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?
- 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:
- “How would you design an OTA update system for an embedded device?”
- “What is A/B partitioning and why is it used?”
- “How do you prevent bricking during firmware updates?”
- “What integrity checks should a bootloader perform?”
- “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
- Bootloader boots app → Can jump to application in Slot A
- A/B switching works → Can update and switch between slots
- Brick protection works → Failed update rolls back correctly
Project 12: PIO-Based I2C/SPI Analyzer
- 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:
- 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
- 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)
- 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:
- 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?
- Timing Analysis
- How do you timestamp events precisely?
- How do you measure clock period for speed detection?
- How do you detect timing violations?
- 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:
- “Explain the I2C protocol and its addressing scheme.”
- “How would you implement a passive protocol analyzer?”
- “What’s clock stretching in I2C and why does it matter?”
- “Compare I2C and SPI in terms of complexity and use cases.”
- “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
- START/STOP detected → PIO recognizes I2C conditions
- Bytes decoded → Address and data bytes appear correctly
- 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:
- Study the Pico/Pico 2 hardware design files (open source)
- Create minimal schematic (RP2040 + flash + USB + power)
- Layout PCB following reference design guidelines
- Order boards and components
- Assemble (hand solder or JLCPCB assembly)
- Debug and bring up
- 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 |
Recommended Learning Path
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:
- SparkFun: RP2350 vs RP2040 Technical Comparison
- Raspberry Pi: RP2350 Announcement
- RP2040 Wikipedia
- RP2350 Wikipedia
- DigiKey: Raspberry Pi Pico PIO Tutorial
- Adafruit: RP2040 PIO with CircuitPython
- Cornell ECE4760: RP2040 DMA
- Bare Metal RP2040 Guide
- TinyUSB HID+CDC Example
- Pico-PIO-USB
- RP2040 Assembly Language Programming Book