#field-value #stm32

no-std stm32ral

所有 STM32 微控制器的寄存器访问层

9 个版本 (破坏性)

0.8.0 2022 年 7 月 10 日
0.7.0 2021 年 10 月 3 日
0.6.0 2021 年 10 月 3 日
0.5.0 2021 年 1 月 8 日
0.1.1 2018 年 11 月 15 日

#514 in 嵌入式开发

每月 31 次下载
用于 3 个包 (2 直接)

MIT/Apache

68MB
1.5M SLoC

stm32ral

本项目为所有 STM32 微控制器提供 Rust RAL(寄存器访问层)。

底层数据是通过在 stm32-rs 中的打补丁 SVD 文件生成的。

Version Documentation Build Status License

文档 · 仓库 · 支持的设备 · 示例项目

这是什么?

stm32ral 是一个轻量级寄存器访问层的实验。它提供了对每个寄存器的访问,并提供了一些常量,这些常量定义了寄存器中的字段和可能的字段值。从这个意义上说,它类似于 C 设备头文件。然而,它还提供了一些简单的宏,允许非常容易地访问寄存器,生成的代码非常简单,即使没有启用优化也非常高效。

主要目标是简单性、紧凑性和完整性。您会得到一个模块结构,它包含一个结构体,该结构体按顺序包含每个外设的寄存器,并为您提供大量用于字段宽度、位置和可能值的常量。除此之外没有其他内容,因此它占用的磁盘空间很少,构建速度非常快。它涵盖了所有 STM32 设备的所有寄存器,包括核心 Cortex-M 外设,并旨在尽快包含每个字段的完整枚举值。

请考虑试用它,并做出贡献或留下反馈!

快速示例

use stm32ral::{read_reg, write_reg, modify_reg, reset_reg};
use stm32ral::{rcc, gpio};

// For safe access we have to first `take()` the peripheral instance.
// This only returns Some(Instance) if that instance is not already
// taken; otherwise it returns None. This ensures that no other code can be
// simultaneously accessing the peripheral, which could lead to a race
// condition. There's `release()` to return it. See below for unsafe use.
let gpioa = gpio::GPIOA::take().unwrap();
let rcc = rcc::RCC::take().unwrap();

// Field-level read/modify/write, with either named values or just literals.
// Most of your code will look like this.
modify_reg!(rcc, rcc, AHB1ENR, GPIOAEN: Enabled);
modify_reg!(gpio, gpioa, MODER, MODER1: Input, MODER2: Output, MODER3: Input);
while read_reg!(gpio, gpioa, IDR, IDR3 == High) {
    let pa1 = read_reg!(gpio, gpioa, IDR, IDR1);
    modify_reg!(gpio, gpioa, ODR, ODR2: pa1);
}

// You can also reset whole registers or specific fields
reset_reg!(gpio, gpioa, GPIOA, MODER, MODER13, MODER14, MODER15);
reset_reg!(gpio, gpioa, GPIOA, MODER);

// Whole-register read/modify/write.
// Rarely used but nice to have the option.
let port = read_reg!(gpio, gpioa, IDR);
write_reg!(gpio, gpioa, ODR, 0x12345678);
modify_reg!(gpio, gpioa, MODER, |r| r | (0b10 << 4));

// Or forego the macros and just use the constants yourself.
// The macros above just expand to these forms for you, bringing
// the relevant constants into scope. Nothing else is going on.
let pa1 = (gpioa.IDR.read() & gpio::IDR::IDR1::mask) >> gpio::IDR::IDR1::offset;
gpioa.ODR.write(gpio::ODR::ODR2::RW::Output << gpio::ODR::ODR2::offset);

// Once you're done with a peripheral, you can release it so it is available
// to `take()` again. You can't use `gpioa` after this line.
gpio::GPIOA::release(gpioa);

// For unsafe access, you don't need to first call `take()`, just use `GPIOA`:
unsafe { modify_reg!(gpio, GPIOA, MODER, MODER1: Output) };
// With the `nosync` feature set, this is the only way to access registers.

请参阅 示例项目,以获取一个更完整的示例,该示例应该可以直接构建。

