公司动态

STM32未用FLASH区域填充:链接脚本配置与固件校验优化

📅 2026/8/30 2:06:29
STM32未用FLASH区域填充:链接脚本配置与固件校验优化
1. 为什么要给未用FLASH区域做填充先聊个实际场景。你在做产品量产的时候固件需要做CRC校验或者签名校验用来保证程序在传输、烧录过程中没有被篡改或者损坏。这时候你写了一个上位机工具把编译出来的hex或者bin文件读进来对整个FLASH区域做校验和计算。结果呢烧进去的芯片第一次上电自检校验失败。排查半天发现问题出在那些“没用到”的FLASH区域上。编译生成的bin文件只包含实际代码和数据占用的空间而芯片FLASH里剩余的部分出厂时可能是0xFF也可能因为之前烧过其他程序残留了其他数据完全不可控。你的校验算法是对整个FLASH区间算的这部分随机数据就会导致校验结果对不上。另一种常见场景是调试阶段。你用调试器反复下载程序FLASH里残留的旧数据和新代码混在一起某些未初始化区域被意外执行程序跑飞了还不好定位。如果能把未用区域统一填充成固定值一般是0xFF或者0x00至少能让行为可预期排查问题会省很多事。LAT1306这个应用笔记讲的就是怎么在STM32CubeIDE里做这件事。它的核心思路并不复杂通过修改链接脚本在编译阶段就把未用区域填充成指定值。这样生成的bin文件天然是“完整”的不需要额外写脚本去处理也不用在烧录后再去擦除改写。适合谁看呢主要三类人做量产固件、需要做校验和或签名的嵌入式工程师。调试阶段被FLASH残留数据坑过想让行为更可控的人。对STM32CubeIDE链接脚本、启动文件机制感兴趣想弄明白编译链接背后逻辑的爱好者。这篇文章我会把原理、配置步骤、常见坑一次讲透按我实际验证过的流程来写。2. 填充方案的原理与选型分析2.1 为什么直接改启动文件不行很多人第一反应是在启动文件里定义一个很大的数组初始化为0xFF不就把区域占住了吗思路方向对但实现上有问题。C语言里定义一个const uint8_t fill_area[0x10000] {0xFF}这样的数组编译后确实会在FLASH里占空间。但问题是这个数组通常会被放在.rodata段里具体位置由链接脚本决定不能精确控制它落在哪个地址区间。你希望填充的是“代码段之后到FLASH末尾”的区域而不是随便一个地方。另外大数组的初始化会导致编译时间变长、生成的中间文件变大而且如果链接脚本里FLASH总容量和实际芯片型号不匹配数组越界都不知道。这种方式只适合“恰好够用”的场景一旦工程代码长大很容易出问题。2.2 链接脚本方案从源头解决正确的做法是改链接脚本.ld文件利用链接器的填充机制。GNU ld 支持一个特性当输出段output section的地址空间大于输入段实际内容的大小时可以用一个填充值来填补空隙。具体来说链接脚本里可以这样写.flash_fill : { . ORIGIN(FLASH) LENGTH(FLASH) - 1; BYTE(0xFF) } FLASH这段脚本的意思是定义一个名为.flash_fill的输出段把它放在FLASH区域的最后一个字节并且这个字节的值强制为0xFF。链接器在布局时发现这个段的起始地址当前.的值和前面代码段结束地址之间存在空洞就会自动用填充值去填补。这个方案的优点编译产物hex/bin里天然包含完整的FLASH区域数据不需要额外工具处理。填充值可以任意指定0xFF、0x00或者别的值都行只要芯片支持。不占RAM不增加运行时开销。填充逻辑集中在链接脚本里工程结构清晰。缺点也有需要对链接脚本有一定了解第一次配置有点门槛而且如果FLASH区域中间有多个不连续的空洞可能需要写多段脚本去覆盖。2.3 填充值怎么选0xFF还是0x00这是实际项目里很多人纠结的点。我的建议是优先用0xFF除非有明确理由不用。原因很简单STM32的FLASH在擦除后所有位都是1也就是0xFF。如果把未用区域填充成0xFF那么这部分区域和“刚擦除的芯片”状态完全一致。这样生产流程里即使你不在固件里做填充烧录器直接擦除再烧录效果也是一样的——校验算法拿到的是一个确定性的全FF区域。反过来如果你填0x00芯片擦除后是0xFF烧录器烧完代码后还要再把未用区域写成0x00。大部分烧录器是支持“擦除后写入完整镜像”的所以也能做但多了一道操作而且对烧录器有额外要求。从调试和排错角度0xFF还有个好处如果你不小心跳到了未初始化区域PC指针跑飞了执行到全0xFF的指令在ARM Cortex-M上会触发HardFault方便你立刻发现程序跑飞了。如果全是0x000x00在ARM里是lsls r0, r0, #0是一句空操作程序会在填充区里一直“滑行”直到遇到有效代码或其他异常排查起来更费劲。提示如果你的产品对功耗有极致要求或者FLASH里有特殊保护逻辑选填充值前先确认芯片型号的数据手册个别芯片对FLASH编程有特殊限制。2.4 直接影响范围hex文件、bin文件和烧录器配置填充之后最直观的变化是编译产物体积变大。原本你的代码只有32KB但芯片FLASH是256KB填充后bin文件会变成256KB。hex文件也会相应变大。这会影响编译时间链接阶段生成大文件会多花一点时间但通常可接受。烧录时间烧录器要写入的数据量变大量产时每片芯片的烧录时间会增加。如果产线对节拍很敏感需要评估这个成本。版本管理bin文件变得很大放在Git里每次提交的diff会不好看。建议在CI流程里用release构建做填充日常开发构建不做。另外一个容易被忽略的点填充后如果后续代码空间不够用了怎么办这是我用这个方案时最担心的问题。FLASH区域被“占满”之后链接器一旦发现代码段增长导致和.flash_fill段重叠会直接报LMA区域溢出错误。这时候你会被迫重新调整填充区域的边界——这是好事因为它强制你关注FLASH占用率。我实际的做法是不把整个FLASH全填满往下留一点安全余量。比如芯片有256KB我只填充到最后一个扇区之前预留几KB空间给后续小改动。这样既保证了校验区域的确定性又给自己留了缓冲。后面在“实操过程”里会详细讲怎么界定边界。3. 实操过程完整配置步骤与核心环节实现3.1 环境准备本文示例基于以下环境硬件STM32F103C8T664KB FLASH项目里LAT1306的对应芯片IDESTM32CubeIDE 1.13.0以上版本调试器ST-Link V2 / J-Link均可固件库STM32CubeF1其他系列同理在开始之前确保你的工程能在默认配置下正常编译、烧录、运行。这个前提很重要因为后续改动只涉及链接脚本和构建配置如果基础工程都不稳很难判断是填充配置引入的问题还是环境本身的问题。STM32CubeIDE新建工程这里就不赘述了重点讲怎么改。如果你用的是STM32CubeMX生成的基础工程改完链接脚本注意别在CubeMX里重新生成配置时不小心覆盖掉最好把修改记录下来或者直接把链接脚本加入版本管理。3.2 修改链接脚本核心配置打开工程里的链接脚本默认路径在工程根目录下文件名一般是STM32F103C8TX_FLASH.ld。用文本编辑器打开重点关注MEMORY和SECTIONS两个部分。先看一眼默认的MEMORY定义MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 64K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这里明确指定了FLASH的起始地址和长度。我们需要在SECTIONS里新增一个输出段用它来触发填充。在SECTIONS的末尾}之前加上如下内容.flash_fill : { . ORIGIN(FLASH) LENGTH(FLASH) - 1; BYTE(0xFF) } FLASH我来逐行解释这段脚本在做什么。第一行的.flash_fill是段名你可以随便起只要不跟已有的段重名就行。:后面的大括号里是这个段的布局规则。. ORIGIN(FLASH) LENGTH(FLASH) - 1;这行是核心技巧。.在链接脚本里表示当前虚拟地址游标。把这个游标直接设置到FLASH区域最后一个字节的地址也就是0x08000000 64K - 1 0x0800FFFF。BYTE(0xFF)在这个地址上放置一个单字节值为0xFF。那么关键问题来了链接器在布局时发现上一段通常是.isr_vector、.text、.rodata这些实际代码数据段结束地址距离0x0800FFFF之间还有一段空洞这个空洞没有对应的输入段内容。链接器会用“填充值”来填充这段空洞。默认的填充值是0x00但我们用BYTE(0xFF)设置了段的最后一个字节为0xFF后链接器会把这个作为整个输出段的填充值模式也就是说空洞部分会用0xFF来填。这里要注意一个细节如果空洞大小不是1字节的整数倍在FLASH上一定是的填充值按字节来填充。使用0xFF时没问题因为0xFF是对齐的。3.3 修改启动文件确保入口最简实际上只改链接脚本就能让生成的bin文件包含填充区域。但有一个环节必须检查启动文件里是否定义了堆栈和中断向量表以外的东西会不会占用额外空间。多数场景下STM32CubeIDE生成的启动文件不用动。但如果你用的是自研启动文件注意确保FLASH区域里除了代码段、只读数据段之外没有其他需要运行时初始化的数据段被错误地放进了FLASH。否则填充区域的边界会和你预期的不一样。这里给一个判断方法编译后打开Project.map文件在Debug/目录下搜索Memory Configuration查看FLASH区域每个段的起始和结束地址确认_estack外的保留段没有异常跳动。3.4 构建并检查产物配置完成后执行一次完整构建Project - Build或直接点锤子图标。构建成功后在Debug目录下检查.bin和.hex文件。如果你用的是Windows可以用HxD这类十六进制编辑器打开bin文件直接看文件尾部是否为连续的FF。如果你用Linux/macOS用xxd或hexdump查看xxd firmware.bin | tail -20正常情况下bin文件的末尾应该是从代码段结束到文件结尾全是ff。文件的整体大小应该等于ORIGIN(FLASH) LENGTH(FLASH) - ORIGIN(FLASH)也就是64KB本例。到这里你可能会问我烧录到芯片里后怎么验证呢很简单用调试器在线读FLASH或者用CubeProgrammer把内部FLASH读出来存成文件再对比填充区域。实际操作中我会用STM32CubeProgrammer的“Read”功能把整个FLASH读出来然后检查数据一致性这是最直观的验证方式。3.5 把填充值改成0x00或其他值有时候你可能确实需要填充成0x00。改法很简单把链接脚本里的BYTE(0xFF)改成BYTE(0x00)。但这里有一个安全提醒STM32的FLASH擦除后是0xFF写入0x00相当于真正“编程”了这些位。如果后续你重新修改代码想擦除整个FLASH时这些0x00的位会被正常擦除回0xFF不会有副作用。但在线调试时如果填充区域正好被写保护了烧录某些页可能会报错。具体看芯片的保护配置。实际上在部分STM32型号上全0x00填充还有一个大坑如果程序计数器跑飞到全0填充区域这些指令会滑行很久而且可能触发总线错误或进入不可预期的状态。所以除非有特殊需求比如用0x00来表示“未使用”语义、配合外部校验算法要求全0基线我仍然建议保持0xFF。3.6 给FLASH区域预留安全余量前面提到过把FLASH完全填满会让后续代码空间变得敏感。一个实用的改进方法是不填满整个FLASH而是留出一段不参与填充的余量。具体做法是不全用LENGTH(FLASH)来定位最后一个字节而是手工指定一个地址.flash_fill : { . ORIGIN(FLASH) 56K - 1; BYTE(0xFF) } FLASH这个写法把填充段锚定在距离FLASH末尾还差8KB的位置。这样代码段 填充段只会占掉56KB剩下8KB处于“未填充”状态作为代码增长的缓冲。如果你的产品后续需要加功能这个缓冲就能派上用场不用每次改FLASH边界。但要注意这种方式下bin文件的体积变成56KB而不是64KB。如果你的校验算法是固定按“整个FLASH”来算的那这8KB的“自由区域”依然会导致校验不稳定。所以这个方案只适用于“校验区域固定为填充后的bin文件”的场景比如上位机直接把bin文件作为整体来计算哈希。实际项目中我更倾向于先用满FLASH做填充等代码接近满时再手动按扇区调整。3.7 多Bank芯片的特殊处理一些大容量STM32比如STM32H7系列有双Bank结构FLASH区域被分成两个独立的区间中间可能存在系统存储区或选项字节区。链接脚本里的MEMORY定义可能包含多个区域比如FLASH和FLASH1。这时需要分别对每个FLASH区域做填充脚本类似.flash_fill : { . ORIGIN(FLASH) LENGTH(FLASH) - 1; BYTE(0xFF) } FLASH .flash1_fill : { . ORIGIN(FLASH1) LENGTH(FLASH1) - 1; BYTE(0xFF) } FLASH1两个段的名字不能重复。而且要注意如果两个FLASH区域之间隔着系统区域bin文件导出时可能会包含不连续的区域。这时候通常建议用hex格式因为hex本身就支持不连续地址区间而bin是线性连续镜像导出的bin会包含中间所有空洞文件体积会虚增。我之前的项目里就遇到这个问题H743的1MB FLASH分两个Bank中间有保留区。用bin做OTA升级包时中间那段保留区数据全是0xFF导致升级包体积大了不少。后来改成只在代码段所在Bank做填充另一个Bank的填充交给上位机脚本在打包时处理才平衡了体积和确定性。4. 常见问题与排查技巧实录4.1 生成的bin文件大小不符合预期现象配置了填充后bin文件大小没有变成64KB还是只有代码实际大小。原因最常见的原因是链接脚本里MEMORY区域的LENGTH定义和实际芯片不匹配。比如芯片是64KB但链接脚本写的是128KB填充段被定位到128KB末尾导致bin只有前64KB代码填充区域在64KB之外的部分没有生效。排查方法打开.map文件搜索.flash_fill看看这个段的起始地址是哪个。正常情况它应该落在实际FLASH容量的最后一个字节地址上。如果对不上修改LENGTH或手工指定的锚点地址。另一个原因你用的是自带Bootloader的工程链接脚本里FLASH的ORIGIN被改过起始地址不是0x08000000比如偏移到了0x08008000。此时用ORIGIN(FLASH) LENGTH(FLASH) - 1计算出的还是正确的末尾地址没问题。但如果你手工指定了锚点记得加上偏移。4.2 链接报错“region ‘FLASH’ overflowed”现象代码实际占用 填充段占用的空间总和超过了FLASH容量链接阶段直接报错。原因FLASH空间不够用了代码膨胀到填充区域。处理办法确认代码占用情况看看是不是优化等级太低导致代码膨胀。在Project Properties - C/C Build - Settings - Tool Settings - MCU GCC Compiler - Optimization里把优化等级调到-Os优化尺寸很多时候可以腾出空间。如果确实代码太大只能换更大FLASH的芯片或者精简功能。临时方案把填充段锚点往前挪让填充区域缩小但这样你的校验区域就不是全FLASH了后续校验逻辑要同步调整。注意这个报错其实是个保护机制。它防止你“无意识”地覆盖掉填充段导致产物重复定义同一地址。我建议把它当成一次设计审查机会而不是强行绕过去。4.3 烧录后填充区域不是0xFF现象烧录完成后用调试器或CubeProgrammer读FLASH发现填充区域有部分不是0xFF。原因这个现象通常不是链接脚本的问题而是烧录器的烧录策略。很多烧录器在下载程序时会先擦除整个FLASH然后只写入hex/bin里实际包含的数据。如果你的bin文件已经包含填充区域0xFF理论上写进去就是0xFF没有问题。但如果你用的是“编程后自动校验”模式某些烧录器只校验它“实际写入的扇区”而填充区域如果全是0xFF还会被跳过扇区擦除导致校验跳过。还有一个隐蔽原因FLASH的写保护。某些芯片出厂时使能了读保护或写保护烧录器写入时静默失败读出来还是旧数据。如果这个排除不掉用CubeProgrammer检查一下选项字节。我的建议量产烧录时统一在烧录器里配置“Erase All Sectors”再编程完整镜像包含填充区域确保FLASH最终状态和bin文件完全一致。不要依赖“只擦除有变化的扇区”这种增量模式因为增量模式在量产现场会因为芯片状态不可控而埋雷。4.4 链接脚本在CubeMX重新生成后被覆盖现象用CubeMX改了Pin配置或时钟树重新生成代码后链接脚本里自定义的.flash_fill段不见了。原因CubeMX在重新生成项目时会对链接脚本做一部分刷新操作尤其是当芯片型号变更时MEMORY部分会重写。SECTIONS部分不一定每次都被覆盖但有一定概率被重置。处理办法把链接脚本纳入版本管理每次CubeMX重新生成后检查diff发现丢了就手动加回来。更“自动”一点的做法把填充逻辑单独放到一个新的.ld文件里然后在主链接脚本的SECTIONS里用INCLUDE xxx.ld引入。这样CubeMX重写主脚本时只要不删除INCLUDE行你的自定义段就能保留。不过实测CubeMX对不同版本的IDE行为不一致最稳妥的还是版本管理 改动记录。4.5 启动文件里增加自定义段导致启动失败现象有人想直接把填充段写进启动文件用汇编.space指令预留空间并初始化结果程序无法启动。原因这种方式本质上是让FLASH空间被占用了但可能影响中断向量表的排列或者你的.space加到了一个不可执行/不可读的段里。处理办法回到链接脚本的方案。启动文件动得越少越好链接脚本是修改FLASH布局的最标准方式。4.6 不同编译优化等级下产物不一致现象在Debug-O0下编译填充正常切到Release-Os后生成的文件大小变化甚至bin文件末尾填充区域长度变化。原因这是正常现象。不同优化等级下代码段长度不同填充区域的起点会跟着变化但填充区域的终点是固定的FLASH末尾所以bin文件的总长度是不变的。如果你发现总长度变了那一定是你链接脚本里锚点没有写死或者手工用了类似. ALIGN(4)导致游标移动后计算出来不同。排查方法确认.flash_fill段的起始地址只依赖ORIGIN(FLASH) LENGTH(FLASH) - 1不要在其他地方修改这个地址。5. 进阶用法把填充与校验流程串在一起到这里基础填充已经能正常工作了。但实际项目里填充往往只是链条的一环。我再分享两个实际工程里的进阶用法帮你把这个能力发挥到最大。5.1 生成带校验值的完整镜像一个常见需求是烧录前自动计算固件的CRC32/SHA256把校验值附加到固定位置通常是FLASH末尾几个字节用于Bootloader在上电时自校验。做法是把填充段和“放校验值的区域”结合在一起。思路如下在FLASH末尾预留一段固定区域专门存放校验值。这个区域在链接脚本中定义为一个独立段初始值可以用0xFF占位。编译出bin文件后用上位机脚本Python计算前面所有区域的校验值填入预留区域。产线烧录时直接烧录这个“已经填好校验值”的完整镜像。这样做的好处是Bootloader不需要在运行时动态计算并烧写FLASH那会很慢而且需要谨慎处理掉电问题而是直接读取固定地址的校验值来比对。因为填充区域是确定性的0xFF校验结果也是确定性的不会出现“上次烧录的数据和这次不一样”的情况。5.2 与CE/OTA升级配合的注意事项如果你的产品支持OTA升级升级包通常是从FLASH某个偏移地址开始的一段数据。这时候未用FLASH的填充值会影响升级包的制作逻辑。比如你的App区是0x08008000到0x0801FFFF共96KB。实际代码只有60KB剩余36KB是填充区域。OTA升级包是只打包那60KB还是打包完整的96KB两种方式各有优劣只打包60KB升级包小传输快但接收方烧录时只能做“部分擦除”未覆盖区域保持旧状态。如果旧固件和新固件在未用区域的填充值不一致整体校验会失败。打包96KB完整镜像升级包大但接收方可以整片擦除后写入镜像完整性和校验都简单可靠。显然如果你的升级流程里有“整体校验”的环节第二种方式更稳妥。而填充区域让第二种方式成为可能——因为bin文件本来就是96KB的完整镜像不依赖现场芯片的擦除状态。这也是“填充未用FLASH区域”在生产环境中最大的价值。5.3 使用脚本批量生成不同填充值的镜像在某些产线场景下不同产品型号可能要求填充不同的值比如用0xAA表示“禁用功能”0x55表示“启用功能”。手动改链接脚本再编译效率太低了。我的做法是写一个Python脚本直接读取链接脚本模板里的锚点配置生成多个版本import re def modify_fill_value(ld_path, fill_byte): with open(ld_path, r, encodingutf-8) as f: content f.read() # 替换BYTE(0xFF)为BYTE(0x..) content re.sub(rBYTE\(0x[0-9A-Fa-f]\), fBYTE(0x{fill_byte:02X}), content) with open(ld_path, w, encodingutf-8) as f: f.write(content)这样每次构建时脚本自动生成不同填充值和镜像我就能区分不同配置的产品不需要维护多套代码分支。提示填充值不能随意指定必须是FLASH可编程的位模式。比如从0xFF只能向0x00方向编程单个位小端模式下字节值可以是0xFE、0xFC等但不是所有值都能从任意初始状态转换过来。如果你的校验算法不依赖特定填充值建议只用0xFF或0x00两个值。6. 一个完整的自动化流程参考最后给一个我在实际生产项目中使用的完整流程从编译到烧录全链路覆盖。第一步编译生成带填充的固件链接脚本里配置好填充段使用Release配置编译产物为firmware.hex或bin。第二步校验产物用脚本检查bin文件最后一个非0xFF的字节是否存在并且文件大小是否符合预期python3 -c data open(firmware.bin, rb).read() print(fSize: {len(data)} bytes) last_non_ff max(idx for idx, b in enumerate(data) if b ! 0xFF) print(fLast non-0xFF byte at: {hex(last_non_ff)}) 如果最后一段全是0xFF说明填充生效。第三步计算并写入校验值用Python的zlib.crc32或hashlib.sha256计算镜像的哈希值写入预留区域比如FLASH末尾某个固定偏移。写完后再次生成最终的烧录包。第四步产线烧录烧录器软件里配置“全片擦除 编程完整镜像 校验”。确保烧录器能识别firmware_with_crc.hex里的不连续地址区间如果用了多段填充。第五步产线抽检用CubeProgrammer随机抽取烧录好的芯片读取整个FLASH区域和烧录包逐字节比对。这一步能有效发现填充不到位、烧录策略配置错误等问题。这套流程在多个项目中跑下来校验不稳定导致的上线返工问题基本被根除了。唯一需要注意的是产线烧录速度会受bin文件体积影响但相比排查随机校验失败的成本这点时间投资非常划算。从我个人的经验看填充未用FLASH区域这个操作初看只是链接脚本里画蛇添足的一两行但它带来的核心收益是让固件镜像的每一个字节都可预测。无论是做校验、做签名、做差分升级还是单纯想排查神秘问题可控的镜像都是基础中的基础。如果你在量产或调试中也被FLASH随机数据坑过不妨花半小时试试这篇文章里的配置大概率能省下后面几天的排查时间。