2. 启动流程解剖:单片机从上电到运行App的完整流程

大家好,我是你们的老朋友。今天我们来聊聊单片机启动这件事。很多同学写了好几年代码,却从没认真想过:按下复位键之后,芯片内部到底发生了什么?

说白了,启动流程就是芯片的"出厂设置"。搞懂它,你才能真正掌控Bootloader。我在项目中遇到过不止一次,因为启动流程理解不到位,导致Bootloader死活跳转不到App。嗯,今天咱们就把这层窗户纸捅破。

2.1 上电复位后的第一件事

单片机一上电,内部硬件会自动完成几件事。你想想看,这时候Flash里还是空的吗?不,芯片内部固化的逻辑会先做这些:

  1. 硬件复位:所有寄存器回到默认值,PC指针指向复位向量
  2. 时钟稳定:内部RC振荡器起振,等待时钟稳定(一般几毫秒)
  3. 读取复位向量:从0x0000 0000(或映射地址)取出栈顶指针,从0x0000 0004取出复位中断服务函数地址
  4. 跳转执行:PC跳转到复位中断服务函数,开始执行用户代码

核心要点:单片机永远从复位向量表开始执行。这是硬件写死的,谁也改不了。

我记得刚入行时,一直以为main函数是程序的起点。后来调试一个启动异常的问题,才发现芯片在进main之前,已经跑了上百行启动代码。从那以后,我再也不敢忽略启动文件了。

2.2 中断向量表:芯片的"电话本"

中断向量表是什么?我习惯把它比作一本电话本。每个中断源对应一个号码(中断号),号码后面存着对应的处理函数地址。

当外部中断触发时,硬件自动去查这个"电话本",找到对应的处理函数,然后跳过去执行。整个过程是硬件完成的,不需要软件干预。

偏移地址 中断类型 存储内容
0x0000 0000 栈顶指针 MSP初始值
0x0000 0004 复位中断 Reset_Handler地址
0x0000 0008 NMI中断 NMI_Handler地址
0x0000 000C 硬错误中断 HardFault_Handler地址
... ... ...

注意:中断向量表必须从对齐的地址开始(通常是256字节或512字节对齐)。如果对齐不对,硬件可能取错向量地址,导致程序跑飞。我曾经因为这个原因,排查了整整两天。

2.3 中断向量表重映射:Bootloader的核心技术

问题来了:如果Bootloader在0x0800 0000,App在0x0801 0000,当中断发生时,硬件还是去0x0800 0000查向量表。这不就乱套了吗?

解决办法就是中断向量表重映射。说白了,就是告诉硬件:"嘿,别去老地方查电话本了,去新地址查。"

具体怎么做?以STM32为例,有两种方式:

  • 方式一:设置VTOR寄存器(Cortex-M3/M4/M7)
    直接修改系统控制块中的向量表偏移寄存器,把基地址指向App的向量表起始地址。
  • 方式二:使用内存映射(部分Cortex-M0/M0+)
    通过修改SYSCFG寄存器,将内存区间重新映射。
// STM32F4 中断向量表重映射示例
#define APP_ADDRESS 0x08010000

void jump_to_app(void)
{
    // 1. 关闭全局中断
    __disable_irq();
    
    // 2. 设置向量表偏移
    SCB->VTOR = APP_ADDRESS;
    
    // 3. 从App向量表取出栈顶指针和复位地址
    uint32_t msp = *(uint32_t*)APP_ADDRESS;
    uint32_t reset_addr = *(uint32_t*)(APP_ADDRESS + 4);
    
    // 4. 设置主栈指针
    __set_MSP(msp);
    
    // 5. 跳转到App复位函数
    void (*app_reset)(void) = (void (*)(void))reset_addr;
    app_reset();
}

我的经验:跳转前一定要关闭所有外设中断,并且清理中断标志位。否则跳过去之后,App可能被残留的中断请求搞崩溃。我曾经在量产前才发现这个问题,差点没赶上交付。

2.4 启动模式分析:三种主流方式

不同的芯片,启动方式也不一样。我整理了一张对比表,方便大家理解:

启动模式 典型芯片 向量表位置 适用场景
主Flash启动 STM32、GD32、AT32 0x0800 0000 正常运行模式
系统存储器启动 STM32 0x1FFF 0000 出厂Bootloader(串口下载)
SRAM启动 STM32、NXP LPC 0x2000 0000 调试、快速原型验证

你可能会问:为什么要有这么多启动模式?其实是为了灵活性。比如系统存储器启动,芯片出厂时固化了串口下载程序,即使Flash被锁死,也能通过串口救回来。这个设计,我当年觉得多余,直到有一次把Flash锁死了……嗯,从那以后我再也不敢说它没用了。

2.5 完整启动流程图

下面这张图,是我根据多年经验总结的。从按下复位键到App运行,每一步都标清楚了。建议你收藏起来,以后调试Bootloader时对照着看。

单片机从上电到运行App的完整启动流程 上电复位 硬件初始化(时钟、寄存器) 读取复位向量(0x0000 0004) 跳转到 Reset_Handler 判断是否有Bootloader? 是 执行Bootloader 重映射向量表 → 跳转App 否(直接运行App) 说明:Bootloader负责检查App有效性、更新固件,最后通过重映射向量表跳转到App

2.6 跳转App时的关键细节

流程图画完了,但有几个坑我必须提醒你。这些是我用加班换来的教训:

  • 栈指针必须正确:跳转前一定要从App向量表头部取出栈顶指针,否则进App后第一个函数调用就崩
  • 中断状态要清理:关闭全局中断还不够,要逐个外设去关,确保没有挂起的中断
  • 外设去初始化:Bootloader用过的外设(如串口、定时器),跳转前要恢复到复位状态
  • 系统时钟不要动:如果Bootloader已经配置好时钟,App可以直接用,不用重新配置

曾经我踩过的坑:有一次跳转App后,串口一直往外吐乱码。查了两天,发现是Bootloader里开了串口中断,跳转前没关。App初始化串口时,中断标志位还是置位的,一开中断就触发。从那以后,我写了个deinit_all_peripherals()函数,跳转前强制调用。

2.7 不同架构的启动差异

最后,简单说说不同内核的启动差异。虽然原理相通,但实现细节不同:

内核架构 向量表重映射方式 注意事项
Cortex-M0/M0+ SYSCFG寄存器映射 不支持VTOR,需通过内存重映射实现
Cortex-M3/M4/M7 VTOR寄存器 直接设置SCB->VTOR,简单可靠
Cortex-A系列 MMU页表映射 需要配置MMU,复杂度高
RISC-V mtvec寄存器 设置机器模式向量基地址

我个人建议,如果你刚开始做Bootloader,先从Cortex-M3/M4入手。VTOR方式最直观,不容易出错。等熟悉了,再挑战M0或者RISC-V。