为什么使用 stm32ral?

  • 小巧轻量(总文件大小约为 30MB,压缩后约为 2MB)
  • 简单(仅 4 个宏和大量常量)
  • 编译速度快(构建时间约为 2 秒)
  • 在一个包中涵盖 所有 STM32 设备
  • 通过 cortex-m-rtrt 功能支持,包括中断
  • 通过 cortex-m-rticrtic 功能支持,暴露一个包含所有外设的 device
  • 不会妨碍您
  • 有点类似于您从 C 头文件中习惯的东西

为什么不使用 stm32ral?

  • 仍然是实验性的,API 设计可能会有破坏性更改
  • 不会让您烧毁 CPU 时间
  • 有点类似于您从 C 头文件中习惯的东西

相反,请考虑使用...

  • svd2rust 是从 SVD 文件生成 Cortex-M 设备 crates 的明显选择,并为本项目提供了灵感。
  • stm32-rs 为本 crate 支持的所有 STM32 设备提供了 svd2rust crates,并使用相同的底层修补过的 SVD 文件。
  • TockOS 使用他们的 svd2regs 工具提供了一个用于寄存器访问的不错的 API。
  • Bobbin 对于寄存器访问也有一些很好的想法(见 bobbin-dsl)。

在您自己的 crates 中使用

在您的 Cargo.toml

[dependencies.stm32ral]
version = "0.4.0"
features = ["stm32f405"]

stm32f405 替换为所需的芯片名称。有关完整列表,请参阅 支持设备

然后,在您的代码中

#[macro_use]
extern crate stm32ral;

let gpioa = stm32ral::gpio::GPIOA::take().unwrap();
modify_reg!(stm32ral::gpio, gpioa, MODER, MODER1: Input, MODER2: Output, MODER3: Input);

crate 功能

  • inline-asm:在 cortex_m 依赖项上启用 inline-asm。如果您使用的是支持它的 nightly 编译器,则建议使用。
  • rt:在 cortex_m_rt 依赖项上启用 device,并提供相关的中断链接脚本。大多数用户推荐使用,但如果您想自己处理中断,则可以禁用。
  • doc:在顶级不使用任何设备的情况下使所有设备可见。理想用于生成文档。实际上构建代码时无实际用途。
  • nosync:禁用了所有同步访问(take()/release() 函数)。访问寄存器的唯一方法是使用直接的 unsafe 访问,例如 write_reg!(stm32ral::gpio, GPIOA, MODER, MODER1: Input)。移除所有相关的同步开销,但用户必须确保不会引起竞争条件。“C”模式。如果通过 HAL crate 启用,特别有用,HAL crate 将执行自己的同步,但仍允许用户通过不安全的方式直接访问外设(这也是为什么这是一个“负”功能的原因)。
  • rtfm:为每个设备模块添加一个包含每个设备外设的 InstancePeripherals 结构,并提供一个 steal() 方法以不安全地创建它并获取所有外设;此功能增加了将 stm32ral 作为 cortex-m-rtfm 设备 crate 使用时的兼容性。如果也启用了 nosync,则 Peripherals 结构将为空,并且具有空的 steal() 方法,保持兼容性(但只能直接不安全访问外设)。
  • CPU 特性如 armv7em:从 CPU 核心本身引入外设,相关的一个会自动由设备特性包含。
  • 设备特性:每个支持设备一个,例如,stm32f405。您应精确启用其中一个。

内部结构

stm32ral 的顶层,为每个支持的设备系列都有一个模块,例如 stm32ral::stm32f4。在每个系列内部,都有为每个支持的设备提供的模块,例如 stm32ral::stm32f4::stm32f405。当指定一个设备功能时,该模块内部的所有内容都会在顶层重新导出,因此例如 stm32ral::stm32f4::stm32f405::gpio 也可以通过 stm32ral::gpio 访问。这意味着对于许多设备,您可以简单地更改构建 stm32ral 时使用的功能,而不必更改使用它的任何代码(因为路径将保持不变)。

