svd2rust
svd2rust
Level 17 — Rust A tool that converts CMSIS-SVD (System View Description) files describing embedded microcontroller peripherals into type-safe Rust register access code.
1. Prerequisites
- Cargo CLI — Cargo command line interface.
2. Term Category
Rust Embedded Tooling (SVD-to-PAC code generator tool): svd2rust generator for Peripheral Access Crates (PACs).
3. Explanation
(1) Design Motivation — "Why did we design this?"
Writing memory-mapped I/O register bitmasks manually in microcontroller development is error-prone. One wrong bit-shift offset can corrupt hardware control registers or trigger hard faults.
svd2rust is a code generator CLI tool that reads CMSIS-SVD (System View Description) XML files provided by silicon manufacturers (STMicroelectronics, NXP, Microchip) and automatically emits type-safe Rust Peripheral Access Crates (PACs).
It generates zero-cost abstractions for every peripheral register, ensuring type safety (preventing illegal bit values) and atomic register modifications via ownership safety.
(2) Reality Metaphor
An automated translation machine converting raw electronics factory schematic drawings into pre-built, color-coded physical control switches with built-in safety interlocks.
(3) Rust Code Examples
Short Snippet
// Generate PAC crate from vendor SVD XML file:
// $ svd2rust -i STM32F401.svd
// $ form -s lib.rs -o src/ && rm lib.rs
Fuller Example
// Code structure generated by svd2rust for GPIO peripheral:
// dp.GPIOA.odr.write(|w| w.odr0().set_bit());
pub struct MockRegister {
bits: u32,
}
impl MockRegister {
pub fn new() -> Self { Self { bits: 0 } }
pub fn write<F>(&mut self, f: F) where F: FnOnce(&mut Self) {
f(self);
}
pub fn set_bit(&mut self, bit: u8) {
self.bits |= 1 << bit;
}
pub fn bits(&self) -> u32 {
self.bits
}
}
fn main() {
let mut reg = MockRegister::new();
reg.write(|w| w.set_bit(0));
assert_eq!(reg.bits(), 1);
}
4. Common Mistakes & Pitfalls
Mistake 1: Writing Raw Volatile Pointer Assignments Instead of PAC Register APIs
The mistake: Writing raw memory addresses directly using raw pointers *(0x40020014 as *mut u32) = 0x01.
Why it is wrong: Raw pointer writes bypass Rust type safety, risking incorrect register offset writes and hard fault crashes.
Incorrect:
unsafe { *(0x40020014 as *mut u32) = 1; }
Fix:
dp.GPIOA.odr.modify(|_r, w| w.odr0().set_bit()); // Use svd2rust generated PAC API!
Mistake 2: Using .write() When Preserving Unchanged Register Bits via .modify() is Required
The mistake: Invoking .write() instead of .modify() on multi-field registers.
Why it is wrong: Calling .write() resets all omitted register bitfields to their zero default values, wiping existing register configurations.
Incorrect:
dp.RCC.cr.write(|w| w.hseon().set_bit()); // Erases all other clock settings!
Fix:
dp.RCC.cr.modify(|_r, w| w.hseon().set_bit()); // Preserves existing bits safely!
Mistake 3: Attempting to Mutate Peripheral Registers From Multiple Threads Without Synchronized PAC Take
The mistake: Obtaining multiple mutable instances of peripheral structs (Peripherals::take()).
Why it is wrong: Peripherals::take() uses singleton ownership rules; calling it twice returns None to prevent data race corruption.
Incorrect:
let p1 = Peripherals::take().unwrap(); let p2 = Peripherals::take().unwrap(); // Panics on 2nd call!
Fix:
Call Peripherals::take() once at startup in main() and pass peripherals to tasks via ownership or Mutex!
5. Practice Exercises
Exercise 1: Simulated Microcontroller PAC GPIO Pin Controller Generator
Scenario: Build a simulated svd2rust Peripheral Access Crate (PAC) register pattern controlling a microcontroller GPIO output data register (ODR).
Requirements:
- Define
GpioOdrregister struct with atomicmodifyandwritemethods. - Implement type-safe bit setting
set_pin_high(pin: u8). - Write unit tests verifying bitmask manipulation.
Answer
Implementation
pub struct GpioOdr {
val: u32,
}
pub struct GpioWriter<'a> {
reg: &'a mut GpioOdr,
}
impl GpioOdr {
pub fn new() -> Self {
Self { val: 0 }
}
pub fn read(&self) -> u32 {
self.val
}
pub fn write<F>(&mut self, f: F)
where
F: FnOnce(&mut GpioWriter),
{
self.val = 0; // write resets register
let mut writer = GpioWriter { reg: self };
f(&mut writer);
}
pub fn modify<F>(&mut self, f: F)
where
F: FnOnce(&mut GpioWriter),
{
let mut writer = GpioWriter { reg: self };
f(&mut writer);
}
}
impl<'a> GpioWriter<'a> {
pub fn set_high(&mut self, pin: u8) -> &mut Self {
if pin < 32 {
self.reg.val |= 1 << pin;
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_svd2rust_pac_gpio_write() {
let mut gpioa = GpioOdr::new();
gpioa.write(|w| {
w.set_high(0).set_high(5);
});
assert_eq!(gpioa.read(), (1 << 0) | (1 << 5));
}
#[test]
fn test_svd2rust_pac_gpio_modify() {
let mut gpioa = GpioOdr::new();
gpioa.write(|w| { w.set_high(1); });
gpioa.modify(|w| { w.set_high(3); });
assert_eq!(gpioa.read(), (1 << 1) | (1 << 3));
}
}
Technical Explanation
svd2rustPAC register pattern exposes closure-based.write()and.modify()methods.- Guarantees zero-cost type-safe register manipulation without raw pointer pointer math.
Exercise 2: Hardware Peripheral Singleton Peripherals::take() Enforcer
Scenario: Simulate the svd2rust Peripherals::take() singleton pattern preventing multiple references to raw hardware registers.
Requirements:
- Define
Peripheralsstruct with a static atomicTAKENflag. - Implement
take() -> Option<Peripherals>returningSomeon first call andNoneon subsequent calls. - Test singleton enforcement.
Answer
Implementation
use std::sync::atomic::{AtomicBool, Ordering};
pub struct Peripherals {
pub gpioa_address: usize,
}
static TAKEN: AtomicBool = AtomicBool::new(false);
impl Peripherals {
pub fn take() -> Option<Self> {
if TAKEN.swap(true, Ordering::SeqCst) {
None
} else {
Some(Peripherals { gpioa_address: 0x4002_0000 })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peripherals_singleton() {
// Reset taken flag for unit test isolation
TAKEN.store(false, Ordering::SeqCst);
let p1 = Peripherals::take();
let p2 = Peripherals::take();
assert!(p1.is_some());
assert!(p2.is_none());
}
}
Technical Explanation
Peripherals::take()uses atomic flags to guarantee only one peripheral owner exists in memory.- Prevents data race conditions on hardware registers.
Exercise 3: UART Baud Rate Clock Divider Calculator Register Simulator
Scenario: Simulate an svd2rust UART peripheral register setting baud rate clock dividers.
Requirements:
- Implement
UartBrdrregister. - Calculate clock divider bitfields.
Answer
Implementation
pub struct UartBrdr {
pub brr: u16,
}
impl UartBrdr {
pub fn set_baud(&mut self, clock_hz: u32, baud: u32) {
self.brr = (clock_hz / baud) as u16;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uart_baud_calc() {
let mut brr = UartBrdr { brr: 0 };
brr.set_baud(16_000_000, 115200);
assert_eq!(brr.brr, 138);
}
}
Technical Explanation
- Demonstrates peripheral access crate parameter calculation.
- Wraps bare-metal clock registers safely.
6. Related Terms
- PAC (Peripheral Access Crate) — PAC generation.
embedded-hal— Embedded HAL traits.
7. Key Takeaways
- CLI code generator tool parsing ARM/RISC-V CMSIS-SVD XML files.
- Emits type-safe Peripheral Access Crates (PACs) for microcontroller hardware.
- Provides closure-based
.write()and.modify()APIs for atomic register access. - Enforces hardware peripheral singleton ownership via
Peripherals::take().