Skip to content

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 的四个字段 ​

text
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. 两个加权和 ​

真正要比较的是下面两个和,两者都不是"四个相加":

text
Flash 占用 = Code + RO-data + RW-data
RAM  占用 = RW-data + (ZI-data 中位于片上 RAM 的部分)

Program Size 四个字段最终落在哪

ZI-data 之所以不能直接和 64 KB 比,是因为它可能包含落到外部 SRAM 的绝对定位段(见第 5 小节)。此时 ZI-data 会虚高得离谱,直接拿它判断"内部 RAM 快满了"会得出完全错误的结论。

4. 一条快速的判断顺序 ​

拿到一次构建结果,按这个顺序看:

  1. 先看 Program Size,算出 Flash 与 RAM 两个加权和;
  2. 再看 .map 的 LR_IROM1.Size 与 ER_RW_IRAM1.Size,与上一步互相验证;
  3. 两个数各自与分区上限比,得到余量与占比;
  4. 若 ZI-data 明显大于 ER_RW_IRAM1.Size,去 .map 里找是不是有段落到了外部内存。

5. 怎么读 .map ​

5.1 Load Region 与 Execution Region ​

text
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。

Load Region 与 Execution Region 的关系

5.2 四处对应关系 ​

拿 Program Size 去对 .map,有四处应当严丝合缝:

对照项应当等于
Max(IROM1 上的那个)链接配置里的 Size,也就是分区大小
LR_IROM1.SizeCode + RO-data + RW-data,即镜像大小
LR - ER_IROM1RW-data,即启动时要拷进 RAM 的 .data
ER_RW_IRAM1.SizeRW-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 与绝对定位段 ​

落到外部内存的段通常长这样:

text
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:

xml
<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 余量与占比 ​

text
余量 = 分区上限 - 实际占用
占比 = 实际占用 / 分区上限

两个都要报:只看余量,128 KB 的 20 KB 与 512 KB 的 20 KB 完全不是一回事;只看占比,小分区里 3 KB 差距也会被放大成"很危险"。

8.2 扩容临界点 ​

即"还差多少字节就会撞线":

text
临界增量 = 分区上限 - LR_IROM1.Size

对 Bootloader 这类运行时不可替换的组件,这个数要额外留出余量——它长过界的后果是设备再也收不到新固件(见第三节第 4 小节)。

8.3 多少算够 ​

没有统一标准,但有几条经验线可以参考:

场景建议余量
仍在频繁加功能的模块不少于当前占用的 50%
稳定模块(Bootloader 这类)不少于 20%,并保证"再塞一个中等模块也够"
一次性验证工程只要不撞线即可

9. 烧写耗时怎么估 ​

不需要实测也能算出量级:

text
耗时 ≈ 擦除页数 × 单页擦除时间 + 写入次数 × 单次编程时间
写入次数 = 镜像字节数 / 编程粒度

代入手册典型值(2 KB 页擦除约 20 ms、半字编程约 50 µs)就能得到下限;实际受 HAL_FLASH_Program 的逐半字循环开销影响,通常比估算值高一截,所以结论要说成"约多少、最坏多少"。

【注意】

耗时估算的用途不是精确预测,而是判断"这段时间里别的东西会不会崩":串口会不会溢出、看门狗会不会复位、网络心跳会不会超时。这三个问题的答案都在"秒"这个量级上,粗估完全够用。

三、 实例(2026-10-14 全量重建) ​

1. 芯片资源与分区 ​

1.1 两个工程 ​

工程路径角色产物
atk-f103atk-f103/Core/Src/main.cBootloader:收帧、校验、烧写、跳转atk-f103.axf / .hex
flash-appflash-app/Core/Src/main.cApp:被升级的目标程序flash-app.bin / app.img

1.2 三块存储资源 ​

器件是 STM32F103ZET6:

资源地址范围容量本工程用途
内部 Flash0x08000000 ~ 0x0807FFFF512 KBBootloader 代码 + App 镜像 + 两块预留
内部 SRAM0x20000000 ~ 0x2000FFFF64 KB栈、堆、全局变量
外部 SRAM0x68000000 ~ 0x680FFFFF1 MBIAP 载荷缓冲(FSMC Bank1 NOR/SRAM3)

1.3 128 KB 四等分与页对齐 ​

内部 Flash 按 128 KB 均分四块,每块用一个工程的链接上限来约束:

