8 ms·
Asynchronous Rust on Cortex-M microcontrollers
- jvanderbot 3y agoI could be wrong, but isn't every multi-task / RTOS essentially supporting this? Wake up, check flags, do work if required. Of course if you're bare-metal programming, it might be nice to have a lightweight mechanism for this.
- Conscat 3y agoIf you're asking why do coroutines exist, it's because context switching in kernel space and synchronization have very high overhead by comparison. You use those features when you want parallelism, not concurrency. 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.
- kevin_thibedeau 3y agoContext switches on Cortex-M are relatively cheap with optimizations to limit the amount of data that has to be stacked.
- lmm 3y agoRelative to what? They might be cheaper than some architectures, but they're still a hell of a lot more expensive than not doing them.
- kragen 3y agowe seem to be talking about cooperative multitasking here 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 for if 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
- kragen 3y agoi should clarify that while i have tested both of these context-switching subroutines, and they do seem to work in simple programs, i haven't tested them on actual cortex-m hardware, much less measured their real-world performance
- tadfisher 3y agoThere's also the simplicity of bare embedded in Rust, because you can mostly[1] use Cargo, depend on the right embedded_hal crates for your target, and be up and running with a hardware-backed async runtime and everything. Zephyr, Tock, Mynewt, etc. all have bespoke build/deployment systems and driver ecosystems. That said, RTOS makes actually deploying working products much more realizable, so pick your poison. 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.
- monocasa 3y ago> 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.
- vvanders 3y agoThat's good to hear, I've used that in the past to drive down codesize even on non embedded targets by compiling for size and rebuilding the stdlib.
- Archit3ch 3y ago> you can mostly[1] use Cargo, depend on the right embedded_hal crates for your target, and be up and running with a hardware-backed async runtime and everything Can one use Rust's embedded_hal from a different programming language?
- sophacles 3y agoIIUC you can make some api in rust that uses the HAL, which ties your code into board specific stuff. 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-with-c.html https://docs.rust-embedded.org/book/interoperability/rust-wi...
- azubinski 3y agoIt's different and very old story. Protothreads are classic now: https://dunkels.com/adam/pt/index.html https://dunkels.com/adam/pt/index.html Contiki OS is also a piece of embedded development classic: https://en.wikipedia.org/wiki/Contiki https://en.wikipedia.org/wiki/Contiki In general, it's more about syntax than semantics. Semantics is generally difficult and rarely changes.
- strangattractor 3y agoI new all that time I spent programming the Mac back in 1987 would come in handy. Asnyc programming - cooperative multitasking never go out of style.
- 0xfedbee 3y agoCooperative multitasking in embedded has to be the worst idea ever. Anyone who has done any serious work in embedded systems will tell you how bad is it when a faulty task blocks everything. Please stop advertising async in embedded.
- junon 3y ago... anything faulty in an embedded world can block the chip. This is why watchdog timers exist. Async does not shut off watchdog. Not sure what your point is.
- kevin_thibedeau 3y agoWatchdogs shouldn't be triggered in the normal course of events. They are a last line of defense for exceptional circumstances that will render a device inoperative, not a cure-all for poor system design.
- rcxdude 3y agoSure, but a system where a task has ceased to execute is in pretty much all circumstances a system where the watchdog (or some other assert) should trigger. (the whole toyota case involved them getting raked over the coals because their system did not in fact detect a failed task and do this).
- junon 3y agoI... know. I'm saying that a stalled chip is a stalled chip. I don't know why async would stall your chip randomly unless you had a bug, which would stall the chip anyway.
- jacquesm 3y agoThink of a watchdog timer in the same way you think about that big red handle on a train, 'emergency use only, abuse will be punished'.
- 3y ago
- AceJohnny2 3y agoEdit: commented on wrong post, meant for https://news.ycombinator.com/item?id=36791506 https://news.ycombinator.com/item?id=36791506 Offtopic: 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 [1] https://rubjo.github.io/victor-mono/ https://rubjo.github.io/victor-mono/
- kibwen 3y agoThat may be specific to your machine, I don't see any italics on mine, and the CSS just looks like `font-family: 'Source Code Pro', monospace;`.
- deleted 3y ago[deleted]
- the__alchemist 3y agoMost of my programming these days is on Cortex-M, using Rust. The article is another prior in my suspicion that Async embedded Rust is designed with toy use cases in mind (blinky); I don't know how it would work in practical firemware because I haven't seen examples. 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)
- steveklabnik 3y agoAt work, we have a non-async RTOS https://hubris.oxide.computer/ https://hubris.oxide.computer/ though it is a bit harder to use than "add a dependency in Cargo.toml" like most projects. 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/async-inversion/ * http://cliffle.com/blog/composed-concurrency-in-drivers/ 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://rtic.rs/2/book/en/ https://rtic.rs/2/book/en/ * https://github.com/embassy-rs/embassy https://github.com/embassy-rs/embassy (this one is used in the article) * https://tockos.org/ https://tockos.org/
- the__alchemist 3y agoCliff's articles are outstanding! 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.
- KingLancelot 3y ago[dead]
- chaxor 3y agoI believe that Tock (tockos.org) and Theseus (https://github.com/theseus-os/Theseus https://github.com/theseus-os/Theseus) are in this area a bit as well, just from an actual OS perspective. 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$.
- dmitrygr 3y agoSo, cooperative multitasking with no control of actual scheduling? Cute, but a real RTOS is the solution to most any "i need to do more than one thing" in the embedded world.
- dhon_ 3y agoYou can run multiple executors at different interrupt priority levels (with multiple tasks per executor), which allows tasks on the higher priority executor to interrupt other tasks. Here's an example https://github.com/embassy-rs/embassy/blob/main/examples/nrf52840/src/bin/multiprio.rs https://github.com/embassy-rs/embassy/blob/main/examples/nrf...
- amluto 3y agospawner.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.)
- swsieber 3y agoIIRC been some proposals floating around, but one major block is asncy drop. This is the article I'm thinking of specifically: https://blog.yoshuawuyts.com/tree-structured-concurrency/ https://blog.yoshuawuyts.com/tree-structured-concurrency/
- dirbaio 3y agoYou 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). (disclaimer: Embassy maintainer here)
- scoutt 3y agoI think the nature of microcontroller programming is already asynchronous. I mean, it's not that the only alternative to "async Rust" is an RTOS; a classic loop program with interrupts is already asynchronous. The only difference is you have to decide how you want to waste time waiting for events, and the same "energy saving" effect can also be achieved with a simple state machine and __WFI(), resulting in cleaner code with no dependencies whatsoever. 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).await
- rcxdude 3y agoYeah, you can always write async code by hand. It's a pain because you have to manually yield (as in, save your context, return to the main loop, and poll for the event to complete yourself) every time you wait for IO or a timer. For simple enough systems it's managable, in larger ones it quickly becomes a pain.
- dingi 3y agoTrying to shoehorn Rust this and Rust that into every freaking thing makes more problems than it solves. Case in point is async rust in microcontrollers. Looks ugly as a sin.