Crate mmio

Source
Expand description

Safe Memory Mapped I/O for Fuchsia drivers.

MMIO allows a program to interact with devices through operations on its address space. While the interface is similar to regular memory access, there are some key semantic differences. This crate provides safe interfaces for interacting with MMIO that are intended to be sound.

§Memory semantics

§Safe Rust

The semantics of a safe Rust program are determined by the Rust Abstract Machine. The Rust Abstract Machine notably does not provide certain guarantees that are required when interacting with devices:

  • Guaranteed execution: I/O operations may be elided if the elision would have no impact on the observable behavior of the executing thread (they may also be repeated).
  • Operation ordering: operations may be reordered if doing so does not impact the observable behavior of the executing thread.

For memory that is under the control of the Rust Abstract Machine, the semantics of a safe Rust program can depend on memory not changing value without a corresponding write.

§Device Memory

Device memory is not under the control of the Rust Abstract Machine. The guarantees around the state and behavior of device memory do not provide the guarantees required for safe Rust. Similarly, safe Rust cannot encode the semantics required to interact correctly with device memory.

§Side Effects

Reading from or writing to device memory may have observable side effects, such as clearing a status flag, advancing a buffer, or changing device state in some other way.

§Value Volatility

The value that would be returned when loading from a device memory address may change completely independently of the program’s control. Two subsequent reads on the same thread with no interleaved write may return different values. A read of a just-written value is not guaranteed to return the just-written value.

§Atomicity

A memory instruction which may be atomic when operating on regular memory is not guaranteed to be atomic when interfacing with a device. For example a memory operation with a width larger than the bus size supported by the device must be broken down into multiple operations.

§Elision and Spurious Accesses

As mentioned already, memory operations may be ellided or duplicated when executing safe Rust provided they don’t change the single-threaded semantics of the program. If a shared or mutable reference to an object is active, memory operations may be performed to the backing memory. For this reason accesses to a memory location via pointers should only be performed when there are no active references that may alias the same memory.

§Compiler Reordering and Volatile

The Rust Compiler has some freedom to reorder instructions. For regular memory accesses the compiler may reorder instructions where a data dependency does not prevent the reordering.

The compiler is not allowed to reorder volatile operations with respect to other volatile operations in the same thread. It is free to reorder volatile operations with other kinds of memory accesses.

By themselves, volatile operations cannot provide any sequencing guarantees with respect to non-volatile operations on the same thread, or any kind of operation on another thread.

§User Guide

Users of this crate interact with device memory through the Mmio trait. This trait exposes the low-level load and store operations to safely interact with device memory. All stores performed through an Mmio instance are issued through a mutable reference.

If an Mmio implementation also implements MmioSplit, it is possible to split off independently owned sub-regions from it. This allows concurrent mutable access to disjoint MMIO regions.

§Operand Types

The Mmio trait provides load and store operations for the following types:

  • u8
  • u16
  • u32
  • u64

Conforming implementations of Mmio must perform these operations in the code order relative to each other on the same thread, and in 1:1 correspondence with calls to corresponding function.

Where the operand type is larger than the bus size, an implementation should perform the sub-operations in increasing address order.

§Dyn Compatibility and Trait Extensions

The Mmio trait is dyn compatible. Callers may want to also import the MmioExt trait which defines some useful utilities on top of Mmio that would otherwise break dyn compatibility. This trait is implemented automatically for any Mmio.

§Alignment

The Mmio trait exposes offsets instead of addresses. Operations must be performed at a suitable offset for the operand type. Valid alignment is not an intrinsic property of an offset. Callers can use Mmio::align_offset to determine the first offset within the MMIO region suitable for a given alignment.

§Usage Examples

§VmoMapping

use mmio::{Mmio, MmioExt};
use mmio::vmo::VmoMapping;
const VMO_OFFSET: usize = 0;
const VMO_LEN: usize = 1024;
// Map the Vmo memory, returning an `MmioRegion`. The returned region implements `Mmio`.
let mut mmio = VmoMapping::map(VMO_OFFSET, VMO_LEN, vmo).unwrap();
// Load 4 bytes starting at offset 0.
let _ = mmio.load32(0);
// Store 2 bytes starting at offset 32.
let _ = mmio.store16(32, 0x1234);

§Splittable VmoMapping