块地址范围页号用途链接上限
第 1 块0x08000000 ~ 0x0801FFFF0 ~ 63Bootloader(atk-f103)0x20000 = 128 KB
第 2 块0x08020000 ~ 0x0803FFFF64 ~ 127App(flash-app)0x20000 = 128 KB
第 3 块0x08040000 ~ 0x0805FFFF128 ~ 191预留,暂未分配—
第 4 块0x08060000 ~ 0x0807FFFF192 ~ 255预留,暂未分配—

这样切的好处是四块彼此独立:将来谁要扩容就动自己那块的上限,不必像"两块相加恒等于 512 KB"那样互相挤(这正是上一版 64 KB + 448 KB 方案最别扭的地方)。

Flash 的擦除单位是 2 KB 一页,512 KB 共 256 页:

c
// atk-f103/Drivers/BSP/Iap/iap.c
/* STM32F103ZET6 是大容量型号, 内部 Flash 按 2KB 分页 */
#define IAP_FLASH_PAGE_SIZE     2048U

0x08020000 正好是第 64 页的起点(0x08020000 ÷ 0x800 = 64),第 1 块与第 2 块的分界天然满足擦除粒度要求。

1.4 两个工程的链接配置 ​

工程IROM1 起始IROM1 大小声明覆盖范围IRAM1
atk-f1030x080000000x20000 = 128 KB0x08000000 ~ 0x0801FFFF0x20000000 / 0x10000
flash-app0x080200000x20000 = 128 KB0x08020000 ~ 0x0803FFFF0x20000000 / 0x10000

两个 IROM1 各占一块,互不重叠;第 3、4 块目前没有任何工程声明,处于完全空白状态。

2. Bootloader 与 App 的实测占用 ​

2.1 Program Size 原文 ​

text
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 代入两个加权和 ​

工程CodeRO-dataRW-dataZI-dataFlash 合计内部 RAM外部 SRAM
atk-f10356472220453615392859212 B(0xE74C)23392 B(0x5B60)131072 B
flash-app622444810021086772 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 的交叉验证 ​

text
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)
对照项BootloaderApp说明
Max(两个 IROM1)0x20000 = 128 KB0x20000 = 128 KB与 OCR_RVCT4 的 Size 完全一致
LR_IROM1.Size0xE74C = 592120x1A74 = 6772等于 Code+RO+RW,也就是镜像大小
LR - ER_IROM10x218 = 5360x64 = 100等于 RW-data,即启动时要拷进 RAM 的 .data
ER_RW_IRAM1.Size0x5B60 = 233920x8A0 = 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_payload0x68000000131072外部 SRAM,IAP 载荷缓冲
s_sector_buf0x200009804096内部 RAM,SPI Flash 扇区缓冲
s_cache0x2000054848内部 RAM,param 参数缓存
s_rx0x2000050848内部 RAM,帧接收器状态
s_header0x200004F816内部 RAM,帧头备份
s_spi_errors0x200001784内部 RAM,SPI 传输错误计数
uwTick0x200001644内部 RAM,HAL 时基
__initial_sp0x20005B60—内部 RAM,栈顶

__initial_sp = 0x20005B60 正好等于 0x20000000 + 0x5B60,与 ER_RW_IRAM1.Size 一致——栈顶落在区域末尾,是链接器布局的直接结果。

s_payload 落在 0x68000000 靠的是绝对定位:

c
// 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 合计536HAL/外设句柄、lwIP 统计量、iap / param / dm9000 / bsp_spi_flash 的小状态量
.bss:lwIP 协议栈15232mem.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:串口行缓冲472usart.o
.bss:FSMC / SPI / HAL Flash / DM9000 句柄280fsmc.o 144 + spi.o 88 + stm32f1xx_hal_flash.o 32 + dm9000.o 16
.bss:IAP 接收器64s_header 16 + s_rx 48
.bss:param 缓存48s_cache
.bss:printf 缓冲96c_w.l(libspace.o)
HEAP + STACK1536startup_stm32f103xe.o,CubeMX 默认 512 + 1024
—23392ER_RW_IRAM1.Size = 0x5B60

其中 SPI Flash 驱动的 4 KB 扇区缓冲是内部 RAM 里最大的单项。它是 spiflash_write() 做"读-改-写"必需的中转:目标扇区里可能有别的数据要保留,必须先整扇区读出来再合并。省掉它的代价是每次写都要调用方自己保证扇区空白。

