Overly verbose abstractions in Embedded Rust

Hi,

first of all, a disclaimer: I'm new to Rust, I haven't actually used Rust for embedded development yet, but I'm learning Rust already.

I have seen some very verbose abstractions used in embedded Rust-related blog posts, tutorials, etc. They are so verbose that it is actually quite difficult to understand what the code is doing at a glance, at least for me, coming from a C background.

For example, I saw this code in this blog post shared in this edition of TWIR (I am in no way or form attacking the author of this post, it's just the most recent place where I saw this kind of code).

/* Disables all wdt stages & the global watchdog flag itself */
rtccntl.wdtconfig0.modify(|_, w| unsafe {
    w
    .wdt_stg0()
    .bits(0x0)
    .wdt_stg1()
    .bits(0x0)
    .wdt_stg2()
    .bits(0x0)
    .wdt_stg3()
    .bits(0x0)
    .wdt_flashboot_mod_en()
    .clear_bit()
    .wdt_en()
    .clear_bit()
});

Why isn't this just rtccntl.wdtconfig0 = 0x00 (or whatever the code is doing with the bits - I didn't really took the time to understand what's going on).

Is this verbose-kind of abstraction the best we can do in Embedded Rust?

Excuse my rant, but it's a bit worrying that single-line-of-code bit-masking operations are exploding to 14 lines of code.

As an old embedded C hand I feel you pain.

Whilst that is a massively verbose way to set some bits I would argue that with rtccntl.wdtconfig0 = 0x00 it is also quite difficult to understand what is going on. If one doesn't know the bit layout of that register you have no clue what writing 0x00 does.

At least the verbosity of Rust, which shocks us C programmers at first, is no worse than the comments one would have to have in the C code to make it clear. Whist at the same time ensuring that what it is saying is what it is doing. Unlike many comments I have seen over the years which get out of sync with code changes, if they were ever correct in the first place.

I will let Rust gurus expound on the reasoning behind all this but to my simple mind each one of those bits is a different thing. It has a meaning of it's own independent of the others. Looked at that way it makes a lot of sense to treat them separately in our source as in your example. Rather that blating all over them at the same time with an anonymous zero.

This is often the case with bit fields in registers. Looked at this way the verbosity starts make a lot of sense.

I'm learning not to worry about lines of code in cases like this. As I said in C one might have a single short line to do all that. A very cryptic line at that. Typically coding standards for the projects I have worked on demanded that such things be commented well. In the case of the example, that would likely be 8 lines of comments. Which are likely to contain mistakes to mislead future readers.

Worse still, we would be expected to use #defines to identify all the bits and fields and use boolean operators to mix them up. Blech!

As long as it all compiles down to the same thing all is well.

1 Like

I won't defend C practices because they are definitely not the best, but I don't think this is the way to go either.

I also fail to see how this coding style helps the programmer at all to understand what's going on. Consider this code:

rtccntl.wdtconfig0.modify(|_, w| unsafe {
    w
    .wdt_stg0()
    .bits(0x1)
    .wdt_stg1()
    .bits(0x1)
    .wdt_stg2()
    .bits(0x1)
    .wdt_stg3()
    .bits(0x1)
    .wdt_en()
    .set_bit()
});

Without any comments, I am as lost as I would be with something like rtccntl.wdtconfig0 = WDTCONFIG_ENABLE_ALL_STAGES | WDTCONFIG_WDT_EN; .

Notice also my omision of

    .wdt_flashboot_mod_en()
    .clear_bit()

What happens if I omit it? Without going to the documentation of modify() I do not know if this bit is kept as it was or if it will be cleared due to my omission of that bit.

I also want to note that in the same code in the same blog post, the author writes to another register like this, apparently (and effectively) just doing rtccntl.wdtwprotect = 0x0 but under some layer of abstraction, where the author doesn't go clearing bit by bit:

/* Re-enables write protection */
rtccntl.wdtwprotect.write(|w| unsafe { w.bits(0x0) });

I find this more reasonable but inconsistent (also obscure but ok).

I also just saw this in the same code:

/* Disables write protection */
rtccntl.wdtwprotect.write(|w| unsafe { w.bits(WDT_WKEY_VALUE) });

showing that Embedded Rust is also not free of #define -like obscure definitions, just like C.

This is an abstraction called 'Peripheral Access Crate'. It is usually directly created by svd2rust. It creates a abstraction over the svd file given by the manufacturer.
If this svd file is a good one, then the manufacturer explicitly listens all the allowed inputs for a register. We can then f.e. enable GPIO22 by typing something like device.GPIO.gpio_set_enable.write(|w| w.enable().gpio22()) instead of device.GPIO.gpio_set_enable.write(|w| unsafe { w.bits(1 << 22) }).
We therefore have a safe abstraction of all possible inputs which are allowed in this register. We can therefore be sure, that only the register for GPIO22 is enabled.
If we had no abstraction, we would write something like:

const GPIO_ENABLE: *mut u32 = 0x4000_0000 as *mut u32;
unsafe {
    let temp = ptr::read_volatile(GPIO_ENABLE);
    ptr::write_volatile(GPIO_ENABLE, temp | 1 << 22);
}

which is clearly unsafe, since we access a raw memory address with a "random" value.

The PAC crate is normally used to then create a HAL, which is then human-usable, f.e.

let mut led1: P22<gpio::Output<PushPull>> = device.port.p22.into_push_pull_output(Level::High);

Which then turns the Pin 22 into a Push-Pull output, set on high. Further adjustment to the level are then a lot simpler:

led1.set_low().unwrap();

For more information, take a look at the rust-embedded books:
https://docs.rust-embedded.org/

Especially, if you are already with embedded development, take a look at the Embedded Book (not Discovery), where the concept is explained better.

Also looking at some source code can help you:
PAC of STM32f0: https://crates.io/crates/stm32f0
HAL of STM32f0: https://crates.io/crates/stm32f0xx-hal

You can take a look at the docs, the source code and example of boths, to see how the HAL creates a usable abstraction of the PAC.
There is a curated list of all embedded-implementations:

P.S: Rust makes it hard, to use raw memory access. It therefore tries to get us to create a (memory) secure abstraction as quickly as possible.

7 Likes

Inconsistent, or maybe, showing flexibility of Rust (and svd2rust).
I prefer the verbose approach, but I know it can take some time to get used to it.