丁丁 软硬件、前后端全栈开发者。热爱,并将终身学习有关计算机的一切 mdi-home 首页 mdi-language-go Golang mdi-cpu-32-bit STM32 mdi-format-list-bulleted-square 文章列表 mdi-share-variant 分享 mdi-book-open-page-variant 推荐书单 mdi-chat-processing 碎语 mdi-help-box ISSUE About Me mdi-message 知乎 mdi-sina-weibo 微博 mdi-television-play bilibili mdi-rss-box RSS

STM32使用HAL库操作FLASH的注意事项——丁丁的个人网站

mdi-heart mdi-login mdi-logout mdi-settings
mdi-chevron-left Last:微软终端神器Windows Terminal的体验及配置 mdi-chevron-right
STM32通常有着丰富的片上Flash空间,这些空间正常情况下是存放指令代码、常量等烧录数据的。平时我们编译出来的bin或者elf文件会按顺序填充进Flash;hex格式的文件会稍许不同,通常情况下hex文件会更小一些,因为它会将无意义的填充段省略。 一般我们的代码逻辑加上常量数据是远远用不到片上Flash那么大的空间的,因此我们可以划分一些区域用来作为掉电存储空间,实现EEPROM的作用。此外,如果产品需要简单实现AOT固件升级,也需要使用到片上FLASH操作。 在使用STM32的HAL库进行FLASH操作的时候,有以下几点值得注意: ### 1. 注意顺序 先解锁Flash,再擦除片区,再写入,写完了别忘了加锁。 ``` HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(); HAL_FLASH_Program(); HAL_FLASH_Lock(); ``` ### 2. 擦除相关 `HAL_FLASHEx_Erase()`函数会入参一个有关清除扇区配置的结构体对象,这个参数对应的结构体定义根据不同芯片的HAL库有所不同,F1/F4/L4均不相同,而且同系列不同Flash大小的芯片所能指向的地址也不相同,同时这里面还存在Sector/Page/Bank等概念,这些概念涉及到一次性所能清除区域的大小,需要适当阅读一下实际使用芯片的Reference manual,比如RM0351(STM32L475),RM0090(STM32F407),RM0008(STM32F103)等,这些文档都会有专门的章节介绍嵌入式Flash的结构,可在ST官网搜索上述代码下载阅读。 在STM32F407VET6上,参考以下代码擦除: ``` rt_err_t FlashEraseSector7(){ rt_uint32_t flashEraseRet; HAL_StatusTypeDef halRet; flashEraseInitType.TypeErase = FLASH_TYPEERASE_SECTORS; flashEraseInitType.Sector = FLASH_SECTOR_7; flashEraseInitType.NbSectors = 1; flashEraseInitType.Banks = FLASH_BANK_1; flashEraseInitType.VoltageRange = FLASH_VOLTAGE_RANGE_3; halRet = HAL_FLASHEx_Erase(&flashEraseInitType, &flashEraseRet); if(halRet != HAL_OK || flashEraseRet != 0xFFFFFFFFU){ return RT_EINTR; } return RT_EOK; } ``` 上述代码清除了STM32F407VET6片上最后一片sector区域的数据,这块数据大小128KByte。为了保证跟正常代码所占用的Flash空间保留安全距离,我们通常使用片上Flash的最后片区。虽然这里128KByte显得太大,但对于这块芯片,只能这么擦除了,其他芯片可以参考相关手册,如果可以按Page擦除,可以只擦除2KByte空间。 示例代码中使用了RT-Thread的相关状态类型,可自行忽略。 ### 3. 编程(写入)相关 `HAL_FLASH_Program()`函数对于不同芯片的HAL库,入参也不甚相同,有的芯片可以按1字节、2字节、4字节、8字节写入,比如STM32F407,有的芯片只能按8字节写入,比如STM32L4。同时,Flash写入时要注意字节对齐,比如现在要写一个双字节到Flash某一地址,那么该地址必须也是双字节的整数倍;如果要写一个4字节,那么同理,地址要是4字节的整数倍;写一个8字节地址就要是8字节的整数倍;如果违反上面的规则,函数返回结果出错,有时候还会报出`Hard fault`异常。当然,如果写单个字节,地址则无要求。如果将一块N字节的buffer拆成N次使用单字节写入,无疑速度是最慢的,这里要权衡地址对齐和写入速度的关系。 参考代码如下: ``` rt_err_t flashWrite(rt_uint32_t address, rt_uint8_t* buffer, rt_uint32_t size){ HAL_StatusTypeDef halRet = HAL_OK; rt_uint32_t pos = 0; while((size > pos) && (halRet == HAL_OK)){ halRet = HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, address + pos, buffer[pos]); pos++; } if(halRet == HAL_OK) return RT_EOK; else return RT_ERROR; } ``` 上述代码按1字节每次写入了一个buffer,没有对字节对齐作任何判断,如果追求速度的话,可以继续优化,加入对齐和2字节、4字节、8字节的写入,速度可以更快。 ### 4. 写入模式 在STM32L475上,有三种写入模式,分别是: ``` #define FLASH_TYPEPROGRAM_DOUBLEWORD ((uint32_t)0x00) /*!<Program a double-word (64-bit) at a specified address.*/ #define FLASH_TYPEPROGRAM_FAST ((uint32_t)0x01) /*!<Fast program a 32 row double-word (64-bit) at a specified address. And another 32 row double-word (64-bit) will be programmed */ #define FLASH_TYPEPROGRAM_FAST_AND_LAST ((uint32_t)0x02) /*!<Fast program a 32 row double-word (64-bit) at a specified address. And this is the last 32 row double-word (64-bit) programmed */ ``` 我只看得懂第一个模式,后面两个模式没看懂,所有英文单词都懂,连成一句话就不懂了,希望理解它们的朋友不吝赐教,在评论区指点我。 我实际测试下来也只有第一个模式好用,其余两个都会报`Hard fault`异常。后来找了好久,才在国外某一论坛看到有大佬说使用后面两个模式,需要在`HAL_FLASHEx_Erase()`时,使用`FLASH_TYPEERASE_MASSERASE`模式,而不是常用的`FLASH_TYPEERASE_PAGES`模式,关于`FLASH_TYPEERASE_MASSERASE`这个模式我也不太理解,希望大佬们继续赐教。 总之我目前就是按Page或Sector擦除,然后用第一种模式写8字节到指定地址就行了。其他的模式我也没继续研究下去。对于STM32L475来说,这里只有使用`FLASH_TYPEPROGRAM_DOUBLEWORDf`。而且要注意传入的地址只能8字节对齐,因为这里只能一次性写入8字节,没有别的模式可选。 ### 5. 读Flash 我现在使用的读Flash的宏定义如下: ``` #define FlashGetChar(addr) *((char*)(addr)) #define FlashGetU8(addr) *((uint8_t*)(addr)) #define FlashGetU16(addr) *((uint16_t*)(addr)) #define FlashGetU32(addr) *((uint32_t*)(addr)) #define FlashGetU64(addr) *((uint64_t*)(addr)) ``` 这里有一些容易出错的细节,我曾经看别人代码Copy过类似这样的精简定义: ``` #define FlashGetU8(addr) *(uint8_t*)addr ``` 上面这种方法,在`addr`两边少一个括号,这样会导致一个潜在的问题,比如: ``` uint8_t i = FlashGetU8(FLASH_USER_ADDRESS + 1); ``` 我们的本意是希望读取`FLASH_USER_ADDRESS`地址向后偏移1位的地址上的值,但如果用没有括号包围的那种宏,语法就发生变化,读取的数据是错误的。我曾经为此调试得差不多怀疑人生,后来才发现是借鉴的这段宏定义出了问题,因此建议用上面我修改后的那种宏定义。 ### 6. 耗时、锁和寿命 擦除和写入片上Flash都需要耗时的,消耗时间跟主频没多大关系,跟芯片自身有关。HAL库会根据主频大小自我调节擦除和写入等待的时间。具体的数值还是要看官方给的文档。 同时要注意,在擦除和写入Flash的等待时间片的前后,系统会调用`__HAL_LOCK(&pFlash);`和`__HAL_UNLOCK(&pFlash);`进行Flash总线的加锁和解锁,Flash总线事关代码指令的读取,锁上它程序几乎无法运行。因此在一些场合会影响通信,比如作为一个高速SPI总线的从站,或者一个高实时的以太网通信从站,建议最好不要操作片上Flash,可使用外部EEPROM进行标准IIC外设+DMA通信。 最后注意一下寿命,片上Flash一般只提供最少10万次写入寿命,而外部EEPROM一般会提供最少100万次的写入寿命,对于产品,可能就是1年和10年设计寿命的差别。当然,您还可以设计得更加精巧一些,实现均匀擦写算法,以延长使用寿命。
mdi-chevron-left Last:微软终端神器Windows Terminal的体验及配置 mdi-chevron-right
Tags JAVA Golang STM32 Links 丁丁喜欢这些网站或者博客 MCU起航 JBlog Advert
Tags JAVA Golang STM32 Links 丁丁喜欢这些网站或者博客 MCU起航 JBlog Advert
{{ $store.state.notice.msg }}