2.5 组件表:谁占的 ​

Image component sizes 按 Code 排前 22 名:

目标文件Codeinc.dataRO DataRW DataZI Data
tcp_out.o817237028500
tcp_in.o6808191803616
tcp.o61822572201320
bsp_spi_flash.o4072165873445128
pbuf.o27541328010
etharp.o27061000651240
mem.o19128800128211
param.o1732744167148
iap.o164863604131136
dm9000.o16443660116
stm32f1xx_hal_uart.o127810000
stm32f1xx_hal_rcc.o1240841800
iap_service.o112269617200
netif.o10063920120
ip4.o938176020
stm32f1xx_hal_spi.o9104000
usart.o848840340472
udp.o818324080
icmp.o816326000
iap_tcp.o780346080
timeouts.o70435616120
bsp_external_sram.o59624218300
Object Totals54266188762144536153832

三点解读:

(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 个字)3044.5%
代码与其余只读常量636894.0%
.data 初值(启动时拷进 RAM)1001.5%
合计(= bin 大小)6772100%
符号地址大小
__Vectors0x08020000__Vectors_Size = 0x130
__Vectors_End0x08020130—
Reset_Handler0x080201DD8(Thumb)

0x080201DD 这个值与上位机 pack_iap.py 打包时打印的复位向量完全一致:

text
复位向量  : 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.axf1139656含调试信息,不能用来判断占用
atk-f103.hex165148ASCII 文本,长度与镜像无关
flash-app.axf576832同上
flash-app.hex19107同上
flash-app.bin6772恰好等于 LR_IROM1.Size
app.img6796IAP 帧文件 = bin + 24 字节帧开销

6796 = 6772 + 24 与第 6 小节的规律一致:帧开销是定值 24 字节(16 字节头 + 8 字节尾),其余 6772 字节与将要写入 Flash 的内容逐字节相同。

3. 余量总表与烧写耗时 ​

3.1 Flash ​

区域起始链接上限结束页数实占占本区余量
第 1 块:Bootloader0x08000000128 KB0x0801FFFF645921245.2%71860
第 2 块:App0x08020000128 KB0x0803FFFF6467725.2%124300
第 3、4 块:预留0x08040000—0x0807FFFF12800%262144
合计0x08000000512 KB0x0807FFFF2566598412.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 ​

工程区间容量实占占比余量
Bootloader0x20000000 ~ 0x20005B5F64 KB2339235.7%42144
App0x20000000 ~ 0x2000089F64 KB22083.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 ~ 0x6801FFFF13107212.5%
未使用0x68020000 ~ 0x680FFFFF91750487.5%
合计—1048576100%

载荷缓冲的上限 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)43386约 0.25 s约 0.5 s
满量程(128 KB)6465536约 4.6 s约 8 s

耗时与镜像体积基本成正比,这一点在串口升级时要有预期:满量程时串口会长时间只输出进度行(每 64 KB 一行,共 2 行)。实际耗时以串口日志为准,上表只用于估算。

4. 分区是一份跨文件的约定 ​

4.1 必须同步的六处 ​

分区不是只写在 .uvprojx 里的一串数字,而是跨多个文件的约定。任何一处单独改动都会静默出错——除了 APP_BASE 本身,其余几处没有任何编译期检查能发现不一致:

位置字段当前值
tools/pack_iap.pyAPP_BASE / APP_END / APP_MAX_SIZE0x08020000 / 0x0803FFFF / 131072
flash-app/MDK-ARM/flash-app.uvprojxOCR_RVCT4 起始与大小0x8020000 / 0x20000
atk-f103/MDK-ARM/atk-f103.uvprojxOCR_RVCT4 起始与大小0x8000000 / 0x20000
atk-f103/Drivers/BSP/Iap/iap.hIAP_APP_BASE / IAP_APP_END0x08020000 / 0x08040000
atk-f103/Drivers/BSP/Iap/iap.hIAP_PAYLOAD_MAX128U * 1024U = 131072
运行时SCB->VTOR(iap_load_app() 内赋值)IAP_APP_BASE
flash-app/Core/Src/main.cFLASH_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:

text
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 之后,风险从"运行时静默踩内存"变成"链接期当场报错":

text
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。

只要这五步还能走通,本文的数字过期与否都不影响你得出当前版本的结论——方法留下来,数字随时重算。