LV040-image镜像文件打包原理与生成
一、 为什么需要 app.img
1. 裸 bin 缺三样东西
sram-app.bin / flash-app.bin 是链接器吐出的纯字节流,从镜像第一个字节开始,到最后一个字节结束。它本身没有任何自描述信息,接收方拿到之后无法回答三个问题:
| 问题 | 裸 bin 的困境 | app.img 的答案 |
|---|---|---|
| 装到哪 | 不知道是 Flash 版还是 SRAM 版 | 头部第 16 字节起 4 字节目标地址 |
| 收多少 | 不知道长度,只能靠"串口安静了"判断 | 头部第 8 字节起 4 字节载荷长度 |
| 收对没 | 没有任何校验 | 头部第 12 字节起 4 字节 CRC32 |
因此 app.img 的本质是"给裸 bin 加一层可自描述的封套",而不是另一种固件格式。
2. 封套解决了什么工程问题
- 同一套协议装两种镜像:
flash-app与sram-app的固件帧完全一样,只有地址字段不同,Bootloader 不需要两套解析; - 结束条件明确:有了长度,状态机知道"收满 N 字节就该看尾部了",不依赖超时;
- 错误可定位:魔数错、版本错、长度越界、CRC 错各自有独立提示,不用猜是链路问题还是镜像问题;
- 可离线核对:
pack_iap.py -x能把头部解析回人可读的字段,排查"烧进去的是不是这个版本"很方便。
二、 帧结构
1. 字段布局
| 偏移 | 长度 | 字段 | 字节序 | 说明 |
|---|---|---|---|---|
| 0 | 8 | 魔数 IAPBIN02 | ASCII | 前 6 字节是 IAPBIN,后 2 字节是版本 |
| 8 | 4 | 载荷长度 N | 小端 | 上限 128KB |
| 12 | 4 | 载荷 CRC32 | 小端 | 只覆盖载荷 |
| 16 | 4 | 目标加载地址 | 小端 | 只接受 0x08020000 / 0x20007000 |
| 20 | N | 载荷 | — | 原始 bin 字节 |
| 20 + N | 8 | 尾部 IAPEOF02 | ASCII | 形式校验,防截断 |
2. 魔数与版本
魔数被拆成"前缀 + 版本"两段判断,这样版本不匹配时能给出准确提示而不是笼统的"格式错误":
// atk-f103/Drivers/BSP/Iap/iap.h
#define IAP_MAGIC_PREFIX "IAPBIN" /* 魔数前 6 字节 */
#define IAP_MAGIC_VERSION "02" /* 帧格式版本, 2 字节 */
#define IAP_MAGIC "IAPBIN02" /* 完整魔数, 8 字节 */
#define IAP_TRAILER "IAPEOF02" /* 尾部魔数, 8 字节 */版本升到 02 的原因就是新增了地址字段:01 版的头部只有 16 字节,接收方无法区分两种镜像。版本号放在魔数里,比单独开一个版本字段省 4 字节,也让旧固件在比对魔数时自然失败。
3. CRC32 为什么只覆盖载荷
CRC 的职责是"确认这段字节没被链路改坏",而头部的长度、地址、CRC 字段本身是控制信息:如果它们也参与校验,接收方就必须先读全帧才能验证头部,超时判断和内存规划都会被迫后移。现在的分工是:
- 头部字段做范围校验(地址是否合法、长度是否超容器),不做 CRC;
- 载荷做 CRC32,收满后由主循环算一次,与头部给出的期望值比对;
- 尾部魔数做截断校验,长度对但尾部错,说明发送方少发了或多发了。
三、 怎么生成 bin
1. fromelf 的两种调用
.bin 由 ARMCC 自带的 fromelf 从 .axf 导出,两种等价写法:
# 只输出 bin
fromelf --bin -o flash-app.bin flash-app.axf
# 同时输出反汇编与 bin(排查时用)
fromelf --bin --text -c -o flash-app.bin flash-app.axf > flash-app.dis--bin 会按链接地址把可加载段拼成扁平镜像,因此 bin 的第一个字节就是向量表的初始栈顶,起始地址等于 IROM1 的基址。
2. 构建脚本里的做法
工程里把这一步注册成了"构建后执行"的用户程序,build_flash.ps1 / build_sram.ps1 也会在构建成功后显式补一次并校验产物:
| 项目 | 做法 | 好处 |
|---|---|---|
| 何时执行 | UV4 构建成功后 | 不会拿旧 axf 生成新 bin |
| 输出位置 | 与 .hex 同目录(Target 的 Output 目录) | 产物集中,-c 清理时一并删除 |
| 是否依赖 PATH | 脚本里带候选路径,最后才回退 PATH | Keil 装在非默认盘也能跑 |
| 校验 | 生成后检查文件存在且非空 | 避免"构建成功但没有 bin"的静默失败 |
3. bin 的边界
bin 只包含已初始化的可加载段:.text、.rodata、.data 的初值。.bss 与栈不占 bin 空间(它们在 RAM 里,由启动代码清零)。所以判断"镜像多大"时看 bin 大小即可,但要判断"占多少 Flash",还得看 map 文件里的 load region size。
四、 怎么生成 app.img
1. 打包
# flash-app:默认地址 0x08020000
python tools\pack_iap.py -p flash-app\MDK-ARM\flash-app\flash-app.bin -o flash-app\MDK-ARM\flash-app\app.img
# sram-app:显式指定 0x20007000
python tools\pack_iap.py -p sram-app\MDK-ARM\sram-app\sram-app.bin -a 0x20007000 -o sram-app\MDK-ARM\sram-app\app.img工程里这一步同样挂在"构建后执行"上,因此 release 构建结束就能直接拿到 app.img。
2. 打包时做的检查
| 检查 | 判据 | 拦下什么 |
|---|---|---|
| 地址合法 | 必须是 TARGETS 里的两个基址之一 | 手工敲错地址、拿 flash-app.bin 当 SRAM 版打包 |
| 容量 | 载荷 ≤ 容器容量(128KB / 24KB) | 链接进错容器的镜像 |
| 向量表 | 初始栈顶落在内部 RAM,复位向量落在容器内且为 Thumb | 打包了空的、错位的、非 Thumb 的 bin |
| 覆盖 | 输出已存在时需 -f | 误覆盖上一次的产物 |
向量表检查可以关(--no-vector-check),但只在"故意打包一个特殊镜像"时才用,日常不该关。
3. 解析与核对
# 只看头部,不导出
python tools\pack_iap.py -x flash-app\MDK-ARM\flash-app\app.img
# 把载荷还原成裸 bin,用于比对
python tools\pack_iap.py -x sram-app\MDK-ARM\sram-app\app.img -o check.bin解析模式会打印目标地址、载荷长度、CRC32,以及容器剩余空间。现场排查"设备里跑的到底是哪一版"时,先看这个头部,再决定要不要重发。
五、 两种镜像的差异
| 对比项 | flash-app | sram-app |
|---|---|---|
| 打包地址 | 默认 0x08020000 | 必须 -a 0x20007000 |
| 容量上限 | 128KB | 24KB |
| 落盘位置 | 与运行位置相同 | 暂存区 0x08040000 |
| 触发运行 | go flash 或上电自动 | go sram(每次上电都要重来) |
| 调试版地址 | 0x08000000 | 0x08000000 |
一个高频错误是把 sram-app.bin 用默认地址打包:pack_iap.py 的向量表检查会拦下它(复位向量落在 0x20007000,不在 Flash APP 区内),提示 vector table check failed。看到这条提示,先确认 -a 参数,再确认 bin 是哪个 Target 编出来的。
本文档由 markdowncli 技能辅助生成