4. 启动模式与硬件配置:启动引脚配置、启动介质选择、时钟与PLL初始化、看门狗处理

Bootloader 启动的第一步,不是读 Flash,也不是搬代码。

是硬件先认路。

芯片上电后,CPU 核心并不知道自己该从哪启动。它得先看几个引脚的电平,再决定去 NAND、NOR、SD 卡还是 UART 里找代码。这个过程,就是启动模式选择。我当年第一次调 STM32 的启动,焊好板子发现程序死活跑不起来,折腾了半天——原来是 BOOT0 引脚虚焊了。嗯,从那以后,我画板子都会在启动引脚上多放两个测试点。

4.1 启动引脚配置

大多数嵌入式 SoC 都会留出 2~4 个专用引脚,用来编码启动介质。比如常见的 BOOT0 和 BOOT1,组合起来能选 4 种模式。

BOOT1 BOOT0 启动介质 典型应用场景
X 0 主 Flash(NOR/NAND) 正常产品运行
0 1 系统存储区(BootROM) 出厂烧录 / 固件恢复
1 1 SRAM 调试 / 临时加载
我的习惯:量产板上 BOOT0 直接下拉到 GND,BOOT1 也固定电平。只在开发板上保留跳线帽或拨码开关,方便切换启动源。

为什么这么设计?因为 BootROM 里有一段出厂固化的 loader,它负责从 UART、USB 或 SPI 等接口接收数据并写入 Flash。你想想看,如果芯片 Flash 里是空的,上电后 CPU 总得有个地方能执行代码吧?BootROM 就是那个「最后保底」的地方。

4.2 启动介质选择

不同的启动介质,硬件接口和时序完全不同。Bootloader 在启动初期就要根据引脚状态,初始化对应的控制器。

4.2.1 NAND Flash

  • 容量大、成本低,适合存大固件(比如 Linux 内核)。
  • 缺点是坏块管理复杂,启动时需要跳过坏块。
  • 我踩过坑:某项目用 NAND 启动,第一批板子没问题,第二批换了 Flash 批次,坏块位置变了,Bootloader 直接卡死。后来我强制在 Bootloader 里加了坏块扫描表,才解决。

4.2.2 NOR Flash

  • 支持 XIP(原地执行),代码可以直接在 Flash 里跑,不用搬 RAM。
  • 适合小容量、高实时性场景。
  • 缺点:写入慢、容量做不大。

4.2.3 SD 卡 / eMMC

  • 常见于树莓派、全志、瑞芯微等方案。
  • BootROM 会先读 SD 卡前几个扇区,找到合法的启动镜像头。
  • 注意:SD 卡启动对时序要求高,我曾经因为 SD 卡座接触不良,排查了整整两天。

4.2.4 UART / USB DFU

  • 主要用于开发阶段或产线烧录。
  • BootROM 里有一段很小的下载程序,通过串口或 USB 接收数据。
  • 速度慢,但胜在不需要额外硬件。
核心原则:启动介质的选择,决定了 Bootloader 第一阶段的复杂度。NOR 最简单,NAND 最麻烦,SD 卡居中。

4.3 时钟与 PLL 初始化

芯片上电后,默认跑的是内部低速振荡器(比如 8MHz 或 16MHz)。这个频率跑 Bootloader 还行,但要初始化 DDR、NAND 这些高速外设,就太慢了。

所以 Bootloader 的第一步,往往是配置 PLL,把系统时钟倍频到目标频率。

// 伪代码示例:STM32 系列 PLL 初始化
void SystemClock_Init(void) {
    // 1. 使能 HSE 外部晶振
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));  // 等待稳定

    // 2. 配置 PLL 参数:HSE 8MHz -> PLL 72MHz
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSE;   // PLL 源选 HSE
    RCC->CFGR |= RCC_CFGR_PLLMUL9;      // 8MHz * 9 = 72MHz

    // 3. 使能 PLL
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    // 4. 切换系统时钟到 PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
注意:PLL 配置必须在切换时钟源之前完成,且要等待锁相环稳定。跳过等待标志位,会导致系统时钟不稳定,外设初始化失败。我曾经见过有人为了省几行代码,直接延时替代等待——结果 10 块板里有 3 块跑不起来。

时钟树的结构,我画了一张图帮你理解:

Bootloader 时钟初始化流程 外部晶振 HSE 内部振荡器 HSI 时钟源选择 PLL 倍频 系统时钟 外设时钟 说明: 1. 上电后默认使用 HSI,频率低但启动快。 2. Bootloader 第一步:切换到 HSE,配置 PLL 到目标频率。 3. 切换系统时钟前,必须等待 PLL 锁定标志位。

4.4 看门狗处理

看门狗(Watchdog)是嵌入式系统的「最后一道防线」。系统跑飞了,看门狗超时就会触发复位。

但问题来了:Bootloader 启动阶段,看门狗可能已经在跑了。

为什么?因为有些芯片上电后默认使能了独立看门狗(IWDG),或者前一个程序(比如 APP)开启了窗口看门狗(WWDG),复位后没有关闭。

我曾经踩过的坑:某次升级固件,Bootloader 里要初始化 NAND Flash,耗时比较长。结果看门狗在 Bootloader 里超时了,芯片反复复位,根本进不了 APP。后来我在 Bootloader 入口第一件事就是关掉所有看门狗,等所有初始化完成、进入 APP 后再重新开启。

看门狗的处理策略,我总结成三步:

  1. 立即关闭:Bootloader 入口处,无条件关闭 IWDG 和 WWDG。
  2. 阶段性喂狗:如果 Bootloader 流程很长(比如从 SD 卡加载大镜像),可以在关键步骤后喂一次狗,防止意外复位。
  3. 移交前恢复:跳转到 APP 之前,根据 APP 的需求重新配置看门狗(或者保持关闭,让 APP 自己管)。
// 伪代码:Bootloader 看门狗处理
void Bootloader_Entry(void) {
    // 第一步:关狗
    IWDG->KR = 0xCCCC;  // 停止 IWDG(STM32 写法)
    WWDG->CR = 0;        // 关闭 WWDG

    // 第二步:初始化硬件
    SystemClock_Init();
    UART_Init();
    NAND_Init();

    // 第三步:阶段性喂狗(如果狗没关干净)
    // IWDG->KR = 0xAAAA;  // 喂狗

    // 第四步:加载并跳转 APP
    LoadAndJumpToApp();
}
我的建议:不要把看门狗当成 Bootloader 的依赖。Bootloader 本身应该足够短、足够可靠,不需要看门狗来兜底。看门狗是留给 APP 用的。

4.5 本章小结

启动模式与硬件配置,是 Bootloader 的「地基」。引脚配置错了,芯片找不到启动介质;时钟没配好,外设跑不起来;看门狗没处理,Bootloader 自己先崩了。

说白了,这四件事做扎实了,后面的代码搬运和跳转才有意义。我见过太多人一上来就写 Flash 驱动,结果发现时钟不对、引脚没拉——白忙活。