LV070-工程占用与分区余量实测
分区图画得对不对,最终要看编译器和链接器吐出来的数字。这篇先讲怎么读这些数字——Program Size 的四个字段落在哪、.map 的区域行怎么对、链接配置里哪一行才真正决定分区、余量与临界点怎么算;再用本工程在某个版本上的实测数据走一遍完整实例。方法长期有效,数字只代表那一刻。
一、 先说清楚数据的时效性
【注意】
本文第三节之后的实例数字,都是某一次构建的快照:atk-f103 与 flash-app 两个工程在 2026-10-14 做的一次全量重建。它们的作用是帮助理解布局与用量,不是长期有效的绝对指标。
(1)代码一改,数字就变。加一个模块、删一段 lwIP 功能、调整一个缓冲大小,Code / ZI-data 都会跟着动,余量表随之失效;分区上限没变时不会报错,只是余量悄悄变少。
(2)分区方案本身也会变。实例对应的方案是"按 128 KB 切四块",此前用过"64 KB + 448 KB"。同一篇文章里的分区表只描述它对应的那一版。
(3)真正长期有效的是第二节的方法。知道怎么从 Program Size、.map、链接配置里把数字读出来、怎么交叉验证、怎么算余量,换一版代码或换一颗芯片都能重做一遍;记住某个具体的 59212 则毫无意义。
(4)复核成本很低。全量重建一次,再看一眼编译输出与 .map 即可,步骤见第四节。凡是拿不准"现在还剩多少"的时候,都应该重新量一次,而不是翻旧表。
二、 通用方法
1. 四个必看的数据源
判断占用不能只看一个数字,这四类来源要一起看:
| 来源 | 提供什么 | 权威性 |
|---|---|---|
编译器输出的 Program Size | 镜像总量与 RAM 总量 | 最高,链接器最终账目 |
.map 的 Load Region / Execution Region 行 | 每个区域的基址、实际占用、上限 | 最高,与产物一一对应 |
.map 的 Memory Map / Image component sizes | 逐段落点、各目标文件的贡献 | 高,可下钻到符号与文件 |
链接配置(Keil 的 OCR_RVCT4、GCC 的 .ld) | 链接期生效的地址与上限 | 高,决定分区 |
2. Program Size 的四个字段
Program Size: Code=6224 RO-data=448 RW-data=100 ZI-data=2108这四个字段的归属是固定的,弄清楚才不会把十几万字节误当成内部 RAM 占用:
| 字段 | 含义 | 是否进镜像 | 落在哪 |
|---|---|---|---|
Code | 指令 .text | 是 | Flash |
RO-data | 只读常量:const 数组、字符串字面量、跳转表 | 是 | Flash |
RW-data | 已初始化全局/静态变量 .data | 是(存初值) | Flash 存一份 + RAM 驻留一份 |
ZI-data | 零初始化变量 .bss | 否 | 只在 RAM |
3. 两个加权和
真正要比较的是下面两个和,两者都不是"四个相加":
Flash 占用 = Code + RO-data + RW-data
RAM 占用 = RW-data + (ZI-data 中位于片上 RAM 的部分)ZI-data 之所以不能直接和 64 KB 比,是因为它可能包含落到外部 SRAM 的绝对定位段(见第 5 小节)。此时 ZI-data 会虚高得离谱,直接拿它判断"内部 RAM 快满了"会得出完全错误的结论。
4. 一条快速的判断顺序
拿到一次构建结果,按这个顺序看:
- 先看
Program Size,算出 Flash 与 RAM 两个加权和; - 再看
.map的LR_IROM1.Size与ER_RW_IRAM1.Size,与上一步互相验证; - 两个数各自与分区上限比,得到余量与占比;
- 若
ZI-data明显大于ER_RW_IRAM1.Size,去.map里找是不是有段落到了外部内存。
5. 怎么读 .map
5.1 Load Region 与 Execution Region
Load Region LR_IROM1 (Base: 0x08020000, Size: 0x00001a74, Max: 0x00020000, ABSOLUTE)
Execution Region ER_IROM1 (Exec base: 0x08020000, Load base: 0x08020000, Size: 0x00001a10, Max: 0x00020000, ABSOLUTE)
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x08021a10, Size: 0x000008a0, Max: 0x00010000, ABSOLUTE)四个字段的含义:
| 字段 | 含义 |
|---|---|
Base | 区域起始地址 |
Size | 实际占用字节数 |
Max | 链接配置里声明的上限,这就是分区边界 |
Load base | 该区域初值在镜像里的存放地址 |
Load Region 是"装进 Flash 的那一份",Execution Region 是"运行时落在哪"。一个 Load Region 可以带多个执行区:代码落在 Flash,.data 的初值跟着镜像走,一旦跑起来就要拷进 RAM。
5.2 四处对应关系
拿 Program Size 去对 .map,有四处应当严丝合缝:
| 对照项 | 应当等于 |
|---|---|
Max(IROM1 上的那个) | 链接配置里的 Size,也就是分区大小 |
LR_IROM1.Size | Code + RO-data + RW-data,即镜像大小 |
LR - ER_IROM1 | RW-data,即启动时要拷进 RAM 的 .data |
ER_RW_IRAM1.Size | RW-data + 片上 ZI |
装载区比执行区大出的那一小段,恰好就是"镜像里存一份、启动时拷进 RAM"的 .data。这四项能对上,说明 Program Size 与 .map 取自同一次构建;对不上,多半是拿了增量构建的旧数据。
5.3 Total ROM Size 与 COMPRESSED
.map 末尾的 Total ROM Size (Code + RO Data + RW Data) 往往比 LR_IROM1.Size 略小,差值来自"初值全为 0 的 RW-data"——链接器把它们归进 ZI,不必在 Flash 里存初值。行尾的 COMPRESSED[...] 才是真正写进 hex 的字节数。
所以:
- 判断有没有超过分区上限,看
LR_IROM1.Size; - 判断hex 里到底写了多少字节,看
Total ROM Size或COMPRESSED。
5.4 UNINIT 与绝对定位段
落到外部内存的段通常长这样:
Load Region LR$$.ARM.__AT_0x68000000 (Base: 0x68000000, Size: 0x00000000, Max: 0x00020000, ABSOLUTE)
Execution Region ER$$.ARM.__AT_0x68000000 (Exec base: 0x68000000, Size: 0x00020000, Max: 0x00020000, ABSOLUTE, UNINIT)两个特征:Load Region 的 Size 是 0(不占镜像任何字节),执行区带 UNINIT(C 启动代码不会去清零它)。
UNINIT 是这类内存能用的前提:如果它是普通 ZI 区,__main 会在外部总线初始化之前就往那个地址写数据,结果要么总线错误,要么数据写丢。用绝对定位挂外部内存时,务必确认这一项。
5.5 组件表怎么用
Image component sizes 按目标文件列出 Code / RO Data / RW Data / ZI Data。它适合回答"是谁占的",但有三点要注意:
(1)各行相加不等于总量。库成员、生成项(向量表、跳转表)与对齐填充另计,通常差几百到几千字节。
(2)inc.data 不要重复计入。它表示该文件的内联数据被合进了别人的 Code,不是独立的占用。
(3)看排名而不是看全部。前十几名通常占总量的九成,剩下的杂项没有分析价值。
6. 产物文件大小不能当占用
同一个工程会产出好几个文件,只有一个能用来判断占用:
| 文件 | 能否判断占用 | 原因 |
|---|---|---|
.axf / .elf | 不能 | 含调试信息,体积与代码无关,动辄是镜像的几十倍 |
.hex | 不能 | ASCII 十六进制文本,长度取决于地址跨度与换行密度 |
.bin | 能 | 纯二进制,大小等于 LR_IROM1.Size |
帧文件(如 .img) | 能,但要减去帧开销 | bin + 头部 + 尾部魔数 |
.bin 与 LR_IROM1.Size 相等是一条很好用的自检:如果两者差了几十上百字节,说明 .bin 是上一次构建留下的。
帧文件那条同样值得记住:镜像 + 帧开销 = 帧文件,帧开销是定值(本工程 16 字节头 + 8 字节尾 = 24 字节)。用这个差可以粗判"上位机打包有没有多加东西"。
7. 链接配置与分区边界
7.1 只认真正参与链接的那一行
Keil 工程的 <umfTarg>1(Use Memory Layout from Target Dialog)+ 空 scatter 文件,意味着唯一生效的布局来源是 .uvprojx 里的 OCR_RVCT4:
<OCR_RVCT4>
<Type>1</Type>
<StartAddress>0x8020000</StartAddress>
<Size>0x20000</Size>
</OCR_RVCT4>GCC 工具链同理,看 .ld 里的 FLASH (rx) : ORIGIN = ..., LENGTH = ...。
7.2 别把芯片描述当分区
.uvprojx 里还有一组 OnChipMemories/IROM,那是芯片物理 ROM 的描述,通常写着整片大小,与链接无关。判断分区时看它,会得出"两个工程都从 0x08000000 起"这种完全错误的结论。
验证方法很直接:去 .map 里看 LR_IROM1 的 Base 和 Max,那才是链接期真正用的值。
7.3 把上限压到分区大小
分区能否被"守住",取决于有没有人把 Size 设成自己那块的大小,而不是整片大小:
- 设成整片:谁都可以长到别人的地盘,链接器一路放行,故障延迟到运行时甚至下一次升级才爆发;
- 设成分区大小:长过界当场报
L6220E: Execution Region ... exceeds maximum size,问题在编译阶段就暴露。
这条防线可以自己验证:把 Size 临时改成明显偏小的值重建一次,应当看到上面那行报错;改回原值即恢复。
7.4 页对齐
Flash 的擦除单位是页(STM32F103 大容量型号 2 KB),所以分区边界必须落在页边界上,否则擦除 App 区时会连带擦掉相邻分区的数据。判断很简单:起始地址能被页大小整除即可。
8. 余量、临界点与经验阈值
8.1 余量与占比
余量 = 分区上限 - 实际占用
占比 = 实际占用 / 分区上限两个都要报:只看余量,128 KB 的 20 KB 与 512 KB 的 20 KB 完全不是一回事;只看占比,小分区里 3 KB 差距也会被放大成"很危险"。
8.2 扩容临界点
即"还差多少字节就会撞线":
临界增量 = 分区上限 - LR_IROM1.Size对 Bootloader 这类运行时不可替换的组件,这个数要额外留出余量——它长过界的后果是设备再也收不到新固件(见第三节第 4 小节)。
8.3 多少算够
没有统一标准,但有几条经验线可以参考:
| 场景 | 建议余量 |
|---|---|
| 仍在频繁加功能的模块 | 不少于当前占用的 50% |
| 稳定模块(Bootloader 这类) | 不少于 20%,并保证"再塞一个中等模块也够" |
| 一次性验证工程 | 只要不撞线即可 |
9. 烧写耗时怎么估
不需要实测也能算出量级:
耗时 ≈ 擦除页数 × 单页擦除时间 + 写入次数 × 单次编程时间
写入次数 = 镜像字节数 / 编程粒度代入手册典型值(2 KB 页擦除约 20 ms、半字编程约 50 µs)就能得到下限;实际受 HAL_FLASH_Program 的逐半字循环开销影响,通常比估算值高一截,所以结论要说成"约多少、最坏多少"。
【注意】
耗时估算的用途不是精确预测,而是判断"这段时间里别的东西会不会崩":串口会不会溢出、看门狗会不会复位、网络心跳会不会超时。这三个问题的答案都在"秒"这个量级上,粗估完全够用。
三、 实例(2026-10-14 全量重建)
1. 芯片资源与分区
1.1 两个工程
| 工程 | 路径 | 角色 | 产物 |
|---|---|---|---|
atk-f103 | atk-f103/Core/Src/main.c | Bootloader:收帧、校验、烧写、跳转 | atk-f103.axf / .hex |
flash-app | flash-app/Core/Src/main.c | App:被升级的目标程序 | flash-app.bin / app.img |
1.2 三块存储资源
器件是 STM32F103ZET6:
| 资源 | 地址范围 | 容量 | 本工程用途 |
|---|---|---|---|
| 内部 Flash | 0x08000000 ~ 0x0807FFFF | 512 KB | Bootloader 代码 + App 镜像 + 两块预留 |
| 内部 SRAM | 0x20000000 ~ 0x2000FFFF | 64 KB | 栈、堆、全局变量 |
| 外部 SRAM | 0x68000000 ~ 0x680FFFFF | 1 MB | IAP 载荷缓冲(FSMC Bank1 NOR/SRAM3) |
1.3 128 KB 四等分与页对齐
内部 Flash 按 128 KB 均分四块,每块用一个工程的链接上限来约束:
| 块 | 地址范围 | 页号 | 用途 | 链接上限 |
|---|---|---|---|---|
| 第 1 块 | 0x08000000 ~ 0x0801FFFF | 0 ~ 63 | Bootloader(atk-f103) | 0x20000 = 128 KB |
| 第 2 块 | 0x08020000 ~ 0x0803FFFF | 64 ~ 127 | App(flash-app) | 0x20000 = 128 KB |
| 第 3 块 | 0x08040000 ~ 0x0805FFFF | 128 ~ 191 | 预留,暂未分配 | — |
| 第 4 块 | 0x08060000 ~ 0x0807FFFF | 192 ~ 255 | 预留,暂未分配 | — |
这样切的好处是四块彼此独立:将来谁要扩容就动自己那块的上限,不必像"两块相加恒等于 512 KB"那样互相挤(这正是上一版 64 KB + 448 KB 方案最别扭的地方)。
Flash 的擦除单位是 2 KB 一页,512 KB 共 256 页:
// atk-f103/Drivers/BSP/Iap/iap.c
/* STM32F103ZET6 是大容量型号, 内部 Flash 按 2KB 分页 */
#define IAP_FLASH_PAGE_SIZE 2048U0x08020000 正好是第 64 页的起点(0x08020000 ÷ 0x800 = 64),第 1 块与第 2 块的分界天然满足擦除粒度要求。
1.4 两个工程的链接配置
| 工程 | IROM1 起始 | IROM1 大小 | 声明覆盖范围 | IRAM1 |
|---|---|---|---|---|
atk-f103 | 0x08000000 | 0x20000 = 128 KB | 0x08000000 ~ 0x0801FFFF | 0x20000000 / 0x10000 |
flash-app | 0x08020000 | 0x20000 = 128 KB | 0x08020000 ~ 0x0803FFFF | 0x20000000 / 0x10000 |
两个 IROM1 各占一块,互不重叠;第 3、4 块目前没有任何工程声明,处于完全空白状态。
2. Bootloader 与 App 的实测占用
2.1 Program Size 原文
bootloader(atk-f103): Program Size: Code=56472 RO-data=2204 RW-data=536 ZI-data=153928
"atk-f103\atk-f103.axf" - 0 Error(s), 0 Warning(s).
flash-app: Program Size: Code=6224 RO-data=448 RW-data=100 ZI-data=2108
"flash-app\flash-app.axf" - 0 Error(s), 0 Warning(s).2.2 代入两个加权和
| 工程 | Code | RO-data | RW-data | ZI-data | Flash 合计 | 内部 RAM | 外部 SRAM |
|---|---|---|---|---|---|---|---|
atk-f103 | 56472 | 2204 | 536 | 153928 | 59212 B(0xE74C) | 23392 B(0x5B60) | 131072 B |
flash-app | 6224 | 448 | 100 | 2108 | 6772 B(0x1A74) | 2208 B(0x8A0) | 0 |
Bootloader 内部 RAM 的 23392 字节这么算出来:153928 - 131072 = 22856(片内 .bss)+ 536(RW-data)。也就是 ZI-data 里 85% 在片外 SRAM(131072 字节的 IAP 载荷缓冲),剩下 22856 字节才是片内的零初始化数据。
这个 153928 如果全落在内部 RAM,早就超过 64 KB 让链接器报 region overflow 了——把载荷缓冲挪到片外 SRAM 这个决定,等于把"链接期就不可能通过"变成了"链接期只占 22 KB"。这正是第二节第 3 小节说的"ZI-data 不能直接和 64 KB 比"。
2.3 与 .map 的交叉验证
atk-f103.map:
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x0000e74c, Max: 0x00020000, ABSOLUTE, COMPRESSED[0x0000e54c])
Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x0000e534, Max: 0x00020000, ABSOLUTE)
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x0800e534, Size: 0x00005b60, Max: 0x00010000, ABSOLUTE, COMPRESSED[0x00000018])
Load Region LR$$.ARM.__AT_0x68000000 (Base: 0x68000000, Size: 0x00000000, Max: 0x00020000, ABSOLUTE)
Execution Region ER$$.ARM.__AT_0x68000000 (Exec base: 0x68000000, Load base: 0x68000000, Size: 0x00020000, Max: 0x00020000, ABSOLUTE, UNINIT)
flash-app.map:
Load Region LR_IROM1 (Base: 0x08020000, Size: 0x00001a74, Max: 0x00020000, ABSOLUTE)
Execution Region ER_IROM1 (Exec base: 0x08020000, Load base: 0x08020000, Size: 0x00001a10, Max: 0x00020000, ABSOLUTE)
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x08021a10, Size: 0x000008a0, Max: 0x00010000, ABSOLUTE)| 对照项 | Bootloader | App | 说明 |
|---|---|---|---|
Max(两个 IROM1) | 0x20000 = 128 KB | 0x20000 = 128 KB | 与 OCR_RVCT4 的 Size 完全一致 |
LR_IROM1.Size | 0xE74C = 59212 | 0x1A74 = 6772 | 等于 Code+RO+RW,也就是镜像大小 |
LR - ER_IROM1 | 0x218 = 536 | 0x64 = 100 | 等于 RW-data,即启动时要拷进 RAM 的 .data |
ER_RW_IRAM1.Size | 0x5B60 = 23392 | 0x8A0 = 2208 | 等于 RW-data + 内部 ZI |
四处都严丝合缝,说明 Program Size 与 .map 取自同一次构建。另外 Total ROM Size 是 58700(0xE54C),比 LR_IROM1.Size 少 512 字节——那 512 字节的 .data 初值全为 0,被归进 ZI,所以 RW_IRAM1 行尾是 COMPRESSED[0x00000018],只有 24 字节真正需要在 Flash 里存初值。
2.4 Bootloader 的关键符号与 RAM 分段
| 符号 | 地址 | 大小 | 所在存储 |
|---|---|---|---|
s_payload | 0x68000000 | 131072 | 外部 SRAM,IAP 载荷缓冲 |
s_sector_buf | 0x20000980 | 4096 | 内部 RAM,SPI Flash 扇区缓冲 |
s_cache | 0x20000548 | 48 | 内部 RAM,param 参数缓存 |
s_rx | 0x20000508 | 48 | 内部 RAM,帧接收器状态 |
s_header | 0x200004F8 | 16 | 内部 RAM,帧头备份 |
s_spi_errors | 0x20000178 | 4 | 内部 RAM,SPI 传输错误计数 |
uwTick | 0x20000164 | 4 | 内部 RAM,HAL 时基 |
__initial_sp | 0x20005B60 | — | 内部 RAM,栈顶 |
__initial_sp = 0x20005B60 正好等于 0x20000000 + 0x5B60,与 ER_RW_IRAM1.Size 一致——栈顶落在区域末尾,是链接器布局的直接结果。
s_payload 落在 0x68000000 靠的是绝对定位:
// atk-f103/Drivers/BSP/ExternalSRAM/bsp_external_sram.h
#define externalSRAM_BASE ((uint32_t)0x68000000) // Bank1 NOR/SRAM3 起始地址
#define externalSRAM_SIZE (1024 * 1024) // 外部SRAM容量大小(单位为字节)ER_RW_IRAM1 从 0x20000000 起连续排布,共 23392 字节。段数很多,按功能分组看更清楚:
| 组 | 大小 | 构成 |
|---|---|---|
.data 合计 | 536 | HAL/外设句柄、lwIP 统计量、iap / param / dm9000 / bsp_spi_flash 的小状态量 |
.bss:lwIP 协议栈 | 15232 | mem.o 8211(内存堆)+ memp.o 6689(内存池)+ etharp.o 240 + lwip_comm.o 52 + ip.o 24 + tcp_in.o 16 |
.bss:SPI Flash 驱动 | 5128 | 扇区缓冲 4096 + 命令层静态缓冲 256 + 256 + 260 + 260 |
.bss:串口行缓冲 | 472 | usart.o |
.bss:FSMC / SPI / HAL Flash / DM9000 句柄 | 280 | fsmc.o 144 + spi.o 88 + stm32f1xx_hal_flash.o 32 + dm9000.o 16 |
.bss:IAP 接收器 | 64 | s_header 16 + s_rx 48 |
.bss:param 缓存 | 48 | s_cache |
.bss:printf 缓冲 | 96 | c_w.l(libspace.o) |
| HEAP + STACK | 1536 | startup_stm32f103xe.o,CubeMX 默认 512 + 1024 |
| — | 23392 | ER_RW_IRAM1.Size = 0x5B60 |
其中 SPI Flash 驱动的 4 KB 扇区缓冲是内部 RAM 里最大的单项。它是 spiflash_write() 做"读-改-写"必需的中转:目标扇区里可能有别的数据要保留,必须先整扇区读出来再合并。省掉它的代价是每次写都要调用方自己保证扇区空白。
2.5 组件表:谁占的
Image component sizes 按 Code 排前 22 名:
| 目标文件 | Code | inc.data | RO Data | RW Data | ZI Data |
|---|---|---|---|---|---|
tcp_out.o | 8172 | 3702 | 85 | 0 | 0 |
tcp_in.o | 6808 | 1918 | 0 | 36 | 16 |
tcp.o | 6182 | 2572 | 201 | 32 | 0 |
bsp_spi_flash.o | 4072 | 1658 | 734 | 4 | 5128 |
pbuf.o | 2754 | 1328 | 0 | 1 | 0 |
etharp.o | 2706 | 1000 | 65 | 1 | 240 |
mem.o | 1912 | 880 | 0 | 12 | 8211 |
param.o | 1732 | 744 | 167 | 1 | 48 |
iap.o | 1648 | 636 | 0 | 4 | 131136 |
dm9000.o | 1644 | 366 | 0 | 1 | 16 |
stm32f1xx_hal_uart.o | 1278 | 10 | 0 | 0 | 0 |
stm32f1xx_hal_rcc.o | 1240 | 84 | 18 | 0 | 0 |
iap_service.o | 1122 | 696 | 172 | 0 | 0 |
netif.o | 1006 | 392 | 0 | 12 | 0 |
ip4.o | 938 | 176 | 0 | 2 | 0 |
stm32f1xx_hal_spi.o | 910 | 4 | 0 | 0 | 0 |
usart.o | 848 | 84 | 0 | 340 | 472 |
udp.o | 818 | 324 | 0 | 8 | 0 |
icmp.o | 816 | 326 | 0 | 0 | 0 |
iap_tcp.o | 780 | 346 | 0 | 8 | 0 |
timeouts.o | 704 | 356 | 16 | 12 | 0 |
bsp_external_sram.o | 596 | 242 | 183 | 0 | 0 |
| Object Totals | 54266 | 18876 | 2144 | 536 | 153832 |
三点解读:
(1)各行相加不等于 Program Size:Code 合计 54266 + 库与生成项 ≈ 56472,RO Data 2144 + 约 60 ≈ 2204,ZI Data 153832 + 约 96 ≈ 153928。
(2)代码大头已经不是自己写的逻辑:lwIP 的 TCP 三件套合计 21162 字节,加上 pbuf / etharp / mem / ip4 / udp / icmp / netif,网络栈占 Code 的一半以上;自写的 iap + iap_tcp + iap_service + param + bsp_spi_flash + bsp_external_sram 约 9950 字节。
(3)iap.o 的 ZI 里 131072 在片外,片内只有 64 字节;bsp_spi_flash.o 是"新代码"里最占 RAM 的。
2.6 App 的镜像构成
| 项 | 大小 | 占镜像比例 |
|---|---|---|
向量表 __Vectors(76 个字) | 304 | 4.5% |
| 代码与其余只读常量 | 6368 | 94.0% |
.data 初值(启动时拷进 RAM) | 100 | 1.5% |
| 合计(= bin 大小) | 6772 | 100% |
| 符号 | 地址 | 大小 |
|---|---|---|
__Vectors | 0x08020000 | __Vectors_Size = 0x130 |
__Vectors_End | 0x08020130 | — |
Reset_Handler | 0x080201DD | 8(Thumb) |
0x080201DD 这个值与上位机 pack_iap.py 打包时打印的复位向量完全一致:
复位向量 : 0x080201DD
载荷 CRC32: 0x683696EE上位机在打包前用同一套判据校验向量表(栈顶必须落在 0x20000000 段内、复位向量必须落在 [APP_BASE, APP_END] 内且 bit0 = 1),两边看到的是同一个地址——这也是 LV030-FLASH APP设计 里"两条都过才跳"的由来。
App 的内部 RAM 只用了 2208 字节:.data 100(main.o 4 + usart.o 80 + stm32f1xx_hal.o 12 + system_stm32f1xx.o 4)、main.o 的 .bss 202、PAD 2、usart.o 的 .bss 272、libspace.o 96、HEAP 512、STACK 1024,__initial_sp = 0x200008A0。其中 1536 字节是 CubeMX 默认的堆栈,业务数据只占 672 字节——两个工程用的是同一份启动文件,堆栈尺寸一致。
2.7 产物文件
| 文件 | 大小 | 说明 |
|---|---|---|
atk-f103.axf | 1139656 | 含调试信息,不能用来判断占用 |
atk-f103.hex | 165148 | ASCII 文本,长度与镜像无关 |
flash-app.axf | 576832 | 同上 |
flash-app.hex | 19107 | 同上 |
flash-app.bin | 6772 | 恰好等于 LR_IROM1.Size |
app.img | 6796 | IAP 帧文件 = bin + 24 字节帧开销 |
6796 = 6772 + 24 与第 6 小节的规律一致:帧开销是定值 24 字节(16 字节头 + 8 字节尾),其余 6772 字节与将要写入 Flash 的内容逐字节相同。
3. 余量总表与烧写耗时
3.1 Flash
| 区域 | 起始 | 链接上限 | 结束 | 页数 | 实占 | 占本区 | 余量 |
|---|---|---|---|---|---|---|---|
| 第 1 块:Bootloader | 0x08000000 | 128 KB | 0x0801FFFF | 64 | 59212 | 45.2% | 71860 |
| 第 2 块:App | 0x08020000 | 128 KB | 0x0803FFFF | 64 | 6772 | 5.2% | 124300 |
| 第 3、4 块:预留 | 0x08040000 | — | 0x0807FFFF | 128 | 0 | 0% | 262144 |
| 合计 | 0x08000000 | 512 KB | 0x0807FFFF | 256 | 65984 | 12.6% | 458304 |
四块之间没有空隙也没有重叠,第 3、4 块整块空着。
扩容临界点:Bootloader 的镜像是 59212 字节,离 128 KB 上限还有 71860 字节(约 2.2 倍的增长空间);涨过 131072 字节,链接期就会报 region overflow。App 侧更宽松(余 124300 字节,约 18 倍)。
真的需要更大空间时,直接吃第 3、4 块即可:把 flash-app 的 OCR_RVCT4 大小改成 0x40000(256 KB)或更大,同时同步 IAP_APP_END 与 IAP_PAYLOAD_MAX。与上一版方案的关键差别在于不必再动 Bootloader 那一块——四块独立,扩容不再互相挤。
唯一要盯住的是 IAP_PAYLOAD_MAX:它是接收侧的溢出防线,必须与 App 区容量对齐,同时受外部 SRAM 的 1 MB 上限约束(当前载荷缓冲 128 KB,理论上最多可放大到约 900 KB)。
3.2 内部 SRAM
| 工程 | 区间 | 容量 | 实占 | 占比 | 余量 |
|---|---|---|---|---|---|
| Bootloader | 0x20000000 ~ 0x20005B5F | 64 KB | 23392 | 35.7% | 42144 |
| App | 0x20000000 ~ 0x2000089F | 64 KB | 2208 | 3.4% | 63328 |
Bootloader 比 App 多用 21184 字节,差额几乎全部来自三处:
- lwIP 协议栈 15380 字节——
mem内存堆 8211 +memp内存池 6689,加上etharp/lwip_comm/ip/tcp_in的缓冲与统计量,以及.data里的 148 字节; - SPI Flash 驱动 5132 字节——4 KB 扇区缓冲 + 命令层静态缓冲 + 错误计数;
- param 模块 49 字节——
s_cache与一个小状态量。
剩下的 623 字节是 FSMC / SPI / HAL Flash / DM9000 句柄、IAP 接收器与更宽裕的串口行缓冲。内部 RAM 还剩 42 KB,是当前最宽裕的一块资源。
3.3 外部 SRAM
| 区域 | 区间 | 大小 | 占比 |
|---|---|---|---|
| IAP 载荷缓冲 | 0x68000000 ~ 0x6801FFFF | 131072 | 12.5% |
| 未使用 | 0x68020000 ~ 0x680FFFFF | 917504 | 87.5% |
| 合计 | — | 1048576 | 100% |
载荷缓冲的上限 128 KB 与 App 区容量严格相等,这个"巧合"同样是设计出来的:iap_write_appbin() 的地址与长度检查要求整段落在 App 区内,所以缓冲不必更大,App 区也不必更大。反过来说,一旦把 App 区扩到 256 KB,这个缓冲也得跟着调大——它是被 IAP_PAYLOAD_MAX 钉住的。
3.4 烧写耗时估算
按第 9 小节的公式代入手册典型值(2 KB 页擦除约 20 ms、半字编程约 50 µs):
| 场景 | 擦除页数 | 编程次数 | 典型耗时 | 最坏参数下 |
|---|---|---|---|---|
| 当前 App(6772 B) | 4 | 3386 | 约 0.25 s | 约 0.5 s |
| 满量程(128 KB) | 64 | 65536 | 约 4.6 s | 约 8 s |
耗时与镜像体积基本成正比,这一点在串口升级时要有预期:满量程时串口会长时间只输出进度行(每 64 KB 一行,共 2 行)。实际耗时以串口日志为准,上表只用于估算。
4. 分区是一份跨文件的约定
4.1 必须同步的六处
分区不是只写在 .uvprojx 里的一串数字,而是跨多个文件的约定。任何一处单独改动都会静默出错——除了 APP_BASE 本身,其余几处没有任何编译期检查能发现不一致:
| 位置 | 字段 | 当前值 |
|---|---|---|
tools/pack_iap.py | APP_BASE / APP_END / APP_MAX_SIZE | 0x08020000 / 0x0803FFFF / 131072 |
flash-app/MDK-ARM/flash-app.uvprojx | OCR_RVCT4 起始与大小 | 0x8020000 / 0x20000 |
atk-f103/MDK-ARM/atk-f103.uvprojx | OCR_RVCT4 起始与大小 | 0x8000000 / 0x20000 |
atk-f103/Drivers/BSP/Iap/iap.h | IAP_APP_BASE / IAP_APP_END | 0x08020000 / 0x08040000 |
atk-f103/Drivers/BSP/Iap/iap.h | IAP_PAYLOAD_MAX | 128U * 1024U = 131072 |
| 运行时 | SCB->VTOR(iap_load_app() 内赋值) | IAP_APP_BASE |
flash-app/Core/Src/main.c | FLASH_APP_BASE(默认值 / debug target 覆盖) | 0x08020000 / 0x08000000 |
4.2 三个隐性依赖
(1)flash-app.ioc 里没有任何 IROM 配置项,App 的链接基址是纯手工设置在 .uvprojx 的 OCR_RVCT4 里。CubeMX 不会替你记住它,重新生成代码后如果发现 App 链到了 0x08000000,就从这张表第二行开始查。
(2)flash-app.uvprojx 的 OnChipMemories/IROM 仍写着 0x08000000 / 0x80000,那是芯片物理描述,不参与链接。查分区时不要看它。
(3)flash-app-debug 这个 target 刻意链在 0x08000000,用途是不经 Bootloader 直接烧进去跑。它的 Define 里用 FLASH_APP_BASE=0x08000000 覆盖默认值,所以它产出的 bin 不能拿去 IAP 升级。
上位机侧还有一层保护:pack_iap.py 打包前会用向量表判据校验镜像,链错基址的 App 会在上位机侧被拦下来,而不是烧进去之后再变砖。
4.3 重叠曾经是怎么发生的
两个工程最初都保留 CubeMX 的默认值——IROM1 = 0x08000000 / 0x80000(整片 512 KB)。早期把 App 基址改到 0x08010000 之后,两者就重叠了 448 KB:
Bootloader 声明 0x08000000 ~ 0x0807FFFF (512 KB)
App 声明 0x08010000 ~ 0x0807FFFF (448 KB)
└──── 重叠 448 KB ────┘链接器不会报错,因为 Bootloader 实际只用了 12 KB,离它自己声明的 512 KB 上限还远得很。隐患被"暂时安全"掩盖住了。现在的四块划分从根上解决了这件事——每块都有明确的主人、明确的上限,谁都不声明别人的地址。
但"上限被收紧"并不等于"永远不会越界":真正的危险变成了"为了让某个工程过编译,顺手把它的 OCR_RVCT4 改大"。
4.4 越界之后会发生什么
如果 Bootloader 某次改动涨过 128 KB,链接期会直接拦下(这是我们要的效果)。但如果为了让它过编译而把 atk-f103.uvprojx 的上限改成比如 0x40000,它就会长进 App 区,然后在下一次升级时出问题:
(1)iap_write_appbin() 擦除 0x08020000 起的若干页;
(2)这些页里已经有 Bootloader 自己的代码(因为它越过了 128 KB 边界);
(3)接收与跳转代码被抹掉,串口从此再也无法收帧。
这个故障的恶劣之处在于它不可远程恢复——设备已经听不到任何指令,只能靠 JTAG 或 ISP 重新烧 Bootloader。App 侧越界没有这么致命(只会踩掉第 3 块的数据),但同样属于"分区约定被破坏",会让后续扩容计划失效。
4.5 现在的防线
把 atk-f103.uvprojx 的 OCR_RVCT4 大小设成 0x20000 之后,风险从"运行时静默踩内存"变成"链接期当场报错":
Error: L6220E: Execution Region ER_IROM1 size (0x...) exceeds maximum size (0x20000).验证方法与第 7.3 小节相同:临时把 Size 改成明显偏小的值(比如 0x4000),全量重建,应当看到上面这行报错;改回 0x20000 重新构建即恢复。
四、 复核:拿到属于你这个版本的数字
数据会过期,方法是固定的。任何时候需要重新判断"还剩多少",按这五步走一遍:
| 步骤 | 动作 | 看什么 |
|---|---|---|
| 1 | 全量重建(UV4 -r,不是增量 -b) | 确保 .map 与产物同源 |
| 2 | 读编译输出的 Program Size | 套第 3 小节的加权和,得到 Flash 与 RAM |
| 3 | 打开 .map 查 LR_IROM1 / ER_RW_IRAM1 行 | Size 与 Max,核对第 5.2 小节的四处对应 |
| 4 | 查 OCR_RVCT4 或 .ld 的 LENGTH | 确认分区上限没变过(这一步最容易被跳过) |
| 5 | 算余量与临界增量 | 与第 8.3 小节的经验线比,判断是否需要扩容 |
三条使用提醒:
(1)不要用 .axf 或 .hex 的大小判断占用。前者含调试信息(本工程 1.1 MB),后者是 ASCII 文本(161 KB),都与镜像无关。唯一可信的是 Program Size 的 Code+RO-data+RW-data,以及 .map 里 LR_IROM1.Size。
(2)Program Size 与 .map 必须来自同一次构建。增量构建只改了部分文件时,两者可能对不上;核对前先做一次全量重建。
(3)ZI-data 不等于内部 RAM 占用。本工程 Bootloader 的 153928 字节里有 131072 字节在片外 SRAM,看这一项时务必结合 ER_RW_IRAM1.Size。
只要这五步还能走通,本文的数字过期与否都不影响你得出当前版本的结论——方法留下来,数字随时重算。