Rust
Install from https://rustup.rs/.
Rust is a modern, practical, performant systems programming language.
It fits the same niche as C++ — nearly as fast as C but more expressive. However, not an abomination of infinite language “features” and implicit gotchas.
High points
- Cross-compilation support built into tooling — just works:
 
$ rustup target add thumbv7em-none-eabihf
$ cargo build --target thumbv7em-none-eabihf
# now compiling for thumbv7 on any host platform
- Great package manager with a wealth of excellent libraries 
- Long list support 
# - Easy to use private packages
 
 - Long list support 
 - Embedded ecosystem 
- Excellent support for: 
- RP2040
 - nRF{51,52,53,91}
 - STM32
 - ESP32 (idf or native rust)
 - I believe atsamd? Haven’t tried it
 
 - Most embedded programs can run on Linux with minimal modifications — there are Linux HAL trait implementations
 
 - Excellent support for: 
 - Catches many mistakes at compile-time 
- No memory bugs 
- Use-after-free
 - Double-free
 - Memory leaks
 
 - No data races
 - No accidental multiple bus access
 
 - No memory bugs 
 - State-machine-based, executor-agnostic asynchrony 
- Futures/async on embedded, no dynamic allocation required
 
 
some great embedded crates (crate = package)
- *-pac, svd2rust: pac = peripheral access crate 
- Autogenerate Rust MCU register-interface crates (pacs) from SVD (vendor-provided register description) files
 - HALs generally written on top of pacs
 
 - embassy: embedded async project 
- Extensive examples for all in-tree platforms
 - Embedded async executor 
- Can spawn multiple at different software interrupt priorities
 - Practically speaking, can be used for soft-realtime. Not heavily battle-tested, but stable in my experience.
 
 - Synchronization primities 
Mutex<T>Channel<T>,Sender<T>,Receiver<T>, novoid*garbage- pubsub — producer-side decides whether it accepts backpressure, so no lockups if not intended
 Pipe(byte stream, implements generic asyncio::{Read, Write}interfaces)Signal<T>
 - Time interface, driver (alarms, uptime) from e.g. systick (pluggable implementation) 
Timer::after(Duration::from_secs(1)).awaitlet mut tick = Ticker::every(Duration::from_micros(100)); loop { tick.await; }
 - Networking (commmon interfaces, ppp, tun/tap, esp-hosted) 
- Wifi, Bluetooth, 802.15.4, ethernet implementations per-HAL made against this interface
 
 - USB interface definition, implementations for common classes 
- CDC-ACM
 - CDC-NDC
 - HID
 - MIDI
 - DFU
 
 - HAL crates for rp2040, nRF, stm32
 - Bootloader implementations for contained HAL^ crates with DFU partition/failback
 
 - embedded-*: official Rust working-group; standard platform-agnostic interface definitions and utilities 
- Buses (SPI, I2C, etc.), sync or async
 - Storage (
NorFlashtrait, FAT32 for sdmmc) - Heap allocator
 
 - rtic: Rust RTOS 
- Can be used alongside parts of embassy
 
 - smoltcp: portable async networking stack 
- Drives embassy networking
 - Ethernet, 802.15.4, IP media
 - 6LoWPAN
 - TCP/IPv{4, 6}, UDP, ICMP, DHCP, DNS, IGMP
 - Non-allocating
 
 -  
heapless: constant-max-size stack-allocated data structures mirroring heap-allocated variants
 - defmt: logging library 
- No strings in flashed binary, just indexes
 - Strings are in special section in ELF
 - Logs also gzip compressed
 - Host-side log decoder program assembles messages from ELF, interpolates arguments
 
 - rtt: segger tech, a bunch of crates support this
 - vergen 
- Compile-in build time, git hash, build system info, compiler version (with hash), ~10-20 lines of code
 
 - prost: protobuf support 
- 10 lines to automatically compile protobuf definitions to rust during build
 MessageType::encode,MessageType::decode
 - corncobs: cobs implementation
 
tools
- probe-rs: debugging toolset for rust 
cargo run-> build +probe-rs run(flash and attach to rtt channel)- automatically discovers attached debuggers
 
 - espflash: dedicated flasher for esp32 
- Optional alternative to probe-rs for esp32, no rtt
 - Can be configured for 
cargo runabove 
 - cargo-binutils: run 
