Skip to content

Add methods for MMIO reads and writes #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4a3aa34
Add methods for volatile reads and writes.
qwandor Feb 18, 2025
1dd811d
Add method to split an array.
qwandor Feb 18, 2025
b17e0f9
Add macro to project a field of an OwnedMmioPointer.
qwandor Feb 18, 2025
aec4eb8
Add methods for OwnedMmioPointer slices.
qwandor Feb 18, 2025
8bb4b08
Use assembly for MMIO reads and writes on aarch64.
qwandor Feb 18, 2025
17324ec
Add assembly read/write methods for signed types too.
qwandor Feb 19, 2025
4069146
Read and write arbitrary types on aarch64 using zerocopy.
qwandor Feb 21, 2025
88b16a5
Rename OwnedMmioPointer to UniqueMmioPointer.
qwandor Feb 28, 2025
bf1156c
Add wrapper types for read-only, write-only or read-write fields.
qwandor Feb 28, 2025
69deb7a
Derive zerocopy traits for all wrapper types.
qwandor Mar 3, 2025
a57c490
Expose field of wrapper types.
qwandor Mar 3, 2025
ce0fc9b
Relax trait constraints.
qwandor Mar 3, 2025
f286952
Derive KnownLayout for wrapper types.
qwandor Mar 3, 2025
179f8a1
Clarify side-effects for reading and require &mut.
qwandor Mar 3, 2025
a0af0e4
Add SharedMmioPointer type.
qwandor Mar 3, 2025
9b8308b
Add wrapper types for pure MMIO reads without side-effects.
qwandor Mar 3, 2025
df56ab7
Move wrapper types to a submodule.
qwandor Mar 3, 2025
fe1889a
Test without temporary variable.
qwandor Mar 5, 2025
f080842
Derive Default for wrapper structs.
qwandor Mar 6, 2025
1c42137
Implement Debug, Eq and PartialEq rather than deriving.
qwandor Mar 6, 2025
2bd676f
Add a method to get a NonNull<T> from a UniqueMmioPointer.
qwandor Mar 6, 2025
3479d61
Send implementation shouldn't require T to be Sized.
qwandor Mar 6, 2025
7a2d648
Add comparison with other crates to readme.
qwandor Mar 10, 2025
808c5ee
Document usage examples.
qwandor Mar 10, 2025
684dfad
Simplify example.
qwandor Mar 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ keywords = ["mmio"]
categories = ["embedded", "no-std"]

[dependencies]
zerocopy = { version = "0.8.18", features = ["derive"] }
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,123 @@ This crate provides types for safe MMIO device access, especially in systems wit

This is not an officially supported Google product.

## Usage

### UniqueMmioPointer

The main type provided by this crate is `UniqueMmioPointer`. A `UniqueMmioPointer<T>` is roughly
equivalent to an `&mut T` for a memory-mapped IO device. Suppose you want to construct a pointer to
the data register of some UART device, and write some character to it:

```rust
use core::ptr::NonNull;
use safe_mmio::UniqueMmioPointer;

let mut data_register =
unsafe { UniqueMmioPointer::<u8>::new(NonNull::new(0x900_0000 as _).unwrap()) };
unsafe {
data_register.write_unsafe(b'x');
}
```

Depending on your platform this will either use `write_volatile` or some platform-dependent inline
assembly to perform the MMIO write.

### Safe MMIO methods

If you know that a particular MMIO field is safe to access, you can use the appropriate wrapper type
to mark that. In this case, suppose that the UART data register should only be written to:

```rust
use core::ptr::NonNull;
use safe_mmio::{fields::WriteOnly, UniqueMmioPointer};

let mut data_register: UniqueMmioPointer<WriteOnly<u8>> =
unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
data_register.write(b'x');
```

### Grouping registers with a struct

In practice, most devices have more than one register. To model this, you can create a struct, and
then use the `field!` macro to project from a `UniqueMmioPointer` to the struct to a pointer to one
of its fields:

```rust
use core::ptr::NonNull;
use safe_mmio::{
field,
fields::{ReadOnly, ReadPure, ReadWrite, WriteOnly},
UniqueMmioPointer,
};

#[repr(C)]
struct UartRegisters {
data: ReadWrite<u8>,
status: ReadPure<u8>,
pending_interrupt: ReadOnly<u8>,
}

let mut uart_registers: UniqueMmioPointer<UartRegisters> =
unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
field!(uart_registers, data).write(b'x');
```

Methods are also provided to go from a `UniqueMmioPointer` to an array or slice to its elements.

### Pure reads vs. side-effects

