8 ms·
I wanted to use Rust on embedded. I probably gave up way to early. But I really could not do it. This is how to set a GPIO bit, from the official "Embedded Rus
by zevv 3y ago
I wanted to use Rust on embedded. I probably gave up way to early. But I really could not do it.
This is how to set a GPIO bit, from the official "Embedded Rust" book:
MY_GPIO.borrow(cs).borrow().as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
I tried. I really tried.
- dijit 3y agobecomes much more readable when you newline before each period. Though to be honest I am sure someone will pop up with a much easier and cleaner way to do this.
- ykonstant 3y agoI'll be keeping an eye on this then, because the above code is ridiculous; if it's the best (code-wise or performance-wise) way to do it in Rust, I can see the complaints.
- dralley 3y agoIt wouldn't be that much simpler if it wanted to do the equivalent thing in C. This is code for "set a pin in a completely thread safe way", whereas a lot of the comparisons are "set a pin". Which, as others have already pointed out, brings us to the very topic of this post. Rust prevents novices from messing up partly by forcing them to acknowledge ways in which the code might break, and it does that via the type system.
- taneq 3y agoThat’s gotta have improved by now… right? Right??
- RecycledEle 3y agoCould a macro simplify that?
- moomin 3y agoA monad could. ‘course, you don’t have monads.
- wizzwizz4 3y agoRust does, in fact, have monads. There are multiple implementations of Haskell-style do syntax in Rust: https://crates.io/crates/do-notation https://crates.io/crates/do-notation https://crates.io/crates/monadic https://crates.io/crates/monadic Not that that's necessary, seeing as Rust is already procedural. You can just use regular old procedural syntax.
- saurik 3y agoA macro? How about a function ;P.
- oblio 3y agoI imagine there might be a crate for embedded? Surely most of that stuff can be syntax-sugared away?
- dale_glass 3y agoI'm feeling very curious why all of that is necessary, and why it can't be hidden away in a function or a macro? Would anybody mind explaining?
- afiori 3y agoit can be hidden in a function
- leoedin 3y agoIt can be hidden away in a function or a macro. In any sensible code base it would be.
- arlort 3y agoThis example is attempting to give the low level view of how peripherals can be safely shared between multiple threads https://docs.rust-embedded.org/book/concurrency/index.html?highlight=MY_GPIO#sharing-peripherals https://docs.rust-embedded.org/book/concurrency/index.html?h... You would definitely hide it behind a function if you need it, but that's not very useful in a guide that wants to teach you what it looks like without the function
- db48x 3y agoEven better than hiding it away is splitting it in half. The code was written that way so that two tasks running simultaneously could not both try to write to the same pin at the same time. But writing it as a one–liner that you use over and over is the wrong approach, because then you’re unlocking the mutex (and other layers) before every write to the pin, and then locking it back down afterwards. If you then sleep before writing the next bit to that pin, some other task could still jump in and write it’s own bit to the pin before you wake up. So what you really want to do is unlock the mutex and unwrap all the layers to get just the io pin object, and give ownership of it to the task. Then the task can write to the pin, sleep, and not worry that any other task can write to the pin before it wakes back up. When it comes time to spawn that other task, you can unlock and unwrap another io pin and pass along ownership of it to the new task. Meanwhile the compiler prevents either task from accessing pins not assigned to them, unless they contain the code to unlock the global mutex. But then you have to talk about how to spawn multiple tasks, and I bet that’s in a different chapter! By the end of the book when all the pieces are in place, there probably is no global variable for the IO pins, no task tries to unlock a global mutex, and instead all tasks take ownership of the pins they need.
- anon23432343 3y agoI don't see the problem. Its pretty clear to me even now knowing rust embedded what is happening hear.
- rgoulter 3y agoAs a hobbyist, I find Rust on embedded is wonderful. e.g. I prefer Rust's traits over C's CPP defines for configuring peripherals. > MY_GPIO.borrow(cs).borrow().as_ref().unwrap() This code looks like it's some kind of global Mutex<RefCell<Option<LedPin>>>. Looking at some other examples, setting an LED pin to high is just `.set_high()` https://github.com/stm32-rs/stm32f4xx-hal/blob/master/examples/blinky.rs https://github.com/stm32-rs/stm32f4xx-hal/blob/master/exampl... Though yeah, this example which passes a pin to an interrupt handler has similar dense `.borrow` calls https://github.com/stm32-rs/stm32f4xx-hal/blob/master/examples/blinky-timer-irq.rs https://github.com/stm32-rs/stm32f4xx-hal/blob/master/exampl... I think for sharing resources across functions, I've enjoyed using rtic; e.g. a similar example there: https://github.com/rtic-rs/rtic-examples/blob/master/rtic_v1/stm32f3_blinky/src/main.rs https://github.com/rtic-rs/rtic-examples/blob/master/rtic_v1...
- MrResearcher 3y agoWhat would C look like in comparison? Is it something along this line *MY_GPIO |= 1; Or more complex?
- biorach 3y agoThe rust version includes reference counting, state verification (preventing access before initialisation) and locking. All "for free" by wrapping the original value. In C you would have to write all that out. And still have a good chance of getting something wrong first try.
- masklinn 3y agoOr, and I assume that’s what the GP was looking for, you write none of that and just fly by the seat of your pants.
- timeon 3y agoWhich brings us back to the topic of the article.
- masklinn 3y agoIndeed.
- z3phyr 3y agoNot the OP, but I do not like reading one line expression bonanzas. I would rather read implementations of different subsystems and then calling them. I mean, I find this simpler procedure x_component .. end procedure y_component .. end procedure assembly x_component y_component .. end In python, rust, C++, java, lisp people seem to form very long procedures with lambdas and some ninja fu to save lines of code and it may look elegant to some people, but I can't read it.
- IshKebab 3y agoPretty much. The issue is that the C version is horribly error prone and unsafe. Any time you lose by writing a load of `borrow()`s you will get back 100 times over by not having to debug races on a microcontroller!
- epilys 3y agoThis is because of how the tutorial author and the crate they use chose to represent the IO. In this case it is a Mutex, wrapping a reference counted pointer, wrapping an Option<[GPIO type]> to prevent uninitialised stuff being accessed before they are ready. So the "problem" here is the secure abstractions they use over I/O. You can write another interface to avoid the borrow().borrow().unwrap().unwrap().whatever() chain calls. It might look better if you split it in lines: let mutex = MY_GPIO.borrow(cs).borrow().as_ref().unwrap(); mutex.odr.modify(|_, w| w.odr1().set_bit());
- z3phyr 3y agoGod! Something about verbosity makes me nervous.
- sophacles 3y agoMy experience with that verbosity is: * its annoying to write, good tooling helps (e.g. using the rust-analyzer in an lsp enabled editor) * it's a godsend when revisiting code months/years later, and in reading unfamiliar code. Some languages have verbosity for it's own sake, but I don't really see rust as one of those - most of it actually inform the reader about what's going on without a need to go track down all sorts of info elsewhere to understand what effects any given line will have.
- capableweb 3y agoMY_GPIO.borrow(cs).borrow().as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit()); or let mutex = MY_GPIO.borrow(cs).borrow().as_ref().unwrap(); mutex.odr.modify(|_, w| w.odr1().set_bit()); Both of those ways are really horrible, and differ almost nothing. I understand and know why it looks like it does, but having to jump through all that mental gymnastics just to understand/write one line is why Rust is just overly verbose for a lot of things.
- FrustratedMonky 3y agoIs that one line more verbose than adding multiple lines of passing information through all the functions to provide all the same features? Probably not clear, but all of those functions are doing something that is helpful in additional checks. As others pointed out, you can set a pointer if you like in a simple line, but that isn't safe. The verbosity is providing the features, at least it is on one line, instead of 20 or 100 lines.
- biorach 3y ago> MY_GPIO.borrow(cs).borrow().as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit()); > I tried. I really tried. You gave up too soon. That's not particularly gnarly rust. It's just chained method calls like in many other languages. Admittedly the `borrow().borrow.as_ref().unwrap()` thing looks a bit excessive - does the book not explain it? Overall the complexity here isn't the language - it's due to the implementation of the access to the GPIO bits.
- giancarlostoro 3y ago> That's not particularly gnarly rust. It's just chained method calls like in many other languages. That was my thought as well, as someone who is not Rust heavy (though I do small projects with it now and then), I get some of what's going on, and I've done a little bit of embedded, I could be misremembering but I don't remember it feeling this confusing or intimidating and that was using C, which to newcomers can be both. I'm really confused by this method chaining, I would argue its too confusing and breaking it up with comments might be best for newcomers.
- crest 3y agoIf that scares you off you must have been spared using the STM32 HAL so far. Have you considered splitting the long line at `MY_GPIO.borrow(cs).borrow().as_ref().unwrap().odr` (assign it to a local variable)?
- ostenning 3y agoI think the embedded book kind of teaches at a very fundamental level, prioritizing safety. It teaches you to use a mutex and critical section when accessing a global variable, which can easily obscure what you are trying to achieve. Globals should be avoided in Rust, unless you have some kind of synchronization strategy because otherwise modifying them is unsafe.
- foooorsyth 3y agoThe Rust apologists replying to this comment are conflating mere understanding of the above chain with the reality of needing to type this slog every time you need to set a pin. When you’re just making a garage door opener in your home electronics lab and a C API offers setBit(&gpioCtx, bitPosition, 1); as an alternative to the above abomination, we all know where developers will be happier and more productive. >oh but you could just use a macro! Or I could just use C and be productive, thanks.
- biorach 3y ago> a C API offers setBit(&gpioCtx, bitPosition, 1) You can almost certainly do the same in Rust in an unsafe block. You will be happier and more productive unless you get bitten by the lack of locking, reference counting and mandatory initialisation. Which is actually quite likely.
- foooorsyth 3y ago> locking, reference counting and mandatory initialisation Don’t need any of this crap for a garage door opener. Most firmware is incredibly simple. pthread_mutex_lock(&mutex); setBit(&gpioCtx, bitPosition, 1); pthread_mutex_unlock(&mutex); Compiled with -Wall is fine and infinitely more readable that the Rust puke above, but you probably won’t need the lock or the warnings in your typical embedded project. The main issue with Rust appears to be that it invites masturbators that invent problem scenarios in their head and then overengineer every std lib API and crate available for consumption. Setting a bit on a dev board should simple. It should not be a chained mess full of operator soup.
- kbelder 3y agoPeople should be far more willing to use unsafe than they currently are.
- gaganyaan 3y agoYou can also write it more simply in Rust as other responses showed, there's no need for the flame war.
- stephen_g 3y agoI have to say I'm excited to see what the Swift embedded subset turns out like, if it gets any hardware support for chips I write code for (mostly stm32 so that seems pretty likely since they're so popular). It looks like a nicer developer experience than what I've seen from Rust, but this is coming from somebody who mostly codes C.
- rich_sasha 3y agoThat's horrible, indeed. But C++ has its own verbosity, just elsewhere. I will never miss writing header files and oh so many constructors.
- gaganyaan 3y agoYou should try again. I think that code is verbose because of the borrowing and because you're trying to do a one-liner. I use the nrf-hal library with the nrf52840, and the code reads pretty nicely. Here's an example: https://github.com/nrf-rs/nrf-hal/blob/master/examples/blinky-button-demo/src/main.rs https://github.com/nrf-rs/nrf-hal/blob/master/examples/blink...
- sophacles 3y agoIt reads like there's a bug in that example: loop { if button.is_high().unwrap() { led.set_high().unwrap(); } else { led.set_low().unwrap(); } } Wouldn't you want to call set_low when is_high is true? Also, woudn't the loop need a sleep to make the blinking be observable? this looks like it will be a roughly 50% pwm.
- gaganyaan 3y agoIt's checking to see if the button is high, not the led. "blinky" is a slightly misleading name. It doesn't cycle the led automatically, it turns the led on when you press a button It is a busy loop though, and in a real scenario you'd want to use interrupts instead of a loop like this.
- sophacles 3y agoOh wow, I read "button" as "blinker" in that code... Thanks for explaining it!
- SAI_Peregrinus 3y agoHuh? Usually (using any of the `embedded_hal` crates) it'll be something like `let mut gpioa = dp.GPIOA.split();` to get the GPIO A port from the Device Peripherals list `dp`. `let pa5 = gpioa.pa5.into_push_pull_output();` to declare which pin in that GPIO port you're using, and set it into a push/pull output (input pull up, input pull down, input floating, and output open drain are of course also defined) and then `pa5.set_high();` to set it high (`pa5.set_low()` for low). There are HAL modules for most common MCUs, and a few uncommon MCUs. Use the HAL. The official embedded Rust book also includes how to write such a HAL, which seems to be the bit you stumbled on. Ignore it if you're not writing a new HAL crate.