deniz.in

Markets

Weather

Loading weather

· via Hacker News – Front Page (native)

Tweede golf benchmarks async Rust Embassy against FreeRTOS on STM32F446

A hands-on comparison runs identical firmware as async Rust on Embassy and as C on FreeRTOS on an STM32F446, scoring interrupt latency, code size, RAM use and ergonomics.

Tweede golf benchmarks async Rust Embassy against FreeRTOS on STM32F446

Tweede golf has published a hands-on comparison that runs the same embedded application twice on an STM32F446ZET6 clocked at 180 MHz — once in C on top of FreeRTOS, once in async Rust using Embassy — and judges both sides on interrupt latency, program size, RAM usage and ease of programming. The post reached the front page of Hacker News.

How the two models work

According to the post, Rust's async functions compile into state machines: the returned future is polled, resumes where it last paused, and keeps the variables that live across await points inside itself. Futures are lazy — nothing runs until polled — and executors rely on wakers so a future is only re-polled once something signals that it can make progress.

Embassy builds on this with restrictions driven by the absence of a heap allocator: every task is statically allocated, the complete set of tasks must be known at compile time, and a nightly compiler is needed for the type_alias_impl_trait feature. Peripheral drivers expose async interfaces, so waiting for a GPIO edge becomes a future that registers its waker in a global array; when the hardware interrupt fires, the matching entry wakes the right task and the executor polls it again. Notably, Embassy is cooperative: a running task is only displaced when it awaits something.

An RTOS takes the opposite approach. Threads run ordinary code, so any existing function can become a thread, but every switch must capture and restore the full processor context. That design makes pre-emption natural: the kernel enforces priorities, shares CPU time between threads and can respond to events and interrupts within predictable bounds.

The workload

To stay realistic without becoming an endless tuning exercise, the benchmark program performs three jobs. An LED blinks on for 100 ms of every 200 ms using the executor's or RTOS's delay primitive. A button task sets up a GPIO interrupt, tracks the user button's level in a shared atomic boolean, and pushes a formatted string — the button level plus a running trigger count — onto a message queue whenever the state changes. A third task drains that queue and prints to serial. The LED must stay dark while the button is held, and that state has to travel between tasks through the shared variable rather than by reading the pin register directly, forcing genuine inter-task communication into the test.

What is measured

Timing is captured with a Rigol DS1054Z oscilloscope, using GPIO pins as probes. One pin goes high on entry to the button interrupt and low on exit, giving the interrupt's execution time. A second pin marks the interval during which the button task runs between waking and blocking again. The gap between the two rising edges yields the interrupt-processing latency — how long a signal takes to travel from hardware interrupt to running task code.

Footprint is measured with arm-none-eabi-size: the .text section stands in for program size, while .data plus .bss captures static RAM. Every task and thread is statically allocated, and the author deliberately leaves dynamic and stack usage out because it is hard to measure, while observing that software which allocates more statically generally needs less stack. Ease of programming is scored as well, with the author openly calling that criterion subjective.

Ground rules

The comparison explicitly avoids chasing peak performance, on the grounds that tuning embedded code can continue almost indefinitely. Both implementations follow the same guidelines: reasonably portable to other chips apart from HAL dependencies, straightforward to read, and tuned only with ordinary settings such as compiler optimization levels, RTOS configuration and thread priorities. The author admits a bias toward Rust, invites suggestions for improving the test, and keeps the familiar C-versus-Rust language debate out of scope in order to focus on the two concurrency models themselves.

Why it matters

Choosing between a pre-emptive RTOS and a cooperative async executor is one of the bigger architectural decisions on a microcontroller project, and abstract arguments rarely settle it. A same-chip, same-workload benchmark turns questions like whether cooperative scheduling is fast enough for interrupt-driven code into measurable data instead of folklore. The measurement technique itself — toggling pins around the interrupt handler and the woken task and reading the pulses on a scope — is a trick any embedded developer can copy for their own latency checks, whatever stack they run. The full post pairs that methodology with an accessible explanation of how futures, wakers, executors and RTOS schedulers actually operate, which makes it useful background reading for teams weighing FreeRTOS against Embassy.

  • #rust
  • #embedded
  • #freertos
  • #embassy
  • #stm32
  • #benchmark