Rust blinky for the RP2040 Pico, flashing the digits of pi

Flashes the on-board LED as 3, 1, 4, 1, 5 with pauses between digits and a
longer pause before repeating. The pattern is stepped from the hardware timer
rather than blocking sleeps, so the USB serial port stays responsive and can
reset the board back into BOOTSEL on request.

Includes the original plain 250 ms blink under alt/ and prebuilt .uf2 images.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FrCPgv2NGSEcyuN9KKQKHc
This commit is contained in:
Mathew Lewis 2026-08-25 13:00:00 -07:00
commit 1f88150d03
10 changed files with 319 additions and 0 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[build]
target = "thumbv6m-none-eabi"
[target.thumbv6m-none-eabi]
rustflags = ["-C", "link-arg=--nmagic", "-C", "link-arg=-Tlink.x"]

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "blinky"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "1.0"
panic-halt = "1.0"
rp-pico = "0.9"
usb-device = "0.3"
usbd-serial = "0.2"
[profile.release]
codegen-units = 1
lto = true
opt-level = "z"
debug = 2

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# pico-blinky
Rust firmware for the Raspberry Pi Pico. Flashes the on-board LED with the
digits of pi: three flashes, a pause, one flash, a pause, four, one, five,
then a longer pause before it repeats.
## Board
This targets the **RP2040**, the original Pico. It will not run on a Pico 2.
Check which one you have by holding BOOTSEL while plugging it in and looking
at the volume that mounts:
| Volume label | Chip | This firmware |
|--------------|---------|---------------|
| `RPI-RP2` | RP2040 | yes |
| `RP2350` | RP2350 | no |
## Build
```
rustup target add thumbv6m-none-eabi
cargo install elf2uf2-rs --locked
cargo build --release
elf2uf2-rs target/thumbv6m-none-eabi/release/blinky pi-blink.uf2
```
## Flash
Hold the BOOTSEL button down **before** the cable goes in, keep it held for a
second after, then release. The button is only read at power-on, so pressing
it while the board is already running does nothing.
The board mounts as `RPI-RP2`. Copy the `.uf2` onto it. The volume ejects
itself once the write completes, which is the bootloader confirming it took
the image and jumped to it.
Prebuilt images are in `uf2/` if you just want to drag one across.
## Getting back into the bootloader
The firmware exposes a USB serial port, so after the first flash you do not
need the button again. Either:
- send `b` to the serial port, or
- open the port at 1200 baud and drop DTR, the convention `picotool` and the
Arduino IDE use
Both call `reset_to_usb_boot`, and the board comes back up as `RPI-RP2`.
The LED pattern is driven from the hardware timer rather than blocking sleeps,
so USB stays responsive while the sequence runs.
## Layout
```
src/main.rs pi digits + USB reboot-to-bootloader
alt/simple-blink.rs plain 250 ms on/off blink, the first version
memory.x flash/RAM layout, reserves 0x100 for the boot2 stage
build.rs puts memory.x on the linker search path
uf2/ prebuilt images
```
## Timing
Edit the constants at the top of `src/main.rs`:
```rust
const FLASH_ON_US: u64 = 150_000;
const FLASH_OFF_US: u64 = 200_000;
const DIGIT_GAP_US: u64 = 800_000;
const CYCLE_GAP_US: u64 = 3_000_000;
```
`DIGIT_GAP_US` is the one worth raising if the sequence is hard to count by
eye, since 1 and 4 sit next to each other and can run together.

47
alt/simple-blink.rs Normal file
View File

@ -0,0 +1,47 @@
#![no_std]
#![no_main]
use cortex_m::delay::Delay;
use embedded_hal::digital::OutputPin;
use panic_halt as _;
use rp_pico::entry;
use rp_pico::hal::{clocks::init_clocks_and_plls, pac, watchdog::Watchdog, Clock, Sio};
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
let sio = Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let mut led = pins.led.into_push_pull_output();
loop {
led.set_high().unwrap();
delay.delay_ms(250);
led.set_low().unwrap();
delay.delay_ms(250);
}
}

