Expand description
§rp-usb-console
When developing for the Raspberry Pi Pico, you typically have two choices: debugging with a hardware probe or deploying programs directly via USB. The latter is quick and convenient, but it doesn’t allow runtime communication with your application. Embassy’s built-in usb-logger lets you stream logs over USB, yet it lacks a way to send commands back (such as rebooting into BOOTSEL mode). rp-usb-console fills this gap by providing a lightweight, zero-heap solution for both logging and command exchange over USB, designed specifically for the RP2040 and the Embassy async framework. Initially, I created this library for my own use.
§Quick start

§Command list
- “/LT”: set global loglevel to TRACE
- “/LD”: set global loglevel to DEBUG
- “/LI”: set global loglevel to INFO
- “/LW”: set global loglevel to WARN
- “/LE”: set global loglevel to ERROR
- “/LO”: set global loglevel to OFF
- “/LM <module_filter>,
”: Set specific loglevel for modules containing module filter - “BS”: Reboot RP2040 to Boot Select (to deploy applications)
- “RS”: Reset RP2040
All command must be ended with /r or /n or both.
§Features
- Zero-heap logging: Fixed-size 255-byte log message buffers with no dynamic allocation
- USB CDC ACM interface: Standard USB serial communication
- Non-blocking operation: Log messages are dropped if the channel is full to maintain real-time behavior
- Packet fragmentation: Large messages are split into 64-byte USB packets for reliable transmission
- Bidirectional communication: Both logging output and line-buffered command input over USB
- Configurable command sink: Forward commands to your own channel or disable forwarding entirely
- Embassy integration: Designed specifically for Embassy executor and RP2040
§Design Goals
- Predictable real-time behavior: No blocking on full channels
- Memory efficiency: Fixed buffers, no heap allocation
- Reliability: Fragmented transmission reduces host latency issues
- Safety: Single-core RP2040 assumptions with proper synchronization
§Usage
Add this to your Cargo.toml:
[dependencies]
rp-usb-console = "0.1.0"Basic usage example:
use embassy_executor::Spawner;
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use log::info;
use rp_usb_console::USB_READ_BUFFER_SIZE;
// Create a channel for receiving commands from USB
static COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4> = Channel::new();
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// Initialize USB logging with Info level
rp_usb_console::start(
spawner,
log::LevelFilter::Info,
p.USB,
Some(COMMAND_CHANNEL.sender()),
);
// Now you can use standard log macros
info!("Hello over USB!");
// Handle incoming commands in a separate task
spawner.spawn(command_handler()).unwrap();
}
#[embassy_executor::task]
async fn command_handler() {
let receiver = COMMAND_CHANNEL.receiver();
loop {
let command = receiver.receive().await;
// Process received command data (terminated by CR/LF; trailing zeros may be present)
info!("Received command: {:?}", command);
}
}§API Reference
§start()
Initialize the USB CDC interface and spawn necessary tasks.
pub fn start(
spawner: Spawner,
level: LevelFilter,
usb_peripheral: Peri<'static, USB>,
command_sender: Option<Sender<'static, CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4>>
)Parameters:
spawner: Embassy task spawnerlevel: Log level filter (e.g.,LevelFilter::Info)usb_peripheral: RP2040 USB peripheralcommand_sender: Optional channel sender for receiving line-buffered USB commands (Nonedisables forwarding)
§LogMessage
Fixed-size message buffer with USB packet fragmentation support.
- Size: 255 bytes maximum
- Truncation: Messages exceeding capacity are truncated with ellipsis
- Fragmentation: Automatically split into 64-byte USB packets
§Implementation Details
§Memory Management
- No heap allocation: All buffers are statically allocated
- Fixed message size: 255 bytes per log message
- Channel capacity: 4 pending log messages maximum
§USB Protocol
- Packet size: 64 bytes per USB transfer
- Fragmentation: Large messages split across multiple packets
- Connection handling: Automatic reconnection support
§Threading Model
- Single-core: Designed for RP2040’s single-core architecture
- Embassy tasks: Three main tasks handle USB device, TX, and RX operations
- Non-blocking: Log operations never block the caller
§Limitations
- Messages longer than 255 bytes are truncated
- Full log channels result in dropped messages
- Currently uses CRLF line endings
- No built-in timestamp support
§Future Improvements
- Backpressure metrics (dropped message counter)
- Configurable line ending options (CRLF/LF)
- Optional timestamp formatting
- Extended formatting features
§License
This project is licensed under the MIT License - see the LICENSE file for details.
§Contributing
Contributions are welcome! Please feel free to submit a Pull Request. USB CDC logging & command channel for RP2040 (embassy).
This crate provides a zero-heap solution for bidirectional USB communication
on RP2040 microcontrollers using the Embassy async framework. It enables both
logging output and line-buffered command input (terminated by \r, \n, or
\r\n) over a standard USB CDC ACM interface.
§Quick Start
use embassy_executor::Spawner;
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use log::info;
use rp_usb_console::USB_READ_BUFFER_SIZE;
// Create a channel for receiving line-buffered commands from USB
static COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, [u8; USB_READ_BUFFER_SIZE], 4> = Channel::new();
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// Initialize USB logging with Info level
rp_usb_console::start(
spawner,
log::LevelFilter::Info,
p.USB,
Some(COMMAND_CHANNEL.sender()),
);
// Now you can use standard log macros
info!("Hello over USB!");
// Handle incoming commands in a separate task
spawner.spawn(command_handler()).unwrap();
}
#[embassy_executor::task]
async fn command_handler() {
let receiver = COMMAND_CHANNEL.receiver();
loop {
let command = receiver.receive().await;
// Process received command data (trailing zeros may be present)
info!("Received command: {:?}", command);
}
}§Features
- Zero-heap operation: All buffers are fixed-size and statically allocated
- Non-blocking logging: Messages are dropped rather than blocking when channels are full
- USB CDC ACM: Standard USB serial interface compatible with most terminal programs
- Packet fragmentation: Large messages are automatically split for reliable transmission
- Bidirectional: Both log output and line-buffered command input over the same USB connection
- Configurable command sink: Forward parsed commands to your own channel or disable command forwarding entirely
- Embassy integration: Designed for Embassy’s async executor on RP2040
§Design Principles
This crate prioritizes:
- Real-time behavior: Never blocks the caller, even when USB is disconnected
- Memory efficiency: Fixed buffers with no dynamic allocation
- Reliability: Fragmented transmission reduces host-side latency issues
- Safety: Single-core assumptions with proper synchronization primitives
Structs§
- LogMessage
- Fixed-size (255 byte) log/command message buffer with USB packet fragmentation support.
Constants§
- USB_
READ_ BUFFER_ SIZE - Size (in bytes) of the receive buffer used to accumulate incoming USB command data before processing.
Functions§
- start
- Initialize USB CDC logging and command channel, spawning all necessary tasks.