Crate svd2rust

source ·
Expand description

Peripheral API generator from CMSIS-SVD files

An SVD file is an XML file that describes the hardware features of a microcontroller. In particular, it lists all the peripherals available to the device, where the registers associated to each device are located in memory, and what’s the function of each register.

svd2rust is a command line tool that transforms SVD files into crates that expose a type safe API to access the peripherals of the device.

§Installation

$ cargo install svd2rust

§Usage

svd2rust supports Cortex-M, MSP430, RISCV and Xtensa LX6 microcontrollers. The generated crate can be tailored for either architecture using the --target flag. The flag accepts “cortex-m”, “msp430”, “riscv”, “xtensa-lx” and “none” as values. “none” can be used to generate a crate that’s architecture agnostic and that should work for architectures that svd2rust doesn’t currently know about like the Cortex-A architecture.

If the --target flag is omitted svd2rust assumes the target is the Cortex-M architecture.

If using the --generic_mod option, the emitted generic.rs needs to be moved to src, and form commit fcb397a or newer is required for splitting the emitted lib.rs.

§target = cortex-m

When targeting the Cortex-M architecture, svd2rust will generate three files in the current directory:

  • build.rs, build script that places device.x somewhere the linker can find.
  • device.x, linker script that weakly aliases all the interrupt handlers to the default exception handler (DefaultHandler).
  • lib.rs, the generated code.

All these files must be included in the same device crate. The lib.rs file contains several inlined modules and its not formatted. It’s recommended to split it out using the form tool and then format the output using rustfmt / cargo fmt:

$ svd2rust -i STM32F30x.svd

$ rm -rf src

$ form -i lib.rs -o src/ && rm lib.rs

$ cargo fmt

The resulting crate must provide an opt-in rt feature and depend on these crates:

Furthermore, the “device” feature of cortex-m-rt must be enabled when the rt feature is enabled. The Cargo.toml of the device crate will look like this:

[dependencies]
critical-section = { version = "1.0", optional = true }
cortex-m = "0.7.6"
cortex-m-rt = { version = "0.6.13", optional = true }
vcell = "0.1.2"

[features]
rt = ["cortex-m-rt/device"]

§the --reexport-core-peripherals flag

With --reexport-core-peripherals flag enabled peripherals from cortex-m crate be reexported by peripheral access crate.

§target = msp430

MSP430 does not natively use the SVD format. However, SVD files can be generated using the msp430_svd application. Most header and DSLite files provided by TI are mirrored in the repository of msp430_svd. The application does not need to be installed; the msp430gen command below can be substituted by cargo run -- msp430g2553 > msp430g2553.svd from the msp430_svd crate root.

When targeting the MSP430 architecture svd2rust will also generate three files in the current directory:

  • build.rs, build script that places device.x somewhere the linker can find.
  • device.x, linker script that weakly aliases all the interrupt handlers to the default exception handler (DefaultHandler).
  • lib.rs, the generated code.

All these files must be included in the same device crate. The lib.rs file contains several inlined modules and its not formatted. It’s recommend to split it out using the form tool and then format the output using rustfmt / cargo fmt:

$ msp430gen msp430g2553 > msp430g2553.svd

$ xmllint -format msp430g2553.svd --output msp430g2553.svd

$ svd2rust -g --target=msp430 -i msp430g2553.svd

$ rm -rf src

$ form -i lib.rs -o src/ && rm lib.rs

$ mv generic.rs src/

$ cargo fmt

The resulting crate must provide opt-in rt feature and depend on these crates:

The “device” feature of msp430-rt must be enabled when the rt feature is enabled. The Cargo.toml of the device crate will look like this:

[dependencies]
critical-section = { version = "1.0", optional = true }
msp430 = "0.4.0"
msp430-rt = { version = "0.4.0", optional = true }
vcell = "0.1.0"

[features]
rt = ["msp430-rt/device"]

§Other targets

When the target is riscv or none svd2rust will emit only the lib.rs file. Like in the cortex-m case, we recommend you use form and rustfmt on the output.

The resulting crate must provide an opt-in rt feature and depend on these crates:

The *-rt dependencies must be optional only enabled when the rt feature is enabled. The Cargo.toml of the device crate will look like this for a RISC-V target:

