5 ms·
This is at the center of a friction point in embedded rust: most of the OSS ecosystem has shifted to this framework, and as a result, is incompatible with, or i
by the__alchemist 10mo ago
This is at the center of a friction point in embedded rust: most of the OSS ecosystem has shifted to this framework, and as a result, is incompatible with, or is high friction if you don't want to make your firmware and control flow Async. This is notable because Rust embedded is nascent and small, so I think splitting the ecosystem along with Async is not ideal. It's also confused some people new to embedded: I regularly hear this dichotomy: "Async vs blocking"; the assumption being if you are not using Embassy, your code blocks the CPU when waiting for I/O, etc.
If you enjoy Async PC rust programming, I think this will be a good starting point. I like how it has unified hardware access to different MCUs, and its hardware support for STM32, for example, is a step up from the initial generation of Trait-based HALs. I seem to be the odd one out as an embedded rust programmer (Personally and professionally) for whom Async is not my cup of tea.
- hannesfur 10mo agoMaybe stuff has changed a lot in the last year but I didn’t experience that problem so far. For me it was the other way around mostly. Where did you encounter that?
- the__alchemist 10mo agoI've had to consistently write my own libraries. HAL for STM32, LoRa support, hardware support for every sensor I use (GPS, IMUs, mag, flash memory etc), ESP-Hosted library, etc. Whenever I design something new or change parts, my assumption is I will have to write my own interface for it. It's not too bad, but is a friction point compared to if I had written the firmware in C or C++. On the other hand, the rust embedded core tooling including the cargo/rustc/it's target system, probe-rs, defmt, and the PAC project are phenomenal, and make the most important parts one of the lowest-friction embedded workflows around!
- hannesfur 10mo agoThat’s fair but when there is an async version of the driver or Hal available it should be pretty straightforward to port it to synchronous, right? Maybe Claude code can even do it with minimal supervision… Edit: Replace blocking with synchronous
- the__alchemist 10mo agoI think that's on a case-by-case basis, but from my own experiences, it's usually easier to start from scratch. A totaled car analogy, where the easier path is not modifying something existing. But it depends. Good pt. I think if the library is documented with datasheet or RM references, things are easier. (Case in point: An example of the the "It's Async or blocking" meme I mentioned.)
- rcxdude 10mo agoIn my experience most drivers are simple enough that there's not a great loss from them being reimplemented in different systems all over the place. They're fundamentally interface code, it makes sense for them to change as on of the sides of the interface changes. (which leads to one of my embedded hot takes which is that I think striving for a generic HAL is kind of misguided. If you're striving for any form of mechanical sympathy, your HAL is almost certainly specific to at least your framework and probably actually your application)
- rcxdude 10mo agoI think it's interesting because they seem to have built some vaguely pretty decent interfaces and drivers. Before that there were some attempts to make a rust embedded HAL but I think they were a bit too basic and didn't seem to get much traction. Also async interfaces are probably the most generic, because you can hook them up to superloops, single-threaded applications, and threaded code relatively easily (at least, more easily than the other way around), and IMO one of the big reasons Arduino stayed firmly hobbyist tier is because it was almost entirely stuck in a single-threaded blocking mindset and everything kind of fell apart as soon as you had to do two things at once.
- alfiedotwtf 10mo ago> superloops I’ve been doing async non-blocking code for decades, but this is the first time I e seen that word used? I’m assume you’re meaning something like one big ass select!() or is this something else? > IMO one of the big reasons Arduino stayed firmly hobbyist tier is because it was almost entirely stuck in a single-threaded blocking mindset and everything kind of fell apart as soon as you had to do two things at once. This. Having to do something like this recently, in C, was not fun and end up writing your own event management layer (and if you’re me, poorly).
- onjectic 10mo agoSuperloop is common terminology in the firmware space. They are cruder than a giant-state-machine-like case statements(but may use still them for control flow). They usually involve many non-nested if statements for handling events, and you usually check for every event one by one on every iteration of the loop. They are an abstraction and organizational nightmare once an application gets complex enough and is ideally only used in places where an RTOS won’t fit. I would not consider asynchronous frameworks like Embassy to be superloops.
- nagisa 10mo agoThis superloop pattern can also appear in more abstract scenarios as well. The wildly popular ESPHome is also driven by a superloop. On every iteration the main loop will call an update handler for each component which then is supposed to check if the timers have elapsed, if there is some data coming from a sensor, etc before doing actual work. This pattern brings with it loads of pitfalls. No component ought to do more than a "tick" worth of work or they can start interfering with other components who expect to be updated at some baseline frequency. Taking too long in any one component can result in serial buffers overrunning in another component, for example.
- bschwindHN 10mo agoIn 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.
- the__alchemist 10mo agoMy 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.
- bschwindHN 10mo agoIs 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/3905ecf8acd3046d35bf750283f74c53 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.
- bfrog 10mo agoHow async works with embassy is also interesting. In effect it works like a work queue, when something is waiting on a waker (interrupt) and is woken, the Future (task) is enqueued to be polled (run) by the executor. There’s good and bad things about this. It’s clever for sure but there can be variable latency between when the hardware event occurs and when the next step in the task starts. This is a lot like zephyr/linux work queues but with linear reading code sprinkled with async/await.
- bschwindHN 10mo agoHere's a good technical writeup on latency and jitter (latency standard deviation) for interrupts when it comes to Embassy, FreeRTOS, and RTIC: https://tweedegolf.nl/en/blog/65/async-rust-vs-rtos-showdown https://tweedegolf.nl/en/blog/65/async-rust-vs-rtos-showdown Obviously if you're working on something truly hard real-time you probably wouldn't be reaching for these tools to begin with, but for the average embedded project it seems you will enjoy quite good latency and jitter characteristics by default.
- bfrog 10mo agoI've read this, and frankly its comparing apples to oranges. These are not the same things though naively they may appear the same.
- bschwindHN 10mo agoI'd be happy to hear some of the differences if you don't mind. Both Embassy and FreeRTOS are often used to organize the various tasks you want to perform in an embedded context so I think it's fair to compare them. I know their implementations and behaviors can be quite different, but I'd like to hear more about what makes this an apples to oranges comparison.
- bfrog 10mo agoShow me how time slicing or deadline scheduling would work with multiple priorities (InterruptExecutor's) involved. Show me how a task in an InterruptExecutor would priority boost a task of a lower priority, switch to it, then switch back to free up a locked resource. I don't see how these things are feasible in Embassy. There's a large number of things that the cooperative scheduler can't do. Maybe you don't need those things, that's fine! Embassy really is quite nice for cooperative task scheduling. But to benchmark a context swapping RTOS which can and pays the cost for doing so, against a cooperative task queue scheduler... these are not at all the same things, and won't have the same performance characteristics in simple scenarios like the one in the blog post.
- junon 10mo agoOn the flip side, the stm32 firmware hello world from cubeide caused temperature spikes due to spinning. Embassy uses power states efficiently to reduce power draw and temp when nothing is scheduled. It is a huge tangible benefit to use async executors for firmware and I hold the strong belief that it should become the norm for general purpose uC firmware.
- the__alchemist 10mo agoThere is nothing unique to Async about this: You just put a cortex_m wfi in the main loop, or depending on the STM32 variant, set the sleep or stop bits and related.
- junon 10mo agoI didn't say it wasn't possible. I said it was the hello world from cubeide.
- 5d41402abc4b 10mo ago>the stm32 firmware hello world from cubeide caused temperature spikes due to spinning That should never happen unless you are using a high end 1GHz+ MPUs.Check your GPIOs to make sure there are no shorts.
- ComputerGuru 10mo agoI was writing async rust on top of the embedded Hal crate for stm32 before embassy was properly “a thing”. Maybe before it existed. Before async/await were even part of the language. It’s not an embassy exclusive even if its api embraces it. (I wanted to test a radio library I wrote with two of the modules connected to one MCU, one sending and one receiving. The normally blocking api meant I would need two devices, so I decided to go with async.)
- jamesmunns 10mo agoAs others have mentioned, ~all of the embassy HALs support nearly 1:1 parity of blocking interfaces for drivers next to the async ones. You really can avoid async entirely while still using embassy hals. The ecosystem is not tightly integrated/locked in. Even data structure libraries, like embassy-sync, all have `try_` methods, which would allow for polling usage outside of async. There's no mandate to use async - and helping folks that DO see value in it (which is a LOT of folks), isn't "splitting the ecosystem" - it's people doing things the way they like to do it. Embassy still works very hard to support folks who DON'T want to use async, to avoid duplicated work. There's nothing stopping you from preferring to write and maintain your own HALs, I know you have been for a while! But it's not something that people necessarily have to do, even if they aren't interested or don't prefer async!
- bfrog 10mo agoIt has literally split the ecosystem I’m not sure why people think it hasn’t. There’s basically embassy then everything else. Embassy has been quite successful in large part because its primary author took the time to make it good. If you look at espressif and how they support rust they have to offer both blocking and non blocking options, they also offer using rust on their freertos and C library setup. It’s quite an undertaking to do all of this.