2. CAN FD基础

做Bootloader升级,说白了就是给ECU装一套「双系统」——一个负责升级的迷你系统,一个负责跑业务的大系统。这两个系统怎么共存、怎么切换、怎么保证不出事,就是架构设计的核心。

我最早做Bootloader时,踩过一个坑:App跑着跑着突然要升级,结果跳转过去发现向量表没改对,直接跑飞。从那以后,我对架构设计就特别较真。今天咱们把分区、启动流程、跳转机制、向量表重映射这四个点彻底讲透。

2.1 分区规划:Flash怎么分才合理?

一个典型的CAN FD Bootloader,Flash分区大概长这样:

区域 起始地址 大小 内容
Bootloader区 0x08000000 32KB 启动代码、升级协议栈、Flash驱动
参数区 0x08008000 4KB 升级标志、版本号、校验值
App区 0x08009000 256KB 应用程序代码
备份区 0x08049000 256KB App备份(可选)

我个人习惯把Bootloader放在最前面,因为MCU上电默认从0x08000000取向量表。参数区紧挨着Bootloader,方便读写。App区从0x08009000开始,这个偏移量要跟App工程的链接脚本对齐。

经验之谈: 备份区不是必须的。如果你的Flash够大,我建议留一份。万一升级到一半断电,还能从备份区恢复。如果Flash紧张,那就靠校验机制兜底。

2.2 启动流程:上电后发生了什么?

MCU上电复位后,Bootloader先跑。流程其实不复杂:

  1. 硬件初始化:关中断、关看门狗、初始化时钟和RAM。
  2. 检查升级标志:读参数区的标志位。如果是0xAA55,说明有升级请求。
  3. 校验App:如果不需要升级,就校验App的CRC。通过则跳转,不通过则留在Bootloader。
  4. 进入升级模式:有升级请求或App校验失败,就初始化CAN FD,等待升级指令。
  5. 跳转到App:一切正常,执行跳转。

嗯,这里要注意:看门狗必须在初始化阶段就喂上。我见过有人把喂狗放在App里,结果Bootloader跑太久,狗叫了,复位——死循环。

核心原则: Bootloader要足够「瘦」。它只做两件事:要么升级,要么跳转。别把业务逻辑塞进来。

2.3 跳转机制:从Bootloader到App的临门一脚

跳转不是简单的goto。你得把MCU的状态「打扫干净」,再交给App。我常用的跳转代码长这样:

typedef void (*AppEntry)(void);

void JumpToApp(uint32_t appAddr)
{
    // 1. 关全局中断
    __disable_irq();

    // 2. 关外设中断(定时器、DMA、CAN等)
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    // 3. 关SysTick
    SysTick->CTRL = 0;

    // 4. 复位所有外设时钟(可选,看芯片)
    RCC->AHB1RSTR = 0xFFFFFFFF;
    RCC->AHB1RSTR = 0;
    // ... 其他总线同理

    // 5. 设置主栈指针
    uint32_t msp = *(uint32_t*)appAddr;
    __set_MSP(msp);

    // 6. 跳转到复位向量
    AppEntry app = (AppEntry)(*(uint32_t*)(appAddr + 4));
    app();
}

这段代码我用了好几年,基本没出过问题。核心就三步:关中断 → 清外设 → 设栈指针 → 跳。

避坑指南: 我曾经在跳转前忘了关DMA,结果App启动时DMA还在搬运数据,直接写飞了RAM。后来我养成了习惯:跳转前把所有外设时钟都复位一遍。

2.4 向量表重映射:App的中断去哪了?

App的向量表不在0x08000000了,它在0x08009000。如果MCU发生中断,它还是去0x08000000找向量表——那不就找错了吗?

解决办法有两个:

  • 硬件重映射:STM32有VTOR寄存器,把它的值改成App的起始地址就行。
  • 软件跳转表:在Bootloader里放一个跳转表,把中断转发到App。

我推荐用硬件重映射,简单可靠。在App的启动代码里加上:

// 在SystemInit或Reset_Handler里
SCB->VTOR = 0x08009000;

就这么一行。但要注意:必须在App启动后第一时间设置。如果App开了中断才改VTOR,那第一个中断就可能跑飞。

一个小技巧: 我习惯在App的链接脚本里定义一个符号,比如__app_base = 0x08009000,然后在代码里用这个符号设置VTOR。这样改分区时,只改链接脚本就行,不用动C代码。

2.5 架构总览:一张图说清楚

下面这张图把整个架构串起来了。你仔细看,每个模块的职责都很清晰:

Bootloader架构总览图 Bootloader架构总览 Flash 分区 Bootloader 0x08000000 | 32KB 参数区 升级标志 App 区 0x08009000 256KB 备份区 256KB(可选) 启动流程 上电复位 硬件初始化 检查升级标志 需升级? 是 进入升级模式 否 跳转到App 关键机制 跳转机制 关中断→清外设 设栈指针→跳转 向量表重映射 VTOR = App基址 或软件跳转表 校验机制 CRC32校验 签名验证

这张图把Flash分区、启动流程、跳转和向量表重映射都串起来了。你仔细看:左侧是静态的Flash布局,中间是动态的启动流程,右侧是支撑整个架构的关键机制。三者缺一不可。

2.6 避坑总结

做Bootloader架构设计,有几个坑我反复踩过,今天一并告诉你:

  • 分区大小要留余量:Bootloader别卡着32KB做,留个4KB给未来扩展。我见过有人把Bootloader塞得满满当当,后来加个功能都加不进去。
  • 升级标志要防误判:参数区上电默认值最好是0xFFFF或0x0000,别跟有效标志冲突。我习惯用0xAA55和0x55AA交替写,防止单bit翻转误触发。
  • 跳转前一定要关中断:这个我说过,但还是要强调。不关中断,跳转后App的中断向量还没设好,一个中断进来就飞了。
  • VTOR设置要趁早:App启动后第一件事就是设VTOR。别等初始化完外设再设,那时候中断可能已经来了。
一句话总结: Bootloader架构设计的核心就四个字——干净跳转。把环境打扫干净,把控制权交给App,别留尾巴。