Skip to content

Infineon/svd2pac

Repository files navigation

svd2pac

Tool to generate Peripheral Access Crates from SVD files

Why another PAC generator ?

This tool has a very different approach compared to svd2rust and it is inspired by chiptool.

Motivation

  • Register access should be unsafe because we consider it as C FFI and many times theres HW undefined behaviour that can be solved only at driver level. (e.g. sometimes also the order of writing register bitfields is important. discussion on this topic available here rust-embedded/svd2rust#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.
  • 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.
  • PAC shall have ideally 0 dependencies to any other crates.

chiptool features implemented by svd2pac

  • No owned data structure -> unsafe api that can be used from any interrupts or thread.
  • Register values are represented by structs. No write to register when updating a bitfield.
  • Enumerated values are represented with a struct + associated constants.
  • Essentially same API to access registers that chiptool uses.
  • Logging of register accesses.

Major differences to chiptool

  • Support for modify atomic assembly instruction (only Aurix).
  • Bitfields are structs with setters and getters. This prevents naming conflicts with bitfield names like set_xxx.
  • Bitfield setters consume the register struct and support a fluent api. Lower coding in closure.
  • Support for the "Cluster" tag in SVD files.
  • Support also for 8,16,64 bit size registers.
  • Avoid long type and bitfield names (chiptool concatenates identifiers) by generating modules for registers and svd clusters.
  • No support for SVD transformation.

Limitations

  • Inheritance via derivedFrom attribute is supported only in cluster and register declaration and requires. in case that parent is an element of an array only the first element is supported.
  • 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

How to install & prerequisite

cargo install cargo-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

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:u32 = 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.

TIMER.bitfield_reg().modify_atomic(|f| {
    f.bitfieldenumerated()
        .set(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.

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.

let reg_array = TIMER.arrayreg();
for reg in reg_array {
    let reg_val = unsafe { r.read() };
    let old_val = reg_val.get();
    unsafe { r.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(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.

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).

 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:?}");

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