diff --git a/src/bare-metal/aps/better-uart/bitflags.md b/src/bare-metal/aps/better-uart/bitflags.md
index 891cd2e0..ab54412b 100644
--- a/src/bare-metal/aps/better-uart/bitflags.md
+++ b/src/bare-metal/aps/better-uart/bitflags.md
@@ -11,5 +11,7 @@ with bitflags.
- The `bitflags!` macro creates a newtype something like `Flags(u16)`, along
with a bunch of method implementations to get and set flags.
+- We need to derive `FromBytes` and `IntoBytes` for use with `safe-mmio`, which
+ we'll see on the next page.
diff --git a/src/bare-metal/aps/better-uart/driver.md b/src/bare-metal/aps/better-uart/driver.md
index 1661f809..488be573 100644
--- a/src/bare-metal/aps/better-uart/driver.md
+++ b/src/bare-metal/aps/better-uart/driver.md
@@ -8,7 +8,16 @@ Now let's use the new `Registers` struct in our driver.
-- Note the use of `&raw const` / `&raw mut` to get pointers to individual fields
- without creating an intermediate reference, which would be unsound.
+- `UniqueMmioPointer` is a wrapper around a raw pointer to an MMIO device or
+ register. The caller of `UniqueMmioPointer::new` promises that it is valid and
+ unique for the given lifetime, so it can provide safe methods to read and
+ write fields.
+- These MMIO accesses are generally a wrapper around `read_volatile` and
+ `write_volatile`, though on aarch64 they are instead implemented in assembly
+ to work around a bug where the compiler can emit instructions that prevent
+ MMIO virtualisation.
+- The `field!` and `field_shared!` macros internally use `&raw mut` and
+ `&raw const` to get pointers to individual fields without creating an
+ intermediate reference, which would be unsound.
diff --git a/src/bare-metal/aps/better-uart/registers.md b/src/bare-metal/aps/better-uart/registers.md
index 48f5e5b1..cf95b665 100644
--- a/src/bare-metal/aps/better-uart/registers.md
+++ b/src/bare-metal/aps/better-uart/registers.md
@@ -1,6 +1,8 @@
# Multiple registers
-We can use a struct to represent the memory layout of the UART's registers.
+We can use a struct to represent the memory layout of the UART's registers,
+using types from the `safe-mmio` crate to wrap ones which can be read or written
+safely.
@@ -15,5 +17,12 @@ We can use a struct to represent the memory layout of the UART's registers.
rules as C. This is necessary for our struct to have a predictable layout, as
default Rust representation allows the compiler to (among other things)
reorder fields however it sees fit.
+- There are a number of different crates providing safe abstractions around MMIO
+ operations; we recommend the `safe-mmio` crate.
+- The difference between `ReadPure` or `ReadOnly` (and likewise between
+ `ReadPureWrite` and `ReadWrite`) is whether reading a register can have
+ side-effects which change the state of the device. E.g. reading the data
+ register pops a byte from the receive FIFO. `ReadPure` means that reads have
+ no side-effects, they are purely reading data.
diff --git a/src/bare-metal/aps/examples/Cargo.lock b/src/bare-metal/aps/examples/Cargo.lock
index 0ff3435f..e121e3e7 100644
--- a/src/bare-metal/aps/examples/Cargo.lock
+++ b/src/bare-metal/aps/examples/Cargo.lock
@@ -31,8 +31,10 @@ dependencies = [
"arm-pl011-uart",
"bitflags",
"log",
+ "safe-mmio",
"smccc",
"spin",
+ "zerocopy",
]
[[package]]
diff --git a/src/bare-metal/aps/examples/Cargo.toml b/src/bare-metal/aps/examples/Cargo.toml
index d7cd5045..38f1de00 100644
--- a/src/bare-metal/aps/examples/Cargo.toml
+++ b/src/bare-metal/aps/examples/Cargo.toml
@@ -12,8 +12,10 @@ aarch64-rt = "0.1.3"
arm-pl011-uart = "0.3.1"
bitflags = "2.9.0"
log = "0.4.27"
+safe-mmio = "0.2.5"
smccc = "0.2.0"
spin = "0.10.0"
+zerocopy = "0.8.25"
[[bin]]
name = "improved"
diff --git a/src/bare-metal/aps/examples/src/logger.rs b/src/bare-metal/aps/examples/src/logger.rs
index e27ee2b9..6e4c73ad 100644
--- a/src/bare-metal/aps/examples/src/logger.rs
+++ b/src/bare-metal/aps/examples/src/logger.rs
@@ -21,7 +21,7 @@ use spin::mutex::SpinMutex;
static LOGGER: Logger = Logger { uart: SpinMutex::new(None) };
struct Logger {
- uart: SpinMutex