2. 启动模式与硬件初始化:复位向量、启动模式选择、最小系统初始化
芯片上电后,第一行代码从哪里开始执行?这个问题,我当年刚入行时也困惑了很久。说白了,Bootloader 的起点就是复位向量。今天咱们就把启动流程从头捋一遍。
2.1 复位向量——CPU 的第一口奶
CPU 复位后,会从一个固定地址取第一条指令。这个地址就是复位向量。不同架构的复位向量位置不一样:ARM Cortex-M 系列通常从 0x00000000 开始,而 ARM7/9 则从 0x00000000 或 0xFFFF0000 取向量表。
复位向量表结构(以 Cortex-M 为例)
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x00 | 栈顶指针(MSP) | 复位后 SP 自动加载此值 |
| 0x04 | 复位向量 | 复位后 PC 跳转地址 |
| 0x08 | NMI 向量 | 不可屏蔽中断入口 |
| 0x0C | HardFault 向量 | 硬件错误处理入口 |
我曾经踩过一个坑:某次移植 Bootloader 时,向量表地址没对齐到 512 字节边界,结果芯片复位后直接跑飞。嗯,从那以后我每次都会检查链接脚本里 .isr_vector 段的对齐属性。
// 典型的 Cortex-M 启动文件片段
__Vectors DCD __initial_sp ; 栈顶指针
DCD Reset_Handler ; 复位处理函数
DCD NMI_Handler
DCD HardFault_Handler
...
AREA |.text|, CODE, READONLY
Reset_Handler PROC
EXPORT Reset_Handler
LDR R0, =__main
BX R0
ENDP
2.2 启动模式选择——芯片从哪“醒”过来
现代 SoC 支持多种启动介质。你想想看,同一颗芯片既能从 NOR Flash 启动,也能从 SD 卡启动,甚至能从 UART 下载代码。这背后靠的是启动模式选择引脚(BOOT pins)。
我个人习惯把启动模式分为三类:
- 本地并行启动:NOR Flash、SRAM,特点是速度快、可直接寻址执行(XIP)。
- 串行启动:NAND Flash、SPI Flash、SD 卡,需要先加载一小段代码到内部 SRAM。
- 调试/下载启动:UART、USB、CAN,通常用于工厂烧录或现场升级。
避坑指南:我曾经在一个量产项目中,发现同一批芯片有的能正常启动,有的却卡死。查了两天才发现——启动模式引脚的上拉电阻虚焊,导致芯片随机选择了启动介质。从那以后,我要求硬件工程师必须在 BOOT 引脚上加 10kΩ 的明确上下拉,绝不依赖芯片内部弱上拉。
下面这张图展示了典型的启动流程决策树:
2.3 最小系统初始化——让芯片“活”过来
不管从哪种介质启动,Bootloader 的第一步都是初始化最小系统。我把它总结为三个核心任务:时钟、堆栈、看门狗。
2.3.1 时钟系统初始化
芯片复位后通常运行在内部低速振荡器上,频率可能只有几 MHz。你需要切换到外部晶振或 PLL 倍频到目标频率。
我个人习惯的时钟初始化顺序:
- 等待外部晶振稳定(检查时钟就绪标志)
- 配置 PLL 倍频系数和分频系数
- 等待 PLL 锁定
- 将系统时钟切换到 PLL 输出
- 配置外设总线时钟分频(AHB、APB1、APB2 等)
注意:切换系统时钟源时,一定要先确认新时钟源已经稳定。我见过有人直接写寄存器切换,结果 PLL 还没锁定就切过去了——芯片直接死掉。正确的做法是:先使能目标时钟源,轮询等待就绪标志,然后再切换。
// STM32 时钟初始化示例(简化版)
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;
RCC->CFGR |= RCC_CFGR_PLLMULL9; // 8MHz * 9 = 72MHz
// 3. 使能 PLL 并等待锁定
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
// 4. 配置 AHB/APB 分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = 36MHz
// 5. 切换系统时钟到 PLL
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
2.3.2 堆栈指针设置
堆栈是 C 语言运行的基础。没有正确的堆栈指针,局部变量、函数调用全部会崩溃。
Cortex-M 系列在复位时会自动从向量表加载 MSP(主堆栈指针),但如果你用的是 ARM7/9 这类架构,就需要在汇编启动代码里手动设置 SP。
; ARM7 启动代码中的堆栈初始化
LDR SP, =SVC_STACK_TOP ; 设置管理模式堆栈
MSR CPSR_c, #0x12 ; 切换到 IRQ 模式
LDR SP, =IRQ_STACK_TOP ; 设置 IRQ 模式堆栈
MSR CPSR_c, #0x1F ; 切换回系统模式
经验之谈:堆栈大小怎么定?我一般按实际需求的两倍来分配。比如预估最大调用深度需要 2KB,我就给 4KB。为什么?因为中断嵌套、异常处理都会额外消耗堆栈。我曾经在一个项目中堆栈只给了 1KB,结果跑几天就随机死机——查到最后是堆栈溢出覆盖了全局变量。
2.3.3 看门狗处理
很多芯片复位后看门狗默认是开启的。如果你不在初始化阶段及时喂狗或关闭它,芯片会反复复位,Bootloader 永远跑不完。
我的处理原则是:
- 开发阶段:在最小系统初始化最开头就关闭看门狗,避免干扰调试。
- 量产阶段:在跳转到主应用前再使能看门狗,并确保主应用有喂狗机制。
重要提醒:有些芯片的看门狗一旦使能就无法通过软件关闭(比如某些 PIC 和 AVR 系列)。这种情况下,你必须在启动代码里周期性地喂狗,直到完成所有初始化。我曾经在给一个工业控制器做 Bootloader 时,没注意看门狗是“熔丝位锁定”的,结果每次初始化到一半就被复位——折腾了一整天才发现是看门狗在捣乱。
2.4 最小系统初始化清单
为了方便你对照检查,我把最小系统初始化的关键步骤整理成了一张表:
| 步骤 | 操作 | 常见陷阱 |
|---|---|---|
| 1 | 关闭全局中断 | 中断向量表未就绪时响应中断会跑飞 |
| 2 | 设置堆栈指针 | 堆栈大小不足或地址未对齐 |
| 3 | 初始化时钟系统 | PLL 未锁定就切换时钟源 |
| 4 | 关闭或配置看门狗 | 看门狗熔丝锁定无法软件关闭 |
| 5 | 初始化内存控制器 | SDRAM 时序参数配置错误 |
| 6 | 复制 .data 段并清零 .bss 段 | 链接脚本中段地址定义错误 |
| 7 | 跳转到 main() | 未正确设置主函数入口地址 |
嗯,到这里,启动模式与硬件初始化的核心内容就讲完了。记住一句话:复位向量是起点,启动模式决定路径,最小系统初始化是必经之路。这三者环环相扣,任何一个环节出问题,Bootloader 都跑不起来。