[dependencies]
critical-section = { version = "1.0", optional = true }
riscv = "0.9.0"
riscv-rt = { version = "0.9.0", optional = true }
vcell = "0.1.0"

[features]
rt = ["riscv-rt"]

§Peripheral API

To use a peripheral first you must get an instance of the peripheral. All the device peripherals are modeled as singletons (there can only ever be, at most, one instance of any one of them) and the only way to get an instance of them is through the Peripherals::take method, enabled via the critical-section feature on the generated crate.

let mut peripherals = stm32f30x::Peripherals::take().unwrap();
peripherals.GPIOA.odr().write(|w| w.bits(1));

This method can only be successfully called once – that’s why the method returns an Option. Subsequent calls to the method will result in a None value being returned.

let ok = stm32f30x::Peripherals::take().unwrap();
let panics = stm32f30x::Peripherals::take().unwrap();

This method needs an implementation of critical-section. You can implement it yourself or use the implementation provided by the target crate like cortex-m, riscv and *-hal crates. See more details in the critical-section crate documentation.

The singleton property can be unsafely bypassed using the ptr or steal static methods which are available on all the peripheral types. This method is useful for implementing safe higher level abstractions.

struct PA0 { _0: () }
impl PA0 {
    fn is_high(&self) -> bool {
        // NOTE(unsafe) actually safe because this is an atomic read with no side effects
        unsafe { (*GPIOA::ptr()).idr().read().bits() & 1 != 0 }
    }

    fn is_low(&self) -> bool {
        !self.is_high()
    }
}
struct PA1 { _0: () }
// ..

fn configure(gpioa: GPIOA) -> (PA0, PA1, ..) {
    // configure all the PAx pins as inputs
    gpioa.moder.reset();
    // the GPIOA proxy is destroyed here now the GPIOA register block can't be modified
    // thus the configuration of the PAx pins is now frozen
    drop(gpioa);
    (PA0 { _0: () }, PA1 { _0: () }, ..)
}

Each peripheral proxy derefs to a RegisterBlock struct that represents a piece of device memory. Each field in this struct represents one register in the register block associated to the peripheral.

/// Inter-integrated circuit
pub mod i2c1 {
    /// Register block
    #[repr(C)]
    pub struct RegisterBlock {
        cr1: CR1,
        cr2: CR2,
        oar1: OAR1,
        oar2: OAR2,
        dr: DR,
    }
    impl RegisterBlock {
        /// 0x00 - Control register 1
        #[inline(always)]
        pub const fn cr1(&self) -> &CR1 {
            &self.cr1
        }
        /// 0x04 - Control register 2
        #[inline(always)]
        pub const fn cr2(&self) -> &CR2 {
            &self.cr2
        }
        /// 0x08 - Own address register 1
        #[inline(always)]
        pub const fn oar1(&self) -> &OAR1 {
            &self.oar1
        }
        #[doc = "0x0c - Own address register 2"]
        #[inline(always)]
        pub const fn oar2(&self) -> &OAR2 {
            &self.oar2
        }
        #[doc = "0x10 - Data register"]
        #[inline(always)]
        pub const fn dr(&self) -> &DR {
            &self.dr
        }
    }
}

§read / modify / write API

Each register in the register block, e.g. the cr1 field in the I2C struct, exposes a combination of the read, modify, and write methods. Which method exposes each register depends on whether the register is read-only, read-write or write-only:

  • read-only registers only expose the read method.
  • write-only registers only expose the write method.
  • read-write registers expose all the methods: read, modify, and write.

Below shows signatures of each of these methods:

(using I2C’s CR2 register as an example)

impl CR2 {
    /// Modifies the contents of the register
    pub fn modify<F>(&self, f: F)
    where
        for<'w> F: FnOnce(&R, &'w mut W) -> &'w mut W
    {
        ..
    }

    /// Reads the contents of the register
    pub fn read(&self) -> R { .. }

    /// Writes to the register
    pub fn write<F>(&self, f: F)
    where
        F: FnOnce(&mut W) -> &mut W,
    {
        ..
    }
}
impl crate::ResetValue for CR2 {
    type Type = u32;
    fn reset_value() -> Self::Type { 0 }
}

§read

