Register Types for bare metal/embedded?

I was going through the STM32F3 Discovery board tutorial, embedonomicon and learning Rust at the same time, and was thinking that perhaps a Register Type might be a useful idea.

  • Imagine an unsigned int where we specify the memory address, not the compiler/linker.
    let DBGMCU_IDCODE: r32 @ 0xE004_2000;

  • This would put the register type under the standard borrow and safety checking rules. unsafe {} no longer needed.

  • The register type can inherit all the methods and traits as its unsigned int cousin.

  • r8, r16, r32, r64 type names.

  • Directly interoperable with unsigned ints of same size.
    let myInt32: u32 = myRegister32;

  • Store addresses of functions in registers (or specific memory locations) for interrupts and call backs.

  • Store addresses of buffers for DMA, IO, encryption and math operations.

  • Avoid a lot of confusing C-like pointer messiness.

It would be different from the unsigned int in these ways:

  • No heap or stack allocation. (except meta data?)
  • Volatile [in the sense that a A/D register will always read a different value.]
  • Never initialized, unless explicitly initialized.
  • Never set to a value when dropped.
  • Never optimized out, or cached.

If registers are possible, then structs can abstract groups of registers.

rstruct! GPIOx {
    MODER: r32 @ BASE,
    OTYPER: r32 @ BASE + 4,
    OSPEEDR: r32 @ BASE + 8,
    ...
    BRR: r32 @ BASE + 28,
}

let mut gpioA: GPIOx @ 0x4800_0000 {
    // Nothing needed here, the address
    // given will become 'BASE' to set
    // the register addresses above
};

And large arrays of register types could be abstracted to buffers:

let mut framebuffer: [r32 @ 0xC000_0000; 480 * 240]; // 480*240*32bpp

--

In short, we could move a lot of unsafe code into the safe realm with some register types.

--

If we take the example from

https://rust-embedded.github.io/bookshelf/discovery/07-registers/index.html

We could change all the
*(GPIOE_BSRR as *mut u32)

into just:

GPIOE_BSRR

We can drop unsafe{}, AND be under the safety/borrow checker:

  ...
fn main() -> ! {
  aux7::init();
  
  let mut PIOE_BSRR: r32 @ 0x48001018;

  GPIOE_BSRR = 1 << 9;
  GPIOE_BSRR = 1 << 11;
  GPIOE_BSRR = 1 << (9 + 16);
  GPIOE_BSRR = 1 << (11 + 16);

  loop { }
}

Anyway, just an idea. I'm new to Rust and haven't enjoyed learning a language this much in a LOOONG time.

1 Like

Note: I'm not working with embedded environments.

The embedded Rust working group, lead by @japaric, has built an extensive set of abstractions/paradigms already. Check out their (landing?) page here: https://github.com/rust-embedded/wg

I remember reading a blog post about abstracting GPIO in combination with the type system similar to what you describe. I cannot recall a title or link but discovered it through Hacker News.
What you're trying to achieve is possible to an extent, in my opinion.

I understand that you're trying to poll the community to become aware of already existing efforts or approaches. Since there aren't any replies at the moment I suggest to pose more specific questions on Discord rust-lang#wg-embedded or on IRC Moznet#rust, info here: Community - Rust Programming Language

1 Like

I have worked with some "state of the art" bindings generated svd2rust and crates using embedded-hal on top of that. The way I see it - the idea is nice, but too lowlevel.

What your proposal would do for the user is to ensure the proper access (write_volatile that cannot be optimized away). But this basically gives safe code access to arbitrary memory locations, which is definitely not safe. So at least construction of the register has to be unsafe.

In any case, what we want from an idiomatic binding has to be far more ambitious: memory-mapped IO is inherently completely untyped, and that makes writing the wrong bit pattern unsafe, even if the location is ok to write to. Access in terms of bit manipulation, as shown in your last code block, is only the very lowest level of abstraction. One level higher than that is bindings to the hardware generated from SVD files, which already abstract this away, and give you a much safer interface to individual registers and their fields.

If two functions or even two different crates declare the same address, the borrow checker won't be able to see it at distance. If other threads, another process or the hardware itself can change value of that address, then such reference would also violate Rust's aliasing rules. So it seems it is unsafe by Rust's definition.

It could behave like Atomic type, and that kind of abstraction is already supported by the language, and you can write your AtomicAtAddress(0x48001018) object today.

Rust has volatile read function, which IMHO is much better than C's concept of volatile. With such pointers you usually want to know exactly when and where they are read or written, so the language/syntax making them look like regular variables that can be used in expressions at will turns out to be counter-productive. You often want let tmp = *volatile_ptr, which can as well be a function/method call rather than mysterious dereference.

3 Likes

OK, well, thanks for the informative feedback. It gave me a bit of a pause to see that accessing registers was more like C than Rust's approach to other primitives.

Anyway, I'll continue to learn Rust's embedded infrastructure, with hopes that Rust becomes a standard embedded language one day soon... :slight_smile:

If two functions or even two different crates declare the same address, the borrow checker won’t be able to see it at distance. If other threads, another process or the hardware itself can change value of that address, then such reference would also violate Rust’s aliasing rules. So it seems it is unsafe by Rust’s definition.

Yeah, I was thinking rustc could track addresses to ensure no conflicts. It would just be a arch-sized int anyway. It would need to store this meta data anyway to pass to llvm, right?

I'm not aware of hardware changing it's own register addresses? Does this happen?

Did not know about AtomicAtAddress, thanks, will take a look.

The AtomicAtAddress was pseudo code. It doesn't exist, but you can make it yourself.

You can cast any integer to a pointer type. I think you may even be able to cast to a pointer to the existing AtomicUsize type, and use that. Or make one with the existing UnsafeCell type.

Hardware may "magically" change content of memory, e.g. via timers or buttons mapped to memory, or DMA. Rust needs to know when that is possible, so that it doesn't optimize out "unnecessary" reads.

IIRC, some older 8-bit and 16-bit microcontroller SOCs had small address spaces where the high-order address bits for a peripheral register block were configurable. That would be a case of "hardware changing it’s own register addresses."

Modern implementation considerations make such a feature less likely. I don't know whether any current SOCs employ such runtime address comparators.

Wow, that's interesting. I was thinking device/bus hot plugging or something.

Thanks for the feedback. clearly not as easy as I had imagined. But thanks for entertaining the idea nevertheless. :slight_smile: