← Back to context

Comment by bschwindHN

1 day ago

In my experience, most of embassy's HALs support blocking variants as well.

I don't quite understand the opposition to async in this context though. Embassy's executor is quite nice. You get to write much more straightforward linear code, and it's more battery efficient because the CPU core goes to sleep at await points. The various hardware interrupts then wake up the core and notify the executor to continue making progress.

The compiler transformation from async/await to a state machine is a godsend for this. Doing the equivalent by hand would be a major pain to get the same power efficiency and code ergonomics.

My general 2c on this, in context with my observations in rust embedded: I think you are overestimating the difficulty of doing these tasks without Async. I point out again, that the Async vs blocking meme, while widespread, is not accurate. There is nothing about Async that makes it more battery efficient than non Async code. Hardware interrupts, sleep, or non-blocking operations are neither unique to Async, nor difficult without it.

  • Is it _that_ hard? No. Is writing assembly _that_ hard? Also no. It's simple, but not ergonomic and takes time.

    In C or "regular" embedded Rust, if I want to compose several tasks together, while sleeping the CPU core while waiting on interrupts, I need to scatter global variables over the code, and write custom state machines for all the "yield" points in my code. Oh and then requirements come in later and I need to add some timeouts to various operations. That gets messy quickly. Yes it's "not that hard" but Embassy is right there and it works. I get the state machines for free, I get CPU sleeps for free, the code is easier for others to jump in and work with, and with async combinators it's significantly easier to rearrange logic when new requirements get added.

    Just for a concrete example from a (somewhat esoteric) project I'm working on:

    https://gist.github.com/bschwind/3905ecf8acd3046d35bf750283f...

    This code is receiving uncompressed video frames over USB High Speed and forwarding them to an OLED display. In this case I have the luxury of having enough SRAM to hold two framebuffers in memory, so it's a classic double-buffering strategy of displaying one buffer while the other is being filled. Using a simple combinator, `join()`, I can kick off two DMA transfers with one filling the back buffer, and the other transmitting the front buffer to the display. I can have timeouts on these operations, the code flows pretty linearly, and no external globals or custom interrupt handlers are needed (obviously these exist, but they're in the Embassy code layer). And while these transfers are happening the core is automatically sleeping, assuming I don't have other async tasks running.

    To me, this is beautiful for embedded code, and brings a major ergonomic gain over the equivalent in C or even regular old embedded Rust. Obviously you don't have to use it, but I see a bright future for embedded Rust if Embassy and others (like RTIC) can keep up the momentum.

  • Rust's async is reminiscent of state machines, which are universal. The issues with the experience come from accidental complexity, in the language or the library ecosystem.

  • I did this in C and writing the state machines for your interrupts by hand gets old really quickly.

    Interrupts map one to one to async execution so I honestly don't even understand what you are arguing for or against.

    • > I honestly don't even understand what you are arguing for or against.

      The notion that Async is the only right or acceptable way to do embedded programming, or embedded programming on rust. The Overton window has shifted so much that I have to state this explicitly.