Expand description
Derive volatile accesses to a register map and memory-mapped IO.
The main entry point of this crate is the derive macro RegMap
, that generates a new pointer
type to a defined register map.
Table of contents
- Basic usage
- Register types
- Access permissions
- Type layout and representation
- Thread safety
- Principle of operation
- Comparison with other crates
- Further reading
§Basic usage
// define struct Registers with the register map
// and derive the pointer RegistersPtr using the RegMap macro
#[repr(C)]
#[derive(RegMap, Default)]
struct Registers {
field1: u64,
field2: u32,
#[reg(RO)]
read_only_field: i8,
#[reg(WO)]
write_only_field: u128,
#[reg(RW)]
read_write_is_default: i16,
}
// initialize the base struct
// and obtain a pointer to the registers
let mut regs = Registers::default();
let ptr = RegistersPtr::from_mut(&mut regs);
// when dealing with e.g. memory-mapped IO (MMIO),
// you'd probably just get a pointer to the data from a known base address
// let ptr = unsafe { RegistersPtr::from_ptr(0xAA55_000 as *mut _) };
// all write() operations are volatile
ptr.field1().write(10);
ptr.field2().write(32);
ptr.write_only_field().write(76);
ptr.read_write_is_default().write(98);
// all read() operations are volatile
assert_eq!(ptr.field1().read(), 10);
assert_eq!(ptr.field2().read(), 32);
assert_eq!(ptr.read_only_field().read(), 0);
assert_eq!(ptr.read_write_is_default().read(), 98);
Read/write permissions are checked at compile time. The following code does not compile:
ptr.read_only_field().write(54); // error[E0277]: cannot write to a read-only register
ptr.write_only_field().read(); // error[E0277]: cannot read from a write-only register
§Register types
§Basic registers
These primitive integer types are supported as basic register types:
The pointer-sized integer types usize
and isize
are not supported.
For a register map containing a basic register:
#[derive(RegMap, Default)]
#[repr(C)]
struct Basic {
field: u64,
}
The RegMap
derive macro will generate the following abridged code:
struct BasicPtr<'a> { ... };
impl<'a> BasicPtr<'a> {
fn field(&self) -> Reg<'a, u64, ReadWrite> { ... }
}
where the read/write operations on the register are performed through the Reg
type, and the
access permissions default to both read and write.
§Nested register maps
Register-map definitions can be nested arbitrarily:
#[derive(RegMap)]
#[repr(C)]
struct Outer {
outer: u64,
inner: Basic,
}
will generate pointer types with the following abridged code:
struct OuterPtr<'a> { ... };
impl<'a> OuterPtr<'a> {
fn outer(&self) -> Reg<'a, u64, ReadWrite> { ... }
fn inner(&self) -> BasicPtr<'a> { ... }
}
where Basic
and BasicPtr
are shown in the previous section.
§Arrays of registers
Fixed-size arrays of registers are also supported, with both basic and nested registers.
#[derive(RegMap, Default)]
#[repr(C)]
struct Many {
basic: [u64; 32],
nested: [Basic; 16],
}
generates the following abridged code:
struct ManyPtr<'a> { ... };
impl<'a> ManyPtr<'a> {
fn basic(&self) -> RegArray<'a, Reg<'a, u64, ReadWrite>, 32> { ... }
fn nested(&self) -> RegArray<'a, BasicPtr<'a>, 16> { ... }
}
where the access to the arrays of registers are provided by the RegArray
type.
Multidimensional arrays are also supported:
#[derive(RegMap)]
#[repr(C)]
struct MultiD {
basic: [[[[u64; 2]; 3]; 5]; 7],
nested: [[[[Basic; 7]; 5]; 3]; 2],
}
§Iterators
It is possible to iterate through arrays using the methods RegArray::iter
and
RegArray::iter_slice
:
let mut reg = Many::default();
let ptr = ManyPtr::from_mut(&mut reg);
for (i, basic) in ptr.basic().iter().enumerate() {
basic.write(i as u64);
}
for (i, basic) in ptr.basic().iter().enumerate() {
assert_eq!(basic.read(), i as u64);
}
for (j, nested) in ptr.nested().iter_slice(2, 7).rev().enumerate() {
nested.field().write(j as u64);
}
for (j, nested) in ptr.nested().iter().enumerate() {
let expected = if (2..7).contains(&j) {
6 - j
} else {
0
};
assert_eq!(nested.field().read(), expected as u64);
}
§Access permissions
Access permissions for each register can be specified with the #[reg()]
attribute, and
default to read-write if not specified:
#[repr(C)]
#[derive(RegMap)]
struct Permissions {
#[reg(RO)] read_only_register: u64,
#[reg(WO)] write_only_register: u64,
#[reg(RW)] read_write_register: u64,
another_read_only_register: u64,
}
Access permission are implemented through the zero-sized structs:
ReadOnly
for read-only registers (#[reg(RO)]
attribute);WriteOnly
for write-only registers (#[reg(WO)]
attribute);ReadWrite
for read-write registers (#[reg(RW)]
attribute, or no attribute).
Access permission are checked at compile time, as the zero-sized structs above are passed as
type parameters to the generic types Reg
and RegArray
upon definition of the derived
pointer types. Specifically, the write
is just not defined for a read-only
register, and so on.
§Type layout and representation
The derive macro RegMap
requires the register-map struct
to have the C
representation
using the #[repr(C)]
attribute. Higher alignment requirements can be specified with the
#[repr(C, align(x))]
attribute. Other representations are not supported and generate a
compile-time error.
Example:
#[repr(C)]
#[derive(RegMap)]
struct Base {
foo: u32,
baz: u32,
aligned: Data,
}
#[repr(C, align(4096))]
#[derive(RegMap)]
struct Data {
data: [u64; 512],
}
In summary:
#[repr(C)]
: TheC
representation is required.- Default/
Rust
representation is not supported. #[repr(transparent)]
: Thetransparent
representation is not supported.#[repr(align(x))]
: Raising the alignment of the register map is supported, in combination with theC
representation.#[repr(packed)]
: Lowering the alignment of the register map is not supported. This is because unaligned reads and writes are not (currently) supported.
§Thread safety
All reads and writes performed through the pointers derived by RegMap
are volatile. However
in Rust, “just like in C, whether an operation is volatile has no bearing whatsoever on
questions involving concurrent access from multiple threads. Volatile accesses behave exactly
like non-atomic accesses in that regard.” See safety docs for
read_volatile
and
write_volatile
.
There is currently no way in Rust to define memory accesses as both volatile and atomic.
Therefore, the pointers derived by RegMap
are generally not thread safe and thus implement
neither Send
not Sync
.
That said, on some platforms and for some use cases, volatile access and relaxed atomic
accesses are the same. If you know that is the case, you can unsafe
ly implement Send
and
Sync
yourself:
#[repr(C)]
#[derive(RegMap)]
struct IPromiseThisIsThreadSafe {
data: u64,
}
// Safety: I did my homework and this is sound on
// my platform and for my use case. I promise!
unsafe impl Send for IPromiseThisIsThreadSafePtr<'static> {}
unsafe impl Sync for IPromiseThisIsThreadSafePtr<'static> {}
If something goes wrong, that’s on you! See also URLO: Volatile + relaxed atomic load/store.
§Principle of operation
The derive macro RegMap
takes as input the definition of a register map (a struct
), and
generates a custom pointer type that is a wrapper around a raw pointer to the original
struct
. This custom pointer provides methods to perform read / write volatile operations on
the fields of the register map.
Importantly, no references to the original register map need to ever be created. Instead, the
derive macro uses the original struct
definition to calculate the offsets needed for each
memory access. The memory accesses are always performed on raw pointers with volatile
semantics.
Avoiding creation of references to volatile memory is important to ensure soundness, as discussed e.g. in rust-lang/unsafe-code-guidelines#33 and rust-lang/unsafe-code-guidelines#411.
§Sample generated code
Some of the content in this section is considered implementation detail and is not subject to stability guarantees. Nonetheless, it might be useful to have a look at the macro-generated code to get a better understanding of the functionality of this crate.
A relatively-simple register-map definition:
#[repr(C)]
#[derive(RegMap)]
struct Test {
scalar_field: u64,
array_field: [u64; 4096],
}
generates the following code (comments and docs omitted):
#[repr(C)]
struct Test {
scalar_field: u64,
array_field: [u64; 4096],
}
#[allow(non_snake_case)]
mod _mod_Test {
use super::*;
pub(super) struct TestPtr<'a> {
ptr: ::core::ptr::NonNull<Test>,
_ref: ::core::marker::PhantomData<&'a Test>,
}
impl<'a> TestPtr<'a> {
#[inline]
const unsafe fn from_nonnull(ptr: ::core::ptr::NonNull<Test>) -> Self {
Self {
ptr,
_ref: ::core::marker::PhantomData,
}
}
#[inline]
pub const unsafe fn from_ptr(ptr: *mut Test) -> Self {
Self::from_nonnull(::core::ptr::NonNull::new_unchecked(ptr))
}
#[inline]
pub fn from_mut(reg: &'a mut Test) -> Self {
unsafe { Self::from_ptr(reg) }
}
#[inline]
pub const fn as_ptr(&self) -> *mut Test {
self.ptr.as_ptr()
}
#[inline]
pub fn scalar_field(&self) -> ::reg_map::Reg<'a, u64, ::reg_map::access::ReadWrite> {
unsafe {
::reg_map::Reg::__MACRO_ONLY__from_ptr(::core::ptr::addr_of_mut!(
(*self.as_ptr()).scalar_field
))
}
}
#[inline]
pub fn array_field(
&self,
) -> ::reg_map::RegArray<'a, ::reg_map::Reg<'a, u64, ::reg_map::access::ReadWrite>, 4096>
{
unsafe {
::reg_map::RegArray::__MACRO_ONLY__from_ptr(::core::ptr::addr_of_mut!(
(*self.as_ptr()).array_field
))
}
}
}
unsafe impl<'a> ::reg_map::RegMapPtr<'a> for TestPtr<'a> {
type RegMap = Test;
#[inline]
unsafe fn from_nonnull(ptr: ::core::ptr::NonNull<Self::RegMap>) -> Self {
Self::from_nonnull(ptr)
}
#[inline]
unsafe fn from_ptr(ptr: *mut Self::RegMap) -> Self {
Self::from_ptr(ptr)
}
#[inline]
fn from_mut(reg: &'a mut Self::RegMap) -> Self {
Self::from_mut(reg)
}
#[inline]
fn as_ptr(&self) -> *mut Self::RegMap {
self.as_ptr()
}
}
}
use _mod_Test::TestPtr;
First of all, the derive macro generates a module _mod_Test
that contains the generated
pointer type TestPtr
. The reason to define the type inside of a module is to enforce that a
new pointer is only created through the pub
associated functions from_ptr
and from_mut
.
The defined TestPtr
type is then re-exported out of the module.
TestPtr
itself is just a wrapper around a NonNull
pointer, plus a
marker field to signal that it is semantically a &'a Test
.
A new TestPtr
can be safely constructed from a &mut Test
through TestPtr::from_mut
, or
unsafe
ly from a *mut Test
through TestPtr::from_ptr
. A raw pointer to the underlying data
can be obtained from a live TestPtr
with the method TestPtr::as_ptr
.
The juice of the generated code are the TestPtr::scalar_field
and TestPtr::array_field
methods, which use addr_of_mut!
to return a Reg
and a
RegArray
, respectively. These provide read / write volatile access without ever creating a
reference to the underlying data.
Finally, the generated code implements the RegMapPtr
trait on TestPtr
so that it can be
stored in a RegArray
, if needed.
§Comparison with other crates
§volatile
The crate volatile uses the same principle of operation as
this crate, reg-map
: a custom pointer type is defined to perform volatile read /write
operations to the underlying memory.
In fact, reg-map
is heavily inspired by volatile
! Differences are mainly ergonomic and in
the exposed API surface.
§volatile-register
The crate volatile-register, based on
vcell, exposes a very clean API by providing a wrapper type that
owns the data. In practice, it offers a VolatileCell
which is “just like
Cell
but with volatile read / write operations”.
Unfortunately, this approach is unsound. See rust-lang/unsafe-code-guidelines#33: What about: volatile accesses and memory-mapped IO
for details. Long story short, every time there is a &UnsafeCell<T>
, the compiler is allowed
to insert spurious reads and writes. That is fine for “normal memory”, but with volatile memory
and memory-mapped IO reads and writes have side effects so this problematic.
This crate, reg-map
, does not use UnsafeCell
and never creates references to the volatile
memory, avoiding the soundness issue above.
For the cases where the volatile-register
approach happens to work, the assembly generated by
the two approaches is identical.
§Further reading
Some links to relevant forum threads and GitHub issues:
- URLO: How to make an access volatile without std library?
- URLO: Volatile + relaxed atomic load/store
- URLO: Why are memory mapped registers implemented with interior mutability?
- rust-embedded/volatile-register#10: Usage of references is in conflict with use for MMIO
- rust-lang/unsafe-code-guidelines#33: What about: volatile accesses and memory-mapped IO
- rust-lang/unsafe-code-guidelines#411: Can we have VolatileCell
Modules§
- Helper types and traits to define read/write permissions on registers.
- Types that can be placed into a
Reg
.
Structs§
- A pointer to a register with volatile reads and writes.
- An array of registers.
Traits§
Derive Macros§
- Derive macro to generate a pointer to a register map with volatile reads and writes.