
1. 内容整体设计与思路拆解1.1 HAL库学习到这个阶段为什么要碰内部Flash做单片机开发的人不管是玩STM32还是其他ARM Cortex-M内核的芯片迟早都要面对一个问题程序运行时的数据保存在哪里。RAM一掉电就没了外挂EEPROM或者Flash虽然能存但要么占IO口要么增加成本。其实芯片自带的内部Flash就是一个现成的非易失存储区读速度够快写入寿命虽然有限但大多数应用场景绰绰有余。很多人在学HAL库的时候前面玩过GPIO、串口、定时器、中断觉得差不多了一听到内部Flash就觉得是不是很难。实际上内部Flash的操作远没有想象中复杂就三个动作解锁、擦除、写入读反倒是最简单的——直接当普通内存访问就行。HAL库把这些底层寄存器操作都封装好了我们只需要搞清楚几个函数的参数含义再注意一下Flash的扇区/页划分就能把它用起来。标题里提到了“反应单片机烧录进去的程序大小”这其实就是内部Flash最典型的一个应用场景通过读取芯片Flash中某个地址的内容反推当前固件实际占用了多少空间。特别是我们做BootLoader或者OTA升级的时候经常需要知道App固件到底写到哪了才能决定下一帧数据往哪个地址放。这篇内容的核心就是两部分第一部分是HAL库操作内部Flash的完整流程第二部分是拿到bin文件、分析固件大小的实用方法。1.2 为什么选“读取Flash内容”来反推程序大小先想一个问题Keil或者IAR编译完之后生成的hex文件里带地址信息我们用烧录器下载到芯片里程序从0x08000000开始逐字节写入。如果我想知道自己写的代码到底占了多少字节最直接的办法就是看编译输出窗口里显示的Code、RO-data、RW-data、ZI-data总和但那个值是编译器的静态估算实际烧进去的bin大小会因为对齐、填充等原因略有出入。而且很多时候我们手里只有一个烧录好的板子没有工程文件这时候就只能从芯片内部去“量”程序的长度。这时候内部Flash就派上用场了。程序烧录进去之后Flash里从起始地址开始连续存放的就是固件的机器码和数据。只要我们从头开始读一直往后读找到“空数据”的位置——也就是Flash擦除状态的值0xFF那第一个出现整段0xFF的地址基本就是固件的边界。当然这里有个细节程序末尾可能还有一小段未使用的对齐填充但误差通常只有几个字节到几十个字节对于估算固件大小来说足够用了。标题里说的“教得到bin文件的方法”其实指的是两种东西一是从Keil或者STM32CubeIDE里直接导出bin文件二是在没有工程的情况下直接从芯片内部Flash读出来新鲜的bin内容。这两种方法各有适用场景我在后面会演示一条完整的实操路径先用HAL库写一个简单的Flash读取程序把固件区域前几百字节读出来通过串口发到电脑上再对比bin文件验证一下哪一段是自己真正的程序代码。1.3 内部Flash操作的基本心智模型很多初学者一看到“Flash操作需要解锁”“擦除前必须先读出来保存”就发怵。其实你可以把内部Flash想象成一块写字板字写上去之后如果想改动某个字不能用橡皮擦擦掉重写必须把整页纸撕掉换成新纸然后再重新把没改的内容抄一遍。Flash的物理特性决定了它只能把1写成0不能把0写成1所以写之前必须先擦除而擦除的最小单位是扇区或页不是字节。这就是为什么你明明只想改一个字节却要把整个扇区都先读出来备份擦掉再把备份数据和修改数据一起写回去。HAL库对这一整套流程做了封装核心就是几个函数HAL_FLASH_Unlock()解锁Flash控制器禁止意外写入。HAL_FLASH_Program()指定地址和数据类型写入数据。HAL_FLASH_Erase()初始化擦除结构体按扇区擦除。HAL_FLASH_Lock()重新锁定Flash防止程序跑飞误改Flash内容。还有一个隐藏的细节STM32的Flash读写是有等待状态要求的。当CPU主频比较高的时候读Flash需要插入等待周期否则会读到乱码。HAL库在SystemClock_Config()里会通过FLASH_SetLatency()自动设置一般不用我们管但如果你自己写裸机程序或者把HAL库的时钟初始化代码删掉了就容易在这上面踩坑。2. 核心细节解析与实操要点2.1 STM32内部Flash的地址划分与容量对照不同型号的STM32内部Flash的地址范围、扇区大小、擦除粒度都不一样。以最常用的STM32F103系列为例Flash从0x08000000开始大小有16KB到512KB不等。F103的小容量芯片扇区是1KB一个中大容量芯片则是前4个扇区16KB总共64KB后面扇区是64KB一个。F407又是另一种划分方式前4个扇区16KB第4个扇区64KB注意这里编号从0开始第4个其实是指扇区4后面7个扇区128KB。F4系列跟F1系列完全不一样操作函数也变了千万不要拿F1的扇区表去套F4的擦除函数。我整理了一个常见对照表方便大家写代码前先确认自己手上的芯片属于哪种划分芯片系列Flash起始地址扇区/页大小擦除最小单位典型容量STM32F103C80x080000001KB小容量/ 前4个扇区16KB中容量按扇区擦除64KBSTM32F103ZET60x08000000前4个扇区16KB后11个扇区64KB按扇区擦除512KBSTM32F407VET60x08000000扇区0~3为16KB扇区4为64KB扇区5~11为128KB按扇区擦除512KBSTM32G4310x080000002KB一页按页擦除128KB从这张表可以看出同样是“按扇区擦除”不同芯片的扇区大小天差地别。比如F103ZET6最后那几个扇区是64KB一个如果你只想存一个4KB的配置数据硬是把整个64KB扇区擦一遍那写入时间就是毫秒级到几十毫秒级的差异而且Flash寿命损耗也不划算。所以选存储区域的时候尽量选尺寸匹配的扇区宁可多花点心思算偏移也不要用大扇区存小数据。2.2 HAL库Flash操作的三个关键函数深入解析HAL库的Flash操作在stm32f1xx_hal_flash.c或stm32f4xx_hal_flash.c这些文件里实现不同系列函数名几乎一样但内部实现细节差别很大。我这里以F1系列为例子讲F4系列的差别我会单独标注。先看写入函数HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data);TypeProgram有两个取值FLASH_TYPEPROGRAM_HALFWORD和FLASH_TYPEPROGRAM_WORD。F1系列还支持FLASH_TYPEPROGRAM_BYTE但F4系列不支持字节写入至少是半字16位。为什么要这样设计因为F4的Flash控制器硬件上就是按32位或者16位粒度写的你直接写一个字节它会把你给的8位数据放到一个半字的低位高8位填充0xFF这样反而可能把相邻字节的内容搞坏。还有一个关键点这个函数的Data参数是uint64_t但实际上用它表示数据时低32位有效就够了。比如写入一个32位数据HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, (uint64_t)data);这里的data会被强制扩展成64位高32位自动补0不用自己手动转。有些人喜欢先定义一个uint64_t buffer再传进去没必要直接传(uint64_t)强转就行。再看擦除函数FLASH_EraseInitTypeDef EraseInitStruct; EraseInitStruct.TypeErase FLASH_TYPEERASE_SECTORS; EraseInitStruct.Sector 4; // 从扇区4开始擦 EraseInitStruct.NbSectors 1; // 擦几个扇区 EraseInitStruct.VoltageRange VoltageRange_3; // F4系列需要这个参数 uint32_t SectorError 0; HAL_FLASHEx_Erase(EraseInitStruct, SectorError);注意F1系列没有VoltageRange这个字段F4系列才需要。如果你的代码在F1上编译报错说结构体里没有这个成员检查一下是不是误用了F4的例程。擦除执行的时间取决于扇区大小和芯片主频通常在20ms到2秒不等。擦除期间CPU会被阻塞如果有中断频繁进来要么临时关闭中断要么把擦除操作放到不影响实时性的时机去执行。擦除完成之后可以用一个简单方法验证读一下擦除区域的第一个地址如果返回0xFFFFFFFF说明擦除成功了。HAL库的HAL_FLASHEx_Erase()本身会返回状态但我们做产品的时候我还是习惯自己再读一遍确认防止因为电压不稳或者时序问题导致擦除不干净。2.3 读取Flash区域数据的正确姿势Flash读取不叫“读Flash函数”不需要解锁也不需要调用任何HAL库API直接像访问普通内存一样操作指针就行。比如我想读地址0x08010000处的4字节数据uint32_t data *(__IO uint32_t *)0x08010000;这里的__IO是volatile修饰告诉编译器这个地址的内容可能随时变化别做优化缓存。如果你要读一段连续的固件数据可以用指针循环遍历uint8_t *p (uint8_t *)0x08000000; uint8_t buf[256]; for (int i 0; i 256; i) { buf[i] p[i]; }这种方式可以直接用来扫描整个Flash找到从哪个地址开始连着几十个字节都是0xFF那基本就是程序结束的位置。不过要注意程序在运行的时候Flash里有些区域可能是中断向量表、只读常量、函数代码这些内容不会是0xFF。真正的空区域是那些从来没有被写入过、或者被擦除过的区域。所以扫描方向一定要从固件起始地址0x08000000往后扫而不是随便找一个地址乱扫。还有一个容易忽略的地方STM32的Flash读取要保证地址对齐。用uint32_t *指针读的时候地址必须是4的倍数用uint8_t *指针逐字节读就没有这个问题。如果你要用memcpy把Flash里的数据拷到RAM数组建议用memcpy而不是自己写指针循环编译器会帮你做优化并且处理对齐问题。2.4 解锁与上锁安全操作的完整边界用HAL库操作Flash之前必须先调用HAL_FLASH_Unlock()。这个函数做的事情是往Flash控制器的KEYR寄存器依次写入两个密钥值0x45670123和0xCDEF89AB。写入顺序不能反而且必须在解锁状态下才能继续擦写操作。如果顺序错了或者上一次操作出现错误标志没清除解锁会失败后续函数返回HAL_ERROR。很多人写完擦除和编程之后忘记重新上锁虽然程序跑起来一般不会出问题但这是一个很严重的安全隐患。一旦Flash控制器处于解锁状态如果程序因为电压波动、指针越界等原因往Flash地址执行了写操作轻则改写配置数据重则把应用程序本身擦掉直接变砖。所以写完Flash之后立刻调用HAL_FLASH_Lock()重新锁上这是一个好习惯。还有一种情况是写Flash过程中出现错误HAL库把错误标志记录在Flash控制器的状态寄存器里。如果你不做处理直接进行下一步操作错误标志会一直存在导致后续的擦写操作被阻塞。遇到这种情况调用一下__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR)把标志清干净再重新尝试。3. 实操过程与核心环节实现3.1 制作一个“Flash内容扫描器”来算固件大小为了把标题里的需求落地我先写了一个非常小的验证程序。它的功能很简单通过串口接收上位机指令收到指令后从Flash起始地址0x08000000开始每次读取256字节检查里面如果连续128字节都是0xFF就认为已经越过了固件边界把边界地址和估算大小返回给上位机。这个程序不需要任何外部存储也不需要BootLoader配合只要烧录一次它就能读自己的Flash来估算自己的固件大小。当然你会说编译输出窗口不是直接显示Program Size吗没错但我们的目的是理解内部Flash读取的方法以后做BootLoader或者程序自检就能直接复用这套逻辑。代码核心部分如下uint32_t find_firmware_end(void) { uint32_t addr 0x08000000; uint32_t end_addr 0x08000000; uint8_t zero_cnt 0; while (addr FLASH_END_ADDR) { uint8_t byte *(__IO uint8_t *)addr; if (byte 0xFF) { zero_cnt; if (zero_cnt 128) { // 连续128个0xFF认为是空闲区域 end_addr addr - 128; return end_addr; } } else { zero_cnt 0; } addr; } return addr; // 没找到边界就返回Flash末尾 }这里为什么选128字节作为判断阈值因为程序结尾处可能有一些对齐填充比如编译器为了4字节对齐在后面填了0~3个字节的0x00或者0xFF但绝不会填128个连续0xFF。所以128这个阈值既能避开对齐填充的干扰又不会因为误判导致把程序截短。当然如果你程序的只读数据区末尾恰好有一段未初始化的全0xFF数据那这个判断就会提前结束但这种情况非常少见通常只出现在使用__attribute__((section(.ARM.__at_0x08000000)))自定义段数据时出现。普通工程不会有这个问题。拿到边界地址之后程序大小就是firmware_size end_addr - 0x08000000;比如找出来的边界是0x0800A200那程序大小就是0xA200 41472字节 ≈ 40.5KB。3.2 从Keil工程导出bin文件的详细步骤标题里特意提到“得到bin文件的方法”我猜不少人是被开发环境里默认生成的hex文件搞懵了。hex文件是带地址信息的文本格式里面每一行都包含起始地址和数据长度烧录器能识别但如果你想直接看这个固件占用多少字节、想把它合并到BootLoader里就必须要一个纯粹的bin文件——说白了就是机器码的裸数据没有地址也没有校验烧录的时候必须告诉它烧到哪个地址。Keil MDK导出bin文件的方法是设置一个用户自定义命令在编译完成后自动调用fromelf.exe。具体步骤打开Keil点击魔法棒Options for Target。切换到“User”选项卡。在“After Build/Rebuild”下面的“User Command”输入框里勾选“Run #1”。填写命令fromelf.exe --bin --output .\Objects\你的工程名.bin .\Objects\你的工程名.axf点击OK重新编译一次然后去Objects文件夹找bin文件。这里必须注意路径和工程名不能写错。fromelf.exe是Keil自带的转换工具它能把axf文件转成bin。如果你的工程是中文名或者路径里有空格命令里的引号要加好fromelf.exe --bin --output .\Objects\我的工程.bin .\Objects\我的工程.axf还有一种更简单的办法是不写命令直接在工程里生成hex文件后用第三方工具转bin但那就多了一步不推荐。STM32CubeIDE用户则是在Project Properties里设置右键工程 - Properties。选择C/C Build - Settings - Build Steps。在Post-build steps的Command输入框里填写arm-none-eabi-objcopy -O binary -S 你的工程名.elf 你的工程名.bin重新Buildbin文件就会生成在Debug或Release目录下。bin文件生成之后用十六进制编辑器打开开头应该是00 01 02 03 ...这样的中断向量表数据取决于芯片从第0字节到第8字节是初始SP指针接着是复位向量地址。这个特征可以用来验证你读出来的Flash数据是不是真的固件。3.3 直接把芯片里的固件读出来保存成bin如果你手里没有工程文件只有一块烧录好程序的板子也别慌。用ST-Link或者J-Link自带的工具就能把Flash内容读出来。最方便的是配合STM32CubeProgrammer连接ST-Link和板子。打开STM32CubeProgrammer选择ST-LINK设置连接方式为“Under reset mode”防止芯片里跑着的程序干扰调试。点击“Connect”连接成功后切到“Memory File”页面在地址输入框填0x08000000大小填芯片Flash容量点击“Read”按钮。读取出来后点击“Save”按钮选择保存为.bin格式文件路径自己定。这样得到的bin文件就是包含芯片全部Flash内容的镜像包括所有已写入的程序数据。如果你只是想看固件截断后的有效部分可以用我前面说的扫描算法把边界算出来然后在保存的时候把Size改成固件实际大小这样导出的bin就只包含有意义的程序代码后面做逆向分析或者做差分包都很方便。这里有个实操心得读Flash的时候最好用“Under reset mode”也就是复位模式下连接。否则如果程序里初始化了调试引脚或者进入了低功耗模式调试器可能连不上。我遇到过好几块板子程序跑飞把SWD引脚复用掉了直接连接连不上必须复位模式下才能握手成功。3.4 验证自己的读取结果与bin是否一致我们前面做了两步一是用内部Flash扫描器找固件边界二是用调试器导出bin。现在可以做一个交叉验证确保你的扫描算法没问题。操作方法是用STM32CubeProgrammer读取整个Flash保存成flash_backup.bin然后在电脑上用Python写个简单脚本检查从0x08000000开始有多少个字节不是0xFFwith open(flash_backup.bin, rb) as f: data f.read() last_non_ff 0 for i, b in enumerate(data): if b ! 0xFF: last_non_ff i print(固件实际占用不含尾部填充: 0x%X bytes % (last_non_ff 1))注意这里用的是“最后一个非0xFF字节的索引”不是连续0xFF判断。它是基于整个bin文件的所以如果固件末尾恰好有一段全0xFF的有意义数据这个结果会把它们当成“空”忽略。不过对于绝大多数普通程序来说这个误差可以忽略。再把之前扫描器算出来的firmware_size和这个last_non_ff 1比较一下如果差值在几百字节以内说明扫描算法和实测基本吻合。为什么要允许几百字节的误差因为扫描器在发现连续128个0xFF时会把前面128字节也算成空闲如果程序末尾真的有128字节的连续0xFF比如大量未初始化的全局变量不变量在RAM不在Flash但比如的的确确有一张图片数组或者字库数据全是0xFF那就误判了。所以扫描器的准确度取决于内容分布正常情况下误差不会超过几个扇区。从实用性角度当你做BootLoader跳转App时你需要知道App固件有多少字节才能决定是从串口一次收完还是分包写Flash。用我上面说的扫描法在App运行时读它自己的Flash把边界算出来再通过串口反馈给上位机这个方案的可靠性已经够高了。4. 常见问题与排查技巧实录4.1 写入Flash之后程序直接跑飞或者HardFault这是新手最容易踩的坑。写完Flash后如果程序跑飞大概率是因为你写的地址本身是非法的或者你在写Flash的时候中断里也访问了Flash导致总线冲突。排查步骤如下检查写入地址是否落在Flash有效范围内。比如F103C8容量是64KB有效范围0x08000000到0x0800FFFF写0x08010000就是超范围了。检查是否在擦除/写入过程中被中断打断。Flash编程期间CPU会暂停执行但中断可能会挂起等Flash操作结束才进入ISR。这通常没问题但如果你在中断里也调用了Flash操作函数就会产生互斥问题。我的建议是Flash操作期间关中断__disable_irq(); // 擦除和写入 __enable_irq();检查数据对齐。F4系列写Flash必须半字对齐如果你传了一个奇数地址进去会返回HAL_ERROR并置位PGAERR错误标志。4.2 擦除返回HAL_ERROR或者擦除后读出来还是旧数据擦除失败大多数人都是因为没解锁。记住擦除和写之前都要解锁HAL库的解锁函数只操作Key寄存器不检查Flash是否已经处于解锁状态。如果你在程序初始化时多次调用HAL_FLASH_Unlock()第一次成功第二次其实也是成功的因为它的实现是往KEYR寄存器写密钥覆盖写不影响状态。但如果你前面调用了HAL_FLASH_Lock()又忘了重新解锁那函数就会失败。另一种情况是Flash操作过程中产生了写保护错误WRPERR。某些芯片支持Flash写保护如果你开启了读保护或者写保护直接操作Flash会失败。检查方法是读FLASH-CR寄存器的WRPERR位。如果置位了先调用__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_WRPERR)清除标志。关于读保护多说一句STM32CubeProgrammer里如果设置了RDP级别1调试器默认无法读取Flash内容会导致你读出来的全是0xFF。这种不是Flash真的空了而是保护机制生效。解决办法是执行“Remove readout protection”但这会触发整片擦除你之前的数据就没了。所以做产品千万别随便开启读保护除非你确认自己能接受芯片变空的风险。4.3 计算出来的固件大小和Keil显示的差异很大Keil编译输出里的Program Size Code RO-data RW-data ZI-data。其中Code是代码段RO-data是只读常量RW-data是需要初始化到RAM的变量初值ZI-data是零初始化变量不占用Flash编译后它只记录大小不实际写入。所以实际上烧到Flash里的内容是Code RO-data RW-data之和ZI-data只在RAM里清零。我遇到过有人直接把四个数加起来算固件大小那就会偏大几个KB到几十KB。比如RW-data很大意味着你要把一坨初值从Flash拷贝到RAM这部分在Flash里是实实在在占空间的但它跟ZI-data没关系。所以严格按照资料上的公式Flash占用 Code RO-data RW-data再加上一些段对齐和填充可能会多出几十字节。如果你的程序里用了__attribute__((aligned(64)))这样的对齐属性那填充会更多。扫描器算出来的值通常会比编译器的静态值略微大一点因为扫描器是把所有写完的扇区都算进去了包括编译器为了对齐产生的填充字节这个差异是正常的不需要纠结。我之前遇到一个极端情况把整个字库数组放在Flash里数组大小256KB但其中大段都是0xFF数据。扫描器会提前判定固件结束导致算出来的大小比实际小了很多。这种场景下你就不能用“连续0xFF”这个判据而是要看编译器的map文件里_erode或者__initial_sp的值。一句话总结扫描法适合普通程序固件的估算不适合包含大段全0xFF数据的特殊固件。4.4 写入失败后Flash中的数据丢失了怎么办这是个残酷的话题。Flash写操作如果失败最典型的后果是那个扇区的部分数据被写坏而且Flash不支持“局部恢复”除非你之前有备份否则数据就是永久丢了。所以在操作Flash之前务必备份目标扇区的数据。一个常用备份思路是把需要修改的扇区内容整体读入RAM缓冲区然后在RAM里修改再擦除扇区、写回。注意F1/F4的Flash扇区大小都不小F407扇区最小16KBRAM不大的芯片可能放不下16KB缓冲区。这时候可以考虑先把数据暂存到另一个Flash扇区但那样逻辑就复杂了。更好的方案是把配置数据放在专门的EEPROM仿真区用两个扇区轮流写实现磨损均衡和掉电保护。不过那属于进阶话题这里只提醒一句任何涉及Flash的写入操作都必须考虑掉电中断和写失败情况下的数据恢复策略。我个人的习惯是配置文件使用固定格式头部有魔数、版本号、CRC校验写入时先写备份扇区确认成功后再更新主扇区最后再更新标志位。这套做法看起来很繁琐但产品量产之后出现莫名其妙的数据丢失问题的概率会急剧降低。4.5 bin文件生成失败fromelf报错或者生成的是空的Keil的fromelf命令如果输出为空最常见的原因是工程名或者路径写错了。你可以在电脑上打开cmd手动跑到工程目录执行fromelf命令试错。还有一点要注意fromelf生成bin是依赖axf文件的axf文件路径如果改变了命令里的路径也要跟着改。如果是STM32CubeIDE生成bin失败先确认一下Post-build steps里的命令是否包含了正确的工程名。很多人把工程重命名之后忘了改命令里的elf文件名自然就生成失败。另外一个非常关键的点bin文件生成后用烧录器烧录时必须指定起始地址。如果你拿的是一个纯bin文件直接像烧hex那样用默认地址烧烧录器会把它放在0x08000000这没错。但如果你想把这个bin作为App烧到偏移地址比如0x08010000那就必须设置烧录地址为0x08010000。很多人在这里翻车原因就是bin文件不带地址烧录软件不知道往哪里放。5. 扩展应用把内部Flash玩出更多花样5.1 用内部Flash存储用户配置参数内部Flash最常见的应用场景就是存用户配置。比如一个温控器用户设定的目标温度、PID参数、报警阈值这些都要掉电保存。用EEPROM当然可以但省掉一颗外挂芯片也是成本优势。设计思路是这样的规划一个独立扇区或者扇区中的一小块存储结构体typedef struct { uint32_t magic; // 魔数用于校验是否已初始化 uint32_t version; // 配置版本号 uint16_t temp_limit; uint16_t temp_alarm; // 其他参数 uint32_t crc32; // 校验值 } SystemConfig;每次读取配置时先检查magic是否符合预设值再计算CRC全部通过才使用。修改配置时先备份旧配置到RAM修改完再擦除整个扇区写回。如果写入过程中掉电重启后magic或CRC不正确程序自动恢复默认配置并做一次完整配置写入。这种设计比简单地把HAL_FLASH_Program调用几次要可靠得多。5.2 做一个简易BootLoader的跳转边界管理很多做OTA的人都会接触到BootLoader。BootLoader放在0x08000000App放在0x08010000或者0x08080000App烧录前BootLoader需要知道App固件的真实大小这样才能决定是否擦除足够的扇区。比如App只占20KB但你擦除了两个64KB扇区虽然不影响功能浪费的擦除时间会让升级体验变差。此时就可以用我前面讲的扫描方法在App包里读取App起始地址后面多少字节是0xFF然后只擦必要的扇区。如果你在BootLoader里做这件事读取的是App区域而不是正在运行的BootLoader自身区域。流程是收到App包先把包存到RAM缓冲区或者外置Flash然后检查App区域从0x08010000开始的连续0xFF长度计算出实际固件大小再根据这个大小决定擦除哪些扇区、写多少数据。这比直接按最大扇区擦除高效得多也减少了对Flash寿命的无谓消耗。5.3 Flash磨损均衡的简化实现内部Flash写入寿命通常在1万次到10万次之间看着不少但如果你每秒钟存一次数据一天就是86400次几天就报废了。所以做存储类功能必须考虑磨损均衡。最简单的策略是“轮流写”准备两个扇区逻辑上叫A和B每次写入先写AA写满再写BB写满又回头擦A。A和B都有最新数据标记比如一个自增计数上电时比较哪一个扇区里的计数大就用哪一个。虽然两个扇区相比单扇区只是延长了一倍寿命但从算法角度已经入门了。更复杂的方案是用多个扇区组成环形缓冲区每次只写一个固定大小的记录块每个块包含序列号和CRC启动时扫描所有块找到最新有效记录。这种方法实现起来稍微麻烦一点但可以在极端情况下让Flash寿命延长几倍到几十倍。后续如果你对OTA和参数存储都有需求很值得深入研究一下。5.4 利用bin文件的边界做固件差分升级拿到bin文件之后除了估算大小还能做很多事情。比如两套固件之间可能只改了几百字节如果我们能定位差异位置就可以做一个差分包只上传变化的数据显著减少升级数据量。这种技术在蜂窝网络的设备OTA里特别有用因为流量费不便宜。具体做法也不复杂把新旧两份bin分别读进内存按块比较找出哪些块不同然后生成一个自定义格式的差分包里面记录差块地址和差块数据。接收端拿到差分包后按地址直接写入对应Flash即可。当然这套流程需要BootLoader侧配合而且要考虑Flash擦除边界的问题——如果差块跨越了扇区边界就得额外处理。但核心思路就是从bin文件出发搞清楚固件内容制作差异补丁。6. 最后的实操心得回到HAL库学习本身内部Flash这一章的价值不在于你会调用几个函数而在于你真的理解了芯片存储结构的工作方式。我最初学的时候也觉得“Flash嘛就是存程序的”直到做BootLoader时被贴脸考了一轮为什么擦除前要备份为什么写完后要上锁为什么bin文件烧录要选地址这些问题一个个逼着我去翻参考手册最后才把整个逻辑串通。建议如果你手头有开发板别光看这篇博客就完事最好动手做做这个小实验写一个程序从0x08000000开始读256字节用串口打印出来对照编译生成的hex或bin文件你会看到完全一致的数据。修改程序大小比如加一张数组或者多写几个函数重新编译并烧录然后扫描器算出来的固件大小应该会跟着变化。把写好的配置数据放在Flash的某个扇区断电重启后再读出来验证掉电保存是否生效。这些实验做完你对内部Flash的信任感和掌控感绝对是看资料体会不到的。踩过几次坑之后我特别感谢那些“为什么不对”的时刻它们逼着我去理解Flash的物理特性。希望这篇内容能让你少走点弯路把时间省下来去玩更有意思的功能。