在每个设备模块内部,都有一个针对每个外设的模块,例如 stm32f405::gpio。在每个外设模块内部,有一个针对每个寄存器的模块,例如 stm32f405::gpio::MODER,在每个寄存器模块内部,有一个针对每个字段的模块,例如 stm32f405::gpio::MODER::MODER15

在每个字段内部都有一个 maskoffset 常量,它们定义了该字段的位掩码以及它在寄存器中的位偏移。每个字段还包含一个 RWRW 模块,它们包含可以从该模块读取、写入或读取+写入的值(映射到 SVD 的 enumeratedValues)。

以下是一个示例

// Equivalent to stm32ral crate root with `--features stm32f405`
pub mod stm32f4 {
    pub mod stm32f405 {
        pub mod gpio {
            pub mod MODER {
                pub mod MODER15 {
                    pub const offset: u32 = 30;
                    pub const mask: u32 = 0b11 << offset;
                    pub mod R {}
                    pub mod W {}
                    pub mod RW {
                        pub const Input: u32 = 0b00;
                        pub const Output: u32 = 0b01;
                        pub const Alternate: u32 = 0b10;
                        pub const Analog: u32 = 0b11;
                    }
                }
            }
        }
    }
}
pub use stm32f4::stm32f405::*;

接下来是 RegisterBlock,这是一个结构体,其中包含该外设所有实例的寄存器。每个寄存器都是 RWRegisterRORegisterWORegister 之一。这些提供 .read().write(value) 方法。

// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`

pub struct RegisterBlock {
    pub MODER: RWRegister<u32>,
    pub OTYPER: RWRegister<u32>,
    // ...
}

然后是 ResetValues 结构体,它为 RegisterBlock 中的每个寄存器都有一个整数字段。每个外设实例都将包含一个相应初始化的 ResetValues 实例,因此您可以

// In reality, you'd use reset_reg!(gpio, gpioa, GPIOA, MODER);
gpioa.MODER.write(stm32ral::gpio::GPIOA::reset.MODER);

存在一个 Instance 结构体,它表示您可以拥有并移动并给出其引用的值,它将指向 RegisterBlock 以实际访问寄存器。对于每个外设实例,只有一个 Instance;您可以使用 take() 获取它,并使用 release() 返回它(见下文)。

// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`

pub struct Instance {
    addr: u32,
}
impl Deref for Instance {
    type Target = RegisterBlock;
    fn deref(&self) -> &RegisterBlock { ... }
}

最后,每个外设实例都有一个模块,包含其 ResetValues,一个 Instance,以及一个 take() 函数来获取它。函数 take() 返回一个 Option<Instance>,如果实例可用则为 Some,否则为 None。这确保了您对外设拥有独占访问权,并且不会与其他安全代码发生数据竞争。您可以通过调用 release() 将实例返回给其他人以使用 take()

// Inside a peripheral module such as `stm32ral::stm32f4::stm32f405::gpio`

pub mod GPIOA {
    pub const reset: ResetValues = ResetValues { ... };
    const INSTANCE: Instance = Instance { ... };
    pub fn take() -> Option<Instance> { ... };
    pub fn release(Instance) { ... };
}

pub mod GPIOB { ... }
pub mod GPIOC { ... }
// and so on

这些实例允许访问相关寄存器

// In reality, you'd use write_reg!(gpio, gpioa, MODER, 0x1234)
// and read_reg!(gpio, gpioa, MODER)
let gpioa = gpio::GPIOA::take().unwrap();
gpioa.MODER.write(0x1234);
let _ = gpioa.MODER.read();

为了在非安全代码中使用方便,每个 RegisterBlock 都有一个直接指向的原始指针

pub const GPIOA: *const RegisterBlock = ...;

这允许在不调用 take() 的情况下直接使用宏(有关宏的详细信息,请参见下文)。

请注意,当启用 nosync 功能时,不会生成 Instancetake()/release() 方法;唯一的访问方式是通过上述描述的原始指针。

作为一个实现细节,许多结构实际上被重构为在家族级别上生存,原始定义被 pub use 语句所取代,以减少在crate源文件中的重复和膨胀。同样,RWRW 模块中的重复值也是如此。

