Why are memory mapped registers implemented with interior mutability?

The peripheral section of the Embedded Rust book instructs to rely on volatile_register to ensure that the single memory mapped words are subject to volatile access. volatile_register uses volatile cells underneath, representing the register word with a transparent UnsafeCell:

#[repr(transparent)]
pub struct VolatileCell<T> {
    value: UnsafeCell<T>,
}

I'm struggling to understand what advantages this approach brings over simply using the final word size as representing type:

#[repr(transparent)]
pub struct VolatileCell<T> {
    value: T,
}

From a memory layout standpoint, #[repr(transparent)] ensures there is no difference between the two. The UnsafeCell allows VolatileCell's contents to be mutated without the cell itself being mutable thus permitting the existence of (internally) mutable shared references. This however clashes with the tendency to only allow a single device through the singleton pattern (and by making devices !Sync).

It seems to me that the ability to share is first granted and then prohibited immediately after. What am I missing?

Volatility is not the same as shared mutability. It is also not the same as atomicity, transactionality, thread safety, etc.

Volatile reads and writes are necessary for MMIO because the compiler is, in general, allowed to optimize out loads and stores that it deems redundant. For example, the following may only generate a single store instead of two:

a = 1; // store 1: dead
a = 2; // store 2: reachable
use(a); // load

However, in the case of MMIO, this is obviously wrong, because stores have observable external side effects. You don't want "set this bit to 1 then after a second, set it to 0" to be "optimized" to "just set it to 0". This is what volatility ensures.

3 Likes

My question pertains the use of UnsafeCell, not the volatility. I understand why VolatileCelluses volatile accesses for the underlying address, I'm asking why an UnsafeCell is used instead of a simply register word type.

Having a reference to something implies a bunch of assumptions that are not true for volatile memory. UnsafeCell turns off some of them, so it improves the situation somewhat. But it's not really enough, and there isn't really any way to implement a correct VolatileCell today.

To be truly correct, you should never have a reference to volatile memory to begin with. Do everything with raw pointers and the addr_of! macro instead.

6 Likes

Actually let me add a bit more here. If you have an immutable reference to something, the compiler may assume that any two consecutive reads will read the same value. This is not necessarily true for volatile memory, as the hardware could change the value in between your two reads.

7 Likes

and for &mut the compiler assumes nothing else can change the value except explicit writes in your own code, so that also ignores changes to the volatile mem.

6 Likes

Having a reference to something implies a bunch of assumptions that are not true for volatile memory. UnsafeCell turns off some of them, so it improves the situation somewhat.

Can you give me some examples?

To be truly correct, you should never have a reference to volatile memory to begin with. Do everything with raw pointers and the addr_of! macro instead.

addr_of! returns a *const, how would I use that to mutate memory? I'm not sure if you're suggesting to avoid core::ptr::write_volatile and core::ptr::read_volatile entirely (in which case how would I tell the compiler not to optimize memory accesses?)

You'd use addr_of! for read-only, and addr_of_mut! for mutation, with core::ptr::write_volatile and core::ptr::read_volatile to do the accesses.

4 Likes

Here's an older post by alice with example code:

3 Likes

You can also use addr_of_mut!.

Sorry I was unclear. That's not what I meant. You can use those methods too. Point is, don't do anything that ever creates a reference to the volatile memory.

1 Like

I already mentioned the one that UnsafeCell turns off. But another one is that a reference allows the compiler to insert reads of the value you don't have in your program. This is ok because references can never be dangling, so reading from them shouldn't cause any harm. But for volatile memory, reading it can have side effects, so you don't want that.

3 Likes

I've had the time to explore the links that have been provided and now the situation is more clear.

My confusion stemmed from the fact that UnsafeCell is mostly advertised as a way to achieve interior mutability, while here it also grants more subtle - and, apparently, incomplete - guarantees about what the compiler is allowed to do with it.

If I understand correctly, using volatile accesses tells the compiler not to skip on any of the explicit accesses you place in your code (i.e. don't cache any read), but it may still decide to include additional accesses to references for whatever memory analysis it's doing. This is an issue, because memory mapped accesses - even just reads - can have side effects too.

1 Like

These are not separate things. In order to allow internal mutability some assumptions that the compiler makes have to be turned off, like the fact that it assumes that the data behind a & reference won't be mutated while that reference exists. That's the whole point of UnsafeCell: disable those optimizations so that internal mutability can be implemented in a sound way.

Using UnsafeCell to disable some unwanted optimizations/transformations when performing volatile access is a misuse of the feature since it was neither designed for that nor provides the correct guarantees for it (it won't disable all the unwanted transformations the compiler could do).

2 Likes