14
build.rs Normal file
View File

@ -0,0 +1,14 @@
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
}

12
memory.x Normal file
View File

@ -0,0 +1,12 @@
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text;

146
src/main.rs Normal file
View File

@ -0,0 +1,146 @@
#![no_std]
#![no_main]
use embedded_hal::digital::OutputPin;
use panic_halt as _;
use rp_pico::entry;
use rp_pico::hal::{
clocks::init_clocks_and_plls, pac, rom_data, timer::Timer, usb::UsbBus, watchdog::Watchdog,
Sio,
};
use usb_device::class_prelude::UsbBusAllocator;
use usb_device::prelude::*;
use usbd_serial::SerialPort;
/// 3.1415, flashed one digit at a time.
const DIGITS: [u8; 5] = [3, 1, 4, 1, 5];
const FLASH_ON_US: u64 = 150_000;
const FLASH_OFF_US: u64 = 200_000;
const DIGIT_GAP_US: u64 = 800_000;
const CYCLE_GAP_US: u64 = 3_000_000;
/// Where the blink pattern currently is. Stepped from the timer rather than
/// slept through, so USB stays responsive while the LED runs.
enum Phase {
On,
Off,
DigitGap,
CycleGap,
}
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
let sio = Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let mut led = pins.led.into_push_pull_output();
let usb_bus = UsbBusAllocator::new(UsbBus::new(
pac.USBCTRL_REGS,
pac.USBCTRL_DPRAM,
clocks.usb_clock,
true,
&mut pac.RESETS,
));
let mut serial = SerialPort::new(&usb_bus);
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x2e8a, 0x000a))
.strings(&[StringDescriptors::default()
.manufacturer("Raspberry Pi")
.product("Pi Blinker")
.serial_number("PIBLINK")])
.unwrap()
.device_class(usbd_serial::USB_CLASS_CDC)
.build();
let mut digit = 0usize;
let mut flashes_left = DIGITS[0];
let mut phase = Phase::On;
let mut next_change = timer.get_counter().ticks() + FLASH_ON_US;
let mut was_dtr = false;
led.set_high().unwrap();
loop {
// USB first, so a reboot request is never more than a poll away.
if usb_dev.poll(&mut [&mut serial]) {
let mut buf = [0u8; 64];
if let Ok(n) = serial.read(&mut buf) {
for &b in &buf[..n] {
if b == b'b' || b == b'B' {
rom_data::reset_to_usb_boot(0, 0);
}
}
}
}
// 1200-baud touch, the convention picotool and the Arduino IDE use:
// host opens the port at 1200 baud then drops DTR.
let dtr = serial.dtr();
if was_dtr && !dtr && serial.line_coding().data_rate() == 1200 {
rom_data::reset_to_usb_boot(0, 0);
}
was_dtr = dtr;
let now = timer.get_counter().ticks();
if now < next_change {
continue;
}
match phase {
Phase::On => {
led.set_low().unwrap();
phase = Phase::Off;
next_change = now + FLASH_OFF_US;
}
Phase::Off => {
flashes_left -= 1;
if flashes_left > 0 {
led.set_high().unwrap();
phase = Phase::On;
next_change = now + FLASH_ON_US;
} else if digit + 1 < DIGITS.len() {
phase = Phase::DigitGap;
next_change = now + DIGIT_GAP_US;
} else {
phase = Phase::CycleGap;
next_change = now + CYCLE_GAP_US;
}
}
Phase::DigitGap => {
digit += 1;
flashes_left = DIGITS[digit];
led.set_high().unwrap();
phase = Phase::On;
next_change = now + FLASH_ON_US;
}
Phase::CycleGap => {
digit = 0;
flashes_left = DIGITS[0];
led.set_high().unwrap();
phase = Phase::On;
next_change = now + FLASH_ON_US;
}
}
}
}

BIN
uf2/pi-blink-usb.uf2 Normal file

Binary file not shown.

BIN
uf2/simple-blink.uf2 Normal file

Binary file not shown.