车载OTA升级编程实现:断点续传+双分区备份杜绝升级变砖
车载OTA变砖基本都栽在同一件事:把新固件直接盖到正在跑的App区,中途掉电/校验失败就没退路。正确架构是「双分区A/B+写非活动Bank+分块续传+整包验签+试运行回滚」。
一、Flash分区与不变原则
最低布局:Bootloader独立常驻、NVRAM存状态、Bank A/B各一份App。
0x08000000 Bootloader(锁死不擦)
0x08008000 NVRAM(版本/状态/offset/crc)
0x08010000 Bank A
0x08080000 Bank B
铁律三条:
升级只写非活动Bank,当前运行Bank不动
Bootloader不能被App升级覆盖
切换前新Bank必须过整包校验+签名
二、断点续传:分块CRC+偏移落盘
弱网/4G场景必须分块,单包建议1~4KB。每包带序号和CRC32,收完即校验,坏包重传。
断点状态结构存NVRAM(或EEPROM),别只存“百分比”:
typedef struct {
uint32_t magic; // 0x0A0A
uint32_t fw_total; // 固件总长
uint32_t offset; // 已写字节偏移
uint32_t chunk_idx; // 当前块号
uint32_t chunk_crc; // 上一块CRC缓存
uint16_t ver; // 版本号,防串包
uint8_t state; // 0空闲 1下载中 2待校验
} ota_meta_t;
写NVRAM要原子:先写临时区再整体提交,掉电不能留下半截状态。
下载主循环伪代码:
int ota_write_chunk(uint8_t *buf, uint32_t off, uint16_t len, uint32_t crc){
if(crc32(buf,len) != crc) return -1; // 块校验失败
uint32_t dst = inactive_bank_addr() + off;
flash_erase_page_align(dst, len);
flash_write(dst, buf, len);
meta.offset = off + len;
meta.chunk_idx++;
nvram_atomic_save(&meta); // 每块落盘
return 0;
}
重连恢复时先读meta,version对得上就从offset继续发Range或自定义续传指令,不对就清掉重下。
三、整包校验+签名验签
分块CRC只防传输位错,防不了篡改。整包收完后做两层:
SHA256整包哈希比对云端下发的digest
ECDSA/RSA验签,公钥来自HSM或eFuse,不进普通RAM
int ota_verify_image(uint32_t bank){
uint8_t hash[32];
sha256_flash(bank, fw_total, hash);
if(memcmp(hash, hdr->expect_hash, 32)) return -1;
if(hsm_verify_signature(hdr->pub_key_id, hash, hdr->sig))
return -1; // 验签失败拒刷
if(hdr->version <= current_version()) // 防回滚攻击
return -2;
return 0;
}
验签过才置state=PENDING_SWITCH,否则清Bank标记上报失败。
四、Bootloader切换与试运行回滚
别写完直接永久激活。正确是「Trial态+看门狗守护」:
void boot_select(void){
meta = nvram_read();
if(meta.state == PENDING_SWITCH){
if(ota_verify_image(inactive_bank())==0){
set_boot_bank(inactive); // VTOR指向新Bank
set_trial_flag(); // 试运行标记
wdt_enable(3000); // 3秒守护
jump_to_app(inactive_bank());
} else rollback();
}
// 正常启动当前Bank
jump_to_app(active_bank());
}
新固件起来后做自检(时钟、CAN、外设、业务心跳),确认OK再调:
mark_bank_permanent(); // 清trial,回滚失效
若新Bank卡死/不喂狗→复位→Bootloader发现trial+异常复位→切回旧Bank。
五、跳转前保命动作
直接函数指针跳App在车规里会HardFault,必须清场:
void jump_to_app(uint32_t addr){
if((*(uint32_t*)addr & 0x2FFE0000)!=0x20000000) return; // SP合法性
__disable_irq();
RCC->CIR = 0; SysTick->CTRL = 0; // 关SysTick/RCC中断
SCB->VTOR = addr; // 重映射向量表
__set_MSP(*(uint32_t*)addr);
((void(*)(void))(*(uint32_t*)(addr+4)))();
}
外设没复位干净、VTOR没改,是新固件“跳过去就死”的常见原因。
六、量产落坑清单
电量门禁:整车OTA前查12V电瓶/BMS,低于阈值不开始,写入中断电最危险
A/B空间:Flash≥2×App+Boot,空间不够别硬做A/B,退外部eMMC中转要接受安全性下降
断点原子写:状态半截=续传偏移错=整包CRC对不上
防回滚:版本号进签名内容,单调计数器挡旧版刷回
差分包谨慎:差分省流量但RAM占用高,MCU资源紧用全量分块更稳
安全启动链:BootROM→BL→App逐级验签,HSM存公钥,App层不信任网络数据
看门狗时长:Trial窗口别太短,复杂ECU初始化可能要几秒;也别太长,不然“假活”拖慢回滚
按这套写,断网可从块续传、掉电可重跑、新固件起不来自动回旧Bank,Bootloader常驻兜底——这才是车载OTA“不变砖”的完整闭环,不是把文件下进去就完事。





