Rust in Everything
基于 embassy-stm32 0.6.0

芯片数据与代码生成

主线位置:中断主线的第 1 步之前。PC13 属于 EXTI13、EXTI13 走 EXTI15_10 向量、EXTI15_10 是向量表第 40 项,这些事实都不是手写的:它们来自 stm32-metapac 里 STM32L475VG 的元数据,由 embassy-stm32 的 build.rs 在编译期写成 Rust 代码。本页讲这条流水线;生成出来的外设单例见外设单例与 Peri,生成出来的中断类型见中断绑定。

flowchart TB
  sdata["stm32-data"] --> mpac["stm32-metapac 21.0.0"]
  feat["feature stm32l475vg"] --> mpac
  mpac --> hostmeta["METADATA 主机侧"]
  mpac --> pac["pac.rs 目标侧"]
  hostmeta --> build["build.rs"]
  build --> cfg["83 个 cfg"]
  build --> gen["_generated.rs"]
  build --> mac["_macros.rs"]
  build --> memx["memory.x"]
  gen --> lib["include! 进 lib.rs"]
  mac --> lib
  cfg --> lib
  pac --> lib

build.rs 2762 行,加上 build_common.rs 94 行;本页的实验是交叉编译:exp-embassy/hal-chips 只依赖 embassy-stm32 0.6.0,用 feature 依次选 stm32l475vg、stm32f429zi、stm32g071rb、stm32f103c8,各编一次(release),然后读 target-hal-qemu/<target>/release/build/embassy-stm32-*/ 下的 output 和 out/。宿主是 M2 Max、macOS 27、rustc 1.99.0。

stm32-metapac:1533 份芯片目录

stm32-metapac 21.0.0 解压后 242 MB:src/chips/ 下 1533 个芯片目录(182 MB),src/peripherals/ 下 414 个寄存器块文件(usart_v3.rs、exti_v1.rs、gpio_v2_l478.rs……)。README 说它「has been automatically generated based on data in the stm32-data project」。embassy-stm32 0.6.0 的 Cargo.toml 列了 1525 个芯片 feature(其中 60 个是双核芯片的 -cm4 / -cm7 一类分核 feature),metapac 里另有 8 个 STM32WB0x 目录没有对应的 embassy-stm32 feature。

每个芯片目录三个文件。pac.rs 是寄存器访问层,按地址定义外设常量,并用 #[path] 挑这款芯片用的寄存器块版本:

pub const EXTI: exti::Exti = unsafe { exti::Exti::from_ptr(0x4001_0400usize as _) };
// ...
pub const USART1: usart::Usart = unsafe { usart::Usart::from_ptr(0x4001_3800usize as _) };
// ...
#[path = "../../peripherals/exti_v1.rs"]
pub mod exti;
// ...
#[path = "../../peripherals/gpio_v2_l478.rs"]
pub mod gpio;

metadata.rs 是给 build.rs 读的数据(存储器分区、中断、引脚复用表、DMA 通道、每个外设的 RCC 使能位和时钟来源),开头一行 include!("../metadata_0685.rs") 引用共享的数据块,相同的内容只存一份。device.x 是给 cortex-m-rt 的弱符号表。

metapac 自己的 build.rs 要求恰好一个 stm32* feature,然后用 cargo:rustc-env 告诉 lib.rs 去 include! 哪个目录。embassy-stm32 用了它两次:[dependencies] 里是目标侧的 PAC,[build-dependencies] 里是 default-features = false, features = ["metadata"] 的主机侧版本。芯片 feature 写成 stm32l475vg = [ "stm32-metapac/stm32l475vg" ],对两个同名依赖都生效。target-hal-qemu/release/build/ 下确实有为主机编译的 metapac,它的 output 是:

cargo:rustc-link-search=~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stm32-metapac-21.0.0/src/chips/stm32l475vg
cargo:rustc-env=STM32_METAPAC_PAC_PATH=chips/stm32l475vg/pac.rs
cargo:rustc-env=STM32_METAPAC_METADATA_PATH=chips/stm32l475vg/metadata.rs
cargo:rerun-if-changed=build.rs

