Reviews embedded Rust firmware for RP2350 (Raspberry Pi Pico 2 W) and ESP32-S3 (Heltec) microcontrollers with a focus on the Embassy async runtime, memory constraints, LED control, CAN attachment protocols, and SLCAN bridging. Use when reviewing MCU firmware changes, debugging LED controller issues, evaluating Embassy async patterns, checking static memory allocation, validating SLCAN implementations, or assessing hardware-specific code for WS2812 LEDs, CAN peripherals, and tool attachments. Covers no_std environments, PIO state machines, RMT peripherals, and USB CDC serial communication.
This skill provides comprehensive code review for embedded Rust firmware targeting RP2350 and ESP32-S3 microcontrollers used in the BVR rover system.
The BVR uses two MCU platforms for peripheral control:
Both run Rust in no_std environments with hardware-specific runtimes and memory constraints.
Architecture:
mcu/
├── bins/
│ ├── rp2350/ # Pico 2 W binaries (LED controller, etc.)
│ │ └── src/main.rs
│ └── esp32s3/ # Heltec binaries (attachments, SLCAN)
│ └── src/main.rs
├── crates/
│ ├── mcu-core/ # Shared protocol and types
│ ├── mcu-leds/ # LED controller library
│ └── ...
Key Differences:
| Feature | RP2350 (Pico 2 W) | ESP32-S3 (Heltec) |
|-------------------|---------------------------------|--------------------------------|
| Runtime | Embassy async (#[embassy_executor::main]) | Polling loop (#[main]) |
| Memory | 520 KB SRAM, static allocation | 512 KB SRAM, heap allocation |
| Edition | Rust 2021 | Rust 2024 |
| LED Control | PIO state machine (WS2812) | RMT peripheral (WS2811/WS2812) |
| USB | USB CDC serial (device mode) | USB serial (via UART bridge) |
| Display | None | OLED SSD1306 (I2C) |
| Wireless | WiFi (Pico W chip) | WiFi, BLE, LoRa |
| CAN | External MCP2515 (SPI) | Built-in TWAI (CAN 2.0) |
Location: mcu/bins/rp2350/src/main.rs
Key Points to Review:
#[embassy_executor::main] on main functionasync fn main(spawner: Spawner)spawner.spawn(task_name()).unwrap()bind_interrupts! macroExample Pattern:
// Good: Embassy async main
#[embassy_executor::main]
async fn main(spawner: Spawner) {
// 1. Initialize peripherals
let p = embassy_rp::init(Default::default());
// 2. Bind interrupts
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => InterruptHandler<USB>;
});
// 3. Create drivers
let driver = Driver::new(p.USB, Irqs);
// 4. Spawn tasks
spawner.spawn(usb_task(driver)).unwrap();
spawner.spawn(led_controller_task(p.PIO0)).unwrap();
// 5. Main loop (or empty if all work in tasks)
loop {
Timer::after_millis(1000).await;
}
}
Red Flags:
std::thread::sleep).await on async operationsKey Points to Review:
#[embassy_executor::task]Example Pattern:
// Good: Embassy task
#[embassy_executor::task]
async fn led_controller_task(mut pio: PIO0) {
loop {
// Wait for command
let cmd = LED_COMMAND_CHANNEL.receive().await;
// Update LEDs
match update_leds(&mut pio, cmd) {
Ok(_) => {}
Err(e) => error!("LED update failed: {:?}", e),
}
}
}
Purpose: No heap allocator in RP2350, all memory must be static.
Key Points to Review:
StaticCell used for static mut datastatic with StaticCellBox, Vec, String (heap types)Example Pattern:
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::{Channel, Sender, Receiver};
use static_cell::StaticCell;
// Good: Static channel for inter-task communication
static LED_CHANNEL: StaticCell<Channel<CriticalSectionRawMutex, LedCommand, 8>> = StaticCell::new();
fn init_channel() -> &'static Channel<CriticalSectionRawMutex, LedCommand, 8> {
LED_CHANNEL.init(Channel::new())
}
// Good: Static USB buffers
static EP_MEMORY: StaticCell<[u8; 1024]> = StaticCell::new();
static CONFIG_DESC: StaticCell<[u8; 256]> = StaticCell::new();
// Bad: Heap allocation (won't compile in no_std without alloc)
// let vec = vec![1, 2, 3]; // ❌ No Vec
// let string = String::from("hello"); // ❌ No String
Red Flags:
Vec, String, Box)StaticCell wrapper for static mutSee: rp2350-patterns.md for detailed patterns.
Purpose: Programmable I/O state machine generates precise timing for WS2812 protocol.
Key Points to Review:
pio.load_program()WS2812 Protocol:
Example Pattern:
// Good: PIO-based WS2812 driver
use embassy_rp::pio::{Pio, Common, StateMachine};
async fn init_ws2812(pio: PIO0, pin: PIN_25) -> Ws2812<'static> {
let Pio { mut common, sm0, .. } = Pio::new(pio);
// Load PIO program (bit-bangs WS2812 protocol)
let prg = pio_proc::pio_asm!(
".side_set 1 opt",
".wrap_target",
"bitloop:",
" out x, 1 side 0 [1]", // Shift out 1 bit
" jmp !x do_zero side 1 [2]", // High if bit 1
"do_one:",
" jmp bitloop side 1 [4]", // Stay high (800ns)
"do_zero:",
" nop side 0 [4]", // Go low (400ns)
".wrap"
);
let program = common.load_program(&prg.program);
// Configure state machine
let mut cfg = Config::default();
cfg.set_out_pins(&[&pin]);
cfg.set_clock_divider(125); // 800kHz for WS2812 timing
let ws2812 = Ws2812::new(sm0, pin, program, cfg);
ws2812
}
// Send color data (GRB format!)
async fn set_led_color(ws2812: &mut Ws2812<'_>, index: usize, r: u8, g: u8, b: u8) {
let color = ((g as u32) << 16) | ((r as u32) << 8) | (b as u32); // GRB
ws2812.write(&[color]).await;
}
Red Flags:
Purpose: USB serial for debugging and SLCAN bridge.
Key Points to Review:
Driver::new()Example Pattern:
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
#[embassy_executor::task]
async fn usb_task(driver: Driver<'static, USB>) {
let mut config = embassy_usb::Config::new(0x1234, 0x5678);
config.manufacturer = Some("Muni Robotics");
config.product = Some("BVR LED Controller");
let mut builder = Builder::new(driver, config, /* ... */);
// Create CDC-ACM class
let mut state = State::new();
let mut cdc = CdcAcmClass::new(&mut builder, &mut state, 64);
let usb = builder.build();
loop {
// Read from USB
let mut buf = [0u8; 64];
let n = cdc.read_packet(&mut buf).await.unwrap();
// Echo back (for testing)
cdc.write_packet(&buf[..n]).await.unwrap();
}
}
Location: mcu/bins/esp32s3/src/main.rs
Key Points to Review:
#[main] entry point (not #[embassy_executor::main])delay.delay_millis() for timingread_ready() for non-blocking readsExample Pattern:
// Good: Polling loop
#[main]
fn main() -> ! {
// 1. Initialize peripherals
let peripherals = Peripherals::take();
let mut delay = Delay::new();
// 2. Initialize UART
let mut uart = Uart::new(peripherals.UART0, /* ... */);
// 3. Main loop
loop {
// Non-blocking UART read
if uart.read_ready() {
let mut buf = [0u8; 64];
if let Ok(n) = uart.read(&mut buf) {
process_command(&buf[..n]);
}
}
// Update LEDs
update_leds();
// Small delay (don't busy-wait)
delay.delay_millis(1);
}
}
Red Flags:
uart.read() without read_ready())See: esp32s3-patterns.md for detailed patterns.
Purpose: ESP32-S3 has a heap allocator, allows Vec, String, etc.
Key Points to Review:
esp_alloc::heap_allocator!() called in mainExample Pattern:
use esp_alloc as _;
#[main]
fn main() -> ! {
// Initialize heap allocator
esp_alloc::heap_allocator!();
// Now can use heap types (use sparingly!)
let mut buffer = Vec::with_capacity(256); // Pre-allocate
loop {
// Heap allocation okay for non-critical paths
let message = String::from("Status: OK");
send_message(&message);
buffer.clear(); // Reuse buffer (don't re-allocate)
}
}
Best Practice: Use heap for initialization and infrequent operations, not in hot loops.
Purpose: Remote Control Transceiver generates precise pulse sequences.
Key Points to Review:
Example Pattern:
use esp_hal::rmt::{Rmt, TxChannel};
fn init_rmt_leds(rmt: RMT, pin: GPIO48) -> TxChannel<'static, 0> {
let rmt = Rmt::new(rmt, 80u32.MHz()).unwrap(); // 80MHz clock
let tx_config = TxChannelConfig {
clk_divider: 1,
idle_output_level: false,
carrier_modulation: false,
idle_output: true,
};
let mut channel = rmt.channel0.configure(pin, tx_config).unwrap();
// WS2812 pulse timings (in RMT ticks)
// Bit 0: 400ns high, 850ns low
// Bit 1: 800ns high, 450ns low
// Each tick = 12.5ns at 80MHz
channel
}
fn send_pixel(channel: &mut TxChannel, r: u8, g: u8, b: u8) {
let grb = ((g as u32) << 16) | ((r as u32) << 8) | (b as u32); // GRB order
for bit in (0..24).rev() {
let is_one = (grb >> bit) & 1 == 1;
let (high, low) = if is_one {
(64, 32) // 800ns, 400ns at 80MHz
} else {
(32, 64) // 400ns, 800ns
};
channel.transmit(&[(high, true, low, false)]).unwrap();
}
}
Red Flags:
Purpose: Status display for standalone operation.
Key Points to Review:
Example Pattern:
use ssd1306::{Ssd1306, mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface};
fn init_display(i2c: I2C0, sda: GPIO17, scl: GPIO18) -> Ssd1306<I2CInterface<I2C0>, ...> {
let i2c = I2c::new(i2c, sda, scl, 400u32.kHz()).unwrap();
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display.init().unwrap();
display.clear();
display.flush().unwrap();
display
}
fn update_status(display: &mut Ssd1306<...>, status: &str) {
display.clear();
Text::new(status, Point::new(0, 0), MonoTextStyle::new(&FONT_6X10, BinaryColor::On))
.draw(display)
.unwrap();
display.flush().unwrap();
}
Location: mcu/crates/mcu-leds/src/lib.rs
Purpose: Manage LED patterns for rover state indication.
Key Points to Review:
LedMode enum with all patterns (Off, Solid, Pulse, Chase, Flash)idle(), teleop(), etc.)LED Modes:
Off: All LEDs offSolid(r, g, b, brightness): Fixed colorPulse(r, g, b, period_ms): Breathing effect (sine wave)Flash(r, g, b, period_ms): On/off strobeChase(r, g, b, speed): Moving patternRover State Convenience Methods:
impl LedMode {
pub fn idle() -> Self {
Self::Solid(0, 0, 255, 200) // Blue solid
}
pub fn teleop() -> Self {
Self::Pulse(0, 255, 0, 2000) // Green pulse, 2s period
}
pub fn autonomous() -> Self {
Self::Pulse(0, 255, 255, 1500) // Cyan pulse, 1.5s period
}
pub fn estop() -> Self {
Self::Flash(255, 0, 0, 200) // Red flash, 200ms (urgent!)
}
pub fn fault() -> Self {
Self::Flash(255, 165, 0, 500) // Orange flash, 500ms
}
}
Key Points to Review:
Purpose: Smooth transitions and effects.
Key Points to Review:
Pulse Animation:
// Good: Sine wave pulse
fn pulse_brightness(base: u8, time_ms: u32, period_ms: u32) -> u8 {
let phase = (time_ms % period_ms) as f32 / period_ms as f32;
let sine = ((phase * 2.0 * PI).sin() + 1.0) / 2.0; // 0.0 to 1.0
let min_brightness = base / 2;
let max_brightness = base;
(min_brightness as f32 + (max_brightness - min_brightness) as f32 * sine) as u8
}
Location: mcu/crates/mcu-core/src/protocol.rs
CAN ID Range: 0x200-0x2FF (16 attachment slots, 0x10 offset each)
Per-Attachment Messages (8 message types per attachment):
+0x00: Heartbeat (A → H, periodic beacon)+0x01: Identify request (H → A, query identity)+0x02: Identity response (A → H, device info)+0x03: Command (H → A, control attachment)+0x04: Acknowledgment (A → H, command ACK)+0x05: Sensor data (A → H, periodic readings)+0x06: Configuration (H → A, set parameters)+0x07: Error report (A → H, fault indication)Example: Attachment 0 (base 0x200)
Key Points to Review:
BASE + SLOT * 0x10 + OFFSETSee: Firmware skill can-protocol.md for full protocol.
Purpose: Serial Line CAN bridge for debugging via USB.
Location: mcu/bins/esp32s3/src/slcan.rs
Standard Frame:
t<ID><LEN><DATA>\r
Extended Frame:
T<ID><LEN><DATA>\r
Example:
T00000B0010A\r # Extended ID 0x0B00, length 1, data 0x0A
t10348AABBCCDD\r # Standard ID 0x103, length 4, data 0xAABBCCDD
| Command | Description | Response |
|---------|-----------------------------|-------------|
| O\r | Open CAN bus | \r (OK) |
| C\r | Close CAN bus | \r |
| S4\r | Set bitrate to 500kbps | \r |
| V\r | Get version | V1234\r |
| N\r | Get serial number | NABCD\r |
Key Points to Review:
\x07)\r) terminates every messageExample Pattern:
// Good: SLCAN command parsing
fn parse_slcan(cmd: &[u8]) -> Result<SlcanCommand, SlcanError> {
if cmd.is_empty() || cmd[cmd.len() - 1] != b'\r' {
return Err(SlcanError::MissingCR);
}
match cmd[0] {
b'O' => Ok(SlcanCommand::Open),
b'C' => Ok(SlcanCommand::Close),
b'S' => {
// Parse bitrate (S0-S8)
let rate = cmd[1] - b'0';
if rate > 8 {
return Err(SlcanError::InvalidBitrate);
}
Ok(SlcanCommand::SetBitrate(rate))
}
b't' => parse_standard_frame(&cmd[1..]),
b'T' => parse_extended_frame(&cmd[1..]),
_ => Err(SlcanError::UnknownCommand),
}
}
See: esp32s3-patterns.md for full SLCAN implementation.
SRAM: 520 KB total
Flash: 4 MB
Key Points to Review:
SRAM: 512 KB total
Flash: 8 MB
Key Points to Review:
Key Points to Review:
#[cfg(test)] module for unit tests#[test] attributestd imports in testsExample Pattern:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_led_mode_pulse() {
let mode = LedMode::Pulse(255, 0, 0, 1000);
match mode {
LedMode::Pulse(r, g, b, period) => {
assert_eq!(r, 255);
assert_eq!(period, 1000);
}
_ => panic!("Wrong mode"),
}
}
}
Pattern: Use defmt logging and probe-rs for debugging.
# Flash and run with logging
cargo run --release
# or
probe-rs run --chip RP2350
// WRONG: ESP32 doesn't use Embassy
#[embassy_executor::main]
async fn main(spawner: Spawner) { // ❌ Won't compile
// ...
}
// CORRECT: Blocking main
#[main]
fn main() -> ! {
loop {
// ...
}
}
// WRONG: No heap allocator
let vec = vec![1, 2, 3]; // ❌ Won't compile (no alloc)
// CORRECT: Static array
static DATA: StaticCell<[u8; 16]> = StaticCell::new();
// WRONG: WS2812 expects GRB
let color = (r as u32) << 16 | (g as u32) << 8 | b as u32; // ❌ RGB
// CORRECT: GRB for WS2812
let color = (g as u32) << 16 | (r as u32) << 8 | b as u32; // ✅ GRB
For more detailed information, see:
# Build RP2350
cd mcu
cargo build --release -p rover-leds
# Build ESP32-S3
cd mcu/bins/esp32s3
cargo build --release
# Flash RP2350 (requires picotool)
picotool load target/thumbv8m.main-none-eabihf/release/rover-leds -t elf -f
# Flash ESP32-S3 (requires espflash)
espflash flash --monitor target/xtensa-esp32s3-none-elf/release/heltec-attachment
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer