#pac #svd #generator #low-level #embedded

bin+lib svd2pac

Tool to generate peripheral access crates from SVD files

2 unstable releases

0.3.0 Oct 10, 2024
0.2.0 Jun 14, 2024
0.1.0 Oct 27, 2023

#59 in FFI

Download history 3/week @ 2024-07-29 10/week @ 2024-09-23 12/week @ 2024-09-30 163/week @ 2024-10-07 10/week @ 2024-10-14

173 downloads per month

MIT license

160KB
3.5K SLoC

Tera 1.5K SLoC // 0.0% comments Rust 1.5K SLoC // 0.1% comments

svd2pac

Tool to generate Peripheral Access Crates from SVD files

This tool has a very different approach compared to svd2rust because our requirements are different and are quite similar to chiptool.

Major Requirements

  • Register access should be unsafe because we consider akin to C FFI. Inherent undefined behavior should be dealt with at the driver layers, trying to handle safety in the PAC often does either not help or make it hard to use. (e.g. sometimes also the order of writing register bitfields is important. discussion on this topic available here https://github.com/rust-embedded/svd2rust/issues/714).

  • No ownership because owned registers are an obstacle to writing low level drivers (LLD). Anyway writing LLDs requires a lot of unsafe code and ownership makes it more complex to access registers from interrupts and other threads. LLDs shall present safe APIs because only they can implement all logic for a safe usage of peripherals. Moreover for many peripherals the splitting of peripheral is smaller unit is not obvious and depends on use cases.

  • Support tracing of register accesses and additionally mocking of registers on non-embedded devices through external libraries. This allows the execution unit tests for code that uses the generated libraries on non-embedded devices.

  • No macros. Absence of macros make easier the debugging.

  • PAC shall have 0 dependencies to any other crates.

    • Exception: --target=cortex-m. In this case the generated PAC has some dependencies in order to be usable in ARM Cortex Rust ecosystem.
  • Use associated constants instead of Enum for bitfield values so users can easily create new values. Enumerations constrain the possible values of a bitfield but many times the SVD enum description has missing enumeration values. There are multiple reasons:

  • Too many values for documentation/SVD.

    • Valid values depend on other register values or conditions so documentation writers could decided to not list them in the SVD.
    • Lazyness of user manual writer. (Sorry but it sometimes happens ;-))

Known Limitations

  • Inheritance via derivedFrom attribute is presently not supported for bitfields declaration. Moreover in the case that a parent is an element of an array, inheritance can only refer to the first element.
  • resetMask tag is ignored
  • protection tag is ignored
  • writeConstraint tag is ignored
  • modifiedWriteValues tag is ignored
  • readAction tag is ignored
  • headerEnumName tag is ignored
  • in enumeratedValue only value tag is supported. No support for don't care bits and isDefault tag
  • alternateGroup is ignored therefore it is not possible to have two registers with same name.

How to install & prerequisite

cargo install svd2pac

if automatic code formatting is desired install rustfmt.

rustup component add rustfmt

How to use the tool

Get a full overview for all cli flags:

svd2pac -h

Generate PAC without any platform specific code:

svd2pac <your_svd_file> <target directory>

To generated Aurix PACs use:

svd2pac --target aurix <your_svd_file> <target directory>

By default svd2pac performs strict validation of svd files.

It is possible to relax or disable svd validation by using option --svd-validation-level

svd2pac --svd-validation-level weak <your_svd_file> <target directory>

Notable CLI flags


Select target :--target option

This option allows to have target specific code generation

--target=generic

This target allows generation of generic code that is independent from any architecture. It ignores nvicPrioBits, fpuPresent,mpuPresent, vendorSystickConfig attributes and interrupt tag.

--target=aurix

Generate the PAC with Aurix platform specific lmst instruction support in addition to normal read/write instructions.

--target=cortex-m

The purpose of this option is generating a PAC that can be used with common cortex-m framework as RTIC. Developer can use CPU register with same API generated by svd2rust but for peripheral he shall use the API of svd2pac In this way he can reuse the code related to CPU and develop peripheral driver using svd2pac style.

Extra feature compared to generic target

  • Re-export of cortex-m core peripherals
  • Peripherals type but now it is possible to call Peripheral::take without limitations.
  • Interrupt table

Enable register mocking: --tracing option

Enable with the --tracing cli flag. Generate the PAC with a non-default feature flag to allow for tracing reads/writes, see below

Environment variables

  • SVD2PAC_LOG_LEVEL sets the log level (see log)
  • SVD2PAC_LOG_STYLE sets whether or not to print styles with records (see env_logger)

How to use the generated code

The generator outputs a complete crate into the provided folder. In the generated PACs all peripherals modules are gated by a feature and therefore by default no peripheral modules is compiled. This is speed-up the compilation process. The features=["all"] enable the compilations of all modules.

Naming

Some examples showing naming/case, given the timer module in test_svd/simple.xml:

  • TIMER instance of a module struct for a peripheral called "timer"
  • timer::Timer type of the module instance above
  • TIMER::bitfield_reg() access function for a register
  • timer::bitfield_reg module containing bitfield structs for the "BITFIELD_REG" register
  • timer::bitfield_reg::Run module containing enumeration values for the "RUN" bitfield
  • timer::bitfield_reg::Run::RUNNING bitfield value constant

Examples

Note

The following examples are based on the test_svd/simple.xml svd used for testing. In this example we mostly use a TIMER module with a few registers, among them:

  • SR a status register that is mostly read-only
  • BITFIELD_REG which is a register with multiple bitfields
  • NONBITFIELD_REG, a register without bitfields

Read

A register is read using the .read() function. It returns a struct with convenient functions to access bitfield values. Each bitfield is represented by a struct that is optimized away, the actual values can be retrieved by calling .get()

use test_pac::{timer, TIMER};

// Read register `SR` and check `RUN` bitfield
let status = unsafe { TIMER.sr().read() };
if status.run().get() == timer::sr::Run::RUNNING { /* do something */ }

// Access bitfield directly inline
while unsafe { TIMER.sr().read().run().get() == timer::sr::Run::RUNNING } { /* ... */ }

// Check bitfield with enumeration
// (r# as prefix must be used here since `match` is a rust keyword)
match unsafe { TIMER.sr().read().r#match().get() } {
    timer::sr::Match::NO_MATCH => (),
    timer::sr::Match::MATCH_HIT => (),
    // since .get() returns a struct, match does not recognize
    // an exhaustive match and a wildcard is needed
    _ => panic!("impossible"),
}

// a register might not have a bitfield at all, then we access the value directly
let numeric_value = unsafe { TIMER.prescale_rd().read() };

Modify (read/modify/write)

The modify function takes a closure/function that is passed to the current register value. The closure must modify the passed value and return the value to be written.

use test_pac::{timer, TIMER};

// read `BITFIELD_REG` register, set `BoolRw` to true, `BitfieldRw` to 0x3,
// then write back to register
unsafe {
    TIMER
        .bitfield_reg()
        .modify(|r| r.boolrw().set(true).bitfieldrw().set(0x3))
}

// write with dynamic value and enum
let x: bool = get_some_bool_value();
unsafe {
    TIMER.bitfield_reg().modify(|r| {
        r.bitfieldenumerated()
            .set(timer::bitfield_reg::BitfieldEnumerated::GPIOA_6)
            .boolrw()
            .set(x)
    })
}

Note: The register is not modified when the set() function is called. set() modifies the value stored in the CPU and returns the modified struct. The register is only written once with the value returned by the closure.

Note: modify(), due to doing a read and write with modification of read data in between is not atomic and can be subject to race conditions and may be interrupted by an interrupt.

Write

A register can be written with an instance of the appropriate struct. The struct instance can be obtained from a read by calling .default() (to start off with the register default value) or from a previous register read/write.

use test_pac::{timer, TIMER};

// start with default value, configure some stuff and write to
// register.
let reg = timer::BitfieldReg::default()
    .bitfieldrw()
    .set(1)
    .boolw()
    .set(true);
unsafe { TIMER.bitfield_reg().write(reg) };

/* do some other initialization */

// set `BoolRw` in addition to the settings before and write that
// note that .set() returns a new instance of the BitfieldReg struct
// with the old being consumed
// additional changes could be chained after .set() as above
let reg = reg.boolrw().set(true);
unsafe { TIMER.bitfield_reg().write(reg) };

Initialization & write-only registers

.init() allows for the same functionality as .write(), but it is limited to start with the register default value. It can also be used as a shorthand for write-only registers.

The closure passed to the .init() function gets the default value as input and writes back the return value of the closure to the register.

use test_pac::{timer, TIMER};

// do some initializations, write `BoolW` and `BoolRW` with given values,
// write others with defaults
unsafe {
    TIMER
        .bitfield_reg()
        .init(|r| r.boolw().set(true).boolrw().set(false))
}

// use init also for write-only registers
unsafe {
    TIMER.int().init(|r| {
        r.en()
            .set(timer::int::En::ENABLE)
            .mode()
            .set(timer::int::Mode::OVERFLOW)
    })
};

Combine all the things

Especially the read and write functionality can be combined, e.g.

let status = unsafe { TIMER.bitfield_reg().read() };
if status.boolr().get() {
    let modified = status.boolrw().set(true);
    unsafe { TIMER.bitfield_reg().write(modified) }
}

Raw access

For use cases like logging, initializing from a table, etc. it is possible to read/write registers as plain integers.

// get register value as integer value
let to_log = unsafe { TIMER.sr().read().get_raw() };

// write register with integer value, e.g. read from table
unsafe { TIMER.bitfield_reg().modify(|r| r.set_raw(0x1234)) };

Modify Atomic (only Aurix)

This function is available only for Aurix microcontrollers. It uses the ldmst instruction to read-modify-write a value in a register. This instruction blocks the bus until the end of the transaction. Therefore it affects the other masters on the bus.

use test_pac::{timer, TIMER};
TIMER.bitfield_reg().modify_atomic(|f| {
    f.bitfieldenumerated()
        .set(timer::bitfield_reg::BitfieldEnumerated::GPIOA_0)
        .bitfieldw()
        .set(3)
});

Code generation for Aurix is enabled using --target aurix

Array of peripherals

SVD arrays of peripherals are modeled using Rust arrays.

use test_pac::{UART,uart};
for peri in UART {
    unsafe {
        peri.reg16bitenum().modify(|r| {
            r.bitfield9bitsenum()
                .set(uart::reg16bitenum::Bitfield9BitsEnum::VAL_0)
        })
    };
}

Array of registers

Arrays of registers are modeled as an array of register structs in the module.

use test_pac::*;
let reg_array = TIMER.arrayreg();
for reg in reg_array {
    let reg_val = unsafe { reg.read() };
    let old_val = reg_val.get();
    unsafe { reg.write(reg_val.set(old_val + 1)) };
}

Array of bitfields

Arrays of bitfields are modeled as an array of bitfield structs in the register.

 let mut reg_value = unsafe { TIMER.bitfield_reg().read() };
 for x in 0..2 {
    reg_value = reg_value.fieldarray(x).set(timer::bitfield_reg::FieldArray::FALLING);
 }
 unsafe { TIMER.bitfield_reg().write(reg_value) };

Write an enumerated bitfield by passing an integer literal

The size of value cannot exceed bit field size. Here the associated struct type can be created from the integer, as the From trait implementation is available for the bitfield structure.

use test_pac::{timer, TIMER};
unsafe {
    TIMER.bitfield_reg().modify(|f| {
        f.bitfieldenumerated()
            .set(0.into())
    });
}

Get mask and offset of a bitfield

It is possible to get mask and offset of a single bitfield using mask and offset. The returned mask is aligned to the LSB and not shifted (i.e. a 3-bit wide field has a mask of 0x7, independent of position of the field).

 use test_pac::{timer, TIMER};
 unsafe {
    let register_bitfield = TIMER.bitfield_reg().read().bitfieldr();
    let _offset = register_bitfield.offset();
    let _mask = register_bitfield.mask();
}

Tracing feature

When generating the PAC with the --tracing cli-flag, the PAC is generated with an optional feature flag tracing. Enabling the feature provides the following additional functionalities:

  • an interface where register accesses can be piped though, enabling developers to log accesses to registers or even mock registers outright. An implementaion of that interface is provided by regmock-rs.
  • a special RegisterValue trait that allows constructing values of registers from integers.
  • an additional insanely_unsafe module which allows reading and writing, write-only and read-only registers (intended for mocking state in tests).
  • an additional reg_name module that contains a perfect hash map of physical addresses to string names of all registers that reside at an address.

Examples

Below, some simple examples on how to use the tracing APIs are shown. For a complete example of how to use the tracing features for e.g. unittesting see the documentation of regmock-rs.

Construcing a register value from a raw value with tracing

When implementing tests using the tracing feature we want to be able to provide arbitrary data during those tests.

use pac::common::RegisterValue;
let value = pac::peripheral::register::new(0xC0FFEE);
unsafe{ pac::PERIPHERAL.register().write(value) };

Reading a value from a write-only register with tracing

Again for testing: in a testcase we need to do the exact opposite of what normal code does, i.e. we need to "write" read-only registers and "read" write-only registers.

Tracing provides a backdoor to allow those actions that are not allowed in normal code.

use pac::tracing::insanely_unsafe;
let value = unsafe{ pac::PERIPHERAL.write_only_register().read_write_only() };

Get the names of registers at a specific address

For better logging a map of address to name translation is generated/available if tracing is enabled.

let regs_at_c0ffee = pac::reg_name::reg_name_from_addr(0xC0FFEE);
println!("{regs_at_c0ffee:?}");

How to use in your build.rs

It is possible to generate the PAC during the build of an application by calling main or main_parse_arguments.

Running tests

To execute the tests it is required to add as target "thumbv7em-none-eabihf". This can be done using

rustup target add thumbv7em-none-eabihf

To test the generation of Aurix PAC it is necessary to install Hightec Rust Aurix compiler and select it as default compiler. build.rs detects automatically the toolchain and add the configuration option to enable Aurix specific tests.

Credits

A small portion of template common.tera is copied from Link to commit from where code has been copies

License: MIT

Dependencies

~11–20MB
~278K SLoC