When developing embedded systems, writing software that “does one thing” is rarely enough. Even the simplest devices often need to perform multiple tasks at once—like blinking an LED while waiting for a user input. Achieving this kind of concurrency efficiently and reliably is one of the major challenges of embedded development, especially on single-core microcontrollers. That’s where Rust and its async capabilities come into play.
This blog explores the fundamental challenge of concurrency in embedded software and how modern Rust features—particularly async—can help developers build more maintainable, non-blocking systems without the complexity that traditionally comes with multi-tasking.
The Real Challenge: Doing More Than One Thing
Most embedded systems spend a lot of time waiting. Waiting for a button press. Waiting for sensor data. Waiting to send or receive over UART. While the actual “work” may be light, coordinating all these inputs and outputs at the right time without missing anything is where things get tricky. On single-core microcontrollers, this has to be done without true parallel execution, relying instead on time-slicing and event handling.
The ultimate goal? Ensuring all tasks complete on time—whether that’s milliseconds in a touchscreen interface or microseconds in a car’s airbag ECU.
Traditional Concurrency Models—and Their Pitfalls
There are several ways developers typically tackle concurrency in embedded systems:
1. Bare-Metal Super Loops
In this architecture, all logic runs inside a giant loop that checks inputs and updates outputs in sequence. It’s simple and works well for the most basic use cases.
But as more tasks are added, the loop becomes harder to maintain. With increasing complexity, subtle timing issues and side effects can emerge. It’s easy to end up with “spaghetti loops” that are nearly impossible to debug.
2. Interrupt-Driven Code
Interrupts offer a more efficient way to handle time-sensitive events. When an event occurs (like a button press or timer firing), the microcontroller jumps to an interrupt handler.
While this avoids the pitfalls of polling, it can introduce new issues. For example, if a UART message is printed directly from an interrupt, that handler could block the system for many milliseconds—preventing other interrupts from being serviced.
3. State Machines
Designing the system as a state machine helps formalize how different events are handled. The system transitions between defined states in response to events. This improves maintainability and testability, but implementation can quickly become intricate—especially when dealing with non-blocking I/O like a networking stack, that requires multiple state transitions and careful timing.
4. RTOS-Based Multithreading
A real-time operating system (RTOS) can manage multiple threads, scheduling them based on priorities and system timing requirements. This makes concurrency more manageable at scale and is a great choice in traditional C/C++ embedded systems. However, the embedded Rust ecosystem doesn’t yet have any widely used RTOS options, and an RTOS can bring increased overhead by requiring a separate region of stack memory for each task.
Enter Rust Async: A Modern Take on Embedded Concurrency
Rust introduces a new paradigm for concurrency that combines the efficiency of bare-metal programming with the expressiveness and composability of asynchronous programming models familiar to developers from the web world.
With async/await, Rust allows developers to write code that looks sequential but actually represents state machines under the hood. These state machines pause execution when waiting (e.g., on I/O) and resume when data is ready—without blocking the CPU.
This helps avoid both the maintainability issues associated with spaghetti-like super loops and the pitfalls of writing complex state transitions manually.
The result is embedded software that is:
- Non-blocking: Time-consuming tasks like output to a slow communications bus can be handled without blocking the rest of the system.
- Composable: Small async functions can be composed into larger tasks without having to manually combine complex state machines, improving maintainability.
- Efficient: Minimal overhead is added, making it suitable even for constrained devices.
- Safe: Rust’s type system ensures memory safety and prevents race conditions by design.
Final Thoughts
Rust’s async model offers embedded developers a new tool to tackle one of their oldest problems: managing multiple tasks on systems that weren’t designed for true multitasking. By enabling expressive, non-blocking code without compromising system safety or performance, async Rust provides a clear path forward for building modern embedded software—without the legacy headaches.
For developers used to C, C++, or even bare-metal real-time loops, the learning curve may feel steep at first. But the payoff in code clarity, system robustness, and long-term maintainability is worth the investment.