为了简化使用所有常量和寄存器,提供了四个宏。有关详细信息,请查阅文档

在下面的定义中

  • peripheral 是一个外设模块的路径,例如 stm32ral::gpio,
  • instance 是任何指向 RegisterBlock 的表达式:一个 Instance&Instance&RegisterBlock,或 *const RegisterBlock,
  • INSTANCE 是实例模块的路径,例如 stm32ral::gpio::GPIOA,但 peripheral 模块内部的所有内容都将处于作用域内,因此您可以简单地指定 GPIOA,
  • REGISTER 是一个标识符和外围设备中任何寄存器的名称,例如 MODER,它必须存在于 RegisterBlock 的字段中,
  • value 可以是一个字面值或来自寄存器模块的任何命名值。

write_reg!(peripheral,instance, REGISTER,value)

  • 直接将 value 写入 instance.REGISTER
// Set PA3 high (and all other GPIOA pins low).
write_reg!(stm32ral::gpio, gpioa, ODR, 1<<3);

write_reg!(peripheral,instance, REGISTER, FIELD1:value1, FIELD2:value2, ...)

  • value 写入到 FIELD 字段,其余所有字段设置为0(对于一个或多个 FIELD 字段)
  • 可以使用 REGISTER 的任何子模块 FIELD
  • 可以指定任何任意的 value,或者也可以使用 WRW 模块内的任何常数值。
// Set PA3 to Output, PA4 to Analog, PA5 to 0b01 (also Output), everything
// else gets set to 0 (Input).
// (In reality, be careful, as this operation will change the state of the
//  JTAG/SWD pins PA13-15, possibly breaking debugger access.
//  Use modify_reg!() instead.)
write_reg!(stm32ral::gpio, gpioa, MODER, MODER3: Output, MODER4: Analog, MODER5: 0b01);

read_reg!(peripheral,instance, REGISTER)

  • 读取并返回 instance.REGISTER 的当前值
// Get the value of the whole register IDR
let val = read_reg!(stm32ral::gpio, gpioa, IDR);

read_reg!(peripheral,instance, REGISTER, FIELD1, FIELD2, ...)

  • 读取并返回 instance.REGISTERFIELD1FIELD2 等的当前值
// Get the value of IDR2 (masked and shifted down to the LSbits)
let idr2 = read_reg!(stm32ral::gpio, gpioa, IDR, IDR2);

// Get the value of IDR2 and IDR3
let (idr2, idr3) = read_reg!(stm32ral::gpio, gpioa, IDR, IDR2, IDR3);

read_reg!(peripheral,instance, REGISTER, FIELD EXPRESSION)

  • 读取 FIELD 的当前值并返回 FIELD EXPRESSION 的值
  • EXPRESSION 可以是任何在上下文中有意义的令牌树,但通常是类似 == value!= value 的东西
  • write_reg!() 类似,将 FIELDRRW 模块的所有值都引入作用域,因此可以使用 MODER5 == Output 这样的例子
// Busy wait while PA2 is high
while read_reg!(stm32ral::gpio, gpioa, IDR, IDR2 == High) {}

modify_reg!(peripheral,instance, REGISTER, |r| fn(r))

  • 读取 instance.REGISTER 作为 r,然后将 fn(r) 写入其中
  • 任何接受寄存器类型的 lambda 或函数都是可接受的
// Set PA3 high without affecting any other bits
// (in reality, use the BSRR register for this).
modify_reg!(stm32ral::gpio, gpioa, ODR, |reg| reg | (1<<3));

modify_reg!(peripheral,instance, REGISTER, FIELD1: VALUE1, FIELD2: VALUE2, ...)

  • 只更新指定的 FIELD 字段到新的 VALUE,不更改任何其他字段
  • 读取 instance.REGISTER,屏蔽与指定的 FIELD 对应的位,将这些位设置为指定的 VALUE,并写回结果
  • write_reg!() 类似,将 FIELDWRW 模块的所有值都引入作用域
// Set PA3 to Output and PA4 to Analog, but without affecting any other pins.
modify_reg!(stm32ral::gpio, gpioa, MODER, MODER3: Output, MODER4: Analog);

