EmbeddedRustFirmware

Why Rust for embedded development?

9 min read

Every embedded team eventually ships a bug it can't reach. The board is on a pole, inside a wall, in a shipping container or strapped to a patient, and the fault only turns up after three weeks of uptime. The language and tools you build that firmware with decide two things: which bugs are even possible, and how stuck you are when the hardware has to change. Those are the two reasons we reach for Rust.

The bugs you stop shipping

In C and C++, the faults that actually take devices down usually aren't logic mistakes. They're memory mistakes. A buffer that overruns by one byte when a packet is a little longer than you expected. A pointer used just after the thing it pointed at was freed. A struct field read before it was set. These slip through code review and pass your tests, then show up months later as a watchdog reset at a customer site or a sensor reading that's quietly wrong.

Rust won't build most of them. The compiler tracks who owns each piece of memory and how long it lives, and it rejects code that could read freed memory or run off the end of a buffer. You don't catch these in testing, because you never get to write them. On a product that ships in the thousands, that's the difference between a quiet fleet and a support queue.

The race conditions that never compile

Embedded code is concurrent whether you planned for it or not. An interrupt fires in the middle of your main loop and pokes the same variable. A DMA transfer finishes while you're still reading the buffer it was filling. In C these are the worst bugs to chase: they only happen sometimes, they depend on timing, and they vanish the moment you attach a debugger.

Rust's ownership rules cover interrupts and tasks too. If two places can touch the same data unsafely, it doesn't compile. You're forced to decide how shared state is protected beforethere's a binary, instead of debugging a once-a-week glitch after a customer files a ticket.

The vendor lock-in nobody warns you about

Here's the part that doesn't bite until year two. Every silicon vendor hands you their own world. Go with Espressif and you live in ESP-IDF, their FreeRTOS build, their way of doing Wi-Fi and flash partitions. Go with ST and it's STM32CubeIDE, generating HAL code from CubeMX and inheriting ST's driver layer. Go with Nordic and you're on the nRF Connect SDK, which is really Zephyr with Nordic's SoftDevices bolted on. Each one is its own SDK, its own build system, its own config tool, its own debugger quirks, and its own way of describing the exact same UART.

So teams do the sensible thing: they learn one vendor deeply and never leave. The firmware gets welded to that SDK, and switching chips turns into a rewrite nobody wants to approve. That's fine right up until it isn't. A part goes end-of-life or onto a 50-week lead time (anyone who lived through 2021 remembers). Finance finds a cheaper MCU. A new product variant needs a smaller chip or a different radio. At that point the thing keeping you on your vendor isn't the silicon, it's the code you can't pull out of their tools.

Rust changes the shape of that. We build on Embassy or RTIC, which give you the same async, no-RTOS-required programming model across nRF, STM32, ESP32, RP2040 and more. Drivers are written against embedded-hal, a shared set of traits for SPI, I2C, GPIO and the like, so a sensor driver doesn't care whose chip it's sitting on. Your application logic, the part that's actually your product, rides on top and barely notices the MCU underneath.

In practice that means moving from, say, an STM32 to a Nordic part stops being a from-scratch rewrite. The pin map and the radio setup change; the business logic, the protocol handling and most of the drivers come along for the ride. And the day-to-day is lighter: one toolchain — rustup, cargo, and probe-rs for flashing and debugging — covers every target, instead of a different IDE per vendor. Need an IMU driver, a CRC routine or a CBOR parser? Pull it from crates.iowith one command and get on with it, rather than copying vendor example code and hoping it fits your build. New engineers are useful in days because the tools don't change from chip to chip.

You should pick an MCU because it's right for the product, not because your firmware can't survive leaving it.

What this adds up to for you

None of this is about tidy code for its own sake. Every fault that reaches the field has a price: a truck roll to a site, an RMA and a replacement, a one-star review, or on a regulated product, a reportable event. Memory bugs are also where security holes live — Microsoft and Google have each reported that around 70% of their serious vulnerabilities were memory-safety issues, and that category stops being optional once your device is on a network and facing rules like the EU Cyber Resilience Act.

Over the life of a product, choosing Rust tends to buy a team:

  • Fewer field failures, so fewer truck rolls, returns and support tickets.
  • A smaller attack surface, so fewer emergency patches and fewer fire-drill OTA rollouts.
  • The freedom to change MCUs for cost, supply or a new variant without rewriting the firmware.
  • One toolchain and a shared library ecosystem, so onboarding is days, not weeks.
  • A codebase your own team can safely change, instead of one only its original author dares touch.
The savings don't show up in the first sprint. They show up across the years the product is in service: in the failures that never happen and the 2am pages nobody gets.

It isn't slower, and it fits on your chip

The usual worry is that safety costs speed or memory. It doesn't. Rust compiles to the same kind of machine code as C, with no garbage collector and no hidden runtime. It runs bare-metal with no_std, and we ship it today on ESP32, STM32 and Nordic nRF. When you want clean, low-power scheduling without dragging in a full RTOS, Embassy's async executor handles it. All the safety checks happen while the code compiles and cost nothing once it's on the device.

When we'd still reach for C

Rust isn't the answer to everything, and we'll tell you when it isn't. If the only SDK for an exotic part is C, or you're dropping a small change into a mature C codebase, forcing Rust in can cost more than it saves. A lot of the time we do both: wrap the vendor's C HAL where we have to, and write the application logic — the part that carries your product's real risk — in Rust. The point is to make the call on the merits, not on hype.

How we build with it

We write production firmware in Rust for connected devices: drivers, BLE and Sub-GHz stacks, secure over-the-air updates, and ultra-low-power scheduling. The goal is plain. The code that runs on your hardware for the next five years should be the kind you could audit yourself, that ports when the hardware has to, and that doesn't wake anyone up at 2am.

If you have a board on the bench or a fleet in the field, see how we approach embedded firmware or tell us what you're building.

Building a connected device?

See how we take firmware from the bench to a fleet in the field.

See our embedded firmware work