We distinguish between fields which for which MMIO reads may have side effects (e.g. popping a byte
from the UART's receive FIFO or clearing an interrupt status) and those for which reads are 'pure'
with no side-effects. Reading from a `ReadOnly` or `ReadWrite` field is assumed to have
side-effects, whereas reading from a `ReadPure` or `ReadPureWrite` must not. Reading from a
`ReadOnly` or `ReadWrite` field requires an `&mut UniqueMmioPointer` (the same as writing), whereas
reading from a `ReadPure` or `ReadPureWrite` field can be done with an `&UniqueMmioPointer` or
`&SharedMmioPointer`.

### Physical addresses

`UniqueMmioPointer` (and `SharedMmioPointer`) is used for a pointer to a device which is mapped into
the page table and accessible, i.e. a virtual address. Sometimes you may want to deal with the
physical address of a device, which may or may not be mapped in. For this you can use the
`PhysicalInstance` type. A `PhysicalInstance` doesn't let you do anything other than get the
physical address and size of the device's MMIO region, but is intended to convey ownership. There
should never be more than one `PhysicalInstance` pointer to the same device. This way your page
table management code can take a `PhysicalInstance<T>` and return a `UniqueMmioPointer<T>` when a
device is mapped into the page table.

## Comparison with other MMIO crates

There are a number of things that distinguish this crate from other crates providing abstractions
for MMIO in Rust.

1. We avoid creating references to MMIO address space. The Rust compiler is free to dereference
references whenever it likes, so constructing references to MMIO address space (even temporarily)
can lead to undefined behaviour. See https://github.com/rust-embedded/volatile-register/issues/10
for more background on this.
2. We distinguish between MMIO reads which have side-effects (e.g. clearing an interrupt status, or
popping from a queue) and those which don't (e.g. just reading some status). A read which has
side-effects should be treated like a write and only be allowed from a unique pointer (passed via
&mut) whereas a read without side-effects can safely be done via a shared pointer (passed via
'&'), e.g. simultaneously from multiple threads.
3. On most platforms MMIO reads and writes can be done via `read_volatile` and `write_volatile`, but
on aarch64 this may generate instructions which can't be virtualised. This is arguably
[a bug in rustc](https://github.com/rust-lang/rust/issues/131894), but in the meantime we work
around this by using inline assembly to generate the correct instructions for MMIO reads and
writes on aarch64.

| Crate name | Last release | Version | Avoids references | Distinguishes reads with side-effects | Works around aarch64 volatile bug | Model | Field projection | Notes |
| --------------------------------------------------------------- | -------------- | ------- | ----------------- | ------------------------------------- | --------------------------------- | ----------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------- |
| safe-mmio | unreleased | 0.2.0 | ✅ | ✅ | ✅ | struct with field wrappers | macro |
| [derive-mmio](https://crates.io/crates/derive-mmio) | February 2025 | 0.3.0 | ✅ | ❌ | ❌ | struct with derive macro | only one level, through derive macro |
| [volatile](https://crates.io/crates/volatile) | June 2024 | 0.6.1 | ✅ | ❌ | ❌ | struct with derive macro | macro or generated methods |
| [volatile-register](https://crates.io/crates/volatile-register) | October 2023 | 0.2.2 | ❌ | ❌ | ❌ | struct with field wrappers | manual (references) |
| [tock-registers](https://crates.io/crates/tock-registers) | September 2023 | 0.9.0 | ❌ | ❌ | ❌ | macros to define fields and structs | manual (references) | Also covers CPU registers, and bitfields |
| [mmio](https://crates.io/crates/mmio) | May 2021 | 2.1.0 | ✅ | ❌ | ❌ | only deals with individual fields | ❌ |
| [rumio](https://crates.io/crates/rumio) | March 2021 | 0.2.0 | ✅ | ❌ | ❌ | macros to define fields and structs | generated methods | Also covers CPU registers, and bitfields |
| [vcell](https://crates.io/crates/vcell) | January 2021 | 0.1.3 | ❌ | ❌ | ❌ | plain struct | manual (references) |
| [register](https://crates.io/crates/register) | January 2021 | 1.0.2 | ❌ | ❌ | ❌ | macros to define fields and structs | manual (references) | Deprecated in favour of tock-registers. Also covers CPU registers, and bitfields. |

## License

Licensed under either of
Expand Down
185 changes: 185 additions & 0 deletions src/aarch64_mmio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2025 The safe-mmio Authors.
// This project is dual-licensed under Apache 2.0 and MIT terms.
// See LICENSE-APACHE and LICENSE-MIT for details.

use crate::{SharedMmioPointer, UniqueMmioPointer};
use core::ptr::NonNull;
use zerocopy::{FromBytes, Immutable, IntoBytes};

macro_rules! asm_mmio {
($t:ty, $read_name:ident, $read_assembly:literal, $write_name:ident, $write_assembly:literal) => {
unsafe fn $read_name(ptr: *const $t) -> $t {
let value;
unsafe {
core::arch::asm!(
$read_assembly,
value = out(reg) value,
ptr = in(reg) ptr,
);
}
value
}

unsafe fn $write_name(ptr: *mut $t, value: $t) {
unsafe {
core::arch::asm!(
$write_assembly,
value = in(reg) value,
ptr = in(reg) ptr,
);
}
}
};
}

asm_mmio!(
u8,
read_u8,
"ldrb {value:w}, [{ptr}]",
write_u8,
"strb {value:w}, [{ptr}]"
);
asm_mmio!(
u16,
read_u16,
"ldrh {value:w}, [{ptr}]",
write_u16,
"strh {value:w}, [{ptr}]"
);
asm_mmio!(
u32,
read_u32,
"ldr {value:w}, [{ptr}]",
write_u32,
"str {value:w}, [{ptr}]"
);
asm_mmio!(
u64,
read_u64,
"ldr {value:x}, [{ptr}]",
write_u64,
"str {value:x}, [{ptr}]"
);

impl<T: FromBytes + IntoBytes> UniqueMmioPointer<'_, T> {
/// Performs an MMIO read and returns the value.
///
/// If `T` is exactly 1, 2, 4 or 8 bytes long then this will be a single operation. Otherwise
/// it will be split into several, reading chunks as large as possible.
///
/// Note that this takes `&mut self` rather than `&self` because an MMIO read may cause
/// side-effects that change the state of the device.
///
/// # Safety
///
/// This field must be safe to perform an MMIO read from.
pub unsafe fn read_unsafe(&mut self) -> T {
unsafe { mmio_read(self.regs) }
}
}

impl<T: Immutable + IntoBytes> UniqueMmioPointer<'_, T> {
/// Performs an MMIO write of the given value.
///
/// If `T` is exactly 1, 2, 4 or 8 bytes long then this will be a single operation. Otherwise
/// it will be split into several, writing chunks as large as possible.
///
/// # Safety
///
/// This field must be safe to perform an MMIO write to.
pub unsafe fn write_unsafe(&self, value: T) {
match size_of::<T>() {
1 => unsafe { write_u8(self.regs.cast().as_ptr(), value.as_bytes()[0]) },
2 => unsafe { write_u16(self.regs.cast().as_ptr(), convert(value)) },
4 => unsafe { write_u32(self.regs.cast().as_ptr(), convert(value)) },
8 => unsafe { write_u64(self.regs.cast().as_ptr(), convert(value)) },
_ => unsafe { write_slice(self.regs.cast(), value.as_bytes()) },
}
}
}

impl<T: FromBytes + IntoBytes> SharedMmioPointer<'_, T> {
/// Performs an MMIO read and returns the value.
///
/// If `T` is exactly 1, 2, 4 or 8 bytes long then this will be a single operation. Otherwise
/// it will be split into several, reading chunks as large as possible.
///
/// # Safety
///
/// This field must be safe to perform an MMIO read from, and doing so must not cause any
/// side-effects.
pub unsafe fn read_unsafe(&self) -> T {
unsafe { mmio_read(self.regs) }
}
}

/// Performs an MMIO read and returns the value.
///
/// # Safety
///
/// The pointer must be valid to perform an MMIO read from.
unsafe fn mmio_read<T: FromBytes + IntoBytes>(ptr: NonNull<T>) -> T {
match size_of::<T>() {
1 => convert(unsafe { read_u8(ptr.cast().as_ptr()) }),
2 => convert(unsafe { read_u16(ptr.cast().as_ptr()) }),
4 => convert(unsafe { read_u32(ptr.cast().as_ptr()) }),
8 => convert(unsafe { read_u64(ptr.cast().as_ptr()) }),
_ => {
let mut value = T::new_zeroed();
unsafe { read_slice(ptr.cast(), value.as_mut_bytes()) };
value
}
}
}

fn convert<T: Immutable + IntoBytes, U: FromBytes>(value: T) -> U {
U::read_from_bytes(value.as_bytes()).unwrap()
}

unsafe fn write_slice(ptr: NonNull<u8>, slice: &[u8]) {
if let Some((first, rest)) = slice.split_at_checked(8) {
unsafe {
write_u64(ptr.cast().as_ptr(), u64::read_from_bytes(first).unwrap());
write_slice(ptr.add(8), rest);
}
} else if let Some((first, rest)) = slice.split_at_checked(4) {
unsafe {
write_u32(ptr.cast().as_ptr(), u32::read_from_bytes(first).unwrap());
write_slice(ptr.add(4), rest);
}
} else if let Some((first, rest)) = slice.split_at_checked(2) {
unsafe {
write_u16(ptr.cast().as_ptr(), u16::read_from_bytes(first).unwrap());
write_slice(ptr.add(2), rest);
}
} else if let [first, rest @ ..] = slice {
unsafe {
write_u8(ptr.as_ptr(), *first);
write_slice(ptr.add(1), rest);
}
}
}

unsafe fn read_slice(ptr: NonNull<u8>, slice: &mut [u8]) {
if let Some((first, rest)) = slice.split_at_mut_checked(8) {
unsafe {
read_u64(ptr.cast().as_ptr()).write_to(first).unwrap();
read_slice(ptr.add(8), rest);
}
} else if let Some((first, rest)) = slice.split_at_mut_checked(4) {
unsafe {
read_u32(ptr.cast().as_ptr()).write_to(first).unwrap();
read_slice(ptr.add(4), rest);
}
} else if let Some((first, rest)) = slice.split_at_mut_checked(2) {
unsafe {
read_u16(ptr.cast().as_ptr()).write_to(first).unwrap();
read_slice(ptr.add(2), rest);
}
} else if let [first, rest @ ..] = slice {
unsafe {
*first = read_u8(ptr.as_ptr());
read_slice(ptr.add(1), rest);
}
}
}
Loading