reset_reg!(peripheral,instance, INSTANCE, REGISTER)

  • 将复位值写入 instance.REGISTER
  • 注意您必须指定一个 instance(任何可以解引用到 RegisterBlock 的东西)和 INSTANCE(外设模块中实例模块的名称,例如 peripheral::INSTANCE::reset 必须存在)。
// Reset GPIOA back to reset state, with JTAG/SWD pins on PA13, PA14, PA15.
reset_reg!(stm32ral::gpio, gpioa, GPIOA, MODER);

reset_reg!(peripheral,instance, INSTANCE, REGISTER, FIELD1, FIELD2)

  • 将复位值写入指定的 FIELD 而不更改其他字段
  • 读取 instance.REGISTER,屏蔽指定的 FIELD,将这些位设置为它们的复位值,并写回结果
  • 注意您必须指定一个 instance(任何可以解引用到 RegisterBlock 的东西)和 INSTANCE(外设模块中实例模块的名称,例如 peripheral::INSTANCE::reset 必须存在)。
// Reset PA13, PA14, PA15 to their reset state.
reset_reg!(stm32ral::gpio, gpioa, GPIOA, MODER, MODER13, MODER14, MODER15);

不安全宏使用

为了方便起见,在使用unsafe上下文中的宏时,您无需首先使用take()获取实例,而是可以直接指定它。

// Unsafely and directly access GPIOE.
unsafe { write_reg!(stm32ral::gpio, GPIOE, 0x01010101) };

// The macro is effectively doing this:
unsafe { (*stm32ral::gpio::GPIOE).MODER.write( 0x01010101 ) };

这是因为每个实例也作为*const RegisterBlock存在于外设模块中,宏将它们引入作用域并解引用。

运行时支持与中断

使用rt功能引入cortex-m-rt支持,提供适当的device.x链接脚本和中断定义。

然后,您可以指定自己的中断处理程序

#[interrupt]
fn TIM2() {
    write_reg!(stm32ral::tim2, TIM2, SR, UIF: 0);
}