于是 embassy-stm32 的 build.rs 里 stm32_metapac::metadata::METADATA 就是 L475 的那一份。

build.rs 产出四样东西

main 先找出唯一的芯片 feature,再按段生成,段之间用 // ======== 注释分开:外设单例、time-driver-* 的处理、中断声明、flash 分区、RccPeripheral 实现、GPIO / DMA 的时钟使能函数、pin_trait_impl!、dma_trait_impl!、触发源、预分频器的乘除法实现、peripheral_interrupts 模块、foreach_* 宏表、gpio_block()、各种常量。最后 rustfmt 排版生成的文件(失败只打日志)。L475 的产物:

产物大小内容
cargo:rustc-cfg83 个外设种类和版本(usart、usart_v3、gpio_v2_l478……)、芯片族(stm32l4、stm32l475、stm32l4x5)、封装和 flash 大小
out/_generated.rs155668 字节,4434 行单例、中断模块、RCC 实现、引脚和 DMA 表,下文细看
out/_macros.rs23581 字节,417 行set_clocks! 和五张 foreach_* 宏表
out/memory.x139 字节链接脚本的 MEMORY,只在开了 memory-x feature 时生成

(字节数和行数随本机的 rustfmt 配置变:rustfmt 读到了 ~/.rustfmt.toml,缩进是 2 空格、use_small_heuristics = "Max"。)memory.x 全文:

MEMORY
{
    FLASH : ORIGIN = 0x08000000, LENGTH = 1024K /* BANK_1 + BANK_2 */
    RAM   : ORIGIN = 0x20000000, LENGTH =   96K /* SRAM */
}

L475 的 SRAM2(32 KB,0x10000000)不在里面,RAM 只取了主 SRAM。

两个 .rs 文件由 lib.rs 包含:include!(concat!(env!("OUT_DIR"), "/_macros.rs")); 在第 16 行,紧跟 mod fmt; 之后,这样所有模块都能用这些宏;_generated.rs 在文件末尾的 pub(crate) mod _generated 里包含,注释是「This must go last, so that it sees all the impl_foo! macros defined earlier」:生成的代码调用的 pin_trait_impl!、dma_trait_impl! 由各驱动模块定义,必须先定义后调用。

_generated.rs 里有什么

按调用次数数(L475):315 条 pin_trait_impl!、94 条 dma_trait_impl!、14 条 dma_channel_impl!,47 个 RccPeripheral 实现,还有 peripherals_definition! / peripherals_struct!(169 项)和 interrupt_mod!(80 个中断)各一次。几段典型的:

RCC。 每个外设一个 SealedRccPeripheral 实现,时钟来源按 RCC_CCIPR 的选择器实时读:

impl crate::rcc::SealedRccPeripheral for peripherals::USART1 {
  fn frequency() -> crate::time::Hertz {
    match crate::pac::RCC.ccipr().read().usart1sel() {
      crate::pac::rcc::vals::Usart1sel::PCLK2 => unsafe {
        unwrap!(
          crate::rcc::get_freqs().pclk2.to_hertz(),
          // ...
        )
      },
      // ... SYS, HSI, LSE
    }
  }
  // ...
  const RCC_INFO: crate::rcc::RccInfo = unsafe {
    crate::rcc::RccInfo::new(
      Some((16u8, 14u8)),
      (24u8, 14u8),
      None,
      #[cfg(feature = "low-power")]
      crate::rcc::StopMode::Stop1,
    )
  };
}

(16, 14) 是「复位寄存器在 RCC 块里的第 16 个字、第 14 位」,即 APB2RSTR(偏移 0x40)的 USART1RST;(24, 14) 是 APB2ENR(0x60)的 USART1EN。驱动里统一调 RccInfo::enable_and_reset_with_cs,按这两个数字算指针去置位,不需要为每个外设写代码。几个外设共用一个使能位时,第三个参数给出 REFCOUNTS 数组里的下标(L475 上这个数组有 2 项)。

引脚复用和 DMA 请求。