The read method “reads” the register using a single, volatile LDR instruction and returns a proxy R struct that allows access to only the readable bits (i.e. not to the reserved or write-only bits) of the CR2 register:

/// Value read from the register
impl R {
    /// Bit 0 - Slave address bit 0 (master mode)
    pub fn sadd0(&self) -> SADD0_R { .. }

    /// Bits 1:7 - Slave address bit 7:1 (master mode)
    pub fn sadd1(&self) -> SADD1_R { .. }

    (..)
}

Usage looks like this:

// is the SADD0 bit of the CR2 register set?
if i2c1.c2r().read().sadd0().bit() {
    // yes
} else {
    // no
}

§reset

The ResetValue trait provides reset_value which returns the value of the CR2 register after a reset. This value can be used to modify the writable bitfields of the CR2 register or reset it to its initial state. Usage looks like this:

if i2c1.c2r().reset()

§write

On the other hand, the write method writes some value to the register using a single, volatile STR instruction. This method involves a W struct that only allows constructing valid states of the CR2 register.

impl W {
    /// Bits 1:7 - Slave address bit 7:1 (master mode)
    pub fn sadd1(&mut self) -> SADD1_W { .. }

    /// Bit 0 - Slave address bit 0 (master mode)
    pub fn sadd0(&mut self) -> SADD0_W { .. }
}

The write method takes a closure with signature (&mut W) -> &mut W. If the “identity closure”, |w| w, is passed then the write method will set the CR2 register to its reset value. Otherwise, the closure specifies how the reset value will be modified before it’s written to CR2.

Usage looks like this:

// Starting from the reset value, `0x0000_0000`, change the bitfields SADD0
// and SADD1 to `1` and `0b0011110` respectively and write that to the
// register CR2.
i2c1.cr2().write(|w| unsafe { w.sadd0().bit(true).sadd1().bits(0b0011110) });
// NOTE ^ unsafe because you could be writing a reserved bit pattern into
// the register. In this case, the SVD doesn't provide enough information to
// check whether that's the case.

// NOTE The argument to `bits` will be *masked* before writing it to the
// bitfield. This makes it impossible to write, for example, `6` to a 2-bit
// field; instead, `6 & 3` (i.e. `2`) will be written to the bitfield.

§modify

Finally, the modify method performs a single read-modify-write operation that involves one read (LDR) to the register, modifying the value and then a single write (STR) of the modified value to the register. This method accepts a closure that specifies how the CR2 register will be modified (the w argument) and also provides access to the state of the register before it’s modified (the r argument).

Usage looks like this:

// Set the START bit to 1 while KEEPING the state of the other bits intact
i2c1.cr2().modify(|_, w| unsafe { w.start().bit(true) });

// TOGGLE the STOP bit, all the other bits will remain untouched
i2c1.cr2().modify(|r, w| w.stop().bit(!r.stop().bit()));

§enumeratedValues

If your SVD uses the <enumeratedValues> feature, then the API will be extended to provide even more type safety. This extension is backward compatible with the original version so you could “upgrade” your SVD by adding, yourself, <enumeratedValues> to it and then use svd2rust to re-generate a better API that doesn’t break the existing code that uses that API.

The new read API returns an enum that you can match:

match gpioa.dir().read().pin0().variant() {
    gpioa::dir::PIN0_A::Input => { .. },
    gpioa::dir::PIN0_A::Output => { .. },
}

or test for equality

if gpioa.dir().read().pin0().variant() == gpio::dir::PIN0_A::Input {
    ..
}

It also provides convenience methods to check for a specific variant without having to import the enum:

if gpioa.dir().read().pin0().is_input() {
    ..
}

if gpioa.dir().read().pin0().is_output() {
    ..
}

The original bits method is available as well:

if gpioa.dir().read().pin0().bits() == 0 {
    ..
}

And the new write API provides similar additions as well: variant lets you pick the value to write from an enumeration of the possible ones:

// enum PIN0_A { Input, Output }
gpioa.dir().write(|w| w.pin0().variant(gpio::dir::PIN0_A::Output));

There are convenience methods to pick one of the variants without having to import the enum:

gpioa.dir().write(|w| w.pin0().output());

The bits (or bit) method is still available but will become safe if it’s impossible to write a reserved bit pattern into the register:

// safe because there are only two options: `0` or `1`
gpioa.dir().write(|w| w.pin0().bit(true));

§Interrupt API

SVD files also describe the device interrupts. svd2rust generated crates expose an enumeration of the device interrupts as an Interrupt enum in the root of the crate. This enum can be used with the cortex-m crate NVIC API.

use cortex_m::peripheral::Peripherals;
use stm32f30x::Interrupt;

let p = Peripherals::take().unwrap();
let mut nvic = p.NVIC;

nvic.enable(Interrupt::TIM2);
nvic.enable(Interrupt::TIM3);

§the rt feature

If the rt Cargo feature of the svd2rust generated crate is enabled, the crate will populate the part of the vector table that contains the interrupt vectors and provide an interrupt! macro (non Cortex-M/MSP430 targets) or interrupt attribute (Cortex-M or MSP430) that can be used to register interrupt handlers.

§the --reexport-interrupt flag (deprecated)

With --reexport-interrupt flag passed interrupt macro from cortex-m-rt or riscv-rt be reexported by peripheral access crate.

§the --atomics flag

The --atomics flag can be passed to svd2rust to extends the register API with operations to atomically set, clear, and toggle specific bits. The atomic operations allow limited modification of register bits without read-modify-write sequences. As such, they can be concurrently called on different bits in the same register without data races. This flag won’t work for RISCV chips without the atomic extension.

The --atomics-feature flag can also be specified to include atomics implementations conditionally behind the supplied feature name.

portable-atomic v0.3.16 must be added to the dependencies, with default features off to disable the fallback feature.

§the --impl-debug flag

The --impl_debug option will cause svd2rust to generate core::fmt::Debug implementations for all registers and blocks. If a register is readable and has fields defined then each field value will be printed - if no fields are defined then the value of the register will be printed. Any register that has read actions will not be read and printed as (not read/has read action!). Registers that are not readable will have (write only register) printed as the value.

The --impl-debug-feature flag can also be specified to include debug implementations conditionally behind the supplied feature name.

Usage examples:

// These can be called from different contexts even though they are modifying the same register
P1.p1out().set_bits(|w| unsafe { w.bits(1 << 1) });
P1.p1out().clear_bits(|w| unsafe { w.bits(!(1 << 2)) });
P1.p1out().toggle_bits(|w| unsafe { w.bits(1 << 4) });
// if impl_debug was used one can print Registers or RegisterBlocks
// print single register
println!("RTC_CNT {:#?}", unsafe { &*esp32s3::RTC_CNTL::ptr() }.options0);
// print all registers for peripheral
println!("RTC_CNT {:#?}", unsafe { &*esp32s3::RTC_CNTL::ptr() });

§the --impl-defmt flag

The --impl-defmt flag can also be specified to include defmt::Format implementations conditionally behind the supplied feature name.

§the --ident-format and --ident-formats-theme flags

The --ident-format type:prefix:case:suffix (-f) flag can also be specified if you want to change default behavior of formatting rust structure and enum names, register access methods, etc. Passingle multiple flags is supported. Supported cases are snake_case (pass snake or s), PascalCase (pass pascal or p), CONSTANT_CASE (pass constant or c) and leave_CASE_as_in_SVD (pass unchanged or ``).

There are identificator formats by default in the table. Since svd2rust 0.32 defaults have been changed.

IdentifierTypePrefixCaseCase 0.31SuffixSuffix 0.31
field_readerpascalconstantR_R
field_writerpascalconstantW_W
enum_name
enum_read_name
pascalconstant_A
enum_write_namepascalconstantWO_AW
enum_valuepascalconstant
interruptunchangedconstant
peripheral_singletonsnakeconstant
peripheral
register
cluster
pascalconstant
register_specpascalconstantSpec_SPEC
cluster_accessor
register_accessor
field_accessor
enum_value_accessor
snakesnake
cluster_mod
register_mod
peripheral_mod
peripheral_feature
snakesnake

To revert old behavior for field_reader you need to pass flag -f field_reader::c:_R.

Also you can do the same in config file:

[ident_formats.field_reader]
case = constant
suffix = "_R"

To revert old behavior for all identifiers you may pass --ident-formats-theme legacy.

Re-exports§

Modules§

Macros§

Structs§

Enums§

Functions§