The OP asked for a solution that is no_std - luckily, read_volatile is actually defined in core::ptr, and the version in std is just a re-export: read_volatile in core::ptr - Rust
There is also the volatile crate, which is a slightly nicer wrapper around core::ptr functions.
This uses a wrapper struct to avoid creating a &UartMmapRegs as &self when calling the method, and uses addr_of! to convert the struct ptr into a field ptr without an intermediate reference.
For this kind of problem, I use volatile-register. It transparently wraps a primitive type, makes access volatile and lets you define access permissions (read only, write only or read write).
Guessing a bit on what permissions you need, something like this:
Regarding that crate, I would like to be clear that volatile_register is in the "works on the compiler today, but the language does not promise that this will continue to work" area, whereas the solution I posted with raw pointers is in the "if this doesn't work, then there's a bug in the compiler" area.
It's because the mere existence of a reference makes promises to the compiler that don't necessarily hold for volatile memory. For example, it promises that its ok for the compiler to insert extra reads to the memory, which could have side effects on some types of volatile memory. My snippet avoids that by using raw pointers instead of references.
Excerpt of code from volatile-register and its dependency vcell
use core::cell::UnsafeCell;
use core::ptr;
/// from crate vcell
#[repr(transparent)]
pub struct VolatileCell<T> {
value: UnsafeCell<T>,
}
impl<T> VolatileCell<T> {
/// Creates a new `VolatileCell` containing the given value
pub const fn new(value: T) -> Self {
VolatileCell {
value: UnsafeCell::new(value),
}
}
/// Returns a copy of the contained value
#[inline(always)]
pub fn get(&self) -> T
where
T: Copy,
{
unsafe { ptr::read_volatile(self.value.get()) }
}
}
/// from crate volatile-register
/// Read-Only register
pub struct RO<T>
where
T: Copy,
{
register: VolatileCell<T>,
}
impl<T> RO<T>
where
T: Copy,
{
/// Reads the value of the register
#[inline(always)]
pub fn read(&self) -> T {
self.register.get()
}
}
In volatile-register, the register is stored in an UnsafeCell and the read/write accesses go through raw pointers. A (direct) reference to the inner register is never created.
What is created is a &UnsafeCell. As far as I understand, before #98017 was merged the compiler was marking the pointer inside the UnsafeCell as dereferenceable, which let LLVM insert spurious read/write accesses as you write. But after that PR was merged (to solve Arc::drop has a (potentially) dangling shared ref #55005 among other things), that is no longer the case.
So volatile-register (and vcell) should now be expected to just work fine... Or am I missing something?
Right, this kind of thing is a valid argument for "works on the compiler today", but it cannot be used to argue that we're in the "if this doesn't work, then there's a bug in the compiler" area.
To make that kind of argument, we should look at what the compiler actually promises, rather than at how the promise is implemented. Here is the relevant documentation:
For both &T without UnsafeCell<_> and &mut T, you must also not deallocate the data until the reference expires. As a special exception, given an &T, any part of it that is inside an UnsafeCell<_> may be deallocated during the lifetime of the reference, after the last time the reference is used (dereferenced or reborrowed). Since you cannot deallocate a part of what a reference points to, this means the memory an &T points to can be deallocted only if every part of it (including padding) is inside an UnsafeCell.
However, whenever a &UnsafeCell<T> is constructed or dereferenced, it must still point to live memory and the compiler is allowed to insert spurious reads if it can prove that this memory has not yet been deallocated.
The "special exception" in this snippet is the reason that &UnsafeCell can't be dereferenceable. However, if you read the last part of this paragraph, you will see that the compiler reserves the right to insert spurious reads even for &UnsafeCell.