汽车电子电气架构正在经历重大变革:动力总成由燃油转向电动,辅助驾驶功能几乎成为标配;而以特斯拉为代表的”硬件预埋、软件迭代”模式,也促使各厂商要求车上的每个控制器都必须支持 OTA 升级。
本文不讨论 OTA 云端方案,只聚焦其中最基础的一环:汽车上的传统 MCU 控制器大多运行在 CAN 网络之上,那么一个运行在 MCU 内部的 CAN bootloader 通常是如何设计、如何工作的。
bootloader 有两大主要功能:
汽车 MCU 的程序通常固化在 Flash 中。MCU 上电后从 Flash 的特定地址取第一条指令,这个起始区间就是 boot 区,存放 bootloader;其余空间划分为 app 区,存放应用程序。下图是一个简单的分区实例,红色区域分别表示 boot 和 app 的中断向量表。

中断向量表的第一项是系统复位向量,存放程序入口函数(通常为汇编)的地址,负责准备 C 运行时环境:初始化 data 段、bss 段和堆栈,然后调用 main 函数。
上图(2)所示为 boot 正常启动 APP 的过程。正常启动路径刻意保持简单,以便尽快把控制权交给 APP,缩短开机时间。
MCU 中的 loader 是一个 UDS(统一诊断服务)服务器,响应诊断仪(客户端)发来的一系列 UDS 请求,配合完成 APP 升级,整体流程如上图(3)所示。具体每一步由哪些 UDS 服务构成,后文实验中会看到。
一个 bootloader 项目通常需要三个工程:
下面以 QEMU Versatilepb 虚拟机为例,介绍这三个工程的样子,以及一次完整升级的流程。该平台已经集成在本仓库中(app/platform/qemu),无需额外下载。
首先参考开发环境搭建准备好编译环境,双击工程目录下的 Console.bat 启动 ConEmu 终端。
# app 页:编译 PC 端工具
D:\repository\as>scons --app=Loader
D:\repository\as>scons --app=CanBridge
D:\repository\as>scons --app=CanDump
# 设置 CAN FD 模式,单帧数据最长 64 字节
D:\repository\as>set LL_DL=64
D:\repository\as>scons --cpl=QemuVersatilepbGCC --app=VersatilepbFlashDriver
# 首次编译需要下载 gcc-arm-none-eabi,时间较长
D:\repository\as>scons --cpl=QemuVersatilepbGCC --app=VersatilepbCanApp
D:\repository\as>scons --cpl=QemuVersatilepbGCC --app=VersatilepbCanBL
编译后会自动调用 Loader 完成签名(SCons post-action,无需手工执行): APP 使用 crc16-v3 签名,签名地址
0x3FE80;Flash Driver 在偏移 4096 处附加 crc16,产物均为*.s19.sign。
此时可以先修改 app/app/main.c 中 main 函数的打印,例如改为:
int main(int argc, char *argv[]) {
ASLOG(INFO, ("application v2 build @ %s %s\n", __DATE__, __TIME__));
然后重新编译 APP,以便刷写后直观确认新版本运行:
D:\repository\as>scons --cpl=QemuVersatilepbGCC --app=VersatilepbCanApp
启动虚拟机与总线桥接:
# app 页:启动 QEMU 虚拟机(自带一个串口转 CAN)
D:\repository\as>scons --cpl=QemuVersatilepbGCC --app=VersatilepbCanBLRun
qemu-system-arm.exe: -serial tcp:127.0.0.1:9000,server: info: QEMU waiting for connection ...
# sim 页:把 QEMU 的串口 CAN 桥接到 v2 虚拟总线
D:\repository\as>build\nt\GCC\CanBridge\CanBridge.exe -d qemu -d simulator_v2
# tool 页(可选):总线抓包监视器
D:\repository\as>build\nt\GCC\CanDump\CanDump.exe
回到 app 页,可以看到 boot 校验通过并启动了旧版 APP(没有 v2 字样):
INFO :bootloader build @ Dec 15 2021 19:26:46
INFO :application is valid
INFO :application build @ Dec 14 2021 22:55:11
在 boot 页执行升级命令(加 -v 可查看每条 UDS 服务的收发明细):
D:\repository\as>build\nt\GCC\Loader\Loader.exe -a build\nt\QemuVersatilepbGCC\VersatilepbCanApp\VersatilepbCanApp.s19.sign -f build\nt\QemuVersatilepbGCC\VersatilepbFlashDriver\VersatilepbFlashDriver.s19.sign -l 64
全系统仿真是软件定时,速度比真实硬件慢;若偶发 CanTp 超时错误,重新执行一次即可。也可以先在 PC 主机上跑 Host CanBL 演示熟悉流程,参见 BL 配置与主机仿真。
刷写成功后 boot 页输出:
loader started:
enter extended session progress 0.10% okay
level 1 security access progress 0.30% okay
enter program session progress 0.40% okay
level 2 security access progress 0.60% okay
download flash driver progress 3.62% okay
erase flash okay
download application progress 95.91% okay
check integrity progress 96.01% okay
ecu reset progress 99.00% okay
loader exited without error
progress 100.00%
app 页随后打印新版本号,说明升级成功、新 APP 已运行:
INFO :bootloader build @ Dec 15 2021 19:26:46
INFO :application is valid
INFO :application v2 build @ Dec 14 2021 21:47:05
加 -v 可以看到,整个升级过程就是一串 UDS 服务的有序组合:
enter extended session
request service 10: TX 10 03 RX 50 03 13 88 00 32
level 1 security access
request service 27: TX 27 01 RX 67 01 CF 71 2C 84
TX 27 02 B7 E2 6A F7 RX 67 02
enter program session
request service 10: TX 10 02 RX 50 02 ...
level 2 security access
request service 27: TX 27 03 / 27 04 ...
download flash driver
request service 34: TX 34 00 44 ... RX 74 20 02 02 # RequestDownload
request service 36: TX 36 01 ... RX 76 01 # TransferData
...
request service 37: TX 37 RX 77 # RequestTransferExit
download application
request service 31: TX 31 01 ... RX 71 01 ... # RoutineControl: erase / check
request service 34 / 36 / 37 ... # download app
check integrity
request service 31: TX 31 01 ... RX 71 01 ... # RoutineControl: CRC check
ecu reset
request service 11: TX 11 01 RX 51 01 # ECUReset
涉及的 UDS 服务依次是:10(会话控制)、27(安全访问)、34(请求下载)、36(传输数据)、37(退出传输)、31(例程控制:擦除与校验)、11(ECU 复位)。
本例把 Versatilepb 的 0x00008000 ~ 0x00140000(1248 KB)模拟为 Flash,0x00140000 ~ 0x00180000(256 KB)模拟为 RAM。实际上该 QEMU 机型只有 RAM,这里只是把一部分 RAM 抽象成 Flash。
bootloader 的链接脚本 linker-boot.lds 片段如下:
MEMORY
{
FLASH (rx) : ORIGIN = 0x00008000, LENGTH = 224K
APPCODE (rx) : ORIGIN = 0x00040000, LENGTH = 1024K
FLSDRV (rwx) : ORIGIN = 0x00140000, LENGTH = 4K
RAM (rwx) : ORIGIN = 0x00141000, LENGTH = 252K
}
0x00008000 ~ 0x00040000 为 boot 区,0x00040000 起为 app 区。app 的链接脚本 linker-app.lds 对应为:
MEMORY
{
FLASH (rx) : ORIGIN = 0x00040000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x00140000, LENGTH = 256K
}
Flash Driver 的链接脚本 linker-flsdrv.lds 为:
MEMORY
{
FLASH (rwx) : ORIGIN = 0x00140000, LENGTH = 4K
}
ENTRY(FlashHeader)
SECTIONS
{
.text :
{
*(.rodata*)
*(.text*)
} > FLASH
}
注意 bootloader 的 RAM 比 app 少了起始处的 4K:这 4K(0x00140000 ~ 0x00141000)预留给下载到 RAM 中执行的 Flash Driver,bootloader 与 Flash Driver 合起来才能完成刷写。BL 侧的分区配置在 BL.json。
这是功能安全上的考量:程序本身可能有 bug,MCU 也可能因电磁干扰跑飞,如果擦/写 Flash 的指令静态链接在常驻代码里,一旦误执行就可能擦坏 boot 或 app,导致控制器变砖。业界常见的两种防护:
分区与链接是 bootloader 移植的主要难点。借助 IDE 把普通工程链接到指定地址并不难,难点在于单独构建一份”只有 Flash Driver”的工程。不同编译器控制链接的方式不同,需要查阅对应文档搞清入口点、段放置等概念——嵌入式开发不要误以为程序入口一定是 main。如上脚本用 ENTRY(FlashHeader) 改写入口,并把 rodata 放在 FLASH 起始;而 Flash.c 中整个工程唯一的 const 常量就是 FlashHeader:
const tFlashHeader FlashHeader = {.Info.W.MCU = 1,
.Info.W.mask = 2,
.Info.W.version = 169,
.Init = FlashInit,
.Deinit = FlashDeinit,
.Erase = FlashErase,
.Write = FlashWrite,
.Read = FlashRead};
因此 FlashHeader 的地址就是 0x00140000。bootloader 按该地址把内存解释为 tFlashHeader,即可取得 Init/Erase/Write/Read 等函数指针和版本信息,并据此做一致性检查。
本文只覆盖了主干内容。认真做完 QEMU 实验后,对 bootloader 的工作原理就能建立基本认识;BL 模块自身的配置项(A/B 分区、元数据、三种签名格式等)见参考文档 BL 配置说明。
如果 QEMU 全系统仿真遇到问题,可以直接在 Windows 主机上运行 CanBL 做刷写实验(用虚拟 Flash 模拟),完整步骤见 BL 配置说明第 6 章。
scons --cpl=QemuVersatilepbGCC --app=VersatilepbCanBLRun gdb
该命令会在 QEMU 参数后附加 -gdb tcp::1234 -S,即虚拟机冻结在第一条指令,等待 GDB 连接。
新开一个终端:
arm-none-eabi-gdb.exe build/nt/QemuVersatilepbGCC/VersatilepbCanBL/VersatilepbCanBL.exe
在 GDB 中:
(gdb) target remote localhost:1234
# 例如单步执行到 PC 离开复位向量
while $pc != 4
stepi
print/x $pc
end