2. Bootloader系统架构:分区、启动与底层驱动
好,咱们进入第二章。这一章,我打算聊聊Bootloader的系统架构。说白了,就是你的芯片里,代码该怎么放,上电后怎么跑,以及怎么跟硬件打交道。
很多新手工程师,上来就写代码,结果写到一半发现:哎?这Flash地址怎么重叠了?跳转过去怎么死机了?嗯,这些问题,其实都是架构设计没想清楚。我个人习惯,在写第一行代码之前,先把架构图画在纸上。
2.1 Bootloader与应用程序分区设计
先说说分区。一个典型的车载ECU,Flash里至少住着两户人家:Bootloader和应用程序(App)。有时候还有第三户——备份区,用来做故障恢复。
为什么要分区? 你想想看,刷写程序的时候,万一写到一半断电了,或者网络断了,应用程序被擦了一半,车就趴窝了。Bootloader分区是只读的,或者有特殊保护,这样就算App刷坏了,Bootloader还能启动,等着你重新刷。
分区的大小怎么定?我一般这么干:
- Bootloader区: 通常64KB到128KB。够放启动代码、Flash驱动、CAN驱动和UDS协议栈。别贪大,够用就行。
- 应用程序区: 剩下的空间都给App。具体看你的功能需求。
- 配置/参数区: 存放VIN码、校准数据等。我习惯单独划一个扇区,方便擦写。
- 备份区(可选): 如果空间允许,我会留一个完整的App备份。这样刷失败了,可以从备份区恢复。
举个例子,一个512KB的芯片,我可能会这么分:
| 分区名称 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动、诊断、刷写服务 |
| App | 0x08010000 | 384KB | 主应用程序 |
| 参数区 | 0x08070000 | 16KB | 校准参数、VIN码 |
| 备份区 | 0x08074000 | 128KB | App备份(可选) |
2.2 启动流程与跳转机制
上电后,芯片从复位向量开始执行。这个向量地址,通常就是Flash的起始地址,也就是Bootloader的入口。
启动流程大致是这样的:
- 硬件初始化: 关中断、设置堆栈指针、初始化时钟、配置PLL。
- 检查启动条件: 比如检测一个特定的GPIO引脚电平,或者检查一个RAM标志位。如果条件满足,进入刷写模式;否则,跳转到App。
- 跳转到App: 这是关键一步。不能直接“goto”过去,得重新设置中断向量表。
跳转的代码,我通常这么写:
/* 定义函数指针,指向App的复位向量 */
typedef void (*pFunction)(void);
/* App的起始地址,假设是0x08010000 */
#define APPLICATION_ADDRESS 0x08010000
void JumpToApplication(void)
{
uint32_t appStack; /* App的堆栈指针 */
pFunction appEntry; /* App的入口函数 */
/* 1. 关闭所有中断,防止跳转过程中被中断打扰 */
__disable_irq();
/* 2. 从App起始地址读取堆栈指针(前4字节) */
appStack = *(uint32_t*)APPLICATION_ADDRESS;
/* 3. 从App起始地址读取复位向量(后4字节) */
appEntry = (pFunction)(*(uint32_t*)(APPLICATION_ADDRESS + 4));
/* 4. 设置主堆栈指针 */
__set_MSP(appStack);
/* 5. 跳转! */
appEntry();
}
这里有个细节:中断向量表的重映射。App编译时,它的中断向量表是放在App起始地址的。但芯片上电后,默认的中断向量表基址是0x08000000(Bootloader区)。所以App启动后,第一件事就是告诉CPU:“嘿,我的中断向量表在0x08010000!”
在STM32上,这行代码通常放在App的启动文件里:
/* 在SystemInit()或main()最开始执行 */
SCB->VTOR = APPLICATION_ADDRESS;
如果不做这一步,App里任何一个中断(比如CAN接收中断)都会跑到Bootloader的中断向量表里去查,结果查到一个空指针,直接死机。
2.3 Flash驱动与底层硬件抽象
Flash驱动,是Bootloader里最敏感的部分。为什么?因为你在擦写Flash的时候,代码本身也在Flash里跑。一个不小心,把自己正在执行的代码给擦了,那就“自宫”了。
我的原则: Flash驱动代码,必须放在RAM里执行。或者,至少把擦写函数放到RAM里。
怎么做?很简单,用“RAM函数”属性。在IAR里是这样:
/* 声明一个RAM函数 */
__ramfunc void Flash_EraseSector(uint32_t sectorAddr)
{
/* 解锁Flash */
/* 等待忙标志 */
/* 执行扇区擦除命令 */
/* 等待完成 */
/* 加锁Flash */
}
在GCC里,需要把函数放到特定的section,然后在链接脚本里指定这个section在RAM里。
底层硬件抽象,说白了就是封装一层。不要让上层代码直接操作Flash寄存器。我习惯定义几个接口:
Flash_Init()— 初始化Flash控制器,设置时钟等。Flash_Erase(uint32_t addr, uint32_t size)— 擦除指定地址和大小的区域。Flash_Write(uint32_t addr, uint8_t* data, uint32_t len)— 写入数据。Flash_Read(uint32_t addr, uint8_t* data, uint32_t len)— 读取数据。
这样,上层UDS协议栈调用这些接口时,根本不用关心底层是哪个型号的Flash,是单bank还是双bank。
嗯,这一章的内容差不多就这些。分区设计是骨架,启动跳转是关节,Flash驱动是肌肉。三者配合好了,Bootloader才能跑得稳。下一章,咱们聊聊UDS诊断协议栈怎么集成进来。