如果您使用的是cortex-m,则Interrupt枚举是兼容的(它实现了Nr

peripherals.NVIC.enable(stm32ral::Interrupt::TIM2);

安全性

首先,一个安全性前言。这个crate在Rust意义上的安全性是严格避免未定义行为,而不是与嵌入式硬件相关的更普遍的概念。我们使用安全性来避免数据竞争,而不是避免硬件短路:这取决于您。鉴于这个crate的低级特性,它通常(尽管不是总是!)会在unsafe上下文中使用,并设计得尽可能方便。像HALs这样的高级crate似乎是促进安全抽象的更好地方。

与寄存器访问crate相关的两个主要安全性问题是。

第一个是外围设备可能在无关内存上执行操作的可能性,例如DMA外围设备或缓存控制寄存器。这样的寄存器被标记为不安全,读取或写入它们始终需要一个unsafe块或函数。在底层,它们使用UnsafeXXRegister类型而不是常规的XXRegister。由于这样的寄存器可能造成未定义行为,用户必须确保在访问它们时提供自己的安全性保证。

大多数寄存器都不是不安全的,可以直接在安全代码中访问。提供的宏用于字段访问确保值被掩码为字段宽度,但除此之外,没有任何东西阻止安全代码将任意值写入未特别标记为不安全的寄存器。这被视为一个可用性权衡;虽然一些设备寄存器中的某些非法值肯定会引起意外行为,但许多合法值也会如此(例如,Rust无法阻止您设置一个硬接到电源轨的输出为低)。除了少数特定的寄存器外,写入这些值不应该在Rust本身中引起未定义行为,因此我们的权衡是尽可能防止未定义行为,而不试图使用安全性系统强制所有寄存器字段只能用合法值写入。

第二个安全性问题是关于对外围设备的同步访问,这些设备实际上是全局共享内存。安全性问题是关于数据竞争:如果您正在从外围设备读取和写入,但中断例程在半途中想要访问同一外围设备,您将与其竞争,导致未定义行为。

此处提供的解决方案类似于 svd2rust,但更加细致:每个外设实例都有一个 take() -> Option<Instance> 函数,如果实例未被占用,则返回 Some(Instance),如果已被占用,则返回 None。因此,您可以在代码中使用此安全函数来获取实例,并将其(或其引用)传递给任何需要它的其他函数,同时确保没有其他线程(或中断例程)可以在安全代码中访问外设。使用完毕后,您可以调用 release(instance) 使其再次可用于 take()

然而,您通常需要在其他上下文中使用外设,在这些上下文中安全地传递 Instance 可能很麻烦或不可能。此crate提供了一个 *const RegisterBlock,可以为此目的不安全地解引用,并可以直接在宏中作为不安全上下文传递。当使用这些不安全功能时,您必须确保不会发生数据竞争(例如,因为中断只会在您完成外设初始化且之后不再访问它之后触发,或者因为您使用自己的互斥锁以确保独占访问等)。

nosync 功能移除了上述所有同步方法,仅留下不安全访问,减少了开销,并允许某些高级crate提供其自己的安全访问保证,而无需在运行时获取每个外设。

贡献

欢迎贡献!

要添加新命名的寄存器值或修复寄存器错误,请更新 stm32-rs 中的 SVD 文件,然后这些文件将用于此crate。

对此crate的更改主要涉及如何从 SVD 文件生成 RAL。

构建stm32ral

只有当您计划修改 stm32ral.py 或以其他方式更改 stm32ral 的构建方式时,才需要这样做;在另一个Rust项目中使用它并不需要。

首先设置stm32-rs子模块

$ git submodule update --init

现在您应该可以简单地运行make,它将在stm32-rs子模块内部自动运行 make patch,以生成最新的补丁SVD。

$ make

如果它在上游发生变化,请务必更新子模块(git submodule update),以确保您使用的是最新的SVD补丁。

待解决问题

有几个待解决的问题需要进一步思考,但不应造成重大的向后兼容性问题。

更多枚举值,错误的SVD文件

stm32-rs 中,始终在进行添加所有可能的枚举值和修复错误SVD文件的工作。

别名寄存器

别名寄存器没有得到很好的处理。这是Rust寄存器访问的典型问题,因为Rust还没有匿名联合体,这在C中通常是解决方案。

目前,stm32ral合并了别名寄存器,尝试选择合适的合并名称,并将所有字段合并在一起。这对许多别名寄存器来说很方便(例如,定时器中的CCMR),但对于其他寄存器(如OTG_HS_DIEPINT5OTG_HS_DIEPTSIZ7,真是糟糕)则不太方便。

一旦有了匿名联合体,可能会有更好的解决方案。

定时器

大多数外设结合得很好,例如GPIOAGPIOK都是gpio::RegisterBlock的实例。同样,其他大多数外设如USARTSPII2C等也是如此。

定时器没有很好地合并,因为它们的层次结构很复杂:我们不可能只有一个TIM,因为有很多不同的寄存器块,但目前(没有合并)的解决方案也不是最佳。

理想情况下,我们可能会识别出各种类别,例如

  • 高级
  • 基本
  • 通用(类型1)
  • 通用(类型2)
  • 通用(32位)
  • 低功耗
  • 高分辨率

然后我们可以尝试将它们分组在一起。

其他外设

一些其他外设也没有很好地合并,尤其是在STM32F373和STM32F3x8上,一些GPIO外设没有LCKR寄存器,真是令人烦恼。最好的解决方案可能就是假装它有这个寄存器。

发布

  • 在stm32ral.py中更新版本号
  • 更新CHANGELOG.md
  • cd stm32-rs/svd; rm *.svd *.svd.patched; ./extract.sh; cd ../..;
  • make
  • git add -u .
  • git commit -am "vX.X.X"
  • git push origin master
  • git tag -a "vX.X.X" -m "vX.X.X"
  • git push origin vX.X.X

许可

根据您的选择,许可协议为以下之一

除非您明确表示,否则根据Apache-2.0许可证定义,您有意提交的任何贡献,将根据上述方式双重许可,没有任何附加条款或条件。

依赖关系