| name | rp2350-c-cpp |
| description | Expert RP2350 C/C++ development with Pico SDK, covering CMake configuration, peripheral drivers (GPIO, SPI, I2C, PWM, UART), hardware optimization, FreeRTOS integration, and bare-metal programming. Use when developing C/C++ firmware for RP2350/RP2040 boards, implementing hardware drivers, optimizing performance, or building real-time embedded applications. |
RP2350 C/C++ Development with Pico SDK
Overview
This skill provides comprehensive guidance for developing high-performance embedded applications on RP2350 and RP2040 hardware using C/C++ and the official Raspberry Pi Pico SDK. It covers project setup, CMake configuration, peripheral programming, memory optimization, multicore development, and FreeRTOS integration.
When to Use This Skill
Use this skill when:
- Developing C/C++ firmware for RP2350 or RP2040 boards
- Setting up Pico SDK projects with CMake
- Implementing hardware drivers (GPIO, SPI, I2C, PWM, UART, ADC)
- Writing interrupt service routines and DMA transfers
- Building multicore applications utilizing both ARM cores
- Integrating FreeRTOS for real-time task management
- Optimizing memory usage and execution speed
- Implementing USB device or host functionality
- Working with PIO (Programmable I/O) for custom protocols
- Debugging with picoprobe or SWD interfaces
- Porting applications from RP2040 to RP2350
Hardware Context
RP2350 Features
- Processor: Dual ARM Cortex-M33 @ 150MHz (or dual RISC-V Hazard3 @ 150MHz)
- Memory: 520KB SRAM, up to 16MB external flash
- Security: ARM TrustZone-M, secure boot, signed firmware
- Peripherals:
- 30 multifunction GPIO pins
- 2× UART, 2× SPI, 2× I2C
- 12-bit ADC with up to 5 channels
- 24× PWM channels
- 12× PIO state machines
- USB 1.1 host/device
- DMA controller with 12 channels
- Power: 1.8-3.6V operation, sleep modes down to 180µA
RP2040 Features (for comparison)
- Processor: Dual ARM Cortex-M0+ @ 133MHz
- Memory: 264KB SRAM, up to 16MB external flash
- Peripherals: Similar to RP2350 but without TrustZone and with lower performance
Core Capabilities
1. Project Setup with CMake
Use assets/project_template/ for a complete CMake-based project structure.
Key Components:
CMakeLists.txt- Main build configurationpico_sdk_import.cmake- SDK integration.vscode/- VSCode configuration for debuggingsrc/- Source code directory
Quick Start:
# Copy template
cp -r assets/project_template/ my_project
cd my_project
# Configure environment
export PICO_SDK_PATH=/path/to/pico-sdk
# Build
mkdir build && cd build
cmake ..
make -j4
CMakeLists.txt Structure:
cmake_minimum_required(VERSION 3.13)
# Include SDK
include(pico_sdk_import.cmake)
project(my_project C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Initialize SDK
pico_sdk_init()
# Add executable
add_executable(my_project
src/main.c
src/peripheral_init.c
)
# Link libraries
target_link_libraries(my_project
pico_stdlib
hardware_spi
hardware_i2c
hardware_pwm
hardware_adc
)
# Enable USB output, disable UART
pico_enable_stdio_usb(my_project 1)
pico_enable_stdio_uart(my_project 0)
# Create map/bin/hex/uf2 files
pico_add_extra_outputs(my_project)
Refer to references/cmake_configuration.md for advanced CMake patterns including multicore builds, FreeRTOS integration, and custom linker scripts.
2. GPIO and Basic I/O
Initialization:
#include "pico/stdlib.h"
// Configure GPIO as output
const uint LED_PIN = 25;
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
// Configure GPIO as input with pull-up
const uint BUTTON_PIN = 15;
gpio_init(BUTTON_PIN);
gpio_set_dir(BUTTON_PIN, GPIO_IN);
gpio_pull_up(BUTTON_PIN);
Interrupt Handling:
void gpio_callback(uint gpio, uint32_t events) {
if (gpio == BUTTON_PIN && (events & GPIO_IRQ_EDGE_FALL)) {
// Button pressed
gpio_put(LED_PIN, !gpio_get(LED_PIN));
}
}
int main() {
stdio_init_all();
gpio_init(LED_PIN);
gpio_set_dir(LED_PIN, GPIO_OUT);
gpio_init(BUTTON_PIN);
gpio_set_dir(BUTTON_PIN, GPIO_IN);
gpio_pull_up(BUTTON_PIN);
// Enable interrupt
gpio_set_irq_enabled_with_callback(
BUTTON_PIN,
GPIO_IRQ_EDGE_FALL,
true,
&gpio_callback
);
while (1) {
tight_loop_contents();
}
}
3. SPI Communication
Standard SPI Setup:
#include "hardware/spi.h"
#define SPI_PORT spi0
#define PIN_MISO 16
#define PIN_CS 17
#define PIN_SCK 18
#define PIN_MOSI 19
void spi_init_device() {
// Initialize SPI at 10 MHz
spi_init(SPI_PORT, 10 * 1000 * 1000);
// Configure GPIO for SPI
gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
// Initialize CS pin as GPIO output
gpio_init(PIN_CS);
gpio_set_dir(PIN_CS, GPIO_OUT);
gpio_put(PIN_CS, 1); // CS high (inactive)
}
uint8_t spi_read_register(uint8_t reg) {
uint8_t data;
gpio_put(PIN_CS, 0); // CS low
spi_write_blocking(SPI_PORT, ®, 1);
spi_read_blocking(SPI_PORT, 0, &data, 1);
gpio_put(PIN_CS, 1); // CS high
return data;
}
void spi_write_register(uint8_t reg, uint8_t value) {
uint8_t buf[2] = {reg, value};
gpio_put(PIN_CS, 0);
spi_write_blocking(SPI_PORT, buf, 2);
gpio_put(PIN_CS, 1);
}
DMA-Based SPI Transfer:
#include "hardware/dma.h"
void spi_dma_transfer(uint8_t *tx_buf, uint8_t *rx_buf, size_t len) {
int dma_tx = dma_claim_unused_channel(true);
int dma_rx = dma_claim_unused_channel(true);
// Configure TX DMA
dma_channel_config c_tx = dma_channel_get_default_config(dma_tx);
channel_config_set_transfer_data_size(&c_tx, DMA_SIZE_8);
channel_config_set_dreq(&c_tx, spi_get_dreq(SPI_PORT, true));
dma_channel_configure(
dma_tx, &c_tx,
&spi_get_hw(SPI_PORT)->dr, // dst
tx_buf, // src
len, // transfer count
false // don't start yet
);
// Configure RX DMA
dma_channel_config c_rx = dma_channel_get_default_config(dma_rx);
channel_config_set_transfer_data_size(&c_rx, DMA_SIZE_8);
channel_config_set_dreq(&c_rx, spi_get_dreq(SPI_PORT, false));
channel_config_set_read_increment(&c_rx, false);
channel_config_set_write_increment(&c_rx, true);
dma_channel_configure(
dma_rx, &c_rx,
rx_buf, // dst
&spi_get_hw(SPI_PORT)->dr, // src
len, // transfer count
false // don't start yet
);
// Start both channels
gpio_put(PIN_CS, 0);
dma_start_channel_mask((1u << dma_tx) | (1u << dma_rx));
// Wait for completion
dma_channel_wait_for_finish_blocking(dma_rx);
gpio_put(PIN_CS, 1);
// Release channels
dma_channel_unclaim(dma_tx);
dma_channel_unclaim(dma_rx);
}
4. I2C Communication
I2C Setup and Operations:
#include "hardware/i2c.h"
#define I2C_PORT i2c0
#define I2C_SDA 4
#define I2C_SCL 5
#define I2C_FREQ 400000 // 400 kHz
void i2c_init_device() {
// Initialize I2C at 400 kHz
i2c_init(I2C_PORT, I2C_FREQ);
// Configure GPIO for I2C
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
gpio_pull_up(I2C_SDA);
gpio_pull_up(I2C_SCL);
}
// Read single byte
uint8_t i2c_read_byte(uint8_t addr, uint8_t reg) {
uint8_t data;
i2c_write_blocking(I2C_PORT, addr, ®, 1, true);
i2c_read_blocking(I2C_PORT, addr, &data, 1, false);
return data;
}
// Write single byte
void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) {
uint8_t buf[2] = {reg, value};
i2c_write_blocking(I2C_PORT, addr, buf, 2, false);
}
// Read multiple bytes
int i2c_read_bytes(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len) {
i2c_write_blocking(I2C_PORT, addr, ®, 1, true);
return i2c_read_blocking(I2C_PORT, addr, buf, len, false);
}
// I2C scanner
void i2c_scan() {
printf("\nI2C Bus Scan\n");
printf(" 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
for (int addr = 0; addr < (1 << 7); ++addr) {
if (addr % 16 == 0) {
printf("%02x ", addr);
}
int ret;
uint8_t rxdata;
ret = i2c_read_blocking(I2C_PORT, addr, &rxdata, 1, false);
printf(ret < 0 ? "." : "@");
printf(addr % 16 == 15 ? "\n" : " ");
}
}
5. PWM for LED/Motor Control
PWM Configuration:
#include "hardware/pwm.h"
void pwm_init_pin(uint gpio, uint freq_hz, float duty_cycle) {
// Configure GPIO for PWM
gpio_set_function(gpio, GPIO_FUNC_PWM);
// Find PWM slice
uint slice_num = pwm_gpio_to_slice_num(gpio);
// Calculate divider and wrap for desired frequency
uint32_t clock_freq = 125000000; // 125 MHz
uint32_t divider = clock_freq / (freq_hz * 65536);
if (divider < 1) divider = 1;
uint32_t wrap = (clock_freq / (freq_hz * divider)) - 1;
// Configure PWM
pwm_set_clkdiv(slice_num, divider);
pwm_set_wrap(slice_num, wrap);
// Set duty cycle
uint16_t level = (uint16_t)(wrap * duty_cycle);
pwm_set_gpio_level(gpio, level);
// Enable PWM
pwm_set_enabled(slice_num, true);
}
// Example: RGB LED control
void rgb_led_init(uint pin_r, uint pin_g, uint pin_b) {
pwm_init_pin(pin_r, 1000, 0.0); // 1 kHz, 0% duty
pwm_init_pin(pin_g, 1000, 0.0);
pwm_init_pin(pin_b, 1000, 0.0);
}
void rgb_led_set_color(uint pin_r, uint pin_g, uint pin_b,
float r, float g, float b) {
uint slice_r = pwm_gpio_to_slice_num(pin_r);
uint slice_g = pwm_gpio_to_slice_num(pin_g);
uint slice_b = pwm_gpio_to_slice_num(pin_b);
uint16_t wrap = pwm_get_wrap(slice_r);
pwm_set_gpio_level(pin_r, (uint16_t)(wrap * r));
pwm_set_gpio_level(pin_g, (uint16_t)(wrap * g));
pwm_set_gpio_level(pin_b, (uint16_t)(wrap * b));
}
6. ADC (Analog-to-Digital Converter)
ADC Reading:
#include "hardware/adc.h"
void adc_init_all() {
adc_init();
// Make sure GPIO is high-impedance, no pullups etc
adc_gpio_init(26); // ADC0
adc_gpio_init(27); // ADC1
adc_gpio_init(28); // ADC2
}
uint16_t adc_read_channel(uint channel) {
adc_select_input(channel);
return adc_read();
}
float adc_read_voltage(uint channel) {
adc_select_input(channel);
uint16_t raw = adc_read();
// Convert to voltage (3.3V reference, 12-bit ADC)
return raw * 3.3f / 4096.0f;
}
// Read internal temperature sensor
float read_onboard_temperature() {
adc_select_input(4); // Temperature sensor on ADC4
uint16_t raw = adc_read();
// Convert to temperature (from RP2040 datasheet)
const float conversion = 3.3f / 4096.0f;
float voltage = raw * conversion;
float temp_c = 27.0f - (voltage - 0.706f) / 0.001721f;
return temp_c;
}
7. Multicore Programming
Core1 Launch:
#include "pico/multicore.h"
void core1_entry() {
while (1) {
// Core 1 work here
printf("Running on core 1\n");
sleep_ms(1000);
}
}
int main() {
stdio_init_all();
printf("Starting core 1\n");
multicore_launch_core1(core1_entry);
while (1) {
// Core 0 work here
printf("Running on core 0\n");
sleep_ms(1000);
}
}
Intercore Communication:
#include "pico/multicore.h"
// Core 0 to Core 1 FIFO
void core0_send_to_core1(uint32_t data) {
multicore_fifo_push_blocking(data);
}
// Core 1 receives from Core 0
uint32_t core1_receive_from_core0() {
return multicore_fifo_pop_blocking();
}
// Example: Core 1 worker
void core1_worker() {
while (1) {
uint32_t cmd = multicore_fifo_pop_blocking();
switch (cmd) {
case 1:
// Process command 1
break;
case 2:
// Process command 2
break;
}
// Send result back to Core 0
multicore_fifo_push_blocking(42);
}
}
8. USB Device Implementation
USB Serial (CDC):
Already enabled with pico_enable_stdio_usb() in CMakeLists.txt.
Custom USB Device:
#include "tusb.h"
// USB HID Keyboard example
void send_key_press(uint8_t keycode) {
if (tud_hid_ready()) {
uint8_t report[8] = {0};
report[2] = keycode; // Keycode
tud_hid_keyboard_report(0, 0, report);
// Release key
sleep_ms(10);
tud_hid_keyboard_report(0, 0, NULL);
}
}
Refer to references/pico_sdk_reference.md for comprehensive USB API documentation including HID, MSC, and CDC examples.
Development Workflow
Step 1: Environment Setup
Install Dependencies:
# Linux/Raspberry Pi
sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi \
build-essential libstdc++-arm-none-eabi-newlib
# macOS
brew install cmake
brew install --cask gcc-arm-embedded
Clone Pico SDK:
cd ~
git clone https://github.com/raspberrypi/pico-sdk.git
cd pico-sdk
git submodule update --init
export PICO_SDK_PATH=$(pwd)
Step 2: Create Project from Template
Use the provided project template:
cp -r assets/project_template/ my_project
cd my_project
Edit CMakeLists.txt to configure your project name and dependencies.
Step 3: Build
mkdir build
cd build
cmake ..
make -j4
Output files:
my_project.elf- ELF executablemy_project.bin- Binary imagemy_project.uf2- USB bootloader formatmy_project.hex- Intel HEX format
Step 4: Flash Firmware
Method 1: USB Bootloader (BOOTSEL mode)
# 1. Hold BOOTSEL button while plugging in USB
# 2. Copy UF2 file to mounted drive
cp my_project.uf2 /media/$USER/RPI-RP2/
Method 2: Using Picoprobe/SWD
# Using OpenOCD with picoprobe
openocd -f interface/cmsis-dap.cfg \
-f target/rp2040.cfg \
-c "program my_project.elf verify reset exit"
Method 3: Using Flash Script
Use scripts/flash.sh for automated flashing:
./scripts/flash.sh build/my_project.uf2
Step 5: Debug
Serial Debug Output:
#include "pico/stdlib.h"
int main() {
stdio_init_all();
while (!tud_cdc_connected()) {
sleep_ms(100);
}
printf("Debug: System initialized\n");
// Your code here
}
GDB Debugging with Picoprobe:
# Terminal 1: Start OpenOCD
openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg
# Terminal 2: Start GDB
gdb-multiarch my_project.elf
(gdb) target remote localhost:3333
(gdb) load
(gdb) break main
(gdb) continue
Refer to .vscode/launch.json in the project template for VSCode debugging configuration.
Common Patterns
Pattern: Non-Blocking Timer
#include "pico/time.h"
typedef struct {
absolute_time_t last_trigger;
uint32_t interval_ms;
} timer_t;
void timer_init(timer_t *timer, uint32_t interval_ms) {
timer->interval_ms = interval_ms;
timer->last_trigger = get_absolute_time();
}
bool timer_expired(timer_t *timer) {
absolute_time_t now = get_absolute_time();
int64_t elapsed = absolute_time_diff_us(timer->last_trigger, now);
if (elapsed >= timer->interval_ms * 1000) {
timer->last_trigger = now;
return true;
}
return false;
}
// Usage
int main() {
timer_t blink_timer;
timer_init(&blink_timer, 500); // 500ms interval
while (1) {
if (timer_expired(&blink_timer)) {
gpio_xor_mask(1 << LED_PIN);
}
// Other non-blocking work
tight_loop_contents();
}
}
Pattern: State Machine
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_COMPLETED
} app_state_t;
typedef struct {
app_state_t state;
uint32_t counter;
bool button_pressed;
} app_context_t;
void state_machine_update(app_context_t *ctx) {
switch (ctx->state) {
case STATE_IDLE:
if (ctx->button_pressed) {
ctx->state = STATE_RUNNING;
ctx->counter = 0;
}
break;
case STATE_RUNNING:
ctx->counter++;
if (ctx->button_pressed) {
ctx->state = STATE_PAUSED;
}
if (ctx->counter >= 1000) {
ctx->state = STATE_COMPLETED;
}
break;
case STATE_PAUSED:
if (ctx->button_pressed) {
ctx->state = STATE_RUNNING;
}
break;
case STATE_COMPLETED:
// Reset on button press
if (ctx->button_pressed) {
ctx->state = STATE_IDLE;
}
break;
}
ctx->button_pressed = false;
}
Pattern: Circular Buffer (Ring Buffer)
typedef struct {
uint8_t *buffer;
size_t size;
size_t head;
size_t tail;
size_t count;
} ring_buffer_t;
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buffer, size_t size) {
rb->buffer = buffer;
rb->size = size;
rb->head = 0;
rb->tail = 0;
rb->count = 0;
}
bool ring_buffer_push(ring_buffer_t *rb, uint8_t data) {
if (rb->count >= rb->size) {
return false; // Buffer full
}
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % rb->size;
rb->count++;
return true;
}
bool ring_buffer_pop(ring_buffer_t *rb, uint8_t *data) {
if (rb->count == 0) {
return false; // Buffer empty
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
rb->count--;
return true;
}
Performance Optimization
Memory Management
Stack Size Configuration:
In CMakeLists.txt:
# Increase stack size if needed
target_compile_definitions(my_project PRIVATE
PICO_STACK_SIZE=0x2000 # 8KB stack
PICO_CORE1_STACK_SIZE=0x2000
)
Static vs Dynamic Allocation:
// Prefer static allocation on embedded systems
static uint8_t buffer[1024];
// Avoid malloc/free if possible
// If needed, consider custom allocator
Memory-Mapped I/O:
#include "hardware/regs/addressmap.h"
// Direct register access (faster than SDK functions)
#define GPIO_OUT_REG ((volatile uint32_t*)(SIO_BASE + 0x10))
#define GPIO_OUT_SET ((volatile uint32_t*)(SIO_BASE + 0x14))
#define GPIO_OUT_CLR ((volatile uint32_t*)(SIO_BASE + 0x18))
#define GPIO_OUT_XOR ((volatile uint32_t*)(SIO_BASE + 0x1C))
// Fast GPIO toggle
*GPIO_OUT_XOR = (1 << LED_PIN);
Code Optimization
Compiler Flags in CMakeLists.txt:
# Optimization level
target_compile_options(my_project PRIVATE
-O3 # Maximum optimization
-flto # Link-time optimization
-ffunction-sections # Each function in own section
-fdata-sections # Each data item in own section
)
target_link_options(my_project PRIVATE
-Wl,--gc-sections # Remove unused sections
)
Inline Functions:
static inline void fast_gpio_toggle(uint gpio) {
sio_hw->gpio_togl = 1u << gpio;
}
Use Hardware Features:
- DMA for large data transfers
- PIO for bit-banging protocols
- Hardware interpolators for calculations
Interrupt Optimization
// Keep ISRs short and fast
void __isr __time_critical_func(gpio_isr)() {
// Clear interrupt
gpio_acknowledge_irq(BUTTON_PIN, GPIO_IRQ_EDGE_FALL);
// Set flag for main loop
volatile bool button_flag = true;
// Don't call printf or blocking functions in ISR
}
Troubleshooting
Build Issues
SDK Not Found:
export PICO_SDK_PATH=/path/to/pico-sdk
Undefined Reference Errors:
Add missing libraries in CMakeLists.txt:
target_link_libraries(my_project
pico_stdlib
hardware_spi # Add this
hardware_i2c # Add this
)
Flashing Issues
Device Not Recognized:
- Check USB cable (data capable, not charge-only)
- Verify BOOTSEL button held during power-on
- Check
lsusbfor RP2040 device (ID 2e8a:0003)
Permission Denied (Linux):
sudo usermod -a -G dialout $USER
# Log out and back in
Runtime Issues
Crash on Startup:
- Check stack size (increase if needed)
- Verify all peripherals initialized before use
- Check for buffer overflows
GPIO Not Working:
- Verify pin numbers match board pinout
- Check if pin already used by another peripheral
- Ensure
gpio_init()called before use
SPI/I2C No Response:
- Verify wiring and pull-up resistors
- Check clock frequencies
- Use oscilloscope to verify signals
- Try
i2c_scan()to detect devices
Memory Issues
Out of RAM:
// Check memory usage
extern char __StackLimit, __bss_end__;
printf("Heap end: %p\n", &__bss_end__);
printf("Stack limit: %p\n", &__StackLimit);
Reduce Memory Usage:
- Use smaller buffers
- Move large arrays to flash:
const uint8_t data[] = {...}; - Optimize struct packing
- Use bit fields for flags
Resources
This skill includes:
scripts/
Build and deployment automation:
build.sh- Automated build script with error handlingflash.sh- Automated firmware flashing (supports BOOTSEL and picoprobe)monitor.sh- Serial monitor for debugging output
references/
Comprehensive technical documentation:
pico_sdk_reference.md- Complete Pico SDK API reference and examplescmake_configuration.md- Advanced CMake patterns and build configurationshardware_interfaces.md- Detailed hardware peripheral programming guidefreertos_integration.md- FreeRTOS setup and real-time task patterns
assets/
Ready-to-use project templates and boilerplate:
project_template/- Complete CMake project structure with examplesCMakeLists.txt- Configured build systempico_sdk_import.cmake- SDK integration.vscode/- VSCode debugging configurationsrc/main.c- Minimal example application
All scripts and templates are production-ready and can be used directly or customized for specific applications.