#pac #svd #generator

bin+lib svd2pac

从 SVD 文件生成外设访问包的工具

1 个不稳定版本

0.2.0 2024年6月14日
0.1.0 2023年10月27日

#79 in FFI

MIT 许可证

155KB
3K SLoC

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

svd2pac

从 SVD 文件生成外设访问包的工具

此工具与 svd2rust 的方法非常不同,因为我们有不同的需求,并且与 chiptool 非常相似。

主要需求

  • 寄存器访问应该是非安全的,因为我们将其视为类似于 C FFI。固有的未定义行为应该在驱动层处理,试图在 PAC 中处理安全性通常不会有所帮助或难以使用。(例如,有时写入寄存器位字段的顺序也很重要。关于此主题的讨论见这里 https://github.com/rust-embedded/svd2rust/issues/714)。
  • 没有所有权,因为拥有的寄存器是编写低级驱动程序(LLD)的障碍。无论如何,编写 LLD 需要大量的非安全代码,所有权使得从中断和其他线程访问寄存器更加复杂。LLD 应该提供安全的 API,因为只有它们可以实现所有外设安全使用的逻辑。此外,对于许多外设,将外设划分为更小的单元并不明显,并且取决于用例。
  • 支持 跟踪 寄存器访问,并且可以通过外部库在非嵌入式设备上模拟寄存器。这允许在非嵌入式设备上对使用生成的库的代码进行单元测试。
  • 没有宏。没有宏使调试更容易。
  • PAC 应该对任何其他包没有依赖。
    • 例外:--target=cortex-m。在这种情况下,生成的 PAC 有一些依赖关系,以便在 ARM Cortex Rust 生态系统中使用。
  • 使用关联常量代替 Enum 作为位字段值,以便用户可以轻松创建新值。枚举限制了位字段的可能值,但很多时候 SVD 枚举描述中缺少枚举值。有多个原因
    • 文档/SVD中值过多。
    • 有效值取决于其他寄存器值或条件,因此文档编写者可以选择不在SVD中列出它们。
    • 用户手册编写者的懒惰。(抱歉,但有时会发生这种情况 ;-))

已知限制

  • 通过derivedFrom属性进行继承目前不支持位字段声明。此外,如果父项是数组的一个元素,继承只能引用第一个元素。
  • resetMask标签被忽略
  • protection标签被忽略
  • writeConstraint标签被忽略
  • modifiedWriteValues标签被忽略
  • readAction标签被忽略
  • headerEnumName标签被忽略
  • enumeratedValue中仅支持value标签。不支持无关位isDefault标签

如何安装 & 预先条件

cargo install svd2pac

如果需要自动代码格式化,请安装rustfmt

rustup component add rustfmt

如何使用此工具

查看所有CLI标志的全貌

svd2pac -h

生成不带任何平台特定代码的PAC

svd2pac <your_svd_file> <target directory>

要生成Aurix PACs,请使用

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

默认情况下,svd2pac对svd文件执行严格的验证。

可以通过使用选项--svd-validation-level来放宽或禁用svd验证。

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

显著的CLI标志


选择目标:--target选项

此选项允许进行针对特定目标的代码生成

--target=generic

此目标允许生成与任何架构无关的通用代码。它忽略了nvicPrioBits、fpuPresent、mpuPresent、vendorSystickConfig属性和中断标签。

--target=aurix

除了常规的read/write指令外,还生成具有Aurix平台特定lmst指令支持的PAC。

--target=cortex-m

此选项的目的是生成一个可以与常见的cortex-m框架(如RTIC)一起使用的PAC。开发者可以使用与svd2rust生成的相同API使用的CPU寄存器,但对于外设,他应使用svd2pac的API。这样,他可以重用与CPU相关的代码,并使用svd2pac样式开发外设驱动程序。

generic目标相比的额外功能

  • 导出cortex-m核心外设
  • 外设类型,但现在可以无限制地调用Peripheral::take。
  • 中断表

启用寄存器模拟:--tracing选项

通过使用--tracing CLI标志启用。使用非默认功能标志生成PAC,允许跟踪读取/写入,见下文

环境变量

  • SVD2PAC_LOG_LEVEL设置日志级别(见log
  • SVD2PAC_LOG_STYLE设置是否打印记录样式(见env_logger

如何使用生成的代码

生成器将一个完整的crate输出到提供的文件夹中。在生成的PACs中,所有外设模块都由一个功能门控,因此默认情况下不编译任何外设模块。这样可以加快编译过程。使用features=["all"]启用所有模块的编译。

命名

一些示例展示了命名/大小写,假设在test_svd/simple.xml中的定时器模块

  • TIMER是名为"timer"的外设的模块结构体实例
  • timer::Timer 上一个模块实例的类型
  • TIMER::bitfield_reg() 寄存器访问函数
  • timer::bitfield_reg 模块,包含 "BITFIELD_REG" 寄存器的位字段结构体
  • timer::bitfield_reg::Run 模块,包含 "RUN" 位字段的枚举值
  • timer::bitfield_reg::Run::RUNNING 位字段值常量

示例

注意

以下示例基于测试使用的 test_svd/simple.xml svd。在此示例中,我们主要使用具有少量寄存器的 TIMER 模块,其中之一是

  • SR 一个主要是只读的状态寄存器
  • BITFIELD_REG 这是一个具有多个位字段的寄存器
  • NONBITFIELD_REG,一个没有位字段的寄存器

读取

使用 .read() 函数读取寄存器。它返回一个包含方便访问位字段值的结构的实例。每个位字段都由一个经过优化的结构体表示,实际值可以通过调用 .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 函数接收一个闭包/函数,该闭包/函数被传递给当前寄存器值。闭包必须修改传递的值并返回要写入的值。

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

注意:当调用 set() 函数时,寄存器不会被修改。set() 修改存储在 CPU 中的值并返回修改后的结构体。寄存器只写入一次,写入的值是闭包返回的值。

注意:由于在读取数据之间进行修改,modify() 不是一个原子的操作,可能会受到竞争条件的影响,并且可能会被中断。

写入

可以使用适当的结构的实例来写入寄存器。可以通过调用 .default()(以寄存器的默认值开始)或从先前的寄存器读取/写入中获取结构实例。

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) };

初始化和只写寄存器

.init() 允许具有与 .write() 相同的功能,但它限制为从寄存器的默认值开始。它还可以用作只写寄存器的简写。

传递给 .init() 函数的闭包接收默认值作为输入,并将闭包的返回值写回寄存器。

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)
    })
};

合并所有东西

尤其是读写功能可以合并,例如。

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

原始访问

对于日志记录、从表初始化等用例,可以以纯整数形式读写寄存器。

// 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)) };

修改原子(仅适用于Aurix)

此函数仅适用于Aurix微控制器。它使用 ldmst 指令在寄存器中读取-修改-写入值。此指令在事务结束时才释放总线。因此,它会影响总线上其他主设备。

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

使用 --target aurix 启用Aurix的代码生成

外设数组

使用Rust数组模拟外设的SVD数组。

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

寄存器数组

在模块中将寄存器数组模拟为寄存器结构体的数组。

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)) };
}

位字段数组

位字段数组在寄存器中模拟为位字段结构体的数组。

 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) };

通过传递整数文字写入枚举位字段

值的尺寸不能超过位字段尺寸。在这里,可以从整数创建关联的结构类型,因为位字段结构已实现 From 特性。

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

获取位字段的掩码和偏移量

可以使用 maskoffset 获取单个位字段的掩码和偏移量。返回的掩码对LSB对齐且未移位(即3位宽的字段具有掩码 0x7,与字段的位置无关)。

 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 命令行标志生成PAC时,PAC将生成具有可选功能标志 tracing 的PAC。启用此功能提供以下附加功能

  • 一个接口,可以通过该接口将寄存器访问管道化,使开发人员能够记录对寄存器的访问,甚至直接模拟寄存器。该接口的实现由 regmock-rs 提供。
  • 一个特殊的 RegisterValue 特性,允许从整数构建寄存器的值。
  • 一个额外的 insanely_unsafe 模块,允许读写、只写和只读寄存器(用于在测试中模拟状态)。
  • 一个额外的 reg_name 模块,其中包含物理地址到所有驻留在地址处的寄存器字符串名称的完美哈希映射。

示例

下面展示了如何使用跟踪API的一些简单示例。有关如何使用跟踪功能进行单元测试的完整示例,请参阅 regmock-rs 的文档。

使用跟踪从原始值构造寄存器值

在实现使用跟踪功能的测试时,我们希望在测试期间提供任意数据。

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

使用跟踪从 只写 寄存器读取值

再次用于测试:在测试用例中,我们需要执行与正常代码相反的操作,即我们需要“写入”只读寄存器和“读取”只写寄存器。

跟踪提供了一种后门,允许执行正常代码中不允许的操作。

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

获取特定地址的寄存器名称

为了更好地记录,如果启用了跟踪,则会生成/提供地址到名称的映射。

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

如何在您的 build.rs 中使用

在构建应用程序的过程中,可以通过调用 mainmain_parse_arguments 来生成 PAC。

运行测试

要执行测试,需要将“thumbv7em-none-eabihf”添加为目标。这可以通过以下方式完成:

rustup target add thumbv7em-none-eabihf

要测试 Aurix PAC 的生成,需要安装 Hightec Rust Aurix 编译器并将其设置为默认编译器。《code>build.rs 会自动检测工具链并添加配置选项以启用针对 Aurix 的特定测试。

致谢

模板 common.tera 的一部分来自 代码复制来源的提交链接

许可证:MIT

依赖项

~11–21MB
~283K SLoC