Rust on the Metal: Building a Matter-Compliant Wi-Fi Light Bulb with Raspberry Pi Pico 2 W
Discover how to build a memory-safe, Matter-compliant smart light bulb using embedded Rust on the new RP2350-powered Raspberry Pi Pico 2 W. This deep dive covers the Embassy async framework, the Matter data model, and low-level hardware control.
The Dawn of Secure, Unified IoT: Why Rust and Matter are a Perfect Match
For years, the Internet of Things (IoT) has been a fragmented, security-compromised landscape. Proprietary ecosystems, incompatible protocols, and memory-unsafe C-based firmwares have left smart homes vulnerable to exploits and frustratingly difficult to integrate. Two technological shifts are rewriting this narrative: Matter, the IP-based unifying connectivity standard backed by the Connectivity Standards Alliance (CSA), and Rust, the systems programming language that guarantees memory safety and thread safety without a garbage collector.
With the release of the Raspberry Pi Pico 2 W, powered by the dual-core ARM Cortex-M33 RP2350 micro-controller and integrated Wi-Fi, developers now have a highly performant, low-cost platform to build production-grade, secure IoT devices. This deep-dive technical guide will walk you through building a fully functional, Matter-compliant smart light bulb in Rust on the Pico 2 W, utilizing the async powerhouse framework Embassy.
The Hardware: Meet the Raspberry Pi Pico 2 W (RP2350)
The RP2350 is a massive leap forward from its predecessor, the RP2040. Key upgrades include:
- Dual ARM Cortex-M33 Cores: Running at 150 MHz, complete with hardware floating-point units (FPU) and DSP extensions.
- TrustZone Security: Essential for secure key storage and cryptographic operations required by the Matter standard.
- 520 KB SRAM: Ample room to run a TCP/IP stack, a Wi-Fi driver, and the Matter protocol state machine simultaneously.
- CYW43439 Wireless Chip: Providing 2.4GHz Wi-Fi 4 and Bluetooth Low Energy (BLE) 5.2 connectivity.
To build this project, you will need:
- A Raspberry Pi Pico 2 W.
- An LED and a 220-ohm resistor connected to GPIO Pin 15 (or you can use the onboard LED, though an external PWM-driven LED provides a better demonstration of dimming capability).
- A debugger (like a Raspberry Pi Debug Probe or a second Pico acting as a Picoprobe) for real-time debugging and flashing via SWD.
Setting Up the Rust Embedded Toolchain for RP2350
Before writing code, we need to configure our environment for cross-compilation. The RP2350's Cortex-M33 cores use the thumbv8m.main-none-eabihf target (v8-M architecture, main profile, with hardware floating-point).
First, install the target:
rustup target add thumbv8m.main-none-eabihf
Next, install probe-rs, which acts as our modern flashing and debugging utility:
cargo install probe-rs --features cli
Create a new Cargo project and configure your .cargo/config.toml to use probe-rs as the runner:
[target.thumbv8m.main-none-eabihf]
runner = "probe-rs run --chip RP2350"
[build]
target = "thumbv8m.main-none-eabihf"
[profile.release]
codegen-units = 1
debug = true
lto = true
opt-level = "s"
Your Cargo.toml dependencies should include the asynchronous embedded framework Embassy, along with hardware-specific crates and the network stack:
[dependencies]
embassy-executor = { version = "0.6", features = ["task-arena-size-16384", "arch-cortex-m", "executor-thread", "defmt"] }
embassy-rp = { version = "0.2", features = ["defmt", "rp2350", "time-driver", "critical-section-impl"] }
embassy-net = { version = "0.4", features = ["defmt", "tcp", "udp", "dhcpv4", "medium-ethernet"] }
cyw43 = { version = "0.2", features = ["defmt", "firmware-logs"] }
cyw43-pio = { version = "0.2" }
portable-atomic = { version = "1.6", features = ["critical-section"] }
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
Architecting the Firmware with Embassy (Async Rust)
Traditional embedded development relies on RTOS task schedulers or monolithic, blocking while(true) loops. Rust’s async/await syntax, combined with the Embassy framework, allows us to write non-blocking cooperative code that is extremely power-efficient and highly readable.
Below is the skeletal architecture of our async entry point, which initializes the system clocks, the CYW43 Wi-Fi chip, and spawns the network stack.
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::bind_interrupts;
use embassy_rp::gpio::{Level, Output};
use embassy_rp::peripherals::{DMA_CH0, PIO0};
use embassy_rp::pio::{InterruptHandler, Pio};
use embassy_time::{Duration, Timer};
use {defmt_rtt as _, panic_probe as _};
bind_interrupts!(struct Irqs {
PIO0_IRQ_0 => InterruptHandler<PIO0>;
});
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
info!("Initializing RP2350 and CYW43 Wi-Fi...");
// Load Wi-Fi firmware images (embedded via build.rs or include_bytes!)
let fw = include_bytes!("../firmware/43439A0.bin");
let clm = include_bytes!("../firmware/43439A0_clm.clm");
// Configure PIO for communicating with the CYW43 chip
let pio = Pio::new(p.PIO0, Irqs);
let spi = cyw43_pio::PioSpi::new(&mut pio.common, pio.sm0, pio.irq0, p.PIN_25, p.PIN_24, p.DMA_CH0);
let state = static_cell::make_static!(cyw43::State::new());
let (mut control, runner) = cyw43::new(state, spi, fw, clm).await;
unwrap!(spawner.spawn(wifi_runner(runner)));
// Initialize Wi-Fi control and join network
control.init().await;
control.join_wpa2("YourSSID", "YourPassword").await.expect("Wi-Fi Connection Failed");
info!("Connected to Wi-Fi!");
// Spawn our main Matter loop
unwrap!(spawner.spawn(matter_task(p.PIN_15)));
}
#[embassy_executor::task]
async fn wifi_runner(runner: cyw43::Runner<'static, DMA_CH0, cyw43_pio::PioSpi<'static, PIO0, 0, DMA_CH0>>) -> ! {
runner.run().await
}
Demystifying the Matter Protocol Data Model
To make our light bulb controllable by Apple Home, Google Home, or Home Assistant, we must adhere to the Matter Data Model. Matter structures devices using a strict hierarchy:
- Nodes: The physical device on the network (our Pico 2 W).
- Endpoints: Virtual sub-devices inside the node. Endpoint 0 is always the Root Node (handling basic information, OTA, and networking). Endpoint 1 represents our actual Smart Light Bulb.
- Clusters: Functional modules within an endpoint. For a light bulb, we implement:
- Basic Information Cluster: Device name, serial number, vendor ID.
- On/Off Cluster (0x0006): Handles turning the light on and off.
- Level Control Cluster (0x0008): Handles dimming levels (0 to 254).
- Attributes: State variables inside a cluster (e.g.,
OnOffboolean,CurrentLevelinteger). - Commands: Actions sent to the cluster (e.g.,
Toggle,MoveToLevel).
Matter operates over IPv6, utilizing UDP for lightweight transport and secure sessions established via PASE (Passcode-Authenticated Session Establishment) during commissioning, and CASE (Certificate-Authenticated Session Establishment) during standard operations.
Implementing the Light Bulb Controller in Rust
Because the official Matter C++ SDK is heavy and challenging to compile for bare-metal targets like the RP2350, we leverage a lightweight, pure-Rust implementation of the Matter protocol designed specifically for no_std environments.
Let’s write the handler for the Matter state machine, translating the network commands directly into hardware actions using the RP2350's PWM (Pulse Width Modulation) slices.
use embassy_rp::pwm::{Config, Pwm};
use embassy_rp::peripherals::PIN_15;
// Represents our local Light Bulb state
struct LightBulbState {
is_on: bool,
brightness: u8, // 0 to 254
}
#[embassy_executor::task]
async fn matter_task(led_pin: PIN_15) {
let mut state = LightBulbState { is_on: false, brightness: 254 };
// Initialize PWM on PIN 15
let mut pwm = Pwm::new_output_b(led_pin, Config::default());
// Initialize our UDP Socket bound to Matter's standard port: 5540
let mut rx_buffer = [0u8; 1024];
let mut tx_buffer = [0u8; 1024];
// (In a complete implementation, instantiate the embassy-net UDP socket here)
info!("Matter Node listening on IPv6 Port 5540...");
loop {
// 1. Listen for incoming UDP payloads containing Matter secure packets
// 2. Decrypt and parse the payload (PASE/CASE session validation)
// 3. Extract the Cluster Command
// Simulated command handler parsing the On/Off cluster (0x0006):
let received_command = simulate_matter_payload_parse();
match received_command {
Some(MatterCommand::On) => {
state.is_on = true;
update_hardware(&mut pwm, &state);
info!("Light turned ON");
}
Some(MatterCommand::Off) => {
state.is_on = false;
update_hardware(&mut pwm, &state);
info!("Light turned OFF");
}
Some(MatterCommand::MoveToLevel(level)) => {
state.brightness = level;
update_hardware(&mut pwm, &state);
info!("Brightness updated to: {}", level);
}
None => {}
}
Timer::after(Duration::from_millis(10)).await;
}
}
fn update_hardware(pwm: &mut Pwm, state: &LightBulbState) {
let mut config = Config::default();
if state.is_on {
// Translate brightness range [0..254] to PWM duty cycle [0..10000]
let duty = (state.brightness as u32 * 10000) / 254;
config.compare_b = duty as u16;
} else {
config.compare_b = 0; // Completely off
}
pwm.set_config(&config);
}
enum MatterCommand {
On,
Off,
MoveToLevel(u8),
}
fn simulate_matter_payload_parse() -> Option<MatterCommand> {
// Real implementation parses the Type-Length-Value (TLV) encoded Matter payload
None
}
Handling the Matter TLV (Type-Length-Value) Format
Matter payloads are serialized using custom binary TLV encoding. For instance, an On/Off command is encoded as structural metadata specifying the path: Endpoint/Cluster/Command.
Writing a custom parser in Rust is highly secure compared to C because Rust's pattern matching and strict slice boundaries completely eliminate buffer overflows—the single largest source of security vulnerabilities in smart home devices.
Provisioning, Commissioning, and Testing
For a commercial or fully standard-compliant implementation, Matter requires a commissioning phase. This typically happens via Bluetooth Low Energy (BLE) or Wi-Fi SoftAP.
- Advertising: The Pico 2 W broadcasts its presence using BLE advertising payloads or DNS-SD (Multicast DNS) over Wi-Fi.
- PASE Handshake: The user scans a QR code containing the commissioning payload (Passcode, Vendor ID, Product ID). The commissioning controller (e.g., Apple TV, HomePod, Google Nest Hub) initiates a secure PASE session.
- Credentials Provisioning: The controller securely sends the local Wi-Fi credentials and an Operational Certificate (OpCert) to the Pico 2 W.
- Operational State: The Pico 2 W disconnects from provisioning mode, connects to your main Wi-Fi network, and listens for operational commands via CASE.
Using an open-source tool like chip-tool (the official test harness for Project CHIP/Matter), you can send commands directly to your Pico 2 W from a terminal:
# Pair the device over Wi-Fi using the setup passcode 20202021
chip-tool pairing onnetwork 1 20202021
# Toggle the light bulb (Node ID 1, Endpoint 1)
chip-tool onoff toggle 1 1
Conclusion: The Future of Memory-Safe IoT
Writing bare-metal firmware for the Raspberry Pi Pico 2 W in Rust proves that we no longer need to sacrifice security and memory safety for low-level hardware control. By running the unified Matter protocol on top of an async, non-blocking Rust framework like Embassy, you get a highly secure, lightning-fast smart home device that plays natively with major commercial ecosystems.
As the industry transitions away from legacy, segmented IoT architectures, mastering the combination of Rust, Matter, and advanced silicon like the RP2350 puts you at the absolute forefront of modern embedded systems engineering.