Hacker News new | ask | show | jobs
by oxidizedcutrace 1643 days ago
Lots of people pointing out that Rust isn't very ergonomic on embedded platforms. IMO one huge issue is how verbose the syntax needs to be.

Look at some of the cases in the article, such as:

    let mut dp = pac::Peripherals::take().unwrap();
    dp.RCC.ahb2enr.modify(|_, w| w.gpioaen().set_bit());
In C that would be:

    RCC->AHBENR2 |= RCC_AHBENR2_GPIOAEN;
Okay, you only have to take ownership of the Peripherals object once, but what about when they want to set a UART register in a function? Rust wants mutable pointers to have a trail of ownership outside of 'unsafe' blocks, so now we need:

    pub fn setup_transmission(regs: &mut R, [...])
    where R: Deref<Target = pac::usart1::RegisterBlock> {
        [...]
        regs.cr2.modify(|_, w| w.stop().bits(config.stop_bits as u8));
        [...]
    }
This is a bit of a construction, in C(++) we can just do something like:

    void setup_transmission([...]) {
        [...]
        USART1->CR2 &= ~(USART_CR2_STOP_BITS);
        USART1->CR2 |= ((uint8_t)config.stop_bits << USART_CR2_STOP_BITS_Pos);
        [...]
    }
The Rust snippet seems like it would be safer in a multi-threaded environment like an RTOS, or in cases where interrupts might modify peripheral registers, but...there are so many syntactical details to remember.

I really tried to like embedded Rust, and I still think it compares favorably with the sort of C HALs that most chip manufacturers distribute. But the syntax seems to get in the way of quick simple access to peripheral registers.

5 comments

I'm not familiar with embedded programming but I don't think this is completely a fair comparison since the Rust implementation is doing error checking, so your C code is missing either some sort of results checking or a macro. The Rust code is also probably presenting some sort of safe API. It's worth mentioning that the C code could most likely be done in Rust with unsafe.
I went through a good bit of the Rust Embedded book, and your assessment is right on the money.

As a long time C programmer who occasionally makes mistakes, I find the type safety of svd2rust-generated APIs incredibly gratifying. When the embedded documentation is inscrutable, the type-safe Rust API prevents you from making whole classes of nasty errors. Especially since the embedded device will probably either fail silently or misbehave in bizarre ways if you mess up.

There may be more elegant ways to design the type-safe API but even having to deal with the verbose syntax it was way easier than stumbling around in C.

The API generated by svd2rust isn't ideal. There's been lots of discussion about ways to improve it, but unfortunately not all that much experimentation of alternate models. So the ubiquitous option generally wins, in a worse-is-better way.

The goal is exactly right: leverage Rust's expressive type system to keep your programs safely composable -- it's a drag trying to debug an issue caused by allocating one peripheral to two different systems. Particularly since the conflicts between peripherals on different models of the same family are sometimes hard to recognize.

Why is it this way though - it seems to me like you could create a type that just works like the C code

  struct Uart {
    u8 CR2,
    // etc.
  }
Instantiate it as 'static with some macro that defines where it lives in memory:

  #[mcu::loc(0x1002010)] static UART1 = Uart::new();
  #[mcu::loc(0x1003010)] static UART2 = Uart::new();

And then have it bound directly to the memory locations on the MCU at compile time/link time.

Still learning rust, so apologies if this syntax doesn't make sense.

Yes, the syntax doesn't have to be this way, it's just the way that the libraries in the ecosystem tend to do it. It works, but it does also have quite a few problems, imho. But since it's just a particular library, you can write your own or use a different one.
Good question! I'm also not a Rust expert, but maybe that syntax would be considered unsafe in some situations?

I think the 'peripheral access crates' try to auto-generate those sorts of struct objects from SVD files, but you still need to obey Rust's ownership rules to get the advantage of its built-in memory safety.

So if a function needs to modify a peripheral's memory, it needs to claim mutable ownership of the relevant peripheral registers. That's a feature of the language which makes it 'safer', but the cost/benefit calculation is a bit different on embedded platforms. They have less memory, and static allocation is usually preferred.

Still, microcontrollers are getting faster quickly. You can even run Linux on some cortex-M4/M7 chips. The verbose syntax might be worthwhile if you're collaborating on complicated firmware.

I'm not sure I agree on the safety aspect - if they only thing preventing unintentional peripheral access is verbosity, that's not really the kind of security that matters, is it?
From my understanding, Rust's HAL APIs are not trying to optimize for ergonomics but for type safety and ownership. It's more tedious to write, but it's also nice to have the standard formatted Rust docs available to see all registers and what you can do with them. It means things like hardware peripherals which only work on certain pins, have those pins defined in the _type system_.

It's more verbose for sure, but I prefer it for it's explicitness and ownership tracking. Definitely not perfect, but it's also very early times for Rust on embedded devices.

Even better! I imagine the ergonomics will improve over time without sacrificing type safety.
In a small embedded system with a 0.1$ single core processor you wouldn't need many of the safety features anyway. Your interrupt routine to read new symbols and your loop processing the content will never collide.

This is the lower end of embedded systems, but the vast majority of them and those don't have any concurrency. Didn't try Rust here, but I would assume you have to jump through some hoops to achieve simple register access or declaring whole blocks as unsafe.