4. 汇编级启动:复位处理函数、中断向量表重定位、关闭看门狗、初始化系统时钟

好,咱们进入第四章节。说实话,很多做应用层的工程师,一辈子可能都不会碰这些代码。但做 Bootloader,这是你的起跑线。芯片上电后第一条指令从哪里取?CPU 怎么知道自己该跑哪个函数?这些全写在汇编里。

我个人习惯,把汇编级启动叫做“从硬复位到 C 环境的桥梁”。你想想看,上电那一刻,RAM 内容不确定,全局变量没初始化,C 语言的堆栈还没建立。这时候你没法写 int main(void)。必须用汇编先把地基打好。

4.1 复位处理函数:芯片的第一口气

每个 ARM Cortex-M 芯片,上电后硬件自动做两件事:

  1. 从地址 0x00000000 加载栈指针(MSP)
  2. 从地址 0x00000004 加载 PC 指针,跳转到复位向量

所以你的启动文件里,必须有一个叫 Reset_Handler 的函数,并且它的地址要放在向量表的第二个位置。我在项目中遇到过有人把函数名拼错,结果芯片一直跑飞,查了两天才发现是符号没对齐。

核心原则:复位处理函数是上电后第一个执行的软件代码。它负责把芯片从“原始状态”带到“可运行状态”。

一个典型的 Reset_Handler 长这样:

Reset_Handler:
    /* 1. 关闭看门狗 */
    ldr r0, =WDT_BASE
    mov r1, #0
    str r1, [r0, #WDT_CR_OFFSET]

    /* 2. 初始化系统时钟 */
    bl  SystemInit

    /* 3. 搬运 .data 段 */
    ldr r0, =_sdata
    ldr r1, =_edata
    ldr r2, =_la_data
    bl  CopyData

    /* 4. 清零 .bss 段 */
    ldr r0, =_sbss
    ldr r1, =_ebss
    bl  ClearBss

    /* 5. 跳转到 main */
    bl  main
    b   .

嗯,这里要注意顺序。先关看门狗,再配时钟。为什么?因为看门狗一旦启动,你不及时喂狗,它就把你复位了。而时钟初始化往往需要几十微秒,足够看门狗咬人了。

4.2 中断向量表重定位:别让中断跑偏

默认情况下,Cortex-M 的中断向量表固定在 0x00000000。但你的 Bootloader 可能跑在 0x08000000,应用程序跑在 0x08020000。如果不重定位,中断来了 CPU 还是去 0x00000000 查表,那不乱套了吗?

重定位的方法很简单——写 SCB->VTOR 寄存器。我记得第一次做 STM32 的 IAP 升级时,跳转到 APP 后按键没反应,查了半天发现是中断向量表没改。那感觉,就像你搬家了但快递还送到老地址。

避坑指南:我曾经在重定位后忘记设置 __align(512) 对齐。Cortex-M 要求向量表必须按 512 字节对齐,否则 VTOR 写入无效。这个坑踩过一次就忘不了了。

代码实现很简单:

/* 假设 APP 起始地址为 0x08020000 */
#define APP_BASE    0x08020000

void JumpToApp(void)
{
    /* 关闭全局中断 */
    __disable_irq();

    /* 重定位向量表 */
    SCB->VTOR = APP_BASE;

    /* 设置栈指针 */
    __set_MSP(*(uint32_t *)APP_BASE);

    /* 跳转到 APP 的复位向量 */
    void (*app_entry)(void) = (void (*)(void))(*(uint32_t *)(APP_BASE + 4));
    app_entry();
}

这里有个细节:跳转前要关中断,跳转后 APP 自己会重新配置中断。如果你不关,跳转瞬间一个中断进来,CPU 去查新向量表,但 APP 还没初始化外设,直接 HardFault。

4.3 关闭看门狗:别让狗咬了你的启动流程

看门狗这东西,设计初衷是好的——防止程序跑飞。但在 Bootloader 阶段,它就是个麻烦。你正在擦除 Flash,耗时几百毫秒,看门狗超时了,啪,复位。然后重来,又复位。死循环。

不同芯片关看门狗的方式不一样:

芯片系列 关闭方式 注意事项
STM32F1/F4 写 WDT_KR 寄存器 需要连续写两个特定值
NXP LPC 写 WDMOD 寄存器 一旦使能,只能复位关闭
TI Tiva 写 WDTCTL 寄存器 有写保护,需先解锁
GD32 写 FWDGT_CTL 寄存器 与 STM32 兼容但地址不同

警告:有些芯片的独立看门狗(IWDG)一旦使能,即使软件也无法关闭,只能靠硬件复位。遇到这种芯片,你必须在进入 Bootloader 前确保看门狗没被使能,或者在 Bootloader 里定期喂狗。

我个人习惯的做法是:在 Reset_Handler 最开头就关看门狗,一条指令都不要耽误。因为有些 Bootloader 是从休眠或看门狗复位唤醒的,这时候看门狗可能已经在跑了。

4.4 初始化系统时钟:给芯片一颗强劲的心脏

时钟是芯片的心脏。默认情况下,大部分芯片上电后使用内部低速振荡器(比如 HSI 8MHz),跑得慢不说,精度也差。你要做 Bootloader,至少得把时钟配到芯片的主频,不然 Flash 擦写慢得让人着急。

时钟初始化的典型步骤:

  1. 等待当前时钟稳定——切换时钟源前,确保目标源已经就绪
  2. 配置 PLL 倍频系数——根据外部晶振和目标频率计算
  3. 使能 PLL 并等待锁定——PLL 需要几十微秒才能稳定输出
  4. 配置 Flash 等待周期——频率越高,Flash 读取需要插入等待周期
  5. 切换系统时钟到 PLL——最后一步,完成切换

我记得有一次做项目,芯片跑 168MHz,但 Flash 等待周期设成了 3 个周期(应该设 5 个)。结果程序偶尔跑飞,查了三天,最后用逻辑分析仪看到 Flash 读取时序不对。说白了,时钟频率和 Flash 速度要匹配,不然数据读出来就是错的。

一个简化的时钟配置代码:

void SystemInit(void)
{
    /* 使能 HSE 外部晶振 */
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));

    /* 配置 PLL: HSE 8MHz -> 168MHz */
    RCC->PLLCFGR = (8 << 0)  |   /* PLL_M = 8 */
                   (336 << 6) |  /* PLL_N = 336 */
                   (2 << 16)  |  /* PLL_P = 4 (168MHz) */
                   (4 << 24);    /* PLL_Q = 7 (USB 48MHz) */

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

    /* 配置 Flash 等待周期 */
    FLASH->ACR = FLASH_ACR_LATENCY_5WS | FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN;

    /* 切换系统时钟到 PLL */
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

