Peripheral Access Crates
A Peripheral Access Crate, or PAC, is the chip-specific Rust crate that exposes typed access to a microcontroller’s memory-mapped registers, usually generated from the vendor SVD.
What it is
PACs sit just above raw MMIO. They know the register blocks, field names, reset values, interrupt names, and addresses for one device or device family. In the Rust embedded stack, the PAC is lower level than a HAL and higher level than handwritten pointer arithmetic.
A PAC does not usually give a polished “send a byte over UART” API. It gives access to the UART control, status, data, and interrupt registers. You still need the reference manual, but the PAC reduces wrong-address and wrong-bit mistakes.
Most modern PACs are generated by tools such as svd2rust. Generated APIs commonly use read, write, and modify closures to distinguish reading fields, replacing registers, and changing selected fields.
How it works
The central safety idea is single ownership of peripheral blocks. A generated Peripherals::take() returns Some(Peripherals) once and None on later calls, so application code cannot accidentally create two independent owners of the same hardware.
Once the program owns a peripheral value, methods on that value perform volatile access internally. The borrow checker then helps with aliasing: &Peripheral can model read-only access, while &mut Peripheral or consumed values can model exclusive configuration.
HAL crates typically consume PAC values with methods like constrain() or split() and return higher-level types. This is why PACs and HALs compose well: the PAC gives exact register access, while the HAL encodes board- or protocol-level invariants.
The generated API mirrors the register semantics as far as the SVD can describe them. read() returns field readers. write() builds a fresh register value, so unspecified fields take generated reset or default values. modify() reads the current value and writes a changed value, which is convenient but still a read-modify-write sequence with all the normal MMIO caveats.
PACs cannot know every board-level rule. They may mark raw bits(...) setters as unsafe when the SVD says a field is a number but cannot enumerate valid meanings. The safety contract then moves to the caller: the value must be accepted by the hardware in the current mode, clock state, and timing context.
Example
use core::cell::Cell;
#[derive(Default)]
pub struct ControlRegister {
bits: Cell<u32>,
}
impl ControlRegister {
pub fn read(&self) -> u32 {
self.bits.get()
}
pub fn write(&self, value: u32) {
self.bits.set(value);
}
pub fn modify(&self, f: impl FnOnce(u32) -> u32) {
let old = self.read();
self.write(f(old));
}
}
pub fn enable(register: &ControlRegister) {
register.modify(|bits| bits | 0b1);
}This is only a host-compilable sketch of PAC shape. Real PAC registers perform volatile MMIO and expose generated field readers and writers instead of plain integers.
More realistic example
pub struct Peripherals {
pub uart0: Uart0,
pub gpioa: GpioA,
}
pub struct Uart0 {
baud: u32,
enabled: bool,
}
pub struct GpioA;
impl Peripherals {
pub fn take_once(slot: &mut Option<Self>) -> Option<Self> {
slot.take()
}
}
impl Uart0 {
pub fn configure(&mut self, core_hz: u32, baud: u32) {
self.baud = core_hz / baud;
self.enabled = true;
}
}This sketch shows the ownership transfer behind Peripherals::take(): once the Option is empty, there is no second safe owner to hand out. Real PACs hide that slot in crate internals and expose a safe singleton API.
Common errors
error[E0382]: use of moved valueThis appears when a PAC peripheral is consumed by a HAL method such as split() or constrain() and then used again as the raw PAC value. Fix it by using the returned HAL parts, or by doing all required low-level PAC configuration before moving the peripheral.
error[E0133]: call to unsafe function `bits` is unsafe and requires unsafe blockFix it only after checking the reference manual. The unsafe block should be close to the field write and explain why that numeric value is valid for the current register mode.
Best practice
- ✅ Use the PAC matching the exact chip or supported device family, not just the CPU core.
- ✅ Call
Peripherals::take()once during initialization and pass owned peripheral values into drivers or HAL constructors. - ✅ Use
modifyfor field changes that must preserve other register bits, andwriteonly when replacing unspecified fields is correct. - ✅ Prefer HAL APIs for routine device behavior, but drop to PAC access when the HAL does not expose a required register feature.
- ✅ Read generated docs together with the chip reference manual; generated field names do not replace timing diagrams and mode restrictions.
- ✅ Keep
unsafe { bits(...) }writes rare and justified by constants, enums, or documented formulas. - ✅ When sharing a PAC value with an interrupt, move it into an interrupt-aware synchronization primitive during startup rather than cloning access through raw pointers.
Pitfalls
- ⚠️ Calling unchecked or stealing constructors casually. Bypassing singleton acquisition can create aliases to the same hardware.
- ⚠️ Assuming a PAC validates every numeric field value. Some generated
bits(...)setters areunsafebecause the SVD cannot prove all values are valid. - ⚠️ Forgetting that
writemay reset fields you did not mention; usemodifywhen preserving existing fields matters. - ⚠️ Sharing PAC peripheral values with interrupts without synchronization; see Critical Sections in Embedded Rust.
- ⚠️ Assuming
modifyis atomic. It is usually a volatile read followed by a volatile write, so an interrupt or hardware side effect can intervene. - ⚠️ Mixing HAL ownership and raw PAC writes to the same peripheral without a clear boundary; the HAL may cache configuration assumptions in its types.
See also
O · Embedded Rust Basics · Bare-Metal Programming · Interrupts and Concurrency (Embedded) · Critical Sections in Embedded Rust · Heapless Collections in Embedded Rust · Ownership · Unsafe Rust · Result · Embedded Rust
Sources
- The Embedded Rust Book, “Memory-mapped Registers” — embedded-book, https://doc.rust-lang.org/stable/embedded-book/start/registers.html
- The Embedded Rust Book, “Singletons” — embedded-book, https://doc.rust-lang.org/stable/embedded-book/peripherals/singletons.html
- The Embedded Rust Book, “HALs” — embedded-book, https://doc.rust-lang.org/stable/embedded-book/design-patterns/hal/index.html
