RP2350 / Pico 2 Platform Guide¶
Xylolabs SDK integration for the Raspberry Pi Pico 2 (RP2350). This is the primary reference target for the SDK.
RP2350 specifications¶
| Resource | Specification | Notes |
|---|---|---|
| CPU | Dual Cortex-M33 @ 150 MHz | ~300 MIPS total; DSP extensions |
| SRAM | 520 KB | Static allocation, no heap |
| Flash | External QSPI (2–16 MB typical) | Firmware + log storage |
| I2S | PIO-based | Handles 96kHz/24-bit with external ADC |
| UART | 2 channels | LTE-M1 modem AT commands |
| SPI | Up to 62.5 MHz | External ADC, SD card |
| GPIO | 30 pins | Sensors, motor signals, status LEDs |
| WDT | Hardware watchdog | Long-term unattended operation |
| DMA | 12 channels | Zero-copy audio capture and transfer |
The built-in ADC is 12-bit/500ksps — insufficient for 96kHz/24-bit audio. An external I2S ADC (e.g. PCM1808, CS5343) is required.
Hardware setup¶
I2S ADC connection¶
RP2350 GPIO I2S ADC (e.g. PCM1808 / CS5343)
─────────────── ────────────────────────────────
GPIO10 (PIO SCK) ───── BCK (bit clock)
GPIO11 (PIO WS) ───── LRCK (word select)
GPIO12 (PIO DIN) ───── DOUT (data out from ADC)
3V3 ───── VDD
GND ───── GND
GPIO13 (GPIO) ───── /RESET or FORMAT select
Configure the PIO state machine for I2S slave mode at 96kHz, 24-bit. The SDK's platform layer uses DMA double-buffering to transfer captured frames to the audio ring buffer without CPU involvement.
LTE modem (UART0) — BG770A¶
RP2350 GPIO LTE Modem
─────────────── ─────────────────
GPIO0 (UART0_TX) ───── RXD
GPIO1 (UART0_RX) ───── TXD
GPIO2 (GPIO) ───── PWRKEY
GPIO3 (GPIO) ───── RESET_N
VSYS (5V) ───── VCC (via LDO; check modem datasheet)
GND ───── GND
Sensor buses¶
| Bus | GPIO | Typical devices |
|---|---|---|
| I2C0 | GPIO4 (SDA), GPIO5 (SCL) | BME280 (temp/humidity), LIS3DH (vibration) |
| I2C1 | GPIO6 (SDA), GPIO7 (SCL) | MAG3110 (magnetic field) |
| SPI0 | GPIO16–GPIO19 + CS | Motor ADC, current sensors |
PIO for I2S¶
The RP2350's Programmable I/O (PIO) subsystem handles I2S timing in hardware, freeing the CPU entirely from bit-banging:
// Rust (Embassy) — two-SM I2S master-mode capture from PCM1860
// See sdk/examples/rp2350-full-hardware/src/main.rs for the full implementation.
use embassy_rp::pio::{Pio, Config as PioConfig, FifoJoin, ShiftConfig, ShiftDirection};
let Pio { mut common, sm0: mut sm_clock, sm1: mut sm_data, .. } =
Pio::new(p.PIO0, Irqs);
// SM0: clock generator — BCK (sideset) + LRCK (set), 32 BCK per LRCK half
let clock_prg = pio_proc::pio_asm!(
".side_set 1",
".wrap_target",
" set pins, 0 side 0", // LRCK=0, BCK low
" set x, 30 side 1", // BCK high, x=30
"left:",
" nop side 0",
" jmp x--, left side 1",
" set pins, 1 side 0", // LRCK=1, BCK low
" set x, 30 side 1",
"right:",
" nop side 0",
" jmp x--, right side 1",
".wrap",
);
// SM1: data receiver — samples DOUT, autopushes 32-bit words to RX FIFO
let data_prg = pio_proc::pio_asm!(
".wrap_target",
" nop", // BCK low — data transitioning
" in pins, 1", // BCK high — sample stable DOUT
".wrap",
);
// Both SMs share the same clock divider (150MHz / 12.288MHz ≈ 12.207)
// SM1 uses FifoJoin::RxOnly for 8-entry FIFO depth
// DMA ping-pong: embassy_futures::join overlaps DMA + CPU decimation
Legacy C equivalent
// Load I2S receive program into PIO0
uint offset = pio_add_program(pio0, &i2s_rx_program);
i2s_rx_program_init(pio0, 0, offset,
GPIO_I2S_SCK, GPIO_I2S_WS, GPIO_I2S_DIN,
96000); // 96kHz sample rate
// DMA channel A: PIO FIFO → ping buffer
// DMA channel B: PIO FIFO → pong buffer (double-buffer)
// On DMA completion IRQ, swap buffers and call xylolabs_audio_feed()
DMA double-buffering ensures continuous capture with zero CPU cycles spent waiting for audio data.
Dual-Core Architecture¶
The RP2350's two cores map naturally to the SDK's audio + network workload:
| Core | Responsibility | Budget |
|---|---|---|
| Core 0 | I2S capture, FIR downsample (96kHz→16kHz), XAP encode, ring buffer write | ~80 MIPS |
| Core 1 | Sensor polling, XMBP assembly, LTE-M1 TCP, xylolabs_tick(), watchdog |
~40 MIPS |
// Rust (Embassy) - Recommended
use embassy_rp::multicore::{spawn_core1, Stack};
use embassy_executor::Spawner;
static mut CORE1_STACK: Stack<4096> = Stack::new();
// Core 1: network + sensor loop
#[embassy_executor::task]
async fn network_task(stack: embassy_net::Stack<'static>) {
let mut client = XylolabsClient::new(stack);
client.set_api_key("xk_your_key");
client.set_device_id(1);
client.session_create("pico_node_1", &streams).await;
loop {
poll_sensors(&mut client).await;
client.tick().await;
Timer::after_millis(10).await;
}
}
// Core 0: audio capture loop
#[embassy_executor::task]
async fn audio_task(mut pio_rx: PioRx<'static>) {
let mut buf = [0i16; AUDIO_BLOCK_SAMPLES];
loop {
pio_rx.read(&mut buf).await;
xylolabs_audio_feed(&buf);
}
}
Legacy C equivalent
// core1_entry: network + sensor loop
void core1_entry(void) {
xylolabs_init(&g_platform);
xylolabs_set_api_key("xk_your_key");
xylolabs_set_device_id(1);
xylolabs_session_create("pico_node_1", streams, stream_count);
while (1) {
poll_sensors();
xylolabs_tick();
sleep_ms(10);
}
}
int main(void) {
multicore_launch_core1(core1_entry);
// Core 0: audio capture loop
while (1) {
// Wait for DMA ping-pong IRQ
uint32_t notif = multicore_fifo_pop_blocking();
int16_t *buf = (int16_t *)(uintptr_t)notif;
xylolabs_audio_feed(buf, AUDIO_BLOCK_SAMPLES);
}
}
XAP Codec on Cortex-M33¶
XAP is the default and recommended codec for RP2350:
XAP and XMBP are patent-pending technologies of Xylolabs Inc.
| Metric | Value |
|---|---|
| Compression ratio | ~10:1 |
| CPU cost (pure C) | ~20 MIPS/ch |
| CPU cost (DSP-accelerated) | ~14 MIPS/ch |
| 4ch @96kHz total | ~56 MIPS (DSP) |
| RP2350 budget | 300 MIPS |
| Remaining for app | ~244 MIPS |
The Cortex-M33 DSP extensions provide single-cycle 32×32 MAC, dual 16-bit SIMD (SMLAD), and saturating arithmetic — reducing XAP encoding cost by ~30% versus plain C. See docs/CODEC-ANALYSIS.md for detailed benchmarks.
Bandwidth result: XAP at 80 kbps/channel × 4ch = 320 kbps = 40 KB/s, within LTE-M1's ~47 KB/s budget.
Memory Budget¶
| Region | Size | Config macro |
|---|---|---|
| Audio ring buffer | 32 KB | XYLOLABS_AUDIO_RING_SIZE |
| XAP batch buffer | ~4 KB | XYLOLABS_XAP_BATCH_BYTES |
| XMBP packet buffer | 16 KB | XYLOLABS_XMBP_BUF_SIZE |
| HTTP buffer | 4 KB | XYLOLABS_HTTP_BUF_SIZE |
| Metadata accumulation | ~12 KB | 26ch × 100 samples × f32 |
| XAP encoder state (4ch) | ~8 KB | — |
| Stack + SDK internals | ~12 KB | — |
| Total SDK | ~88 KB | 17% of 520 KB |
| Available for app | ~432 KB | — |
DMA Double-Buffering for Audio¶
// Rust (Embassy) - Recommended
use embassy_rp::dma::{AnyChannel, Channel};
// Embassy handles DMA double-buffering automatically via async read.
// The PIO DMA transfer yields to the executor while waiting,
// then returns the completed buffer for processing.
#[embassy_executor::task]
async fn audio_capture(mut rx_dma: AnyChannel, pio_rx: &mut PioStateMachine<'static>) {
let mut ping = [0i16; AUDIO_BLOCK_SAMPLES * AUDIO_CHANNELS];
let mut pong = [0i16; AUDIO_BLOCK_SAMPLES * AUDIO_CHANNELS];
let mut use_ping = true;
loop {
let buf = if use_ping { &mut ping } else { &mut pong };
pio_rx.rx().dma_pull(&mut rx_dma, buf).await;
xylolabs_audio_feed(buf);
use_ping = !use_ping;
}
}
Legacy C equivalent
// Two ping-pong buffers for continuous DMA capture
static int16_t s_audio_ping[AUDIO_BLOCK_SAMPLES * AUDIO_CHANNELS];
static int16_t s_audio_pong[AUDIO_BLOCK_SAMPLES * AUDIO_CHANNELS];
static volatile bool s_use_ping = true;
void dma_irq_handler(void) {
dma_hw->ints0 = 1u << DMA_CHANNEL_AUDIO;
// Restart DMA on the other buffer
int16_t *next = s_use_ping ? s_audio_pong : s_audio_ping;
dma_channel_set_write_addr(DMA_CHANNEL_AUDIO, next, true);
// Signal Core 0 to process completed buffer
int16_t *done = s_use_ping ? s_audio_ping : s_audio_pong;
multicore_fifo_push_blocking((uint32_t)(uintptr_t)done);
s_use_ping = !s_use_ping;
}
This pattern guarantees zero-copy audio capture: the PIO fills one buffer via DMA while Core 0 encodes the other.
Rust Build (Recommended)¶
# Install dependencies
rustup target add thumbv8m.main-none-eabihf
cargo install probe-rs-tools
# Build
cd sdk/examples/rp2350-sensor
cargo build --release
# Flash
cargo run --release # Uses probe-rs via .cargo/config.toml
See sdk/examples/ for all RP2350 examples.
Legacy C Build System¶
cmake_minimum_required(VERSION 3.13)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(my_xylolabs_pico C CXX ASM)
pico_sdk_init()
add_subdirectory(sdk/c/pico)
add_executable(my_xylolabs_pico
src/main.c
src/platform_pico.c
src/sensors.c
)
target_link_libraries(my_xylolabs_pico
pico_stdlib
pico_multicore
hardware_pio
hardware_dma
hardware_i2c
hardware_spi
hardware_watchdog
hardware_timer
xylolabs_sdk
)
# Generate UF2 for drag-and-drop flashing
pico_add_extra_outputs(my_xylolabs_pico)
Requirements:
- Pico SDK >= 2.0.0 (for RP2350 support)
- CMake >= 3.13
- arm-none-eabi-gcc toolchain
Platform Callbacks Implementation¶
// Rust (Embassy) - Recommended
use embassy_rp::uart::{Uart, Config as UartConfig};
use embassy_rp::watchdog::Watchdog;
use embassy_time::Instant;
struct PicoPlatform {
uart: Uart<'static, embassy_rp::peripherals::UART0>,
watchdog: Watchdog,
}
impl PicoPlatform {
async fn tcp_connect(&mut self, host: &str, port: u16) -> Result<(), XylolabsError> {
let cmd = format!("AT+QIOPEN=1,0,\"TCP\",\"{}\",{},0,1\r\n", host, port);
at_send_expect(&mut self.uart, &cmd, "CONNECT", 10_000).await
}
async fn tcp_send(&mut self, data: &[u8]) -> Result<(), XylolabsError> {
at_tcp_send(&mut self.uart, data).await
}
fn get_time_us(&self) -> u64 {
Instant::now().as_micros()
}
fn watchdog_feed(&mut self) {
self.watchdog.feed();
}
}
Legacy C equivalent
// platform_pico.c
#include "xylolabs/client.h"
#include "pico/stdlib.h"
#include "hardware/watchdog.h"
#include "hardware/timer.h"
#include "uart_at.h" // AT command driver for LTE modem
static xylolabs_err_t pico_tcp_connect(const char *host, uint16_t port) {
// AT+QIOPEN or AT+CIPOPEN depending on modem
char cmd[128];
snprintf(cmd, sizeof(cmd),
"AT+QIOPEN=1,0,\"TCP\",\"%s\",%u,0,1\r\n", host, port);
return at_send_expect(cmd, "CONNECT", 10000) == AT_OK
? XYLOLABS_OK : XYLOLABS_ERR_NETWORK;
}
static xylolabs_err_t pico_tcp_send(const uint8_t *data, size_t len) {
return at_tcp_send(data, len) == AT_OK
? XYLOLABS_OK : XYLOLABS_ERR_NETWORK;
}
static uint64_t pico_get_time_us(void) {
return time_us_64();
}
static void pico_sleep_ms(uint32_t ms) {
sleep_ms(ms);
}
static void pico_watchdog_feed(void) {
watchdog_update();
}
const xylolabs_platform_t g_platform = {
.tcp_connect = pico_tcp_connect,
.tcp_send = pico_tcp_send,
.tcp_recv = pico_tcp_recv,
.tcp_close = pico_tcp_close,
.tcp_is_connected = pico_tcp_is_connected,
.get_time_us = pico_get_time_us,
.sleep_ms = pico_sleep_ms,
.watchdog_feed = pico_watchdog_feed,
};
LTE-M1 Modem Integration¶
Tested modems and recommended AT command sets:
| Modem | Interface | Notes |
|---|---|---|
| BG770A | UART | Cat-M1/NB-IoT; PSM and eDRX support |
| SIM7670G | UART | LTE-M1/NB2/GNSS combo |
Modem Boot Sequence¶
// Rust (Embassy) - Recommended
use embassy_rp::gpio::{Output, Level};
use embassy_time::Timer;
async fn modem_boot(pwrkey: &mut Output<'static>, uart: &mut Uart<'static>) {
// 1. Assert PWRKEY for 1.5s to power on
pwrkey.set_high();
Timer::after_millis(1500).await;
pwrkey.set_low();
Timer::after_millis(5000).await; // Wait for modem to register
// 2. Init AT sequence
at_send_expect(uart, "AT\r\n", "OK", 1000).await.unwrap();
at_send_expect(uart, "ATE0\r\n", "OK", 1000).await.unwrap();
at_send_expect(uart, "AT+CMEE=2\r\n", "OK", 1000).await.unwrap();
at_send_expect(uart, "AT+CEREG=1\r\n", "OK", 1000).await.unwrap();
at_send_expect(uart, "AT+CGDCONT=1,\"IP\",\"your.apn\"\r\n", "OK", 5000).await.unwrap();
at_send_expect(uart, "AT+CGACT=1,1\r\n", "OK", 30000).await.unwrap();
}
Legacy C equivalent
// 1. Assert PWRKEY for 1.5s to power on
gpio_put(GPIO_MODEM_PWRKEY, 1);
sleep_ms(1500);
gpio_put(GPIO_MODEM_PWRKEY, 0);
sleep_ms(5000); // Wait for modem to register
// 2. Init AT sequence
at_send_expect("AT\r\n", "OK", 1000);
at_send_expect("ATE0\r\n", "OK", 1000); // Echo off
at_send_expect("AT+CMEE=2\r\n", "OK", 1000); // Verbose errors
at_send_expect("AT+CEREG=1\r\n", "OK", 1000); // Network registration URC
at_send_expect("AT+CGDCONT=1,\"IP\",\"your.apn\"\r\n", "OK", 5000);
at_send_expect("AT+CGACT=1,1\r\n", "OK", 30000); // Activate PDP
Power Management¶
Dormant Mode Between Batches¶
The RP2350 supports dormant mode (deep sleep with GPIO wakeup) and sleep mode (timer wakeup). For continuous streaming, use sleep between ticks:
// Rust (Embassy) - Recommended
// Embassy executor automatically enters WFE between async polls.
// PLL stays on; wakeup via timer interrupt.
Timer::after_millis(10).await;
Legacy C equivalent
Watchdog Configuration¶
The SDK feeds the watchdog on every xylolabs_tick(). Configure the hardware watchdog:
// Rust (Embassy) - Recommended
use embassy_rp::watchdog::Watchdog;
// 8-second watchdog timeout
let mut watchdog = Watchdog::new(p.WATCHDOG);
watchdog.start(Duration::from_millis(8_000));
// SDK feeds watchdog on every tick
watchdog.feed();
Legacy C equivalent
Rust (Recommended)¶
Rust is the recommended language for new RP2350 projects. The Embassy-based Rust example provides async task management with compile-time safety guarantees.
cd sdk/examples/rp2350-audio
cargo build --release
# Flash the resulting ELF via probe-rs
probe-rs run --chip RP2350 target/thumbv8m.main-none-eabihf/release/rp2350-audio
The Rust example implements the same dual-core audio + sensor streaming, using Embassy tasks instead of raw C callbacks. See sdk/examples/README.md for detailed build instructions and all 10 MCU variants.
C Examples (alternative)
### C Examples The `sdk/c/pico/examples/` directory contains ready-to-run C examples: | Example | Description | |---------|-------------| | `continuous_stream.c` | Full dual-core audio (XAP) + 26ch sensor streaming | | `periodic_sampling.c` | Sensor-only node with periodic LTE-M1 uploads | | `audio_upload.c` | XAP audio encoding demo with bandwidth measurement |Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Watchdog reset loop | xylolabs_tick() not called frequently enough |
Ensure tick runs at least every 4 seconds |
| Audio ring overflow | Core 1 network blocking for too long | Increase XYLOLABS_AUDIO_RING_SIZE or reduce batch interval |
| Modem not responding | UART baud mismatch or PWRKEY timing | Check baud rate (default 115200) and PWRKEY pulse duration |
| XAP encode too slow | Clock too low or optimization flags missing | Ensure -O2 or -O3 and -mcpu=cortex-m33+nodsp with CMSIS-DSP |
| I2S data corrupted | PIO clock divider miscalculated | Recalculate: clkdiv = sys_clk / (96000 * 64 * 2) |
| High latency on send | TCP send blocking Core 1 | Move TCP into its own task with mutex-protected ring handoff |
Further Reading¶
docs/FEASIBILITY-RP2350.md— Full bandwidth and CPU feasibility analysisdocs/CODEC-ANALYSIS.md— Detailed XAP vs ADPCM benchmark resultssdk/c/pico/README.md— SDK file listing and quick-start build instructions- Pico SDK documentation
- RP2350 datasheet