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());
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());
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.
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.
* 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.