ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H55 FLASH写入字节统计:没有寄存器也能精准计数

STM32H55 FLASH写入字节统计:没有寄存器也能精准计数 嵌入式开发里经常有人问我类似的问题代码跑了一段时间想知道自己到底往FLASH里写了多少字节翻遍了参考手册和调试器窗口居然找不到一个现成的寄存器能把这个累计值说出来。我先给你一个明确结论——你问的STM32H55以及整个STM32 H5/F/G/L系列FLASH控制器只负责把数据写进去、把扇区擦掉它本身不记账。没有任何一个内存地址能直接告诉你“我总共写了多少字节”。但你确实有办法把这个问题搞定而且方案不止一种。这篇内容主要面向两类人一类是做OTA升级、数据记录、参数存储的固件工程师需要评估写入量、估算FLASH寿命另一类是调试现场问题的人比如怀疑某些异常写入把FLASH内容冲掉了想追溯到“到底谁写的、写了多少”。我会把原理、可行方案、可复现代码和踩坑经验都写出来你看完基本可以照着自己项目改。1. 先理清需求你统计的到底是被写字节数还是物理擦写量1.1 典型场景谁需要关心 FLASH 被写了多少字节我遇到过的场景很杂但归纳起来大概是这几类。第一类是OTA升级。升级包下载完成后要写入FLASH你总得知道这个包占了多少扇区从哪里开始覆盖到哪里结束。如果升级包长度是128KB而你的分区边界是64KB一个扇区那至少要擦4个扇区实际占用很可能超过128KB这个“多出来”的部分就是物理擦写带来的开销。第二类是数据记录仪或者掉电保存逻辑。应用层每隔几秒往FLASH刷一次数据长期运行后必须回答寿命问题。FLASH都有擦写次数上限芯片手册上写的是10万次H5系列的数据通常也是这样但那是按“擦除次数”算的不是按“写入多少字节”算的。所以你得搞清楚自己每次写操作背后产生了多少次擦除。第三类是故障排查。产品现场出问题返回来一读FLASH内容发现某些数据变了这时候你最想知道的是有没有代码路径意外触发了FLASH写入以及它写到了哪个地址、写了多长。这三种需求对应的统计口径不一样后面说方案时我会分开讲。1.2 逻辑写入量与物理擦写量的差别“写入字节数”这个词有两个含义必须区分开否则后面统计出来的数字没有意义。逻辑写入量是指你应用层调用了一次写FLASH的接口传进去一个指针和一个长度比如FLASH_Write(0x08040000, buf, 1024)那逻辑写入量就是1024字节。这个数字反映的是业务层的数据流量适合用来估算固件映像大小、日志累计量。物理擦写量不一样。FLASH的物理特性是“只能把1变成0不能把0变成1”所以想往一个已经有数据的区域写入新内容必须先擦除。而擦除的最小单位是扇区不是字节。你写1个字节进去如果该扇区之前有数据整扇区都要先擦一遍擦除这个动作的“成本”是整个扇区的容量。比如扇区是8KB你哪怕只写1字节物理上也产生了一次8KB的擦除开销。再加上H5系列的FLASH往往分成多个bank同一个bank内擦写时不能同时执行代码这个限制下面细说有时候你为了改一个字节还得先把整个扇区内容读到RAM擦掉再把修改后的数据写回去这一来一回逻辑上是1字节物理上却是“读8KB 擦8KB 写8KB”。你如果拿逻辑写入量去评估寿命会严重低估。1.3 官方 FLASH 控制器到底提供了什么“地址信息”这是很多人卡住的地方。STM32的FLASH控制器有一套寄存器比如控制寄存器FLASH_CR、状态寄存器FLASH_SR、地址寄存器之类的。H5系列里还能读到类似于编程地址、操作模式的字段。但这些寄存器描述的是“当前这一次编程操作”的状态比如正在擦哪个扇区、正在写的地址是多少。一旦操作完成这些位就恢复到空闲状态不会累加也不会保留历史痕迹。所以你从“内存地址信息”里找不到累计写入字节数不是因为你查得不够仔细而是硬件设计上就没做这个东西。CPU有性能计数器能统计执行了多少条指令FLASH控制器可没有“写入字节计数器”。想拿到累计值要么在应用层自己记要么用外部工具对读出的FLASH内容做离线分析没有第三条魔法捷径。2. FLASH 编程原理地址映射、扇区结构与对齐规矩2.1 STM32H55 的 FLASH 组织架构与地址空间STM32H55属于H5系列虽然具体型号在容量和外设上略有差异但FLASH的地址映射方式基本一致从0x08000000开始一直往后排列到0x08000000 内部FLASH大小。你可以在stm32h55xx.h里看到FLASH_BASE这个宏定义就是整个片上FLASH的基地址。H5系列的FLASH按照扇区组织扇区大小不是平均切的。典型布局可能是整个FLASH分为两个bank前半部分一个bank后半部分另一个bank每个bank内部由若干个小扇区比如8KB和几个大扇区比如128KB混合组成。具体扇区表要以参考手册里的memory map为准因为不同容量的型号扇区划分不完全一样。为什么要关心bank和扇区因为擦除操作是按扇区进行的而你要执行擦除时必须告诉Flash控制器“擦哪个bank、第几个扇区”如果填错了轻则返回错误重则把别的分区擦掉。另外H5的双bank设计有一个重要特性对bank1执行擦写时如果代码在bank1上运行会产生总线阻塞表现就是代码“卡死”或者erase失败反过来代码放在bank1、操作bank2就相对安全。这也是我后面写日志扇区时会特意强调“把日志扇区放在另一个bank”的原因。2.2 编程宽度、对齐规则和数据对齐的坑STM32的FLASH支持多种编程宽度H5系列通常支持字节、半字16位、字32位、以及更高效率的双字或四字编程。每种编程宽度都有地址对齐要求字节编程地址任意半字编程地址必须2字节对齐字编程地址必须4字节对齐四字编程64位地址必须8字节对齐。很多人第一次写FLASH程序就踩了这个坑直接定义一个uint8_t buf[]然后调用HAL库想以“字”的方式写入结果一跑就HardFault或者数据写进去布局完全是乱的。原因就是数组起始地址是1字节对齐的强转成32位指针后在HAL内部执行32位存储操作处理器直接报总线错误。更隐蔽的坑是“末尾补全”。编程宽度决定了一次操作至少写多少字节比如你用双字编程传进来的数据长度是14字节那最后一次操作会包含16字节内容其中后2字节是无效填充。如果你的写入函数不做处理计数器按实际传入字节数2或者按编程宽度8两种统计方式都会让人困惑。所以我在第四节给的示例统一用word编程4字节地址要求4字节对齐长度按4字节处理省去很多边界麻烦。如果你追求性能可以改成双字编程但统计逻辑必须相应调整。2.3 为什么从内存地址里找不到“累计写入字节数”这个问题值得展开说清楚。你在调试器里查看内存地址看到的是FLASH的实际内容一堆数据字节。如果某个区域的值是0xFFFFFFFF说明它要么从来没被写过要么被擦除过如果看到一些有规律的数据说明这里有程序写入的内容。问题是FLASH内容本身不带“写入元数据”。它不会在某个特定地址记录“这一行数据是XX时间由XX函数写进来的”。这不是STM32的缺陷而是所有通用NOR FLASH的设计思路——存储介质只负责存数据不负责存历史。你唯一能利用的元数据是某些区域里你自己写进去的文件系统超级块、固件头、日志记录等结构。另外调试器里能看到FLASH地址空间但FLASH控制器没有把“编程次数”“写入字节数”这样的统计项映射到任何可读地址。某些高端MCU内部有NVM控制器固件可能有一些health report但STM32量产芯片没有开放这种接口。所以老老实实从“自己写代码统计”或“读回后离线分析”这两条路走。3. 没有寄存器记账但有三种靠谱的统计方案3.1 方案一应用层加一个计数器变量最简单粗暴也是我最推荐你在开发初期先做的事定义一个volatile uint32_t变量在自己的FLASH写入函数里每次调用成功后就累加长度。代码长这样static volatile uint32_t g_flash_written_bytes 0; int my_flash_write(uint32_t addr, const uint8_t *buf, uint32_t len) { // ... 调用HAL_FLASH_Program等执行写入 ... if (write_ok) { g_flash_written_bytes len; // 这里的len是逻辑长度 } return status; }这个方案的好处是零额外FLASH开销、实现简单调试时打开Live Watch就能看到当前写入量。坏处是掉电就丢而且程序复位后清零。如果项目一旦进入正常运行模式你很难再把历史累计值捞回来。所以它更适合开发调试阶段或者“只需要统计本次运行期间写入量”的场景。还有一个容易忽略的细节如果多个任务会并发调用写入函数这个变量必须加临界区保护否则累加操作可能被中断打断丢失计数。3.2 方案二利用 FLASH 的 1→0 特性做持久化日志计数如果你想做到掉电不丢失那就要在FLASH里开一个专门的日志扇区把写入计数周期性地存下去。这里有一个非常巧妙的设计利用的是FLASH“只能把1变成0”的特性在一个扇区里每次写入新记录时不需要擦除整个扇区只需要在下一个空闲位置把某些字节从0xFF写成具体值。我之前做数据记录仪时就用过这个方案效果很好。基本思路是把日志扇区当成一个顺序写的环形缓冲区每条记录固定8字节比如4字节magic加4字节计数。上电时从扇区头开始扫描找到最后一个magic完整的记录那就是上次保存的计数。写入时从下一个空闲位置追加直到扇区写满再整扇区擦除重写。这个方案的妙处在于扇区不需要频繁擦除假设一条记录8字节、日志扇区8KB那可以写1024次才需要擦除一次。FLASH的10万次擦写寿命瞬间被放大了1000倍以上特别适合高频统计场景。3.3 方案三整片读回后用脚本统计已写入区域有些情况不适合在MCU里加代码比如问题已经发生现场固件不可能重新烧录你只能把FLASH内容读出来做离线分析。这时候可以用STM32CubeProgrammer或者J-Link Commander把整个FLASH dump成bin文件然后用Python脚本统计非0xFF的字节分布。import sys def count_flash_used(filename, base_addr0x08000000): with open(filename, rb) as f: data f.read() used_bytes sum(1 for b in data if b ! 0xFF) print(fFLASH region: 0x{base_addr:08X} - 0x{base_addr len(data):08X}) print(fUsed/non-0xFF bytes: {used_bytes}) print(fTotal bytes: {len(data)}) # 按1KB块统计方便看写入分布 for i in range(0, len(data), 1024): chunk data[i:i1024] used sum(1 for b in chunk if b ! 0xFF) if used 0: print(f 0x{base_addr i:08X}: {used:4d} / 1024 bytes used)这种方案能直观地看到哪些地址区间有数据、各区间占了多少字节。但注意它统计的是“当前时刻FLASH里有多少非0xFF字节”不是“历史上累计写了多少字节”。如果某个扇区被反复擦写最终读到的还是最后一次的结果之前写过的数据已经不存在了脚本统计不出来的。所以这个方案适合排查代码异常写入后的现场不适合评估寿命。3.4 方案四用硬件监视点去“抓”写入现场不推荐有人可能想到用调试器的数据观察点Data Watchpoint或者MPU来监控对FLASH地址空间的写操作。理论上调试组件可以设置一个地址范围监视点当CPU执行写FLASH指令时触发断点然后你就能看到是哪个函数、写了什么。这个方案在实际开发中作用有限。第一FLASH编程是由Flash控制器完成的CPU写的是FLASH控制器的寄存器不是直接往FLASH地址写数据所以监视FLASH地址空间根本触发不了。第二数据观察点数量极其有限几个寄存器根本覆盖不了整个FLASH区域。第三它只在调试会话中有效产品部署到现场后没法用。我建议你了解原理就行别在方案设计时选它。4. 实战在 STM32H55 上实现写入字节统计并保存日志4.1 环境准备与 HAL 库关键接口我用的环境是STM32CubeIDE 对应H5系列的HAL库。H5的HAL库FLASH接口和F系列差别不大核心就两个函数HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data); HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError);HAL_FLASH_Program的第三个参数是uint64_t哪怕你写32位数据也要按它的类型传否则编译器告警。HAL_FLASHEx_Erase需要填FLASH_EraseInitTypeDef结构体包括banks、Sector、NbSectors等。在调用编程或擦除函数前必须调用HAL_FLASH_Unlock()解锁结束后调用HAL_FLASH_Lock()上锁。这个顺序很多人会忘导致报HAL_ERROR或者直接硬错误。另外H5系列编程前建议清一下错误标志调用__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS)防止上一次异常擦写遗留的状态位影响本次操作。4.2 封装一个带计数功能的 FLASH 写入函数下面这个封装我用的编程宽度是word4字节地址按4字节对齐长度也按4字节处理。如果你传的地址或长度不满足对齐要求函数会直接返回错误避免后续数据错乱。static volatile uint32_t g_flash_written_bytes 0; int flash_write_with_counter(uint32_t addr, const uint8_t *buf, uint32_t len) { if ((addr FLASH_BASE) || (addr len FLASH_BASE FLASH_SIZE)) { return -1; // 地址越界 } if ((addr 0x03u) || (len 0x03u)) { return -2; // 不满足4字节对齐要求 } HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS); for (uint32_t i 0; i len; i 4) { uint32_t data 0xFFFFFFFFu; memcpy(data, buf[i], 4); // 从buffer复制4字节 if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, (uint64_t)data) ! HAL_OK) { HAL_FLASH_Lock(); return -3; // 编程失败常见原因是区域未擦除 } g_flash_written_bytes 4; // 按实际编程宽度累加 } HAL_FLASH_Lock(); return 0; }这里有个关键点要强调统计口径是“按编程宽度累加”不是按你业务层的逻辑长度。比如你应用中认为要写100字节但函数内部按4字节宽度编程实际写入102字节因为100不是4的倍数你传参时就得补到尾数对齐。如果你更关心业务层数据量就在调用这个函数前另建一个变量传多少字节就加多少。两者的差正好就是你因为对齐浪费的写入量。4.3 把累计计数持久化到日志扇区含掉电恢复计数器变量随时会丢所以要把它定期保存到FLASH日志扇区。我常用一个简单的顺序追加方案。假设日志扇区基地址是0x081FF000大小8KB一条记录8字节。#define LOG_SECTOR_ADDR 0x081FF000u #define LOG_SECTOR_SIZE 0x2000u #define LOG_RECORD_SIZE 8u #define LOG_MAGIC 0xA5A5A5A5u void log_counter_load(uint32_t *counter) { uint32_t value 0; uint32_t off 0; while (off LOG_RECORD_SIZE LOG_SECTOR_SIZE) { uint32_t magic *(volatile uint32_t *)(LOG_SECTOR_ADDR off); uint32_t cnt *(volatile uint32_t *)(LOG_SECTOR_ADDR off 4); if (magic ! LOG_MAGIC) { break; } value cnt; off LOG_RECORD_SIZE; } *counter value; }读的时候从扇区起始地址顺序扫描遇到magic不匹配就停止这样能天然跳过“写了一半”的无效记录。为什么有效因为我写记录时是先写计数再写magic如果中途掉电magic还是0xFFFFFFFF下次加载时会认为这条记录无效直接使用上一条有效记录的值。写入记录的函数void log_counter_store(uint32_t counter) { uint32_t off 0; while (off LOG_RECORD_SIZE LOG_SECTOR_SIZE) { uint32_t magic *(volatile uint32_t *)(LOG_SECTOR_ADDR off); if (magic ! LOG_MAGIC) { break; } off LOG_RECORD_SIZE; } if (off LOG_RECORD_SIZE LOG_SECTOR_SIZE) { // 当前扇区已满擦除后从头写 FLASH_EraseInitTypeDef erase; uint32_t error 0; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Banks FLASH_BANK_2; // 根据日志扇区实际所在bank调整 erase.Sector FLASH_SECTOR_NUMBER_LOG; // 根据日志扇区实际扇区号调整 erase.NbSectors 1; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(erase, error); off 0; } HAL_FLASH_Unlock(); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, LOG_SECTOR_ADDR off 4, (uint64_t)counter); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, LOG_SECTOR_ADDR off, (uint64_t)LOG_MAGIC); HAL_FLASH_Lock(); }这段代码里的扇区号和bank需要根据你芯片的实际FLASH布局修改。我之前在某型号上把日志扇区放在了bank2代码烧在bank1这样运行时对日志扇区做擦写不会阻塞主程序访问代码所在bank非常顺滑。4.4 验证读回分析确认统计无误写完这些逻辑怎么确认统计是对的我推荐两种验证手段。第一种是在调试器里直接看日志扇区。打开Memory窗口跳到LOG_SECTOR_ADDR你能看到一长串交替的magic和计数数值。如果计数每次递增的值和业务写入量一致说明统计逻辑没毛病。如果出现了magic缺失或者计数断层就要去查是不是有别的代码路径绕过了封装函数、直接擦写了日志扇区。第二种是用CubeProgrammer整片读回跑我第三节的Python脚本看看实际使用的FLASH区域分布。你会发现日志扇区占用的非0xFF字节数远小于业务数据区因为它是顺序追加的一个8KB扇区要写满才擦除。这个现象本身就说明了追加写方案的价值它减少了擦除次数同时也减少了非0xFF字节的占比降低了“看起来占用很多”的错觉。5. STM32H55 FLASH 写入常见报错与排查思路5.1 erase failed / cannot access memory 报错用调试器下载程序或者运行中执行擦除操作时可能会遇到类似erase failed! cannot access memory internal command error flash download failed这样的报错。这个报错很典型通常不是单个原因。最常见的几个原因一是地址越界你尝试擦除的扇区地址超过了芯片实际FLASH容量。二是代码正在当前bank执行却去擦同一个bank的另一个扇区导致总线访问冲突。H5的双bank特性在这里很容易踩坑。三是对齐错误擦除参数里的Sector号填错了实际扇区号与地址不匹配。四是电压问题FLASH编程期间供电不稳PVD触发复位或欠压。五是RDP级别设置导致调试接口访问受限。排查顺序我建议先看现场的错误标志读FLASH_SR确认是否有PGAERR、WRPERR、PGSERR之类的位被置起。再看参数表的banks和Sector值手动算出擦除地址跟实际操作地址比对。最后检查时钟配置和代码段所在位置。5.2 写入字节数和预期完全对不上怎么办很多人写完统计逻辑后回来问我我明明往FLASH写了1KB怎么统计出来是8KB怎么读出来的数据还是错的第一层原因是编程宽度。如果你用双字编程每次至少写8字节你业务层传1024字节如果代码里是 4累加的那自然少了如果按“最后一次补全”方式算又可能多算。解决办法是统计必须在封装函数内部做而且以实际编程操作次数乘以编程宽度来算物理写入量另存一个业务长度变量用来算逻辑写入量。第二层原因是擦除比写入影响更大。你写记录前可能执行了扇区擦除那个擦除动作虽然不增加“写入字节数”但实际消耗了FLASH寿命。如果你只统计program的字节数你的寿命评估还是偏乐观。所以在日志计数里应该把擦除次数也单独存一个counter这样你才能算出真正的物理擦写量。第三层原因是数据没有先擦后写。FLASH只能把1变0如果没擦除就去program写入函数会报错或者写入的数据和预期不符。你统计到“写失败”时计数器不应该增加。5.3 跨领域“字节数统计”问题速查FLASH写入字节统计这个事本质上和很多开发中的报错是同一个问题你在统计“字节”时必须清楚统计口径和存储介质特性。我见过不少相关的报错虽然领域不同但排查思路很像。比如跑文件系统时遇到file_put_contents(): write of 471 bytes failed with errno28 no space left这是典型的“逻辑上写了471字节但文件系统在底层做了额外开销”导致空间不足。又比如数据库报1071 - specified key was too long; max key length is 767 bytes这是索引key的长度限制不是数据本身的问题。再比如密码哈希报password cannot be longer than 72 bytes是底层算法固定输入块大小导致的。这些报错都在提醒你字节数只是一个维度关键是理解底层机制对“可写空间”的约束。回到MCU这边我给你的速查表就是这三条现象大概率原因排查手段写入字节数比预期大很多编程宽度或对齐导致多重写入检查HAL_FLASH_Program的TypeProgram和地址对齐写入完成后读回是全0xFF写入前没擦除或写错地址读FLASH_SR检查地址映射erase报错调试器无法下载双bank冲突或RDP保护确认代码运行bank与擦除bank是否冲突5.4 几个现实中容易忽略的坑第一个坑是日志扇区本身的擦除寿命。如果你用“先擦后写”的简单方案每次保存计数都擦一次那10万次寿命很快耗尽。即使你的业务数据量很小一个长时间运行的产品也可能因为日志扇区频繁擦除而挂掉。所以我在4.3节特意给了顺序追加的实现它的擦除频率会低很多但也要求你在设计分区时预留足够的扇区空间。第二个坑是缓存。STM32H5可能有Flash加速器或者Cache如果你通过调试器直接读FLASH数据可能读到的是Cache里的旧数据。做验证时先SCB_InvalidateDCache()或者在调试器里关掉Cache再读。我遇到过明明写进去了读出来还是老数据折腾半天才发现是Cache捣乱。第三个坑是看门狗。FLASH擦写是一个耗时操作一个扇区的擦除可能持续毫秒级如果你的IWDG超时时间很短在擦除过程中可能触发看门狗复位。一旦复位发生在擦除半途整个扇区的数据完整性就无法保证。实践经验是在进入擦写流程前喂狗或者临时把看门狗超时设长一点擦写完成后恢复。第四个坑是中断。FLASH编程期间最好不要有高优先级中断去访问同一个bank的FLASH内容否则会延长编程时间甚至触发总线错误。如果必须保留中断响应比如通信协议栈不能停那就要把代码和数据放到不同的bank确保中断处理函数本身不在正在擦写的bank上运行。给你一个我的习惯做法在新项目的链接脚本里我会明确把代码段放到bank1把配置参数区和日志区放到bank2。这样不管以后是做OTA还是记录日志都少踩至少一半的FLASH并发问题。再分享一个小技巧开发阶段在CubeProgrammer里把整片FLASH读出来配合Python脚本按1KB块统计非0xFF字节分布能快速看清哪些区域被频繁写入。这个图像比任何日志都直观我第一次用这个办法排查一个“周期性数据被改花”的bug时几分钟就定位到了问题函数比瞎猜快得多。
返回列表