Analysis

Rust async frameworks meet Arm timers for embedded concurrency

Async Rust only feels dependable when the timer underneath it can keep up. On Arm, SysTick is fine for simple ticks, but the Generic Timer is what unlocks real concurrency.

Jamie Taylor··5 min read
Published
Listen to this article0:00 min
Rust async frameworks meet Arm timers for embedded concurrency
AI-generated illustration

The timer is the hidden constraint in embedded async Rust

The hardest part of embedded async Rust is not getting a task to run, it is proving that wakeups happen exactly when the hardware says they should. Once you move past blinking an LED, you need scheduling, timeouts, precise wakeups, and a way to juggle many future events without burning the CPU on busy loops. That is where the timer choice quietly decides whether async feels solid on your board or flaky under load.

Why Embassy and RTIC matter once the problem gets real

This is the space where frameworks like Embassy and RTIC earn their keep. Embassy presents itself as a next-generation embedded framework for safe, correct, energy-efficient async Rust, and its runtime is built as an async/await executor with support for interrupts and timers. RTIC, in its current v2 documentation, frames itself as a framework for building real-time systems and as a hardware-accelerated concurrency framework.

That distinction matters in practice. A plain polling loop can be fine when you have one peripheral to watch and plenty of idle time between checks. As soon as you want several concurrent waits, deadlines, or responsive event handling, the framework is no longer just syntactic sugar, it becomes the layer that decides how often you wake, how you sleep, and how much timer plumbing you must write yourself.

SysTick is simple, but it is not a general-purpose future alarm

On Cortex-M parts, the default timing primitive is often SysTick. Arm documents SysTick as a 24-bit system timer on Cortex-M devices, counting down from a reload value to zero. Arm also notes that on security-enabled implementations there can be separate secure and non-secure SysTick timers, and that writing zero to the reload register disables the counter on the next wrap.

That is enough for a regular tick source, but the 24-bit width is the catch. Because the counter is only 24 bits wide, the maximum interval depends directly on the clock rate, so at higher system clocks the gap between ticks gets short fast. That makes SysTick a decent fit for preemptive scheduling and periodic interrupts, but a poor fit when you want to sleep until an event far in the future without constantly reprogramming the timer.

For embedded Rust, that distinction is not academic. If your executor wants to park a task until a timeout expires, a narrow timer forces more wakeups, more bookkeeping, and more opportunities for drift or jitter. In other words, SysTick can keep time, but it is not the timer you want to lean on when your concurrency model depends on long, flexible delays.

The Generic Timer is the stronger foundation on larger Arm systems

Arm’s Generic Timer is built for a different class of machine. It is a standardized timer framework with a shared system counter and per-core timers, and Arm says the counter value is broadcast to all cores so they share a common view of time. The counter itself is 56 to 64 bits wide, which is a very different world from SysTick’s 24 bits.

The frequency story is different too. From Armv8.6-A and Armv9.1-A, the count frequency is fixed at 1 GHz. Before Armv8.6-A, the frequency was a system design choice and commonly ranged from 1 MHz to 50 MHz. That flexibility is useful, but the broad point is simple: the Generic Timer is designed to give runtimes and schedulers a much better time base than a narrow system tick.

This is why the Generic Timer becomes the obvious target for async runtimes on larger Armv7-R, Armv8-R, and similar systems. If you are trying to build a responsive executor, the timer has to support more than a fixed heartbeat. It has to feed deadlines, wakeups, and cross-core consistency without turning every future event into a reprogramming exercise.

The architecture boundary is real, not just a software preference

The aarch32-cpu crate makes that boundary concrete by including support for Arm Generic Timer interrupts. Its documentation also states that the Generic Timer existed in Armv7-A but not in Armv7-R. Arm’s own architecture reference agrees on the key point, describing the Generic Timer on Armv7-A and Armv7-R as an optional extension rather than a mandatory part of the architecture.

That means the timer you can use depends on the core family you actually bought. The rust-embedded aarch32 project targets Armv7-R, Armv8-R AArch32, Armv7-A, and Armv8-A AArch32 processors, but not M-profile parts. So if you are moving between Cortex-M boards and larger A-profile or R-profile systems, you are not just changing firmware style, you are changing the timing hardware your concurrency stack can rely on.

The aarch32-rt runtime support sits in the same practical lane. It exists for AArch32 processors, which makes it part of the bridge between Rust abstractions and the timer hardware underneath them. In that sense, the runtime story is not about choosing a fashionable executor, it is about matching the runtime to the timer source that your chip actually exposes.

How to choose between polling, interrupts, and async on your board

The decision becomes much clearer once you frame it around the timer.

  • Use polling when the work is simple, short-lived, and you can afford to keep the CPU active.
  • Use interrupts when you need immediate response to a hardware event but do not need a full task scheduler.
  • Use async executors like Embassy when you want multiple timed waits, structured wakeups, and a cleaner model for managing future events.
  • Reach for RTIC when the workload is real-time and hardware-accelerated concurrency is the better fit than a purely async executor.

On Cortex-M, SysTick is usually enough for simple timing and periodic scheduling, but its 24-bit limit makes it a weak foundation for long-horizon async timing. On larger Arm systems, the Generic Timer gives you the broader and more stable time base that async frameworks need to feel dependable instead of improvised.

That is the real takeaway: async Rust on Arm does not fail because the syntax is wrong, it fails when the timer model underneath it is too small for the concurrency you are trying to build. Once you line up the board, the architecture, and the timer source, frameworks like Embassy and RTIC stop feeling abstract and start feeling like the right way to make embedded concurrency behave.

This article was produced by Prism’s automated news system from verified source data, official records, and press releases, then run through automated quality and moderation checks before publishing. The system is built and supervised by the people who set the standards it runs under. Read our full AI policy.

Did this article answer your question?

Discussion

More Rust Programming News