Let’s build the embedded equivalent of “Hello World” - a blinking LED program for the RP2040 using Rust. We’ll use the rp2040-project-template which comes with defmt logging, panic handling, and probe-rs debugging ready to go.

Template Features

  1. defmt logging - Fast, compact logging over RTT
  2. Stack overflow detection - flip-link will catch stack overflows
  3. Better panic handling - Get actual panic info over the debug probe
  4. Fast iteration - probe-rs gives you quick flash/debug cycles

Setup

You’ll need:

  • Rust toolchain (get it from rustup)
  • An RP2040 board (like the Raspberry Pi Pico)
  • Optionally: A debug probe (another Pico flashed with probe-rs firmware works fine)

Install the required tools:

# ARM Cortex-M0+ compiler target
rustup target install thumbv6m-none-eabi

# Stack overflow detection
cargo install flip-link

Debugging tools

  • probe-rs-debugger
    Step 1 - Install Visual Studio Code from https://code.visualstudio.com/

    Step 2 - Install probe-rs

    $ cargo install --locked probe-rs-tools
    

    Step 3 - Open this project in VSCode

    Step 4 - Install debugger for probe-rs via the VSCode extensions menu (View > Extensions)

    Step 5 - Launch a debug session by choosing Run>Start Debugging (or press F5)

Or if you don’t want to use a debugger. You can upload it over USB after holding down the BOOTSEL button when attaching the usb cable.

  • Loading a UF2 over USB
    Step 1 - Install elf2uf2-rs:

    $ cargo install elf2uf2-rs --locked
    

    Step 2 - Modify .cargo/config to change the default runner

    [target.`cfg(all(target-arch = "arm", target_os = "none"))`]
    runner = "elf2uf2-rs -d"
    

Grab the template:

git clone https://github.com/rp-rs/rp2040-project-template

The Code

Here’s our blink program (src/main.rs):

#![no_std]
#![no_main]

use bsp::entry;
use defmt::*;
use defmt_rtt as _;
use embedded_hal::digital::v2::OutputPin;
use panic_probe as _;
use rp_pico as bsp;

use bsp::hal::{
    clocks::{init_clocks_and_plls, Clock},
    pac,
    sio::Sio,
    watchdog::Watchdog,
};

#[entry]
fn main() -> ! {
    // Grab singleton instances of peripherals
    let mut pac = pac::Peripherals::take().unwrap();
    let core = pac::CorePeripherals::take().unwrap();
    let mut watchdog = Watchdog::new(pac.WATCHDOG);
    let sio = Sio::new(pac.SIO);

    // Configure system clocks
    // External crystal on the Pico is 12MHz
    let clocks = init_clocks_and_plls(
        12_000_000u32,  // External xtal frequency
        pac.XOSC,       // Crystal oscillator
        pac.CLOCKS,     // Clock configuration
        pac.PLL_SYS,    // System PLL (can go up to 133MHz)
        pac.PLL_USB,    // USB PLL 
        &mut pac.RESETS,
        &mut watchdog,
    ).ok().unwrap();

    // Set up the delay provider using SYST
    let mut delay = cortex_m::delay::Delay::new(
        core.SYST, 
        clocks.system_clock.freq().to_Hz()
    );

    // GPIO initialization
    let pins = bsp::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    // LED is on pin 25 (onboard LED for Pico)
    let mut led_pin = pins.led.into_push_pull_output();

    loop {
        info!("high");  // This shows up in defmt output
        led_pin.set_high().unwrap();
        delay.delay_ms(500);
        
        info!("low");
        led_pin.set_low().unwrap();
        delay.delay_ms(500);
    }
}

Code Breakdown

Let’s break down the important bits:

  1. #![no_std] - We’re running on bare metal, no standard library
  2. #![no_main] - Using our own entry point, not the standard Rust one
  3. Peripherals - We get single-instance access to hardware:
    • pac::Peripherals - All RP2040-specific peripherals
    • CorePeripherals - ARM Cortex-M0+ core peripherals
    • Watchdog - Required for clock setup
    • Sio - Single-cycle I/O (fast GPIO access)
  4. Clock Setup - Configure system from the 12MHz crystal to:
    • System clock at default speed (125MHz)
    • USB clock at 48MHz
    • Other peripherals get their required clocks
  5. GPIO - Set up pin 25 (the onboard LED) as a push-pull output
  6. Main Loop - Toggle LED every 500ms and log state changes

Running It

  1. Connect your debug probe
  2. Hook up the target board
  3. Run:
    cargo run --release
    

Expected behavior: You’ll see the LED blink and get debug output:

└─ high
└─ low
└─ high
└─ low

Acknowledgements

This project uses the rp2040-project-template from the rp-rs organization. The template includes:

probe-rs for debugging and flashing defmt for efficient debug logging flip-link for stack overflow detection panic-probe for better panic handling

Special thanks to creators and maintainers of the tools above as well as to the contributors who built and maintain this template. Their work has created an excellent foundation for RP2040 development in Rust. The template is dual-licensed under MIT/Apache-2.0.