init 与时钟
主线位置:中断主线的前提。
let p = embassy_stm32::init(Default::default());返回外设令牌之前,EXTI 的所有向量已经在 NVIC 里打开,GPIO 端口的时钟已经开了,rcc::frequency能查到 USART 的输入时钟。本页讲这一次调用里按什么顺序做了什么,以及它对硬件的假设。
flowchart TB cs["critical_section::with"] --> take["Peripherals::take_with_cs"] take --> dbg["DBGMCU"] dbg --> sys["SYSCFG PWR FLASH 使能复位"] sys --> io["PWR CR2 IOSV"] io --> gpio["gpio::init"] gpio --> dma["dma::init"] dma --> exti["exti::init"] exti --> rcc["rcc::init_rcc"] rcc --> td["time_driver::init"] td --> ret["返回 Peripherals"]
lib.rs 743 行,rcc/ 目录 15 个文件 8365 行,其中 L0 / L1 / L4 / L5 / WB / WL / U0 共用的 l.rs 714 行。运行结果都来自 QEMU 11.1.2 的 b-l475e-iot01a,embassy-stm32 打了注释掉 rcc/bd.rs 里 while !cr.read().dbp() {} 的补丁(下文第三节讲这一行),另有一处注明「未打补丁」;实验程序在 exp-embassy/hal-qemu 和 exp-embassy/hal-unpatched,release。宿主是 M2 Max、macOS 27、rustc 1.99.0。
init 的顺序
init 在单核芯片上直接调 init_hw。整个函数体在一个 critical_section::with 里,第一步是 Peripherals::take_with_cs(cs)(重复调用就 panic,见外设单例与 Peri)。之后按芯片族用 cfg 拼出一串步骤,L475 上走到的是(删去了其他芯片的分支):
#[cfg(dbgmcu)]
crate::pac::DBGMCU.cr().modify(|cr| {
// ...
{
cr.set_dbg_sleep(config.enable_debug_during_sleep);
cr.set_dbg_stop(config.enable_debug_during_sleep);
cr.set_dbg_standby(config.enable_debug_during_sleep);
}
// ...
});
// ...
#[cfg(not(any(stm32f1, stm32wb, stm32wl, stm32h7rs)))]
rcc::enable_and_reset_with_cs::<peripherals::SYSCFG>(cs);
#[cfg(not(any(stm32h5, stm32h7, stm32h7rs, stm32wb, stm32wl)))]
rcc::enable_and_reset_with_cs::<peripherals::PWR>(cs);
#[cfg(all(flash, not(any(stm32f2, stm32f4, stm32f7, stm32l0, stm32h5, stm32h7, stm32h7rs))))]
rcc::enable_and_reset_with_cs::<peripherals::FLASH>(cs);
// Enable the VDDIO2 power supply on chips that have it.
// Note that this requires the PWR peripheral to be enabled first.
#[cfg(any(stm32l4, stm32l5))]
{
crate::pac::PWR.cr2().modify(|w| {
// ...
w.set_iosv(config.enable_independent_io_supply);
});
}
// ...
unsafe {
// ...
gpio::init(cs);
// ...
dma::init(
cs,
// ...
#[cfg(bdma)]
config.bdma_interrupt_priority,
// ...
);
#[cfg(feature = "exti")]
exti::init(cs);
rcc::init_rcc(cs, config.rcc);
// ...
// must be after rcc init
#[cfg(feature = "_time-driver")]
crate::time_driver::init(cs);
// ...
}
p
每一步做的事:
- DBGMCU:默认
enable_debug_during_sleep: true,让调试器在WFE睡眠、停机模式下还能连着。代价是功耗,注释写着「May increase power consumption」。 - SYSCFG、PWR、FLASH:用生成的
RccInfo置使能位、拉一下复位位。EXTI 的端口选择寄存器EXTICR在 SYSCFG 里,所以它必须在 EXTI 之前。 PWR_CR2.IOSV:L4 有一部分引脚由独立的 VDDIO2 供电(Config的注释:「some GPIO pins are powered by an auxiliary, independent power supply」),默认打开。gpio::init:调生成的init_gpio(),打开所有 GPIO 端口的时钟(L475 是 8 行ahb2enr写)。端口时钟一律常开,引脚驱动构造时不再管时钟。dma::init:给每个 DMA 通道的中断设Config里的优先级(默认 P0)并在 NVIC 打开,再打开 DMA 控制器时钟。exti::init:用foreach_exti_irq!把这款芯片的每个 EXTI 向量在 NVIC 里打开。之后某条线触发了、而用户没绑那个向量,就会跳进DefaultHandler。rcc::init_rcc:配时钟树,下一节。time_driver::init:开了time-driver-*feature 才有,注释写着「must be after rcc init」:gp16.rs的init_timer用T::frequency()读定时器的输入时钟,算出预分频psc。TIM 驱动本身见时间驱动;本卷 QEMU 实验用的是自己写的 SysTick 驱动,这一步不存在。
这些写操作在 QEMU 的 -d unimp,guest_errors 日志里一一对得上(默认配置,ticker 程序):
Read of unassigned area of PPB: offset 0x42004
Write of unassigned area of PPB: offset 0x42004
stm32l4x5_rcc_write: Side-effects not implemented for APB2RSTR
stm32l4x5_rcc_write: Side-effects not implemented for APB2RSTR
stm32l4x5_rcc_write: Side-effects not implemented for APB1RSTR1
stm32l4x5_rcc_write: Side-effects not implemented for APB1RSTR1
stm32l4x5_rcc_write: Side-effects not implemented for AHB1RSTR
stm32l4x5_rcc_write: Side-effects not implemented for AHB1RSTR
PWR: unimplemented device read (size 4, offset 0x004)
PWR: unimplemented device write (size 4, offset 0x004, value 0x00000200)
PWR: unimplemented device read (size 4, offset 0x000)
PWR: unimplemented device write (size 4, offset 0x000, value 0x00000100)
FLASH: unimplemented device read (size 4, offset 0x000)
FLASH: unimplemented device write (size 4, offset 0x000, value 0x00000100)
FLASH: unimplemented device read (size 4, offset 0x000)
FLASH: unimplemented device write (size 4, offset 0x000, value 0x00000000)
FLASH: unimplemented device read (size 4, offset 0x000)
- PPB 偏移
0x42004是0xE004_2004,DBGMCU 的CR,QEMU 没有这个寄存器,读改写一次。 - 三对复位寄存器写:SYSCFG 在 APB2、PWR 在 APB1、FLASH 在 AHB1,各「置位、清位」一次。
PWR_CR2 = 0x200是IOSV(第 9 位);PWR_CR1 = 0x100是DBP(第 8 位),来自时钟配置里解锁备份域那一步。FLASH_ACR先写0x100(PRFTEN,预取),再写0(LATENCY = 0)。第二次是读改写,QEMU 读回 0,所以PRFTEN也被清掉了;在真实芯片上读回的是0x100。最后一次读是while FLASH.acr().read().latency() != latency {},4 MHz 时latency是 0,循环立刻退出。
DMA 控制器本身一次也没被访问:dma::init 只碰 NVIC 和 RCC。
rcc::Config 与频率表
Config 的 rcc 字段是按芯片族换掉的类型:rcc/mod.rs 用一串 #[cfg_attr(..., path = "...")] 把 mod _version; 指到 f013.rs、f247.rs、l.rs 等文件之一,L4 用的是 l.rs 的 Config。默认值:
pub const fn new() -> Self {
Config {
hse: None,
hsi: false,
msi: Some(MSIRange::RANGE4M),
sys: Sysclk::MSI,
ahb_pre: AHBPrescaler::DIV1,
apb1_pre: APBPrescaler::DIV1,
// ...
pll: None,
// ...
ls: crate::rcc::LsConfig::new(),
// ...
mux: super::mux::ClockMux::default(),
}
}
就是芯片复位后的状态:MSI 4 MHz 当系统时钟,不开 PLL,总线不分频,低速域开 LSI 给 RTC。配置是数据,不是一串方法调用:Config 是 Copy 的普通结构体,在 init 之前改字段,init_rcc 一次性落实。l.rs 的 init 先切回 MSI 4 MHz(避免在 PLL 正被使用时改它),配低速域,开各个振荡器和 PLL,按 hclk1 定 flash 等待周期,切系统时钟,最后用生成的 set_clocks! 宏把算出来的频率写进全局的 CLOCK_FREQS。
查频率有两条路。rcc::clocks(&p.RCC) 要一个 &Peri<RCC>:拿得到这个令牌就说明 init 已经跑过,CLOCK_FREQS 一定已初始化,这是用类型代替运行期检查。rcc::frequency::<T>() 调生成代码里该外设的 frequency(),它读 CCIPR 里的时钟选择器再查表(见芯片数据与代码生成)。QEMU 上默认配置的输出(rcc_default.rs):
Clocks { hclk1: MaybeHertz(4000000), hclk2: MaybeHertz(4000000), hclk3: MaybeHertz(4000000), hse: MaybeHertz(0), hsi: MaybeHertz(0), hsi48: MaybeHertz(0), lse: MaybeHertz(0), lsi: MaybeHertz(32000), msi: MaybeHertz(4000000), pclk1: MaybeHertz(4000000), pclk1_tim: MaybeHertz(4000000), pclk2: MaybeHertz(4000000), pclk2_tim: MaybeHertz(4000000), pll1_p: MaybeHertz(0), pll1_q: MaybeHertz(0), pllsai1_p: MaybeHertz(0), pllsai1_q: MaybeHertz(0), pllsai2_p: MaybeHertz(0), rtc: MaybeHertz(32000), sai1_extclk: MaybeHertz(0), sai2_extclk: MaybeHertz(0), sys: MaybeHertz(4000000) }
USART1 kernel clock Hertz(4000000)
LPTIM1 kernel clock Hertz(4000000)
MaybeHertz(0) 表示那个时钟没开;生成的 frequency() 遇到它会 panic,消息是「peripheral '...' is configured to use the '...' clock, which is not running」。运行中想换时钟用 rcc::reinit,它要 &mut Peri<RCC>,保证没有别人正拿着 clocks() 返回的引用;它只重新配时钟树和时间驱动,已经建好的其他驱动(例如按旧时钟算好 BRR 的 UART)不会跟着改。
HAL 假设硬件会回应
时钟配置里到处是「写一个使能位,然后等就绪位」的循环,没有超时。这在真芯片上合理:振荡器起不来,程序本来也跑不下去。在 QEMU 这种只模拟了一部分寄存器的环境里,这些循环就成了死等。本卷实验撞上了三处:
| 位置 | 等什么 | 为什么不退出 |
|---|---|---|
rcc/bd.rs 的 unlock 里 while !cr.read().dbp() {} | 备份域写保护解除(PWR_CR1.DBP 读回 1) | L475 的 PWR 在 QEMU 里是未实现设备,读出来永远是 0 |
rcc/l.rs 第 380 行 while FLASH.acr().read().latency() != latency {} | flash 等待周期生效 | FLASH 也是未实现设备;只在 latency != 0(hclk1 超过 16 MHz)时卡住 |
rcc/f247.rs 第 165 行 while !RCC.cr().read().hsirdy() {} | HSI 就绪 | F405(netduinoplus2 机器)的 RCC 模型只支持使能和复位,不置 HSIRDY(编辑在 exp-embassy/probe-f4 实测) |
第一处每个程序都会遇到,因为 LsConfig::init 在判断要不要开 LSI / LSE 之前就无条件 unlock();本卷的 QEMU 模板因此注释掉了那一行。第二处用 80 MHz 的 PLL 配置复现(rcc80.rs:MSI 4 MHz、PllPreDiv::DIV1、PllMul::MUL40、PllRDiv::DIV2,sys = PLL1_R):
init with 80 MHz PLL ...
[runq: killed after 4s]
4 秒里 QEMU 记下 1648624 次 FLASH: unimplemented device read (size 4, offset 0x000)。PLL 本身配好了:L4 的 RCC 模型会置 PLLRDY,程序越过了 PLL 的等待,停在后面的 flash 等待周期上。80 MHz 对应 latency = 4,读回的 0 永远不等于 4。
问: 不要 RTC,把 config.rcc.ls 设成 LsConfig::off(),是不是就不会碰 PWR、不用打补丁了?
答: 不行。LsConfig::init 先算 RTC 时钟、拆出 LSE 参数,接着就是 // Disable backup domain write protection 和 unlock();,之后才看 self.lsi。用未打补丁的 0.6.0(exp-embassy/hal-unpatched/src/bin/lsoff.rs):
unpatched 0.6.0, LsConfig::off(), init ...
[runq: killed after 4s]
4 秒里 1530531 次 PWR: unimplemented device read (size 4, offset 0x000)。解锁放在最前面、不看配置;后面读 RCC_BDCR 和期望的配置比较(注释:「backup domain configuration (LSEON, RTCEN, RTCSEL) is kept across resets」),不一致才去改它,而改它需要先解除 DBP 保护。
这几处都不是 bug:参考手册要求等就绪位,真芯片上它们都会在几个时钟周期到几毫秒内退出。它们说明的是 embassy-stm32 的风格:初始化是同步的忙等,没有超时、不返回错误,init 的返回类型就是 Peripherals 而不是 Result。
小结
init在一个临界区里完成:拿令牌、DBGMCU、使能并复位 SYSCFG / PWR / FLASH、GPIO 端口时钟全开、DMA 中断设优先级并打开、所有 EXTI 向量打开、配时钟树、最后起时间驱动。rcc::Config按芯片族换类型(#[cfg_attr(path = ...)]),默认值等于复位状态;配置是普通的Copy结构体,init_rcc一次落实,结果存进CLOCK_FREQS。rcc::clocks要&Peri<RCC>,用令牌证明init跑过;rcc::frequency::<T>()走生成的每外设实现,时钟没开就 panic。- 时钟配置的就绪等待没有超时;QEMU 上 PWR 的
DBP、FLASH 的LATENCY、F4 RCC 的HSIRDY三处永不退出,其中DBP那一处每个程序都会遇到,关掉低速域也绕不开。