1. 项目概述
在嵌入式系统开发中,程序升级是一个永恒的话题。想象一下,你开发的设备已经部署在客户现场,突然发现一个关键bug需要修复,或者需要增加新功能。如果每次都要把设备拆下来用烧录器更新,那简直是工程师的噩梦。这就是为什么我们需要Bootloader技术。
我最近在N32G455芯片上实现了一个基于串口的Bootloader方案,整个过程踩了不少坑,也积累了一些实战经验。这个方案特别适合以下场景:
- 设备安装位置难以触及(比如高空或密闭环境)
- 批量部署的设备需要统一升级
- 需要频繁迭代功能的开发阶段
2. Bootloader核心原理解析
2.1 单片机启动机制
要理解Bootloader,首先要明白单片机的启动过程。当MCU上电时,它会从Flash的起始地址(通常是0x08000000)开始执行指令。传统做法是把用户程序直接放在这个位置,但这样就没法实现远程升级了。
Bootloader的聪明之处在于:它在Flash起始位置放一个小型引导程序,这个程序负责检查是否需要升级,如果需要就接收新程序并写入Flash的其他区域,最后跳转到新程序执行。
2.2 HEX文件结构解析
HEX文件是Intel定义的一种标准格式,它包含以下关键信息:
- :BBAAAATT[DDDD...]CC
- BB:字节数
- AAAA:地址
- TT:记录类型(00=数据,01=文件结束,04=扩展线性地址)
- DD:数据
- CC:校验和
比如这个HEX记录:
:1000000000400020210000083502000839020008B1
表示在地址0x00000000处写入16字节数据,内容是00400020210000083502000839020008
2.3 内存布局设计
合理的Flash分区是Bootloader稳定的关键。以N32G455的256KB Flash为例,我的分区方案是:
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x08000000-0x08007FFF | 32KB | Bootloader区域 |
| 0x08008000-0x0803FFFF | 224KB | 用户程序区域 |
| 0x0807F800-0x0807FFFF | 2KB | 配置参数区 |
注意:Bootloader区域要留足够空间,建议至少16KB。同时要确保用户程序链接脚本中的ROM起始地址与这个分区一致。
3. 关键实现步骤
3.1 Bootloader程序设计
3.1.1 初始化流程
c复制void Bootloader_Init(void)
{
SystemInit(); // 系统时钟初始化
GPIO_Config(); // 用于升级指示的LED引脚
USART_Init(115200); // 串口通信
IWDG_Init(7, 2046); // 看门狗,超时约1秒
// 检查升级标志位
if(FLASH_ReadWord(UPGRADE_FLAG_ADDR) == 0x5A5A5A5A)
{
StartUpgradeProcess();
}
else
{
JumpToApp();
}
}
3.1.2 升级协议设计
我设计了一个简单的文本协议,方便调试:
code复制[PC] -> "UPGRADE_START"
[MCU] <- "READY"
[PC] -> "FILE_SIZE=123456"
[MCU] <- "SIZE_OK"
[PC] -> 发送HEX文件数据
[MCU] <- "WRITE_OK" after each block
...
[PC] -> "UPGRADE_END"
[MCU] <- "SUCCESS"
实际项目中建议使用二进制协议提高效率,比如:
- 0xA1: 开始升级
- 0xA2: 数据块
- 0xA3: 结束升级
- 每个数据包添加CRC校验
3.2 Flash操作关键代码
3.2.1 Flash擦除
c复制void Flash_Erase(uint32_t start_addr, uint32_t size)
{
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_ALL_ERRORS);
uint32_t sector_start = start_addr - FLASH_BASE;
uint32_t sector_end = sector_start + size;
for(uint32_t i = sector_start; i < sector_end; i += FLASH_SECTOR_SIZE)
{
FLASH_ErasePage(i);
while(FLASH_GetStatus() != FLASH_COMPLETE);
}
FLASH_Lock();
}
3.2.2 数据写入
c复制void Flash_Write(uint32_t addr, uint8_t *data, uint32_t len)
{
FLASH_Unlock();
for(uint32_t i = 0; i < len; i += 4)
{
uint32_t word = *(uint32_t *)(data + i);
FLASH_ProgramWord(addr + i, word);
while(FLASH_GetStatus() != FLASH_COMPLETE);
}
FLASH_Lock();
}
重要提示:STM32的Flash写入必须以字(32bit)为单位,且地址必须4字节对齐。写入前必须确保目标区域已擦除。
3.3 应用程序跳转
这是整个Bootloader最关键的代码:
c复制void JumpToApp(void)
{
typedef void (*pFunction)(void);
pFunction Jump_To_Application;
uint32_t JumpAddress = *(__IO uint32_t*)(APP_ADDRESS + 4);
Jump_To_Application = (pFunction)JumpAddress;
// 重置所有外设
RCC_DeInit();
// 关闭中断
__disable_irq();
// 设置主堆栈指针
__set_MSP(*(__IO uint32_t*)APP_ADDRESS);
// 跳转
Jump_To_Application();
}
4. 实战经验与避坑指南
4.1 常见问题排查
-
跳转失败
- 检查APP_ADDRESS是否与链接脚本一致
- 确认中断向量表已重定位(在用户程序启动代码中设置SCB->VTOR)
- 用调试器查看PC指针是否跳转到正确地址
-
升级后程序跑飞
- 检查Flash写入是否正确(用调试器查看Flash内容)
- 确认没有擦除Bootloader区域
- 检查堆栈指针初始化(用户程序的启动文件)
-
通信超时
- 增加看门狗喂狗点
- 实现断点续传功能
- 添加数据校验(CRC32推荐)
4.2 性能优化技巧
-
加速Flash写入
- 使用半字(16bit)写入模式(如果芯片支持)
- 批量写入多个字后再检查状态
- 合理设置Flash等待周期
-
减小Bootloader体积
- 使用-Os优化选项
- 避免使用printf等大函数
- 必要时用汇编重写关键部分
-
安全增强
- 添加AES加密传输
- 实现数字签名验证
- 加入防回滚机制
5. 进阶话题
5.1 差分升级实现
对于大文件升级,可以只传输差异部分:
- 在PC端用bsdiff生成差分包
- 传输差分包到设备
- Bootloader用bspatch合并更新
python复制# PC端生成差分包示例
import bsdiff4
with open('old.bin', 'rb') as f:
old = f.read()
with open('new.bin', 'rb') as f:
new = f.read()
diff = bsdiff4.diff(old, new)
with open('patch.bin', 'wb') as f:
f.write(diff)
5.2 无线升级方案
基于串口Bootloader可以轻松扩展为无线升级:
-
WiFi方案:
- 通过ESP8266/ESP32透传串口数据
- 实现HTTP固件下载
-
4G/NB-IoT方案:
- 使用移远EC20等模块
- 通过MQTT接收升级包
-
蓝牙方案:
- 使用CC2541等BLE芯片
- 实现OTA特性
我在一个物联网项目中实测,通过4G网络升级1MB固件约需3分钟(取决于信号强度)。
6. 开发工具推荐
-
串口调试工具:
- Tera Term(开源)
- SecureCRT(商用)
- 自建Python工具(灵活性强)
-
Flash编程工具:
- STM32CubeProgrammer
- J-Flash
- OpenOCD
-
辅助工具:
- Bin2Hex转换器
- CRC计算工具
- 内存布局查看器
这里分享一个我常用的Python升级脚本片段:
python复制import serial
import time
def send_upgrade_file(port, filename):
ser = serial.Serial(port, 115200, timeout=1)
with open(filename, 'rb') as f:
data = f.read()
# 发送升级开始指令
ser.write(b'UPGRADE_START\n')
time.sleep(0.1)
# 等待设备响应
while True:
if ser.in_waiting:
resp = ser.readline().decode().strip()
if resp == 'READY':
break
# 发送文件大小
size_msg = f'FILE_SIZE={len(data)}\n'.encode()
ser.write(size_msg)
# 分段发送数据
chunk_size = 1024
for i in range(0, len(data), chunk_size):
chunk = data[i:i+chunk_size]
ser.write(chunk)
while ser.in_waiting < 7: # 等待"WRITE_OK"
pass
resp = ser.readline().decode().strip()
print(f'Sent {i}/{len(data)}, response: {resp}')
ser.write(b'UPGRADE_END\n')
ser.close()
这个方案我已经在多个量产项目中验证,稳定性非常好。最关键的体会是:Bootloader要尽可能简单可靠,升级失败时要有恢复机制(比如保留上一个可用版本),同时要做好安全防护,防止未授权升级。
