Introduction
Microcontrollers expose their peripherals through memory-mapped registers — fixed addresses where reading and writing bits configures hardware. Peripheral Access Crates (PACs) are auto-generated Rust wrappers around these registers, giving you type-safe access instead of raw pointer arithmetic.
Key Concepts
- SVD (System View Description): An XML file from the chip vendor describing every peripheral register, its fields, bit positions, and access permissions.
- svd2rust: A tool that converts SVD files into Rust code with typed register access.
- PAC API: Three main operations —
read(),write(), andmodify()— each with type-safe field accessors.
Real World Context
Without a PAC, configuring a UART means writing magic numbers to memory addresses — a single wrong bit crashes the system with no compiler help. With a PAC, the compiler rejects invalid field values and documents every register field as a method name.
Deep Dive
What SVD Files Contain
An SVD file describes the full register map of a microcontroller. It includes peripheral names, base addresses, register offsets, field bit ranges, and enumerated values. ARM CMSIS provides SVD files for most Cortex-M chips.
Generating a PAC
The svd2rust tool converts an SVD file into a Rust crate:
bashsvd2rust -i STM32F411.svd
This produces a full Rust crate with typed register access. Most developers use pre-generated PAC crates from crates.io (e.g., stm32f4, nrf52840-pac) rather than running svd2rust themselves.
PAC API Patterns
The generated code provides three core operations:
rustuse stm32f4::stm32f411; let dp = stm32f411::Peripherals::take().unwrap(); // Read: returns a reader with field accessors let is_high = dp.GPIOA.idr.read().idr5().bit(); // Write: replaces the entire register value dp.GPIOA.odr.write(|w| w.odr5().set_bit()); // Modify: read-modify-write, change specific fields dp.GPIOA.moder.modify(|_, w| { w.moder5().output() .moder6().input() });
The read() method returns a reader struct with accessor methods for each field. The write() closure receives a writer that resets the register to its default value first. The modify() closure receives both a reader (current value) and a writer, allowing you to change specific fields without touching others.
Type Safety
The PAC only allows valid values for each field:
rustdp.GPIOA.moder.modify(|_, w| { w.moder5().input() // Binary 00 — valid .moder5().output() // Binary 01 — valid .moder5().alternate() // Binary 10 — valid .moder5().analog() // Binary 11 — valid // .moder5().bits(5) // Would not compile — only 2-bit values allowed });
This eliminates a huge class of bugs where a wrong bit pattern silently misconfigures hardware.
Common Pitfalls
- Forgetting to enable peripheral clocks — Most peripherals on ARM chips are clock-gated. You must enable the clock in the RCC peripheral before accessing any registers. Without it, reads return zero and writes are silently dropped.
- Using
write()when you meanmodify()—write()resets all fields to their default value before applying your changes. If you only want to change one field, usemodify()to preserve the others. - Assuming SVD files are perfect — Vendor SVD files often have errors (missing fields, wrong bit widths). Community-patched versions like
stm32-rsfix known issues.
Best Practices
- Use community-maintained PACs — Crates like
stm32f4(fromstm32-rs) apply patches to fix SVD errors. Always prefer these over raw svd2rust output. - Prefer HAL crates over raw PAC — Use the PAC only when the HAL does not expose the functionality you need. HAL crates handle clock setup, pin configuration, and error handling.
- Read the reference manual alongside PAC docs — The PAC method names match register field names from the reference manual. Use both together.
Summary
- PACs are auto-generated from SVD files using svd2rust.
- Three core operations:
read(),write(), andmodify(). - Type safety prevents invalid register field values at compile time.
- Enable peripheral clocks before accessing registers.
- Use community-maintained PAC crates from
stm32-rsand similar projects.
Code Examples
// Low-level UART setup using PAC register access
use stm32f4::stm32f411;
fn setup_uart(dp: &stm32f411::Peripherals) {
// Step 1: Enable USART2 clock in the RCC peripheral
dp.RCC.apb1enr.modify(|_, w| w.usart2en().enabled());
// Step 2: Configure baud rate (assuming 84 MHz clock)
// Baud = fck / (16 * USARTDIV)
// For 115200: USARTDIV = 84_000_000 / (16 * 115200) = 45.57
dp.USART2.brr.write(|w| unsafe {
w.div_mantissa().bits(45)
.div_fraction().bits(9) // 0.5625 * 16 ~ 9
});
// Step 3: Enable transmitter, receiver, and the USART itself
dp.USART2.cr1.modify(|_, w| {
w.te().enabled() // Transmitter enable
.re().enabled() // Receiver enable
.ue().enabled() // USART enable
});
}
fn uart_write_byte(dp: &stm32f411::Peripherals, byte: u8) {
// Wait until the transmit data register is empty
while dp.USART2.sr.read().txe().bit_is_clear() {}
// Write the byte
dp.USART2.dr.write(|w| unsafe { w.dr().bits(byte as u16) });
}