PAC (Peripheral Access Crate)
PAC (Peripheral Access Crate)
Level 17 — Embedded & Systems Programming A low-level, auto-generated crate providing type-safe, zero-cost access to a specific microcontroller's raw hardware registers (Memory-Mapped I/O addresses) parsed directly from the manufacturer's SVD (System View Description) XML file using
svd2rust.
1. Prerequisites
- Raw Pointers (
*const T,*mut T) — Register accesses perform volatile memory reads and writes. - HAL (Hardware Abstraction Layer) — Higher-level driver crate built on top of a PAC.
svd2rust— Tool that generates PAC crates from vendor SVD files.
2. Term Category
Rust Embedded Driver (peripheral access crate hardware registers): A Peripheral Access Crate (PAC) sits at the lowest layer of Rust embedded software. It exposes raw memory-mapped hardware registers (GPIO, USART, SPI, Timers, ADC) as strongly typed Rust structs and bit-field methods, eliminating dangerous raw pointer dereferencing (*(0x40021000 as *mut u32) = 1).
3. Explanation
(1) Design Motivation — "Why did we design this?"
In C embedded development, register manipulation uses unsafe macro bitwise shifts:
// C code: Easy to accidentally shift wrong bits or overwrite adjacent register fields!
RCC->AHB1ENR |= (1 << 3);
Rust PAC crates use Type-Safe Register Closures:
- Field methods enforce valid bit ranges at compile time.
- Prevents writing invalid bit patterns to hardware registers.
- Generated automatically from chip vendor SVD files using
svd2rust.
(2) Code Examples
Manipulating Hardware Registers with a PAC
// Using an STM32F4 Peripheral Access Crate (PAC)
use stm32f4::stm32f407;
pub fn enable_gpio_port_a(peripherals: &stm32f407::Peripherals) {
// Type-safe, closure-based register field modification:
// Enables Clock for GPIOA in RCC AHB1ENR register safely
peripherals.RCC.ahb1enr.modify(|_r, w| w.gpioaen().enabled());
}
4. Common Mistakes & Pitfalls
Mistake 2: Performing Non-Atomic Read-Modify-Write Operations on Shared Registers
The mistake: Reading a register, modifying bits, and writing back without atomic locks or critical sections.
Why it's wrong: An interrupt handler modifying the same register midway causes data loss.
Fix: Perform register updates using PAC closure methods (modify(|r, w| ...)) inside critical sections.
Mistake 3: Writing Reserved Register Bits During Bitwise Operations
The mistake: Overwriting reserved register bits with raw bitwise bitmasks.
Why it's wrong: Modifying reserved hardware bits can trigger undefined chip behavior or device locks.
Fix: Use PAC builder methods (w.field().variant()) that automatically preserve reserved bits.
Mistake 1: Using PAC Register Access directly in Application Code
The mistake: Writing application logic by directly manipulating raw PAC register bits everywhere.
Why it's wrong: PAC code is chip-specific and un-portable. Prefer using higher-level HAL (Hardware Abstraction Layer) crates.
5. Practice Exercises
Exercise 1: PAC Read-Modify-Write Register Pattern
Scenario: In microcontroller programming (such as configuring peripheral clock gates on STM32 or ATSAMD microcontrollers), hardware registers contain multiple independent bitfields. Modifying a specific bit field using a naive write operation (.write()) zeroes out or overwrites adjacent configuration bits. PAC crates generated by svd2rust provide a .modify(|r, w| ...) closure pattern that reads existing register bits into proxy r and allows applying mutation builder methods to proxy w.
Implement a PAC-style register wrapper Register with .read(), .write(), and .modify() methods. Write unit tests proving that calling .modify() to enable gpioaen retains adjacent gpioben bits, whereas .write() resets unconfigured bitfields.
Answer
Implementation
#![no_std]
/// Simulates a 32-bit hardware peripheral register (e.g., RCC AHB1ENR).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Register(u32);
/// Read proxy (`R`) offering bitfield inspection methods.
pub struct R(u32);
/// Write proxy (`W`) offering bitfield builder methods.
pub struct W(u32);
impl Register {
pub const fn new(val: u32) -> Self {
Self(val)
}
/// Perform a raw read of the register contents.
pub fn read(&self) -> R {
R(self.0)
}
/// Overwrite the entire register using default zero-initialization for W.
pub fn write<F>(&mut self, f: F)
where
F: FnOnce(&mut W) -> &mut W,
{
let mut w = W(0);
f(&mut w);
self.0 = w.0;
}
/// Perform an atomic Read-Modify-Write operation on existing bits.
pub fn modify<F>(&mut self, f: F)
where
F: FnOnce(&R, &mut W) -> &mut W,
{
let r = self.read();
let mut w = W(r.0);
f(&r, &mut w);
self.0 = w.0;
}
}
impl R {
/// Returns true if GPIOA clock enable bit (bit 0) is set.
pub fn gpioaen(&self) -> bool {
(self.0 & (1 << 0)) != 0
}
/// Returns true if GPIOB clock enable bit (bit 1) is set.
pub fn gpioben(&self) -> bool {
(self.0 & (1 << 1)) != 0
}
pub fn raw(&self) -> u32 {
self.0
}
}
impl W {
/// Configure GPIOA clock enable bit (bit 0).
pub fn gpioaen(&mut self, enable: bool) -> &mut Self {
if enable {
self.0 |= 1 << 0;
} else {
self.0 &= !(1 << 0);
}
self
}
/// Configure GPIOB clock enable bit (bit 1).
pub fn gpioben(&mut self, enable: bool) -> &mut Self {
if enable {
self.0 |= 1 << 1;
} else {
self.0 &= !(1 << 1);
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pac_modify_preserves_adjacent_bits() {
// Initial register state: GPIOB clock enabled (bit 1 set: 0b0010 = 0x2)
let mut ahb1enr = Register::new(0b0010);
// Modify: Enable GPIOA clock (bit 0)
ahb1enr.modify(|_r, w| w.gpioaen(true));
// Both GPIOA (bit 0) and GPIOB (bit 1) must be set (0b0011 = 0x3)
assert_eq!(ahb1enr.read().raw(), 0b0011);
assert!(ahb1enr.read().gpioaen());
assert!(ahb1enr.read().gpioben());
}
#[test]
fn test_pac_write_overwrites_all_bits() {
// Initial state: GPIOB clock enabled (bit 1 set)
let mut ahb1enr = Register::new(0b0010);
// Raw write: Enable GPIOA clock (bit 0) without preserving existing bits
ahb1enr.write(|w| w.gpioaen(true));
// Only GPIOA is enabled; GPIOB clock bit is cleared to 0 (0b0001 = 0x1)
assert_eq!(ahb1enr.read().raw(), 0b0001);
assert!(ahb1enr.read().gpioaen());
assert!(!ahb1enr.read().gpioben());
}
}
Technical Explanation
- Closure-based API: In
svd2rustgenerated PACs,.modify()receives a closure|_r, w| ....ris a read snapshot (R), whilewis a write proxy pre-populated with the current register valuer.0. - Read-Modify-Write Safety: Modifying bitfields through
modifyperforms bit manipulation over the existing bit pattern, preserving adjacent control fields. Callingwritestarts fromW(0), effectively zeroing unconfigured fields. - Fluent Interface Pattern: Writer methods return
&mut Self, allowing chained method calls likew.gpioaen(true).gpioben(false).
Exercise 2: Type-Safe Enum Bitfield Encoding in PAC Registers
Scenario: Microcontroller peripherals frequently use multi-bit configuration fields (such as timer clock division, UART baud rate prescalers, or ADC sampling time). Writing raw integers directly into bit positions can introduce invalid out-of-range bit patterns. PACs generate strongly-typed Rust enums for register bitfields to enforce valid choices at compile time.
Design a PAC timer register structure TimerCr1 with an enum CounterMode representing timer counting direction (Up, Down, CenterAligned1, CenterAligned2) encoded in bits 4..=5. Implement getter and setter methods that convert between the typed enum and raw bit ranges, along with unit tests validating the bit encoding.
Answer
Implementation
#![no_std]
/// Type-safe counter mode bitfield enum matching hardware register specs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CounterMode {
Up = 0b00,
Down = 0b01,
CenterAligned1 = 0b10,
CenterAligned2 = 0b11,
}
/// Simulated Timer Control Register 1 (CR1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimerCr1(u32);
impl TimerCr1 {
pub const fn new() -> Self {
Self(0)
}
/// Set counter mode field (bits 4..=5) safely using `CounterMode` enum.
pub fn set_counter_mode(&mut self, mode: CounterMode) {
let mask = !(0b11 << 4);
self.0 = (self.0 & mask) | ((mode as u32) << 4);
}
/// Extract and parse counter mode field (bits 4..=5) into `CounterMode` enum.
pub fn counter_mode(&self) -> CounterMode {
match (self.0 >> 4) & 0b11 {
0b00 => CounterMode::Up,
0b01 => CounterMode::Down,
0b10 => CounterMode::CenterAligned1,
0b11 => CounterMode::CenterAligned2,
_ => unreachable!(),
}
}
pub fn raw(&self) -> u32 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timer_cr1_counter_mode_encoding() {
let mut cr1 = TimerCr1::new();
// Default counter mode is Up (bits 4..=5 = 0b00)
assert_eq!(cr1.counter_mode(), CounterMode::Up);
assert_eq!(cr1.raw(), 0);
// Set mode to Down (0b01 << 4 = 0x10)
cr1.set_counter_mode(CounterMode::Down);
assert_eq!(cr1.counter_mode(), CounterMode::Down);
assert_eq!(cr1.raw(), 0x10);
// Set mode to CenterAligned1 (0b10 << 4 = 0x20)
cr1.set_counter_mode(CounterMode::CenterAligned1);
assert_eq!(cr1.counter_mode(), CounterMode::CenterAligned1);
assert_eq!(cr1.raw(), 0x20);
}
}
Technical Explanation
- Strong Type Enums in PACs: Using
#[repr(u8)]enums maps domain states directly to raw bit vectors, eliminating invalid bit combinations at compile time. - Bit-masking & Shifting: Setting bits 4..=5 requires clearing existing bits using bitwise AND with mask
!(0b11 << 4)before applying bitwise OR with shifted value(mode as u32) << 4. - Zero-Cost Abstraction: Rust optimizes these enum conversions into simple bitwise instructions (
AND/OR/LSL), producing machine code identical to hand-written C macros while providing compile-time type safety.
Exercise 3: Peripherals Singleton Pattern (Peripherals::take())
Scenario: In embedded Rust applications, multiple driver modules or interrupt handlers must not concurrently manipulate the exact same physical peripheral hardware registers. PAC crates enforce peripheral safety by making hardware peripherals available only through a global singleton pattern (Peripherals::take()), which returns Some(Peripherals) on the first invocation and None on subsequent calls.
Implement a thread-safe Peripherals singleton using AtomicBool to manage ownership of mock hardware blocks (GpioA, Usart1). Write unit tests verifying that take() returns Some exactly once and None thereafter.
Answer
Implementation
#![no_std]
use core::sync::atomic::{AtomicBool, Ordering};
pub struct GpioA {
pub moder: u32,
pub odr: u32,
}
pub struct Usart1 {
pub cr1: u32,
pub dr: u32,
}
/// The top-level Peripheral Access Crate struct grouping all chip peripherals.
pub struct Peripherals {
pub gpioa: GpioA,
pub usart1: Usart1,
}
static TAKEN: AtomicBool = AtomicBool::new(false);
impl Peripherals {
/// Safely acquire ownership of hardware peripherals.
/// Returns `Some(Peripherals)` once per application lifetime, `None` on subsequent calls.
pub fn take() -> Option<Self> {
if TAKEN.swap(true, Ordering::SeqCst) {
None
} else {
Some(Peripherals {
gpioa: GpioA { moder: 0, odr: 0 },
usart1: Usart1 { cr1: 0, dr: 0 },
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peripherals_singleton_take() {
// First call successfully takes peripheral ownership
let p1 = Peripherals::take();
assert!(p1.is_some());
// Second call fails because peripherals are already owned
let p2 = Peripherals::take();
assert!(p2.is_none());
}
}
Technical Explanation
- Single-Ownership Guarantee: Physical hardware registers are unique resources. By exposing
Peripherals::take() -> Option<Self>, PAC crates guarantee single ownership at runtime. - Atomic Swap Synchronization: Using
AtomicBool::swap(true, Ordering::SeqCst)guarantees lock-free, atomic check-and-set operations even in multi-threaded host tests or interrupt-driven embedded environments. - HAL Consumption: A high-level Hardware Abstraction Layer (HAL) crate calls
Peripherals::take()during system startup to split raw peripherals into individual driver instances (e.g.,dp.GPIOA.split()), moving ownership tracking to compile-time types.
6. Related Terms
- HAL (Hardware Abstraction Layer) — Higher-level driver crate built on top of a PAC.
svd2rust— Related concept:svd2rust.embedded-hal— Embedded HAL traits.
7. Key Takeaways
- PAC (Peripheral Access Crate) provides type-safe, zero-cost access to raw microcontroller registers.
- Auto-generated from vendor SVD files using
svd2rust. - Uses closure-based
.read(),.write(), and.modify()methods for register bitfields. - Serves as the foundation for higher-level HAL crates.