Asynchronous Rust on Cortex-M microcontrollers
interrupt.memfault.com
interrupt.memfault.com
Another possibility is that my brain and learning patterns don't work well with Async. I have a hard time understanding it, and it feels like a layer of obfuscation or misdirection. It's an abstraction that may be more suitable for other learning styles, or different program structures or requirements than the ones I've worked on.
I'd love to see an RTOS for Rust, but am worried it will be Async.
(I am referring to the Async/Await pattern; not asynchronous programming or concurrency)
The original designer of Hubris, Cliff, has his own async RTOS that he uses for personal projects. He recently has been writing some blog posts on this that may be of interest to you:
* http://cliffle.com/blog/async-inversion/
* http://cliffle.com/blog/composed-concurrency-in-drivers/
The former is a general introduction to async, and the second is about applying it to write an I2C driver.
There already are some general RTOSes for Rust, and they do tend to use async:
* https://github.com/embassy-rs/embassy (this one is used in the article)
Re Rtic: I'm using the V1 of it, which is non-Async; it seems to offer smarter locking than using critical sections + Mutex, since it's capable of interrupting a lower-priority task with a higher one, depending on which resources are locked.
Another thing that helps me get it is comparing it to the continuation passing style, where you never return from a function, you just take an argument that's basically a function pointer bound to an environment, and at the end of the function, instead of returning, you call your input function, giving it another function and environment as input, repeating the cycle. It's very similar to the transformation of callbacks within callbacks within callbacks pattern in JavaScript to the async/await pattern.
[1] namely, your futures will need to be Send + 'static.
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.
(disclaimer: Embassy maintainer here)
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.
Maybe the implementation just wasn't up to where it needs to be with these newer/slicker/more integrated versions, but mine (and others') issues with them wasn't the weakness/caveats of the implementation, but rather with the mess of spaghetti it made as your coroutine use grew with any degree. In onesie twosies under nice demo cases (look ma, I get some data from the intertubes with this syncy thing), they're great, but my experience was that they're a mess when scaled.
I'm happy to be proven wrong. I get to use them a bunch in Kotlin, I'm trying not to be a victim of my experience. I'm still on the fence.
> but mine (and others') issues with them wasn't the weakness/caveats of the implementation, but rather with the mess of spaghetti it made as your coroutine use grew with any degree.
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:
- https://github.com/nviennot/turbo-resin
- https://github.com/ivmarkov/ruwm
- https://github.com/dkhayes117/propane-monitor-embassy
(disclaimer: Embassy maintainer, Akiles CTO here)
spawner.spawn(blinky(led)).unwrap();
Has anyone seriously tried structured concurrency for single-threaded async Rust? The pattern where main effectively leaks a task seems kind of gross to me, and it’s exactly what structured concurrency tries to solve.(I realize that Rust’s async is inherently semi-structured.)
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).
(disclaimer: Embassy maintainer here)
Of course if you're bare-metal programming, it might be nice to have a lightweight mechanism for this.
For parallelism, you also usually want a userspace task scheduler because simply spawning threads in kernelspace is much slower. Cilk and Go provide this built into their language runtime.
correct me if i'm wrong, but i think that even with an almost standard abi it's five instructions and 27 cortex-m3 clock cycles; here we reserve r10 for the current task pointer (with the bonus that you can use it for a tls base register) and round-robin among the tasks in a circularly linked list:
yield: push {r4-r9, r11, lr} @ save all callee-saved regs except r10
str sp, [r10], #4 @ save stack pointer in current task
ldr r10, [r10] @ load pointer to next task
ldr sp, [r10] @ switch to next task's stack
pop {r4-r9, r11, pc} @ return into yielded context there
i think in gcc that's more or less -mpic-register=r10 -msingle-pic-base, though that isn't what those options are meant forif you're willing and able to deform the abi a little by having less callee-saved registers, something you might want to do anyway for efficiency, you can cut r4-r9 down to r4-r5 and shave that down to i think 19 clocks? if you're willing to eliminate callee-saved registers entirely you can get to 15, but that seems like a heavy price to pay
if you have to use the standard abi it's i think 37 clocks on a cortex-m3 or better:
yield: push {r4-r12, lr}
ldr r9, =current_task_pointer
ldr r10, [r9]
str sp, [r10], #4
ldr r10, [r10]
str r10, [r9]
ldr sp, [r10]
pop {r4-r12, pc}
i feel like something in the range of 20-40 clocks is really not that expensive? i mean an ldr or str is 3 or 4 clocks if it's not accessing tcm, right? and on cortex-m0 every taken branch is 3 clocks because you don't have branch prediction?i mean it's not going to compete with barrel processors on context switch time, but if it's really the cost of accessing seven fields of structs on the heap, it hardly seems like it's "a hell of a lot more expensive" than even zero
if we're talking about preemptive context switching, that's a different ball of wax; you have to save all the registers that you use at all, not just callee-saved registers, and depending on your application maybe vfp registers as well as integer registers
1: I think you still need xargo/cargo-xbuild for no-std embedded sysroots, but there may have been some upstream development on that front since I last played in this sandbox.
Cargo's build-std flag is the bespoke way now. Unstable, but working it's way to stable. AFAIK cargo/build aren't getting any more development in lieu of build-std.
Can one use Rust's embedded_hal from a different programming language?
That api you write can be exposed via C abi so you can link C or languages that have an FFI to C to the rust code. https://docs.rust-embedded.org/book/interoperability/rust-wi...
Now if Cargo had a way of producing multiple variants of the same project (e.g. different --targets with pre-set features) just with `cargo build` then things would be a lot better.
As of now, a Makefile is still needed to bring it all together.
I don't use it in my firmwares because:
- All of the embedded-HAL libs I've found (eg drivers) have had notable limitations or ergonomics problems.
- It appears unsuitable for use with DMA, which I use for all runtime IO
- The APIs tend to be a mess due to heavy use of typestatesThe `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.
(disclaimer: Embassy maintainer here)
Protothreads are classic now: https://dunkels.com/adam/pt/index.html
Contiki OS is also a piece of embedded development classic: https://en.wikipedia.org/wiki/Contiki
In general, it's more about syntax than semantics. Semantics is generally difficult and rarely changes.
I don't know much about this area, but it would be wonderful if these could work with the Libre compute boards, like the AM Logic S905X (Lepotato) or the Rock chip, since they're so much cheaper than a Pi. Would be great to get a dedicated simple system up to toy with Rust OS for only 30$.
Also, it seems to me that Embassy takes over interrupt handlers? If that's the case, what if I want to deal with interrupts in a different way? Or what happens with interrupts that deal with many pins, like EXTI9_5_IRQHandler(), as if I want to use pin 9 with Async Rust, but the rest as a regular interrupt?
And Pin type is a poor name choice for embedded.
Also:
ExtiInputFuture::new(self.pin.pin.pin.pin(), self.pin.pin.pin.port(), true, false).awaitOfftopic:
The cursive italics are apparently a feature of the Victor Mono [1] font used for the full page. While it'd be amusing in Tumblr context (where cursive is used for hyperbolic emphasis), I can't fathom why one would consider it in a code context.
You can change it (at least on Safari) by going into developer tools, clicking any node, and removing "Victor Mono" from --font-family
This is why watchdog timers exist. Async does not shut off watchdog. Not sure what your point is.
I have no desire to use embedded rust async, I use C++ with none of the allocating containers, and honestly it’s more of a C with classes style than modern C++, plus FreeRTOS in my production embedded code. I would happily trade out C++ for the memory safety of rust.
I would pick up a lightweight RTOS written in non-async rust if one existed and I could have faith in it, but other than personal projects I can’t advocate for building a hardware project on something like this yet.
My point is that async doesn't introduce stalls unless you have a bug - a bug that could introduce a stall anyway. I don't see how async changes that.
I write firmware and operating systems for a living, by the way.
But if you have many long running tasks that only need to keep a handful of bytes of state when they're waiting, a mechanism like rust async can allow for huge memory savings while mostly retaining the same code style as stackful tasks.
And you can even use both approaches in the same program, with isolated stacks for realtime critical tasks and cooperative state machines for everything else.
If I understand the article correctly, it seems that in this case the multitasking also isn't controlled by calling yield in custom user code, but rather always from the implementation that makes async/await work.
Faults are a way of life in software, they are unavoidable because each and every piece of software rests on a bunch of assumptions and if any one of those or a combination of them do not hold then you have a fault. Failure to envision that fault and a way to deal with it in embedded systems can cause damage to property, injury and loss of life.
None of this is simple, not in theory and definitely not in practice.
Yeah I would love to spot that my car’s LED task is faulty while trying to brake on a 100mph highway.
Because cooperative multitasking is bog-standard in OS kernels.