| name | pico-tooling |
| description | Interfacing with Raspberry Pi Pico using picotool, serial communication, and defmt logging. Use when flashing firmware, debugging, setting up serial telemetry, or configuring defmt/probe-rs logging. |
| keywords | ["picotool","flash","bootsel","uf2","elf","serial","usb","cdc","defmt","probe-rs","debug","logging","pyserial","terminal","pico.sh"] |
Pico Tooling & Serial Communication
Picotool
Installation
brew install picotool
git clone https://github.com/raspberrypi/picotool.git
cd picotool
mkdir build && cd build
cmake ..
make
sudo make install
Flashing Firmware
picotool load -u -v -x -t elf target/thumbv8m.main-none-eabihf/release/pico_logger
.cargo/config.toml Setup
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "picotool load -u -v -x -t elf"
[build]
target = "thumbv8m.main-none-eabihf"
Other Picotool Commands
picotool info
picotool reboot
picotool save -t uf2 backup.uf2
Serial Communication (USB CDC)
CRITICAL: 64-Byte Packet Size Limit
USB CDC write_packet() has a 64-byte maximum packet size. Messages larger than 64 bytes will be silently dropped or truncated. This is a common source of bugs where data appears to send successfully but never arrives.
Symptoms:
write_packet() returns Ok but host never receives data
- Short messages work, long messages don't
- Streaming works initially then "stops" when data grows
Solutions:
- Use compact formats (CSV instead of verbose labels)
- Split large messages across multiple
write_packet() calls
- Count characters before sending - stay under 64 bytes
Example of problematic vs working format:
"#1 P:6593600 T:8615424 A:-204,87,2161 G:5,5,-11 M:-1234,5678,-9012\r\n"
"#1,6593600,8615424,-204,87,2161,5,5,-11,-1234,5678,-9012\r\n"
Cargo.toml Dependencies
[dependencies]
embassy-usb = "0.5"
embassy-usb-logger = "0.4"
USB Serial Setup
use embassy_rp::usb::{Driver, InterruptHandler};
use embassy_rp::bind_interrupts;
use embassy_rp::peripherals::USB;
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_usb::{Builder, Config};
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => InterruptHandler<USB>;
});
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let driver = Driver::new(p.USB, Irqs);
let mut config = Config::new(0x1209, 0x0001);
config.manufacturer = Some("NCSSM Rocketry");
config.product = Some("Pico Logger");
config.serial_number = Some("001");
let mut builder = Builder::new(
driver,
config,
&mut make_static!([0u8; 256])[..],
&mut make_static!([0u8; 256])[..],
&mut make_static!([0u8; 256])[..],
&mut make_static!([0u8; 64])[..],
);
let mut class = CdcAcmClass::new(&mut builder, &mut make_static!(State::new())[..], 64);
let mut usb = builder.build();
spawner.spawn(usb_task(usb)).unwrap();
loop {
class.wait_connection().await;
let mut buf = [0u8; 64];
match class.read_packet(&mut buf).await {
Ok(n) => {
let _ = class.write_packet(&buf[..n]).await;
}
Err(_) => continue,
}
}
}
#[embassy_executor::task]
async fn usb_task(mut usb: embassy_usb::UsbDevice<'static, Driver<'static, USB>>) {
usb.run().await;
}
macro_rules! make_static {
($val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<_> = static_cell::StaticCell::new();
STATIC_CELL.init($val)
}};
}
Reading Serial on Host
ls /dev/tty.usb*
ls /dev/ttyACM*
screen /dev/ttyACM0 115200
minicom -D /dev/ttyACM0 -b 115200
picocom -b 115200 /dev/ttyACM0
defmt Logging (Recommended for Development)
defmt provides efficient, lightweight logging that transmits formatted strings over RTT (Real-Time Transfer) or probe-rs.
Cargo.toml
[dependencies]
defmt = "1.0"
defmt-rtt = "1.0"
panic-probe = { version = "1.0", features = ["print-defmt"] }
[features]
default = ["defmt"]
defmt = ["dep:defmt", "embassy-rp/defmt"]
Usage
#![no_std]
#![no_main]
use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
info!("Pico Logger starting...");
debug!("Peripherals initialized");
let pressure = read_pressure().await;
info!("Pressure: {} Pa", pressure);
#[derive(defmt::Format)]
struct SensorData {
pressure: u32,
temp: i16,
}
let data = SensorData { pressure: 101325, temp: 25 };
info!("Sensor data: {:?}", data);
}
Reading defmt Output
probe-rs run --chip RP2350 target/thumbv8m.main-none-eabihf/release/pico_logger
DEFMT_LOG=info cargo run --release
Log Levels
defmt::trace!("Very verbose");
defmt::debug!("Debug info");
defmt::info!("General info");
defmt::warn!("Warnings");
defmt::error!("Errors");
Filter with environment variable:
DEFMT_LOG=debug cargo run --release
DEFMT_LOG=pico_logger=trace,embassy_rp=warn cargo run --release
Telemetry Logging Pattern
Combining SD card logging with serial telemetry:
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
#[derive(Clone, defmt::Format)]
struct TelemetryPacket {
timestamp_ms: u32,
pressure_pa: u32,
altitude_m: f32,
accel: [i16; 3],
gyro: [i16; 3],
}
static TELEMETRY: Channel<CriticalSectionRawMutex, TelemetryPacket, 8> = Channel::new();
#[embassy_executor::task]
async fn sensor_task() {
loop {
let packet = TelemetryPacket {
timestamp_ms: now_ms(),
pressure_pa: read_pressure().await,
altitude_m: calculate_altitude(),
accel: read_accel().await,
gyro: read_gyro().await,
};
TELEMETRY.send(packet).await;
Timer::after_millis(10).await;
}
}
#[embassy_executor::task]
async fn telemetry_task(mut serial: CdcAcmClass<'static, Driver<'static, USB>>) {
loop {
let packet = TELEMETRY.receive().await;
defmt::info!("{:?}", packet);
let mut buf = [0u8; 64];
let len = encode_packet(&packet, &mut buf);
let _ = serial.write_packet(&buf[..len]).await;
}
}
probe-rs (Alternative to picotool)
probe-rs provides debugging and RTT support but has limited RP2350 support currently.
cargo install probe-rs-tools
probe-rs run --chip RP2350 target/thumbv8m.main-none-eabihf/release/pico_logger
probe-rs attach --chip RP2350
USB Command Protocol
The firmware can implement a command handler to receive control commands over USB serial. This enables remote control without reflashing.
Command Format
Commands are newline-terminated strings: CMD:<ACTION>\n
| Command | Description |
|---|
CMD:PING | Check if device is responsive |
CMD:STATUS | Query device status |
CMD:VERSION | Get firmware version |
CMD:REBOOT | Reboot to application |
CMD:BOOTSEL | Reboot to BOOTSEL mode for flashing |
CMD:LOG_START | Start SD card logging |
CMD:LOG_STOP | Stop SD card logging |
CMD:SENSORS | Get current sensor readings |
CMD:CALIBRATE | Trigger sensor calibration |
Responses: OK\n, OK:<data>\n, or ERR:<reason>\n
Embassy Command Handler Implementation
use embassy_rp::rom_data::reset_to_usb_boot;
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use heapless::String;
static LOG_START: Signal<CriticalSectionRawMutex, ()> = Signal::new();
static LOG_STOP: Signal<CriticalSectionRawMutex, ()> = Signal::new();
fn handle_command(cmd: &str) -> String<64> {
let cmd = cmd.trim();
let mut response: String<64> = String::new();
match cmd {
"CMD:PING" => {
let _ = response.push_str("OK:PONG");
}
"CMD:VERSION" => {
let _ = response.push_str("OK:");
let _ = response.push_str(env!("CARGO_PKG_VERSION"));
}
"CMD:STATUS" => {
let _ = response.push_str("OK:RUNNING");
}
"CMD:REBOOT" => {
let _ = response.push_str("OK:REBOOTING");
cortex_m::peripheral::SCB::sys_reset();
}
"CMD:BOOTSEL" => {
let _ = response.push_str("OK:ENTERING_BOOTSEL");
reset_to_usb_boot(0, 0);
}
"CMD:LOG_START" => {
LOG_START.signal(());
let _ = response.push_str("OK:LOGGING_STARTED");
}
"CMD:LOG_STOP" => {
LOG_STOP.signal(());
let _ = response.push_str("OK:LOGGING_STOPPED");
}
"CMD:SENSORS" => {
let _ = response.push_str("OK:P=101325,T=25,A=0,0,16384");
}
"CMD:CALIBRATE" => {
let _ = response.push_str("OK:CALIBRATING");
}
_ => {
let _ = response.push_str("ERR:UNKNOWN_CMD");
}
}
response
}
#[embassy_executor::task]
async fn command_task(mut serial: CdcAcmClass<'static, Driver<'static, USB>>) {
let mut buf = [0u8; 64];
let mut cmd_buf: String<64> = String::new();
loop {
serial.wait_connection().await;
match serial.read_packet(&mut buf).await {
Ok(n) => {
for &byte in &buf[..n] {
if byte == b'\n' {
let response = handle_command(&cmd_buf);
let mut out: String<66> = String::new();
let _ = out.push_str(&response);
let _ = out.push('\n');
let _ = serial.write_packet(out.as_bytes()).await;
cmd_buf.clear();
} else if byte != b'\r' {
let _ = cmd_buf.push(byte as char);
}
}
}
Err(_) => {
cmd_buf.clear();
}
}
}
}
Using Signals for Cross-Task Control
#[embassy_executor::task]
async fn logging_task() {
loop {
LOG_START.wait().await;
defmt::info!("Logging started");
loop {
if LOG_STOP.signaled() {
LOG_STOP.reset();
defmt::info!("Logging stopped");
break;
}
Timer::after_millis(10).await;
}
}
}
Utility Scripts
This skill includes helper scripts in the scripts/ directory.
Install dependencies:
pip install pyserial matplotlib
pico_control.py
Send commands to control the Pico remotely.
python scripts/pico_control.py status
python scripts/pico_control.py bootsel
python scripts/pico_control.py start-logging
python scripts/pico_control.py stop-logging
python scripts/pico_control.py send "CMD:CUSTOM"
python scripts/pico_control.py interactive
serial_monitor.py
Monitor serial output with duration/line limits.
python scripts/serial_monitor.py --duration 10
python scripts/serial_monitor.py --lines 100
python scripts/serial_monitor.py --log flight.log --timestamp --duration 60
telemetry_parser.py
Parse binary telemetry packets with limits.
python scripts/telemetry_parser.py --duration 10
python scripts/telemetry_parser.py --packets 100 --csv output.csv
Note: Edit the TelemetryPacket struct and PACKET_SIZE to match your firmware.
plot_telemetry.py
Real-time plotting of telemetry data.
python scripts/plot_telemetry.py /dev/tty.usbmodem2101
python scripts/plot_telemetry.py --file data.csv
flash.sh
Build and flash firmware.
./scripts/flash.sh
./scripts/flash.sh --release
pico_info.sh
Display connected device information.
./scripts/pico_info.sh
Debugging USB CDC Issues
Data Not Being Received
When write_packet() appears to succeed but host doesn't receive data:
- Check packet size - Must be ≤64 bytes (see above)
- Add breadcrumb messages - Send short debug markers before/after suspect code:
let _ = class.write_packet(b"BEFORE_OP\r\n").await;
let _ = class.write_packet(b"AFTER_OP\r\n").await;
- Test with simple data first - Replace complex telemetry with
"TEST\r\n" to isolate issue
- Check for blocking operations - I2C reads, timers, or other async calls might hang
Streaming Data Pattern
When implementing continuous data streaming:
for _ in 0..50 {
let _ = sensor::read(&mut i2c, &mut data).await;
let msg = format_telemetry(counter, &data);
let _ = class.write_packet(msg.as_bytes()).await;
Timer::after_millis(100).await;
}
Note: embassy_futures::select with read_packet() and Timer may not work as expected for interruptible streaming - the timer branch may never fire. Use fixed iteration counts or dedicated tasks instead.
Quick Serial Test Script
import serial
import time
ser = serial.Serial('/dev/tty.usbmodem0011', 115200, timeout=3)
time.sleep(0.5)
ser.write(b'PING\n')
time.sleep(0.2)
print('Response:', repr(ser.read(ser.in_waiting or 100)))
ser.close()
Resources