HAL:embassy-stm32
执行器、时间和同步原语都不碰硬件。真正去读写寄存器、接中断的是 HAL。这一部分只讲一个 HAL:embassy-stm32 0.6.0。它是 embassy 仓库里最大的 crate(.rs 共 86129 行,其中 src/ 83273 行、build.rs 2762 行),Cargo feature 里列了 1525 个芯片型号,靠的是两样东西:stm32-metapac 21.0.0 里每款芯片的元数据,和一个在编译期把元数据变成 Rust 代码的 build.rs。和它配套的 embassy-hal-internal 0.5.0(2330 行)放各个 HAL 共用的零件:Peri、peripherals! 宏、中断的类型层、原子环形缓冲。
- 外设单例与 Peri:
init返回一个 0 字节的Peripherals,每个引脚、每个外设是一个零大小的类型;驱动构造函数吃掉Peri<'d, T>,借用检查器保证同一个引脚不会落进两个驱动,reborrow借出去、drop 时还回来,steal是唯一的后门。 - 中断绑定:
bind_interrupts!展开成一个#[no_mangle]的中断向量函数加几行unsafe impl Binding;驱动的_irq: impl Binding<..>参数是一份零大小的证明,忘了绑或绑错向量都在编译期报错;优先级的Priority一律按 4 个优先级位生成。 - 芯片数据与代码生成:芯片 feature 选中
stm32-metapac里的一份元数据,build.rs输出 83 个cfg和_generated.rs(L475 上 4434 行)、_macros.rs、memory.x;同一份 USART 驱动用usart_v1到usart_v4四个 cfg 覆盖 F1、F4、L4、G0。 - init 与时钟:
init在一个临界区里依次使能 SYSCFG / PWR / FLASH、GPIO、DMA、EXTI,再配时钟树、最后起时间驱动;HAL 假设每个「等就绪位」的循环都会退出,QEMU 上三处不退出。 - GPIO 与 EXTI:中断主线的完整版。
ExtiInput每条线一个AtomicWaker,处理函数屏蔽 IMR 位再唤醒,future 看 IMR 位判断就绪;QEMU 上用写SWIER触发,实测每一步的寄存器状态;GPIO + EXTI 的程序比只点灯多 1 KB 代码。 - USART 与 DMA:阻塞、中断 + 环形缓冲、DMA 三种 UART;查清了 QEMU 上 USART 不出字的两个原因(只接受 32 位访问;
ICR写入会清掉TXE),打了补丁后BufferedUart收发都通;DMA 版在 QEMU 上「立刻成功」却什么也没发出去。
flowchart TB data["stm32-metapac"] --> build["build.rs"] build --> gen["_generated.rs"] build --> cfgs["cfg usart_v3 ..."] gen --> periph["Peripherals"] gen --> irqmod["interrupt::typelevel"] init["init"] --> periph periph --> drv["驱动 Peri d T"] irqmod --> bind["bind_interrupts!"] bind --> drv drv --> waker["AtomicWaker"] waker --> exec["执行器"]
前两页回答「驱动凭什么拿到外设和中断」,第三页回答「一份代码怎么适配上千款芯片」,第四页是启动顺序,后两页是两个具体驱动。中断主线(ExtiInput::wait_for_rising_edge().await)在一次中断唤醒里走过一遍概览,这里第 2 页讲 bind_interrupts! 那一头,第 5 页讲驱动和处理函数那一头。embassy-stm32 自带的 TIM 时间驱动在时间驱动;低功耗执行器在各平台的执行器。
本部分的实验有三种:交叉编译(看 OUT_DIR、编译错误、代码体积);QEMU 11.1.2 的 b-l475e-iot01a(STM32L475VG)上运行,embassy-stm32 打了「注释掉 rcc/bd.rs 里 while !cr.read().dbp() {}」这一行补丁;USART 一页另加了自己的补丁,在那一页写明。QEMU 里 TIM、DMA、PWR、FLASH 都没有模型,凡是 QEMU 上的结果都不代表真实硬件。