Rust in Everything
基于 embassy-stm32 0.6.0

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。

📝NOTE
没有蠢问题

问: 不要 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 那一处每个程序都会遇到,关掉低速域也绕不开。

💬 关联讨论

发起讨论
加载中...