use mmio::{Mmio, MmioExt};
use mmio::vmo::VmoMapping;
const VMO_LEN: usize = 1024;
// Map the Vmo memory and convert it into a splittable `MmioRegion`.
// The returned region implements `Mmio + MmioSplit + Send`. If you only need split and not
// you can use `into_split` instead.
let mut mmio = VmoMapping::map(0, VMO_LEN, vmo).unwrap().into_split_send();

// Split off a number of regions which have exclusive ownership of their ranges.
// reg1 owns the first 8 bytes, which start at offset 0 in the original mapping.
let mut reg1 = mmio.split_off(8);
// reg2 owns the next 4 bytes, which start at offset 8 in the original mapping.
let mut reg2 = mmio.split_off(4);
// reg3 owns the next 4 bytes, which start at offset 12 in the original mapping.
let mut reg3 = mmio.split_off(4);
// The original mmio owns the rest of the region, starting at offset 16 in the original
// mapping.

let _ = reg1.load64(0);
reg1.store16(2, 0x1234);

let _ = reg2.load32(0);
reg2.store8(3, 0xff);

let _ = reg3.load32(0);
reg3.store16(0, 0xff00);

§Alignment and Capacity

use mmio::{Mmio, MmioExt};
use mmio::vmo::VmoMapping;
const VMO_LEN: usize = 1024;
// Map the Vmo memory. The returned mapping can be converted into an `MmioRegion`.
let mut mmio = VmoMapping::map(0, VMO_LEN, vmo).unwrap().into_split();

// Split off a number of regions which have exclusive ownership of their ranges.
// reg1 owns the first 8 bytes, which start at offset 0 in the original mapping.
let mut reg1 = mmio.split_off(8);
// reg2 owns the next 4 bytes, which start at offset 8 in the original mapping.
let mut reg2 = mmio.split_off(4);
// reg3 owns the next 4 bytes, which start at offset 12 in the original mapping.
let mut reg3 = mmio.split_off(4);
// The original mmio owns the rest of the region, starting at offset 16 in the original
// mapping.

// reg1 owns the region starting at offset 0 in the Vmo, which is mapped to a page aligned
// boundary. It is suitably aligned for any type with an alignment <= Fuchsia's page size. It
// covers an 8 byte range, to can hold any `MmioOperand` type.
reg1.check_suitable_for::<u64>().unwrap();

// reg2 owns the region starting at offset 8. It is aligned for any `MmioOperand` type but
// covers a 4 byte range.
reg2.check_aligned_for::<u64>().unwrap();
reg2.check_capacity_for::<u64>().expect_err("expect MmioError::OutOfRange");

reg2.check_suitable_for::<u32>().unwrap();

// reg3 owns the region starting at offset 12. It is aligned for types with an alignment <= 4
// and covers a 4 byte range.
reg3.check_aligned_for::<u64>().expect_err("expect MmioError::Unaligned");
reg3.check_capacity_for::<u64>().expect_err("expect MmioError::OutOfRange");

reg3.check_suitable_for::<u32>().unwrap();

// The original mmio object owns the region starting at offset 16 and covering the rest of the
// mapping.
mmio.check_suitable_for::<u64>().unwrap();
mmio.check_suitable_for::<[u64; 8]>().unwrap();

§Testing

§Regular Memory Implementation

use mmio::memory::Memory;
use std::mem::MaybeUninit;

let mut mem = MaybeUninit<[u64; 8]>::uninit();
// A MaybeUninit's memory can be borrowed as an Mmio region.
let mmio = Memory::borrow_uninit(&mut mem);

run_test(mmio);

// Safety: any bit pattern is valid for [u64; 8].
let mem = unsafe { mem.assume_init() };

// Check that the memory is in the expected state.
validate_mem(mem);

§Implementers Guide

Implementers of Mmio and MmioSplit are required to uphold some guarantees. These are discussed more thoroughly in the corresponding trait’s documentation. These requirements are intended to guarantee the semantics required to interface with devices correctly, as discussed earlier.

Modules§

region
Support for implementing splittable MMIO regions.
vmo
MMIO regions backed by Fuchsia Virtual Memory Objects.

Enums§

MmioError
An error which may be encountered when performing operations on an MMIO region.

Traits§

Mmio
An Mmio implementation provides access to an MMIO region.
MmioExt
This trait extends Mmio with some useful utilities. There is a blanket implementation for all types implementing Mmio.
MmioOperand
Implemented for the fundamental types supported by all Mmio implementations.
MmioSplit
An MMIO region from which ownership of disjoint sub-regions can be split off.