You can get preemptive scheduling of async tasks with InterruptExecutor. You create one executor for each priority level, then spawn the tasks in the right one. The latency of the executor and the compiler-generated async state machines is predictable, so you can use Embassy for hard real-time work. See example: https://github.com/embassy-rs/embassy/blob/main/examples/nrf...
Additionally the executor has support for scheduling tasks by priority or deadline within a single priority level. (in latest git, will be in next crates.io release)
Async Rust does definitely work for non-toy use cases. As a data point, we use Embassy for all production firmware at my startup (https://akiles.app/en), using async tasks for everything: Bluetooth, TCP/IP networking, motor control, user interface (LEDs, keypad), a key-value database in flash, stats collection... Async helps with battery life too since it allows putting the core to sleep when no task has work to do, it allows us to build devices with 1-2 years of battery life.
There's other companies using Embassy in production. Sadly firmwares are usually not open source. There's a few non-toy open-source projects using Embassy though:
Rust async is a bit different than in other languages. It's more like sugar over state machines instead of sugar over callbacks.
This is what makes it work nicely on embedded. The compiler-generated state machines are structs with fixed size so they can be statically allocated. Callbacks would have to be heap-allocated and garbage-collected/refcounted.
> It's more like sugar over state machines instead of sugar over callbacks
they are equivalent [1]. There are scheme compilers (a language with have first class continuations and often heap allocated stack frames) that compile everything down to a giant C switch statement.
[1] well, continuations are strictly more powerful of course, but the stackless subset needed for async/await is the same.
You can use futures combinators like `join`, `select`, `with_timeout` with async Rust. (crates like `embassy-futures` or `futures` have implementations of these). They work nicely even in no-std embedded.
They're different tools for different use cases. Structured concurrency is nice when doing related actions concurrently where one might need to cancel the other, while unstructured task spawning makes more sense if they're truly unrelated tasks, that live for the entire duration of the program or where you don't care for how long they live (for example concurrently handling requests in a server).
> It appears unsuitable for use with DMA, which I use for all runtime IO
The `embedded-hal-async` traits can be implemented with DMA, the Embassy HALs do so. This is a problem with the `nb` traits only.
> The APIs tend to be a mess due to heavy use of typestates
This is an issue of some HAL crates implementing the traits, not with the `embedded-hal` traits themselves. I also dislike the heavy use of typestates/generics, Embassy tries to implement the traits while keeping typestates at a minimum.
You can get preemptive scheduling of async tasks with InterruptExecutor. You create one executor for each priority level, then spawn the tasks in the right one. The latency of the executor and the compiler-generated async state machines is predictable, so you can use Embassy for hard real-time work. See example: https://github.com/embassy-rs/embassy/blob/main/examples/nrf...
Additionally the executor has support for scheduling tasks by priority or deadline within a single priority level. (in latest git, will be in next crates.io release)