Embedded Rust with Embassy skill for Codex
Async firmware in Rust without the yak shaving. Codex sets up no_std Rust firmware with the right target triple, memory.x, probe-rs runner and defmt logging, then structures it as Embassy tasks that never block the executor. Drivers are written against embedded-hal 1.0 traits and tested on the host with mocks, shared state uses embassy-sync instead of static mut, and release profiles are tuned and measured for flash size.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- embedded-rust Installs to ~/.agents/skills/embedded-rust/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install embedded-rust The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project embedded-rust Installs to .agents/skills/embedded-rust/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/embedded-rust.zipunzip -o embedded-rust.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) embedded-rust PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $embedded-rust, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds no_std embedded Rust firmware with Embassy async executors and HALs (embassy-stm32, embassy-nrf, embassy-rp, esp-hal), embedded-hal 1.0 driver traits, probe-rs flashing with defmt logging, memory.x and target selection, shared state with embassy-sync and static_cell, and size-tuned release profiles. Use when starting a Rust firmware project, porting a C driver to embedded-hal, an async task starves others, a build fails on target or linker errors, or flash is too large.
Use it when
- Starting or restructuring a no_std Rust firmware project
- Writing a portable driver on embedded-hal 1.0 traits
- An async task starves others or shared state needs a safe pattern
- Linker, target or memory.x errors, or flash size is too large
Use something else when
- The question is Rust borrowing and lifetimes in general rather than firmware: use rust-ownership
- The firmware is C on the STM32 Cube HAL: use stm32-hal
How to check the result
Build with --locked for the target, run driver tests on the host with embedded-hal-mock, compare cargo size with the baseline, and check the probe-rs defmt log on hardware.
The mistake it prevents
Calling a blocking delay or driver inside an Embassy task, which silently stalls every other task on the same executor.
Done well means: the firmware builds for the right target from a pinned toolchain and
lockfile, drivers are generic over embedded-hal traits and tested on the host, tasks never
block the executor, and defmt logs plus probe-rs give a working debug loop on hardware.
Inspect first
- Read
AGENTS.md,Cargo.toml,Cargo.lock,rust-toolchain.toml,.cargo/config.toml(target, runner, rustflags) andmemory.xorbuild.rs. - Note the Embassy crate versions and the HAL’s chip feature (for example
embassy-stm32 = { features = ["stm32f411re", "time-driver-any", ...] }). Embassy APIs move between releases; follow the examples from the tag matching the lockfile, notmain. - Build once:
cargo build --releaseandcargo size --release -- -A(cargo-binutils) for a baseline.
Adding targets and crates needs network (rustup target add, crates.io). In the sandbox,
request approval or check whether the registry is vendored.
Targets
| Core | Target triple | Examples |
|---|---|---|
| Cortex-M0 / M0+ | thumbv6m-none-eabi | RP2040, STM32G0, nRF51 |
| Cortex-M3 | thumbv7m-none-eabi | STM32F1 |
| Cortex-M4F / M7F | thumbv7em-none-eabihf | STM32F4, nRF52, STM32H7 |
| Cortex-M33F | thumbv8m.main-none-eabihf | nRF5340, RP2350, STM32U5 |
| RISC-V ESP32-C3 / C6 | riscv32imc-unknown-none-elf / riscv32imac-unknown-none-elf | esp-hal |
| Xtensa ESP32 / S3 | xtensa-esp32-none-elf (needs the espup toolchain) | esp-hal |
Using eabi instead of eabihf on an FPU core works but wastes the FPU; the reverse fails
to link against soft-float code.
Project wiring
# .cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32F411RETx"
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "info"
# Cargo.toml (excerpt)
[profile.release]
opt-level = "s" # or "z" when flash is tight; measure both
lto = "fat"
codegen-units = 1
debug = 2 # symbols live in the ELF, not in flash; keeps defmt and backtraces useful
incremental = false
memory.x must match the chip (flash and RAM origin and length); a wrong RAM size links
fine and then corrupts the stack. With embassy-stm32’s memory-x feature the HAL generates it.
Minimal Embassy application
#![no_std]
#![no_main]
use defmt::info;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after_millis(500).await;
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let led = Output::new(p.PA5, Level::Low, Speed::Low);
let button = Input::new(p.PC13, Pull::Up);
// embassy-executor 0.10+: the task call returns a Result; older versions use
// spawner.spawn(blink(led)).unwrap()
spawner.spawn(blink(led).unwrap());
loop {
info!("button {}", button.is_high());
Timer::after_secs(1).await;
}
}
Check the executor version in Cargo.lock and use the spawn form that matches it. Pin
types like p.PA5 are chip-specific; read the HAL’s examples for the exact chip family.
Async rules
- Never block inside a task: no busy loops, no blocking driver calls, no
cortex_m::asm::delayfor long waits. One blocking task stalls every task on that executor. - Each
#[embassy_executor::task]has a static pool; spawning the same task twice needs#[embassy_executor::task(pool_size = 2)]. - Time-critical work goes on a higher-priority interrupt executor
(
InterruptExecutor), not on the thread-mode executor with everything else. - Timeouts: wrap I/O in
embassy_time::with_timeout(Duration::from_millis(50), fut).
Sharing state
| Need | Use |
|---|---|
| Pass messages between tasks | embassy_sync::channel::Channel<CriticalSectionRawMutex, T, N> |
| Latest-value signal (no queue) | embassy_sync::signal::Signal or watch::Watch |
| Shared peripheral or bus across tasks | embassy_sync::mutex::Mutex (async), or embassy-embedded-hal shared bus types |
'static data built at runtime | static_cell::StaticCell (CELL.init(value)) |
| Data shared with a raw interrupt handler | critical_section::Mutex<RefCell<T>> |
Avoid static mut; references to it are denied by default in edition 2024 and easily become undefined
behaviour under aliasing anyway.
Drivers on embedded-hal 1.0
Write drivers generic over the traits so they work on any HAL and on the host:
use embedded_hal_async::i2c::I2c;
pub struct Bme280<I> { i2c: I, addr: u8 }
impl<I: I2c> Bme280<I> {
pub fn new(i2c: I, addr: u8) -> Self { Self { i2c, addr } }
pub async fn chip_id(&mut self) -> Result<u8, I::Error> {
let mut id = [0u8; 1];
self.i2c.write_read(self.addr, &[0xD0], &mut id).await?;
Ok(id[0])
}
}
- Use
SpiDevice(with chip select handled by the bus manager) rather thanSpiBusin device drivers. - Test drivers on the host with
embedded-hal-mock(enable its async feature for async traits) in a separate crate or acfg(test)module built for the host target, sincecargo testcannot run onthumbv*targets. - Keep the driver crate
#![no_std]and free of HAL dependencies.
Debug loop
cargo run --release # runner: flash with probe-rs and stream defmt logs
probe-rs list # attached probes
probe-rs run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/release/app
cargo size --release -- -A # section sizes
cargo bloat --release -n 20 # biggest functions (cargo-bloat)
The probe is attached to the user’s machine; give them the command and ask for the log.
panic-probe prints the panic message and a backtrace through defmt; set DEFMT_LOG=debug
per build, not in code.
Size and panics
panic_probeorpanic_haltin release; formatting withcore::fmtin panics and logs adds kilobytes.defmtdefers formatting to the host and is far smaller.- Index with
.get()and handleNonein hot paths; every[]index carries a panic path. - Check that
ltoandcodegen-units = 1are set before hand-optimising anything.
Verification
-
cargo build --releasefor the target with--locked;cargo clippyclean - Host tests for drivers and logic pass (
cargo teston the host target) -
cargo sizecompared with the baseline; RAM includes executor task arenas - Hardware log via
probe-rs runshows all tasks running (user-provided) - A forced panic prints a readable defmt message and location
Report
**Target:** STM32F411 (thumbv7em-none-eabihf), embassy-stm32 0.4, embassy-executor 0.10, Rust 1.90
**Change:** BME280 driver generic over embedded-hal-async I2c; sensor task + channel to uplink task
**Size:** .text 41.8 KB -> 38.2 KB after lto=fat + codegen-units=1; RAM 9.1 KB
**Tests:** 7 host tests with embedded-hal-mock; probe-rs log shows both tasks at expected rates What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Embedded Rust with Embassy"
short_description: "Async firmware in Rust without the yak shaving"
default_prompt: "$embedded-rust Start an Embassy project for my STM32F411 Nucleo with defmt logging" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.