pin_trait_impl!(crate::usart::RxPin, USART1, PA10, 7u8, crate::gpio::AfioRemapNotApplicable);
// ...
pin_trait_impl!(crate::usart::TxPin, USART1, PB6, 7u8, crate::gpio::AfioRemapNotApplicable);
pin_trait_impl!(crate::usart::RxPin, USART1, PB7, 7u8, crate::gpio::AfioRemapNotApplicable);
// ...
dma_trait_impl!(crate::usart::TxDma, USART1, DMA1_CH4, 2u8, {});
dma_trait_impl!(crate::usart::RxDma, USART1, DMA1_CH5, 2u8, {});
dma_trait_impl!(crate::usart::TxDma, USART1, DMA2_CH6, 2u8, {});
dma_trait_impl!(crate::usart::RxDma, USART1, DMA2_CH7, 2u8, {});

pin_trait_impl! 为 PB7 实现 usart::RxPin<USART1>,af_num() 返回 7;dma_trait_impl! 为 DMA1_CH4 实现 usart::TxDma<USART1>,request() 返回 2(写进 L4 的 DMA_CSELR)。驱动签名写 rx: Peri<'d, impl RxPin<T>>,于是「PA0 能不能当 USART1 的 RX」也成了编译期问题。rustc 的报错直接指回生成文件(exp-embassy/hal-qemu/errs/wrong_pin.txt):

error[E0277]: the trait bound `PA0: embassy_stm32::usart::RxPin<embassy_stm32::peripherals::USART1>` is not satisfied
    --> src/bin/err_wrong_pin.rs:23:43
     |
  23 |   let _uart = BufferedUart::new(p.USART1, p.PA0, p.PB6, TX.init([0; 32]), RX.init([0; 32]), Irqs, Config::default());
     |               -----------------           ^^^^^ the trait `embassy_stm32::usart::RxPin<embassy_stm32::peripherals::USART1>` is not implemented for `PA0`
     |               |
     |               required by a bound introduced by this call
     |
help: the following other types implement trait `embassy_stm32::usart::RxPin<T>`
...
     |         `PA10`
     |         `PB7`
     |
    ::: ~/exp-embassy/target-hal-qemu/thumbv7em-none-eabihf/release/build/embassy-stm32-2e000fa7a9a50358/out/_generated.rs:3421:1
     |
3421 | pin_trait_impl!(crate::usart::RxPin, USART1, PA10, 7u8, crate::gpio::AfioRemapNotApplicable);
...

它列出了 USART1 合法的 RX 引脚 PA10 和 PB7,各自来自 _generated.rs 的哪一行。

中断归属。 peripheral_interrupts 模块把「外设的某个信号」映射到向量,中断绑定里的 EXTI13 = EXTI15_10 就在这里;GPIO 端口时钟由 init_gpio() 打开,它是 8 行 RCC.ahb2enr().modify(..),一个端口一行。

_macros.rs:五张宏表

