Introduction: The era of "hardware" is changing the language
\n\nIf you've ever written firmware for a microcontroller in C, you know that feeling. The feeling when, after three hours of debugging, you find a crash — a pointer flew into the sky, and the watchdog endured it all. Embedded development has long been the territory of C and assembly. But in the last couple of years, a new player has entered the arena — Rust. And it's not just "another language," but a real breakthrough for embedded systems.
\n\nIn this article, we'll break down why embedded development in Rust is not hype, but a necessity. We'll look at real code examples, compare it with C, and understand where to start. If you write for STM32, ESP32, or ARM Cortex-M — this is for you.
\n\nWhy Rust for embedded is an ideal match
\n\nEmbedded systems are a world of constraints. Little memory, no OS, no heap, no exceptions. A compile-time error is good, a runtime error is bad. Rust was designed precisely for such conditions.
\n\nMemory safety without a garbage collector
\n\nThe main pain of an embedded developer in C is use-after-free, double memory freeing, and buffer overflows. Rust solves this at the type level. The ownership and borrowing model works even in a no_std environment — without an allocator, without an OS, without GC.
Imagine you are writing a driver for an LED. In C you would write:
\n\n// C code\nvoid led_on(GPIO_TypeDef *gpio, uint16_t pin) {\n gpio->BSRR = pin;\n}\n\nEverything is fine until someone passes a null pointer. Rust won't let you do that:
\n\n// Rust code\nfn led_on(gpio: &mut GpioRegisters, pin: u16) {\n gpio.bsrr.write(|w| w.bits(pin.into()));\n}\n\nHere, &mut GpioRegisters is a guarantee that the reference is valid and access to the registers is exclusive. The compiler simply won't allow dangerous code.
Zero-cost abstractions
\n\nOne of the main fears of embedded developers is that a high-level language will "eat" processor cycles. Rust adheres to the principle of zero-cost abstractions: you only pay for what you use. Iterators, closures, traits — all of this compiles into machine code as efficient as manually unrolled loops in C.
\n\nMoreover, in the Rust embedded ecosystem, there are crates that work at the level of individual instructions. For example, cortex-m-rt generates the interrupt vector and startup code without a single extra instruction.
Real project: blinking an LED in Rust (STM32)
\n\nLet's write a simple program for the STM32F3Discovery board. We'll use cortex-m-quickstart and the HAL from the embedded rust community.
Step 1: Project setup
\n\nMake sure you have Rust and the toolchain for ARM Cortex-M installed:
\n\nrustup target add thumbv7em-none-eabihf\ncargo install cargo-generate\n\nCreate a project from the template:
\n\ncargo generate --git https://github.com/rust-embedded/cortex-m-quickstart\n\nStep 2: Write the code
\n\nOpen src/main.rs and write the classic Blinky:
#![no_std]\n#![no_main]\n\nuse cortex_m_rt::entry;\nuse panic_halt as _;\nuse stm32f3xx_hal::{pac, prelude::*};\n\n#[entry]\nfn main() -> ! {\n // Gain access to peripherals\n let dp = pac::Peripherals::take().unwrap();\n let mut rcc = dp.RCC.constrain();\n let mut gpioe = dp.GPIOE.split(&mut rcc.ahb);\n\n // Configure pin PE9 as output (LED LD1)\n let mut led = gpioe\n .pe9\n .into_push_pull_output(&mut gpioe.moder, &mut gpioe.otyper);\n\n loop {\n led.set_high().unwrap();\n cortex_m::asm::delay(8_000_000);\n led.set_low().unwrap();\n cortex_m::asm::delay(8_000_000);\n }\n}\n\nWhat's happening here?
\n- \n
#![no_std]— we a