binutilson any host platform, with cross-platform supportcargo objcopy --target thumbv7em-none-eabihf --bin my_project --release -- -O ihex my_project.hex- Automatically compiles the project in release mode for the 
thumbv7em-none-eabihftarget and usesobjcopyto produce a hex file - That^ works on Windows without msys/mingw/cygwin
 
- Automatically compiles the project in release mode for the 
 
 
Downsides
- Initial learning curve is steep 
- Ownership rules, lifetimes
 - Nathan is happy to give pointers :)
 
 - Embedded ecosystem is still developing 
- Functionality is great / haven’t had problems, but a fair amount of boilerplate to stand everything up
 - Nathan is working on this — putting together a crate to make bringup easier / closer to an Arduino-like experience
 
 - Compile times are slow-ish 
- ~On par with C++, I feel it’s not as bad with bigger projects though — incremental compilation is pretty good
 - Error messages are much much better!
 
 - (Not really a downside) No escape hatches, just do it right 
- If you’re used to being able to be able to cheat / tell the compiler you know what you’re doing (even when it’s UB or just wrong), that doesn’t exist in Rust
 - The compiler won’t compile if your program is wrong in this way — usually that’s because there’s a correct way to do it. Rust gives you tools to make this easy.
 - Representative example: integer over-/underflow disallowed for normal addition according to spec. In development mode, overflow is checked and panics. In release mode, silently overflows. So, specify what kind of addition you want. Rather than 
x += 1,x = x.saturating_add(1)orx = x.wrapping_add(1). - Or 
Mutex<T>. Types can beSyncor not — in order to share a value that is notSyncto another thread (or task) (capture it in thethread::spawn(|| { /* closure in thread */})closure, share a global), it needs to be contained in a mutex. Then accessing is as easy as: 
 
struct Val { inner: usize };
impl !Sync for Val {} // Assert val is not safe to access across threads.
let m: Mutex::<Val> = Mutex::new(Val { inner: 4 });
// Increase `inner` by 2 every second and print the value.
thread::spawn(|| {
    loop {
        {
            let guard = m.lock();
            *guard.inner += 2;
            println!("count: {}", guard.inner);
            // `guard` provides access to the inner value (witnesses that the lock was acquired).
            // When it is dropped at the end of this block, the lock is automatically released.
        }
        thread::sleep(Duration::seconds(1));
    }
});
Embedded sample (blink with channels)
#[global_allocator]
static HEAP: embedded_alloc::Heap = embedded_alloc::Heap::new();
// RawMutex type specifies where it's appropriate to use this value.
// Select CriticalSectionRawMutex instead to make it usable from within ISRs.
static CHANNEL: embassy_sync::Channel<ThreadModeRawMutex, (), 32> = embassy_sync::Channel::new();
#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) -> anyhow::Result<()> {
    defmt::info!("boot");
    init_heap();
    // Contains unique handles to all register blocks. HAL types (e.g. Output below) wrap these
    // handles to provide a nice interface.
    let periphs = embassy_nrf::init(Default::default());
    let led = Output::new(periphs.P0_01.degrade(), Level::Low(), OutputDrive::Standard);
    spawner.must_spawn(indicate_traffic(led));
    spawner.must_spawn(simulate_traffic());
}
/// Toggle LED when "traffic" (e.g. TX/RX on some peripheral) is detected.
#[embassy_executor::task]
async fn indicate_traffic(led: Output<'static, AnyPin>) -> ! {
    loop {
        CHANNEL.receive().await;
        led.toggle();
        defmt::trace!("led toggled"):
    }
}
/// Simulate traffic to drive the LED to toggle.
#[embassy_executor::task]
async fn simulate_traffic() -> ! {
    let mut tick = Ticker::every(Duration::from_millis(100));
    loop {
        defmt::trace!("ping");
        CHANNEL.send(()).await;
        tick.await;
    }
}
/// Boilerplate heap init.
fn init_heap() {
    // Grab symbols from linker.
    extern "C" {
        static __sheap: u32;
        static __eheap: u32;
    }
    let (sheap, eheap) = unsafe {
        let sheap = &__sheap as *const _ as usize;
        let eheap = &__eheap as *const _ as usize;
        (sheap, eheap)
    };
    HEAP.init(sheap, eheap - sheap);
    defmt::info!("heap initialized");
}