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.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.> 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/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/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...
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());
* 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.
MY_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.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.
If you rewrite the code above in a proper C, you will have a page of code at least.
You and other Rust enthusiasts are missing the point.
In C or any C-based language or even Python, you can just call either a function passing the reference to the GPIO pointer, or just GPIO |= 1.
You can't do that in Rust, or no one has shown a much better way to set a bit in Rust without writing verbose code that has dozens of borrowings done.
Your reply amounts to “I don’t wanna tho”.
OP picked upon a sophisticated example that's multithreaded; the example shows off some fancy Rust techniques which deals with this complex case.
It's 'fair' to say that OP's code snippet looks complex. But, it's not 'fair' to treat OP's snippet as if it's how one must write a simple blinky program.
And it's good to explain that the code is complex because it's dealing with a complex case.
I'd somewhat acknowledge the frustration, though. If you're trying to learn something and you take a wrong step or look in the wrong place, you don't have the intuition to figure out "here's what you want to be doing instead".
This specific example is from the concurrency chapter so it's already handling multiple threads and that's why all those checks are needed.
This is a random example I found to blink a led on a rp2040:
> let mut led_pin = pins.gpio25.into_push_pull_output();
>
> loop {
> led_pin.set_high().unwrap();
> delay.delay_ms(500);
> led_pin.set_low().unwrap();
> delay.delay_ms(500);
> }
And this is how to do it using embassy, which is an async framework for embedded in rust:
https://github.com/embassy-rs/embassy/blob/main/examples/rp/...
How can we make code be portable (e.g node.js like) between embedded and other platforms if they differ so much?
By embedded here we are talking about running bare metal on single core microcontrollers with kilobytes of memory and megabytes of storage which are more interested in driving or reading gpio/adc pins than serving a rest api or rendering a webpage
To answer your question, you can't because the difference in code comes from huge differences in the use case and requirements. And trying to force using the same runtime for anything from webservers with dozens of processors and hundreds of gigabytes of ram to the kind of device that barely draws any power at all is going to make no one happy
I only have a passing knowledge of embedded programming but it already seems to me that being able to effectively use this kind of abstractions in this domain is already halfway through a miracle, and probably only possible because the rust team decided to decouple the async model from a specific runtime
*GPIO |= 1;
Where GPIO is a pointer that's mapped to a register.There's almost certainly some Rust code doing just that somewhere down the call stack in your code above. You just don't get a lot of the safety benefits that come with Rust if you do it like that (although you do get some), so a lot of the library ecosystem has invested in higher level wrappers.
The motivation for doing so is good, but IMO it's been taken a little too far and led to things being over-abstracted. I suspect this will even itself out as embedded Rust matures.
// Set the SDA and SCL pins to input, and enable the internal pullups.
DDRC::clear_bits(DDRC::DDRC4 | DDRC::DDRC5);
PORTC::set_bits(PORTC::PORTC4 | PORTC::PORTC5);
// Initiliazing the prescaler to 1, and bit rate for a 400KHz transmission rate.
TWSR::clear_bits(TWSR::TWPS0 | TWSR::TWPS1);
TWBR::set_raw_value(TWI_BIT_RATE);
// Enable the TWI module, and the ACK.
TWCR::set_value(TWCR::TWEN | TWCR::TWEA);
While my representation doesn't protect against races or aliased access, I did manage to get it to protect against reading write-only bits, writing read-only bits, and using bits with the wrong register, all at compile time. The error messages were a bit unpleasant, but I was quite happy with how it turned out.I could have implemented the *Assign operators for the register structs, but I'm not a huge fan of those. I feel it's a bit too opaque and error-prone, and preferred the clear function names.
One thing that complicates matters is that you need to do volatile read and writes, not just standard pointer access. So using raw pointers, while possible, actually becomes kinda verbose.
As you mentioned and made clear, the implementations in other languages barely have any checks for memory safety.
In Rust, you can do that but you have to write a verbose code.
Why not just abstract this on the SDK level and provide a way so that if you call *GPIO |= 1; it will already do that for you? Or as others mentioned, make a macro?
I believe this is the point the author was trying to make. It's not about the niceties Rust have that make it complicated, it's the fact that you do have to write a whole lot more code for a simple task.
Please, there's no need to make this "us vs them" :) I explained what's happening without saying it's the best approach possible.
> In C or any C-based language or even Python, you can just call either a function passing the reference to the GPIO pointer, or just GPIO |= 1.
You can do this in Rust just fine by using raw pointers (references). The point is, in complex programs (not the example from the embedded rust book necessarily) it's possible to make memory safety mistakes. While in Rust it is not unless you choose so.
This is not about which approach is better, it's a matter of fact. Whether you prefer the one or the other is your personal choice.
Some people have written the equivalent C code for this (`*GPIO |= 1;`). Are the existing C embedded programmers typically not using refcounts / mutexes in their code since they know that they are upholding the invariants in their hardware and therefore do not have to employ these runtime checks, or are they just writing this one-liner out of laziness?
1.Superloop, for simpler stuff. No worries about atomic register writes, at most you have a few interrupts. You don't need mutexes here. The overhead will always be low but it gets very hairy quickly with the more things you try to do concurrently.
2. Rtos-based. Think microkernel but with just a thread scheduler. You might have a dedicated IO thread that handles messages from your worker thread, which may listen to a GUI thread. In this case, you will need a way to prevent multiple threads from trying to use the same resource, whether that is a pin, a configuration register, an interrupt flag that is in the same register as an exception flag and must be cleared atomically via a special shadow register, etc...
If you do all hardware touching through 1 thread you don't really need them, but otherwise, you absolutely need mutexes. And there can be a substantial performance cost to using them too much.
As an example, say you had a UART receive thread that fired from an ISR handler each time a byte was received. You could naively grab the UART mutex and read the new data, clear the flag, and deal with any error conditions, then release it again. Even a 100mhz MCU could have trouble keeping up with a trivial 100kbaud serial stream due to the overhead of using the mutex too much.
RTOSes do have other means for locking resources, but you would want to understand exactly what you're doing and minimize overhead as much as possible. I am not at all experienced in Rust but it seems that touching the hardware directly could end up being a large pain point.
> 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.
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.
https://github.com/nrf-rs/nrf-hal/blob/master/examples/blink...
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.
It is a busy loop though, and in a real scenario you'd want to use interrupts instead of a loop like this.
Thanks for explaining it!
Globals should be avoided in Rust, unless you have some kind of synchronization strategy because otherwise modifying them is unsafe.
`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.
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
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.
Now, something like setting up a DMA on one of the 2 available DMA engines you may have, and blocking til completion via interrupt, that is complex enough it has a much better justification for the abstraction salad.
Though to be honest I am sure someone will pop up with a much easier and cleaner way to do this.
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.
I will never miss writing header files and oh so many constructors.
Its pretty clear to me even now knowing rust embedded what is happening hear.
>oh but you could just use a macro!
Or I could just use C and be productive, thanks.
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.
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.But this is never done, because the language attracts people who overengineer. The original commenter was following a beginner’s tutorial in Embedded Rust. They didn’t just unluckily stumble upon something esoteric with too many layers of safety for the functionality they were aiming to achieve. This is how people in the Rust Foundation actually use their language, and it’s how they teach people to use it.
pthread_mutex_lock(&mutex);
setBit(&gpioCtx, bitPosition, 1);
pthread_mutex_unlock(&mutex);
It's not.POSIX threads are far more battle-proven than any threading API in Rust. All of the underlying multi-threading on the system you made your comment from is using pthreads. It works fine in reality, despite all the pearl clutching.
>Rust makes it so that you can't forget.
And the tradeoff is an unreadable, puke-inducing one-liner. You can do a (readable) scoped lock with a macro in C (GCC defines the __attribute__ cleanup) or RAII-style with destructors in C++.
You should probably see a doctor about your hyperemesis.
I'm not taking anything out of context. That code is hideous. It's terse. It uses controls and defenses that aren't needed for most embedded applications. It turned a beginner away from the language. When I provided much simpler C code as an alternative, Rust aficionados started to cry about unneeded mutexes, unneeded ref counting, the inability of programmers to remember to call free or mutex_unlock, etc. etc.
> But [writing it more simply] is never done
You're not saying anything interesting, you're just making grandiose false claims to start a flame war. Approach it from a good-faith perspective, please.
*MY_GPIO |= 1;
Or more complex?In C you would have to write all that out. And still have a good chance of getting something wrong first try.
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.
Your trivial one-liner is an example of terribly seductive simplicity enabled by a broken design. It works until MY_GPIO is concurrently modified. Which can happen if the memory mapped peripheral changes the register content on its own or if other code can preempt read/modify/write sequence. To make it worse unless MY_GPIO is heavily contested it will work most of the time.
I'm not familiar with embedded Rust, but it looks like APIs are designed to make the programmer explicitly lay claim hardware peripheral (or fail trying) instead of corrupting the system state. Most of the unwrap() and unsafe code is executed once during setup. Once the hardware resources are divided up and configured the normal runtime code should be a lot less verbose (from a quick look at the API documentation).
‘course, you don’t have monads.
Not that that's necessary, seeing as Rust is already procedural. You can just use regular old procedural syntax.