macro_rules! foreach_interrupt {
    ($($pat:tt => $code:tt;)*) => {
        macro_rules! __foreach_interrupt_inner {
            $(($pat) => $code;)*
            ($_:tt) => {}
        }
        __foreach_interrupt_inner!((ADC1,adc,ADC,GLOBAL,ADC1_2));
        // ...
        __foreach_interrupt_inner!((DMA1,bdma,DMA,CH1,DMA1_CHANNEL1));
        // ...

调用方写若干个模式,宏对表里每一行都试一遍,匹配上的展开、匹配不上的走 ($_:tt) => {} 变成空。dma_bdma.rs 的 init 用它给所有 bdma 类的中断设优先级并打开,exti/mod.rs 用 foreach_exti_irq! 包一层 foreach_interrupt! 打开所有 EXTI 向量。build.rs 生成这些表的地方有一句注释:「DO NOT ADD more macros like these. These turned to be a bad idea! Instead, make build.rs generate the final code.」

📝NOTE
冷知识:宏表曾经来自 PAC 本身

PR #601(「[part 1/n] Change macrotables to build.rs codegen」,2022-02,首个包含它的发布是 embassy-stm32 v0.1.0)之前,stm32-metapac 直接导出 stm32_metapac::peripherals!、peripheral_versions! 这类宏表,build.rs 里写宏模式去匹配它们。PR 说明列了宏表的问题:难调试;模式写错一个字就「nothing matches and the macro now expands to nothing, silently!」;不会计数、不会去重、不能把结果当列表传给另一个宏,这些情况只好回落到 build.rs 里的 Rust 代码。于是改成 metapac 导出一个 const METADATA: Metadata,build.rs 读结构体、用 quote 生成最终代码。现在的 _macros.rs 由 build.rs 生成,形状仍是当年的宏表,只剩五张;那条「DO NOT ADD」注释是 2023-11 的提交 bc65b8f7e 加上的。

同一份驱动,四个外设版本

build.rs 对每个有寄存器块的外设调 foreach_version_cfg:版本 v2_l478 会依次打开 gpio_v2 和 gpio_v2_l478,所以驱动可以按粗粒度或细粒度写条件。四款芯片的生成结果:

芯片目标cfg 数USARTEXTIGPIODMA_generated.rs单例引脚中断
STM32F103C8thumbv7m54usart_v1exti_v1gpio_v1、afio_f1bdma_v152108 B893743
STM32F429ZIthumbv7em79usart_v2exti_v1gpio_v2dma_v2193976 B20911490
STM32L475VGthumbv7em83usart_v3exti_v1gpio_v2_l478bdma_v2155668 B1698280
STM32G071RBthumbv6m70usart_v4exti_g0gpio_v2bdma_v1、dmamux_v1133393 B1276031

usart/mod.rs(2199 行)里有 74 处 usart_v* 条件。最根本的一处在文件开头,按版本挑寄存器块类型:

#[cfg(not(any(usart_v1, usart_v2)))]
use crate::pac::usart::Lpuart as Regs;
#[cfg(any(usart_v1, usart_v2))]
use crate::pac::usart::Usart as Regs;

v1 / v2 只有一个 SR 和一个 DR,读 DR 顺带清标志;v3 / v4 有分开的 ISR、ICR、TDR、RDR。驱动把差别收进几个小函数:v1 / v2 的 tdr() 返回 DR 的地址,v3 / v4 的 tdr() 返回 TDR 的地址;clear_interrupt_flags 在 v1 / v2 上什么也不做,在 v3 / v4 上写 ICR。v4 多一个波特率预分频器,find_and_set_brr 里只有 v4 的 DIVS 表有 12 项(DIV1 到 DIV256),其他版本只有 (1, ()) 一项。EXTI 也一样:exti_g0 这类有独立的上升沿 / 下降沿挂起寄存器 RPR / FPR,exti_v1 只有一个 PR,GPIO 与 EXTI 里的 cfg_has_rpr_fpr! 就是为此。

F103 那一行的引脚复用不走 AF 号,而是 AFIO 重映射:_generated.rs 里是 66 条 pin_trait_afio_impl!(rustfmt 不排这种带花括号参数的调用,原样留着 pin_trait_afio_impl ! (crate :: usart :: TxPin , USART1 , PA9 , { mapr , set_usart1_remap , AfioRemapBool , [false] });),另有 9 条普通的 pin_trait_impl!。

小结

  • stm32-metapac 是从 stm32-data 生成的一份大数据包:1533 个芯片目录,每个有 pac.rs、metadata.rs、device.x;寄存器块按版本共享,414 个文件。
  • embassy-stm32 把 metapac 用作普通依赖(目标侧 PAC)和构建依赖(主机侧元数据),芯片 feature 同时选中两者。
  • build.rs 产出 cfg 和三个文件;_macros.rs 在 lib.rs 开头包含,_generated.rs 在末尾包含。L475 上 83 个 cfg、4434 行生成代码,具体字节数受本机 rustfmt 配置影响。
  • 引脚复用、DMA 请求号、RCC 使能位、中断归属都是生成的 trait 实现,用错引脚或通道在编译期报错,报错指回 _generated.rs 的具体行。
  • 外设版本 cfg 带前缀层级(gpio_v2 / gpio_v2_l478);USART 一个文件用 usart_v1 到 usart_v4 覆盖 F1、F4、L4、G0。

💬 关联讨论

发起讨论
加载中...