Rust in embedded and edge devices

Rust in embedded and edge devices
By Marco Lopes · February 7, 2025 · Hardware & Gear
At Kordspace, we work across software, AI, Web3, hardware, and design. Connected products bring those disciplines together: a physical interface, firmware that must survive imperfect conditions, an application that makes the device useful, and infrastructure that keeps it manageable after deployment.
Rust is increasingly relevant at that intersection. It offers low-level control without requiring us to accept every memory-safety risk traditionally associated with systems programming. But choosing Rust is not the same as choosing a complete device platform. Boards, drivers, debugging tools, update mechanisms, and team experience still determine whether a project ships reliably.
As of February 2025, our position is practical: Rust deserves serious consideration for embedded firmware and edge applications, but we should evaluate the exact hardware and operational requirements before committing. Here is how we approach that decision.
Separate embedded firmware from edge applications
We start by distinguishing two environments that often get grouped together.
Embedded firmware usually runs directly on a microcontroller or alongside an RTOS. Memory is limited, peripherals matter, and timing constraints may be strict. A sensor controller might spend most of its life asleep, wake on an interrupt, sample a signal, and transmit a small message.
Edge applications often run on Linux-based gateways, industrial computers, or single-board systems. They may aggregate device traffic, process camera feeds, perform local inference, or coordinate equipment when connectivity disappears.
Rust can serve both, but the engineering approach changes. On a microcontroller, we care about interrupt behavior, flash usage, and register access. On a Linux gateway, we also care about process supervision, storage integrity, package updates, and operating-system security.
We define which environment we are building for before selecting libraries or copying an example project.
What Rust improves, and what it does not
Rust’s ownership and borrowing rules help us prevent categories of memory errors before code reaches hardware. In safe Rust, those rules guard against problems such as use-after-free and data races. That is valuable in devices where failures can be intermittent, difficult to reproduce, and expensive to investigate remotely.
Its type system also lets us express useful constraints. We can represent a configured peripheral differently from an unconfigured one, or make invalid protocol states harder to construct. Instead of relying entirely on comments, we move some assumptions into interfaces the compiler checks.
However, Rust does not make firmware automatically correct. We can still configure the wrong clock, misinterpret a sensor reading, deadlock an application, or miss a deadline. Hardware access and foreign-function interfaces frequently involve unsafe code, where correctness depends on manually upheld contracts.
Our goal is therefore not “zero risk.” We want smaller, clearer boundaries around risky operations, plus tests and measurements that validate behavior the language cannot prove.
Choose the board by its weakest dependency
Before selecting a microcontroller, we inspect more than its headline specifications. We check Rust target support, the maturity of the chip’s peripheral access crate and hardware abstraction layer, debugging support, and examples for the exact peripherals we need.
A working LED demo tells us little about a product that needs USB, DMA, low-power modes, and a particular radio. We investigate the hardest dependency first. If wireless support depends on a vendor SDK or binary component, we establish how Rust integrates with it and who maintains that integration.
Support also varies within processor families. We do not assume that compatibility with one Cortex-M device, ESP chip, or RISC-V board guarantees the same experience on another.
A useful early milestone is a vertical slice: read the actual sensor, process one sample, transmit it through the intended interface, and recover from a forced failure. That exercise exposes platform gaps before enclosure design and application work become expensive to change.
Build a small, understandable firmware foundation
For bare-metal development, we commonly work with #![no_std], which removes the assumption that an operating system and Rust’s standard library are available. Core language facilities remain usable. Heap allocation is not automatically available; adding it requires an allocator and deliberate memory planning.
We prefer to start with bounded buffers, explicit capacities, and clear ownership of peripherals. That makes memory use easier to reason about and avoids discovering allocation behavior under field conditions.
The usual building blocks include a peripheral access crate, a chip-specific HAL, and driver interfaces such as embedded-hal. Compatibility matters: we check trait versions and ecosystem support rather than assuming every driver will fit every HAL.
We also establish these foundations immediately:
- A pinned Rust toolchain, a committed application lockfile, and documented build commands.
- A linker configuration that matches the device’s actual memory layout.
- A defined panic policy for development and production.
- A debug probe and a reliable flash-and-inspect workflow.
Tools such as probe-rs and compact logging through defmt can make bring-up more productive where supported. We verify support on our board instead of treating tooling availability as universal.
Treat concurrency as a hardware decision
Firmware concurrency is not simply a choice between threads and async functions. Interrupt priorities, peripheral ownership, timing requirements, and power management all influence the architecture.
Embassy offers an async approach for supported embedded platforms. RTIC provides a task and resource model oriented around interrupt-driven concurrency. Either can be useful, but we choose based on the workload and hardware rather than familiarity with server-side Rust.
Async does not mean parallel execution, and it does not guarantee deadlines. A task that performs lengthy blocking work can delay other work sharing its executor. Interrupt handlers can also undermine responsiveness if they do too much.
We keep interrupt work short, bound queues, and specify overflow behavior. Should we drop an old reading, reject a command, or signal a fault when a queue fills? That decision belongs in the design, not in an accidental runtime outcome.
For hard timing requirements, we measure worst-case behavior on hardware. Language safety is not a substitute for scheduling analysis.
Use Rust selectively on Linux edge devices
Linux-based edge systems offer a gentler entry point because we can use Rust’s standard library, familiar networking libraries, and conventional process tools. Rust is a strong candidate for protocol gateways, local APIs, telemetry collectors, and services that need predictable resource use.
We do not need to rewrite every component. An AI pipeline might retain Python for experimentation or vendor-supported inference tooling while using Rust for device communication, input validation, and supervision. Native libraries can remain behind carefully reviewed interfaces.
Cross-compilation deserves an early test. CPU architecture is only part of the problem: the target’s C library, system libraries, kernel interfaces, and accelerator drivers can affect deployment. A successful local build does not prove that a binary will run on the gateway.
Containers can help package dependencies when resources permit, but they do not remove hardware compatibility issues. For smaller systems, a supervised native service may be simpler. We choose the deployment unit that our update and recovery process can reliably manage.
Design cloud connectivity around disconnection
At Kordspace, we generally prefer Google Cloud, while also working with AWS and Azure. For device projects, we keep the on-device protocol and data model as portable as practical so infrastructure choices do not unnecessarily shape firmware.
A typical architecture sends authenticated device traffic to an ingestion service or MQTT broker, then routes validated messages into cloud processing. On Google Cloud, that might involve an HTTPS ingestion service on Cloud Run and Pub/Sub behind it. MQTT requires an appropriate broker or managed offering; Pub/Sub is not itself a device-facing MQTT endpoint.
We avoid placing broad cloud permissions or shared fleet credentials on devices. Each device needs an identity strategy, narrowly scoped access, and a plan for credential rotation or revocation.
Connectivity failures should be normal operating conditions. We define bounded local storage, retry backoff, and message identifiers for duplicate handling. We also decide how timestamps behave before clock synchronization is available. If a gateway runs local AI, it should continue its essential function without assuming every decision can reach a cloud service.
Make updates and recovery part of the first prototype
A device that cannot recover from a failed update is not ready for unattended deployment. We establish the boot and update strategy while the prototype is still easy to change.
Where hardware and storage permit, we consider separate application slots or another recoverable installation scheme. We test power loss during download, verification, installation, and first boot. We also define how the device decides that a new release is healthy enough to keep.
Firmware authenticity matters independently of transport encryption. We use signed update artifacts and protect the trust anchors used to verify them. Secure boot and rollback protection depend on platform capabilities and provisioning choices; Rust does not provide them automatically.
Watchdogs need similar care. Feeding a watchdog from an unrelated timer can conceal an application failure. We tie health checks to meaningful progress and provide a recovery path that does not repeatedly reboot into the same broken state.
Test the physical system, not just the code
Rust’s testing tools are useful for parsers, state machines, calculations, and protocol handling that can run on a development machine. We separate that logic from hardware access so we can test it quickly and exercise malformed inputs without repeatedly flashing a board.
Then we add hardware-in-the-loop tests for behavior that a host cannot reproduce faithfully: timing, peripheral interactions, power transitions, and recovery after resets. We measure stack use, flash size, latency, and power consumption on representative builds.
For a remote collaborative studio, reproducible benches matter. We document wiring, board revisions, probe connections, and test procedures so another teammate can reproduce a failure without guessing how a prototype was assembled.
Observability also needs a budget. Logs can consume bandwidth, storage, CPU time, and power. We choose useful fault codes and diagnostic events, avoid collecting secrets, and make production logging levels deliberate. Debug visibility should help maintain a product without quietly becoming another source of instability.
A short checklist before committing
Before we make Rust the default for a device, we confirm:
- The exact chip, peripherals, and required drivers have a viable support path.
- The most difficult hardware interaction works on a real board.
- Memory, timing, and power budgets have measurable acceptance criteria.
- Unsafe code and native-library boundaries have clear owners.
- Offline behavior, device identity, and provisioning are defined.
- Updates survive interruption and have a tested recovery path.
- Another teammate can build, flash, debug, and test the system.
If several answers are uncertain, we scope a focused feasibility phase rather than promise a full product schedule.
Build for the life of the device
We see Rust as a useful engineering tool, not a requirement to abandon working C, C++, or vendor software. Its strongest contribution is helping us build boundaries we can reason about while retaining the control embedded and edge systems demand. The right adoption plan may be a new firmware module, a Linux gateway service, or an entirely new device.
If we can help turn your hardware idea into a maintainable connected product, start a project with Kordspace. To explore how we connect hardware, software, AI, cloud infrastructure, and design, read our related services pages.
Conversation (0)
No comments yet. Start the conversation.
Join the conversation
Create a free member account to comment.