关键点:时钟切换后,一定要等待状态寄存器确认切换完成。我见过有人不检查 SWS 位,结果系统还在跑 HSI,外设时序全乱。

4.5 本章知识结构图

下面这张图把汇编级启动的四个核心步骤串起来了。你可以把它当作 Bootloader 启动的“路线图”。

汇编级启动流程结构图 汇编级启动核心流程 芯片上电 / 复位 步骤1:复位处理函数 从向量表加载 MSP 和 PC 步骤2:关闭看门狗 防止启动过程中被复位 步骤3:初始化系统时钟 配置 PLL → 切换主频 步骤4:中断向量表重定位 写 SCB->VTOR 寄存器 跳转到 C 环境 main() 注意顺序 先关看门狗 再配时钟 最后重定位向量表 常见陷阱 VTOR 未 512 对齐 跳转前未关中断 Flash 等待周期不足

这张图把四个步骤串成了流水线。你仔细看,每一步都依赖上一步的结果。关看门狗是为了让后续操作不被中断;配时钟是为了让 Flash 操作跑得快;重定位向量表是为了让中断能找到新家。一步错,步步错。

4.6 写在汇编与 C 之间

说实话,汇编级启动这部分,很多工程师觉得“反正有厂家提供的启动文件,我不需要自己写”。这话对了一半。做应用开发确实不用动,但做 Bootloader,你迟早要面对这些问题:

  • 芯片上电默认从 Flash 启动,但你要从 RAM 运行代码怎么办?
  • 双 Bank 启动,怎么在汇编里判断从哪个 Bank 启动?
  • 低功耗唤醒后,时钟配置和上电复位不一样,怎么区分?

这些问题的答案,都藏在你对复位处理函数、向量表、看门狗和时钟初始化的理解里。把这一章吃透,后面的章节你会走得很顺。