[技术问答] 单片机内部FLASH的字节操作

[复制链接]
1783|12
 楼主| louliana 发表于 2025-4-6 06:49 | 显示全部楼层 |阅读模式
一般32位单片机的内部FALSH是不支持字节操作的,有的可以按字节读取,但是不能按字节写入。

而且,一般单片机内部FALSH擦除的最小单位都是页,如果向某页中的某个位置写入数据,恰好这个位置的前面存了其他数据,那么就必须把这页擦除,存的其他数据也会丢失。
实际上就是说内部的FALSH不好做改写的操作,如果有很多数据需要存放,最好是分页存储。这也是FALSH与E2PROM最大的区别,后者支持按字节操作且无需擦除,即使某一个地址写坏了,也不影响其他地址。
下面介绍一种方法让内部FLASH"支持"字节操作,且同一页的其他数据不受影响。
方法原理很简单,下面简单介绍下原理:
1.根据要写入地址,计算出该地址位于哪一页;
2.读出整个页,存入缓存BUF;
3.将要写入的数据按位置更新到BUF中;
4.擦除该页;
5.写入整个BUF。
可以看出这种方法弊端很明显:
1.耗时长  每次写都要读整个BUF,然后还要先把数据存到BUF里,然后再写入整个BUF;
2.FALSH擦写次数增加,降低使用寿命;
下面给出测试代码:



  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>//C语言标准库
  4. #include "flash.h"

  5. #define USER_FLASH_START_ADDR   0x01070000   //FLASH最后两个扇区  供用户使用


  6. u32tou8 u32data;//定义一个联合体

  7. //==================================================================================
  8. // 获取某个地址所在的页首地址
  9. // addr:FLASH地址
  10. // 返回:该地址所在的页 共128页(0~127)
  11. //==================================================================================
  12. unsigned int FLASH_GetFlashPage(unsigned int addr)
  13. {
  14.           if (IS_FLASH_ADDRESS(addr))
  15.                 {
  16.                    return  (addr&(~0xFFF));//清0低12位就是该页的起始地址
  17.           }
  18. }
  19. //==================================================================================
  20. // 从FLASH中读取 一个字(32位)
  21. // addr:读取地址
  22. // 返回: 读到的字数据
  23. //备注: 地址为4字节对齐
  24. //==================================================================================
  25. unsigned int FLSAH_ReadWord(unsigned int addr)
  26. {
  27.     return (*(unsigned int *)addr);
  28. }


  29. //==================================================================================
  30. //从FLASH指定地址 读取数据
  31. //备注: 读取数据类型为32位  读取地址为4字节对齐
  32. //==================================================================================
  33. void  FLASH_Read(unsigned int        ReadAddr,unsigned char *pBuffer,unsigned int NumToRead)
  34. {
  35.     unsigned int i;
  36.           u32tobyte cache;
  37.     for(i=0; i<NumToRead; i+=4)
  38.     {
  39.             cache.u32data=FLSAH_ReadWord(ReadAddr+i);
  40.                                     pBuffer[i]=cache.buf[0];
  41.                                            pBuffer[i+1]=cache.buf[1];
  42.                                           pBuffer[i+2]=cache.buf[2];
  43.                                           pBuffer[i+3]=cache.buf[3];
  44.     }
  45. }

  46. //==================================================================================
  47. // 向FLASH指定地址 写入大量数据
  48. // WriteAddr:写入首地址
  49. // pBuffer:数据首地址
  50. // NumToWrite:需要写入数据的大小
  51. // 返回: 4=成功  1,2,3,5=失败
  52. // 备注:
  53. //==================================================================================
  54. FLASH_Status  FLASH_Write(unsigned int        WriteAddr,unsigned char *pBuffer,unsigned int NumToWrite)
  55. {

  56.     FLASH_Status status = FLASH_COMPLETE;
  57.           u32tobyte cache;//联合体定义
  58.     unsigned int startaddr,endaddr,pageaddr=0;
  59.           unsigned char buffer[4096];//4K缓冲区 对应FALSH 1页
  60.           unsigned int i;
  61.           unsigned int index,remain;
  62.     startaddr = WriteAddr;
  63.     endaddr = startaddr+NumToWrite;//结束地址
  64.        
  65.     FLASH_Unlock();
  66.     FCU->RO = 0;//去掉所有扇区写保护
  67.     //==================================================================================
  68.     // 判断写入地址是否非法  起始地址或者结束地址不在FALSH范围内则退出
  69.     //==================================================================================
  70.     if(!(IS_FLASH_ADDRESS(startaddr)&& IS_FLASH_ADDRESS(endaddr))) return FLASH_ERROR_PG;
  71.    
  72.            while(startaddr < endaddr)
  73.                  {
  74.                          
  75.                 //==================================================================================
  76.     //1.计算起始地址在FALSH哪一页,并获取该页的首地址
  77.                 //2.计算起始地址在该页的偏移量
  78.     //3.计算该页还剩余多少字节没写入数据                         
  79.     //==================================================================================
  80.                           pageaddr = FLASH_GetFlashPage(startaddr);//获取起始地址所在页的页首地址
  81.                           index = startaddr-pageaddr;//4K缓冲区内偏移地址
  82.                           remain=4096-index;//缓存区剩余大小
  83.     //==================================================================================
  84.     // 将该页数据读入4K缓冲数组,后面读写都是对该缓冲数组操作
  85.     //==================================================================================                         
  86.                     for(i=0;i<4096;i+=4)//读取一页到缓冲buff
  87.                           {
  88.                                     cache.u32data=FLSAH_ReadWord(pageaddr+i);
  89.                                     buffer[i]=cache.buf[0];
  90.                                            buffer[i+1]=cache.buf[1];
  91.                                           buffer[i+2]=cache.buf[2];
  92.                                           buffer[i+3]=cache.buf[3];
  93.                                 }
  94.     //==================================================================================
  95.     // 擦除FALSH对应的页,FLASH只能按页擦除,
  96.                 // 这一页数据已经被读到缓冲数组中了 之前的数据也保留下来了                
  97.     //==================================================================================                               
  98.                                 status = FLASH_ErasePage(startaddr);
  99.         if(status != FLASH_COMPLETE) return status;//擦除1页 4K字节                                       
  100.     //==================================================================================
  101.     //1.判断要写入的数据是否大于该页剩余容量(即计算写入的数据长度是否跨多页)
  102.                 //2.将需要写入的数据转存到缓冲数据               
  103.     //==================================================================================                               
  104.                                 if(NumToWrite > remain)//需要写入的数据量大于缓冲buf剩余字节数
  105.                                 {
  106.                                         for(i=index;i<4096;i++)//将需要写入FALSH的数据写入缓冲buff
  107.                                         {
  108.                                                          buffer[i]=*(pBuffer++);                               
  109.                                         }
  110.                                         NumToWrite-=remain;//需要写入的数据长度-本次已经写入的数据长度       
  111.           startaddr+=remain;//地址向后偏移本次写入的字节数                                       
  112.                           }
  113.                                 else
  114.                                 {
  115.                                   for(i=index;i<NumToWrite+index;i++)//将需要写入FALSH的数据写入缓冲buff
  116.                                         {
  117.                                                          buffer[i]=*(pBuffer++);                               
  118.                                         }
  119.           startaddr+=NumToWrite;//地址向后偏移本次写入的字节数                                                               
  120.                                 }       
  121.     //==================================================================================
  122.     // 将缓冲数组(4K)写入FLASH 对应的页
  123.                 // 此处必须从页首写入,因为缓冲数组正好4K,对应FALSH 1页       
  124.     //==================================================================================                               
  125.                                 for(i=0;i<4096;i+=4)//将缓冲buffer写入 FALSH
  126.                           {
  127.                                     cache.buf[0]=buffer[i];
  128.                                            cache.buf[1]=buffer[i+1];
  129.                                           cache.buf[2]=buffer[i+2];
  130.                                           cache.buf[3]=buffer[i+3];
  131.                                        
  132.                                           if((status=FLASH_ProgramWord(pageaddr+i,cache.u32data))!= FLASH_COMPLETE)
  133.                                                 {
  134.                                                                 FLASH_Lock();
  135.                                                                 return status;//写入失败 FLASH上锁
  136.                                                 }                                                                    
  137.                                 }                       
  138.                  }
  139.     FLASH_Lock();
  140. }



其中还有个联合体的定义:



  1. typedef union
  2. {
  3.     unsigned int  data;
  4.     unsigned char buf[4];
  5. }
  6. u32tou8;



FLASH_ErasePage、FLASH_ProgramWord、IS_FLASH_ADDRESS 这三个都是单片机FLASH的库函数
各家单片机不同,但功能基本相同,这里不再提供源码。
最后提供以下两个FLASH接口即可:



  1. FLASH_Write(unsigned int    WriteAddr,unsigned char *pBuffer,unsigned int NumToWrite);

  2. FLASH_Read(unsigned int    ReadAddr,unsigned char *pBuffer,unsigned int NumToRead)



演示:1.为方便查看结果,测试从0x1070FFC的位置开始写入数据,FLASH地址分布如下图所示:
这里展示了FLASH连续两页的地址,首先将这两页全部擦除。






2.接着从1070FFC的位置开始写入56个1,这样就保证了数据跨越了1页。



  1. unsigned char write[]= {"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111"};

(,writesizeof);





  1. unsigned char write2[]={"23456789"};
  2. FLASH_Write(0x01070FFE,write2,sizeof(write2));

注意:最后的00是因为字符串的结尾字符是“\0”
3.紧接着,在0x1070FFE位置写入新的字符串,也要保证写入长度跨越1页






可以看出,0x1070FFE~0x1071006的位置被写入了新的字节,但这两页的其他位置数据保持不变。
总结:1.实际使用时,如果不是受限于成本或者FLASH大小,不建议这样读写内部FLASH,以为stm32内部FLASH也就
10W次寿命,这样频繁擦写会大大降低FLASH寿命。
2.如果保存的数据不多,建议每个数据都单独存1页,这样不用考虑擦除时会把其他数据也一并擦除。

清芯芯清 发表于 2025-5-7 12:22 | 显示全部楼层
这不都是有例程的吗,一般来说
pe66ak 发表于 2025-5-7 13:35 | 显示全部楼层
FLASH存储器核心特性物理单元:以页(Page)或扇区(Sector)为单位,典型页大小为512B~2KB(如STM32为1KB/页,ESP32为4KB/页)。
nqty 发表于 2025-5-7 14:39 | 显示全部楼层
操作限制擦除:必须整页/扇区擦除(不可部分擦除)。写入:仅支持将1→0(需擦除后才能重新写入1)。寿命:典型1万~10万次擦写(高端芯片可达100万次)。
一切D都好 发表于 2025-5-7 16:09 | 显示全部楼层
使用CRC校验(如STM32的硬件CRC模块)验证整页数据。
wamed 发表于 2025-5-7 17:13 | 显示全部楼层
磨损均衡避免固定地址反复擦写,延长FLASH寿命。
nuan11nuan 发表于 2025-5-7 19:21 | 显示全部楼层
写入对齐优化最小写入单位:STM32为半字(16位),ESP32为字(32位)。建议:按最小单位对齐写入,避免跨半字/字操作。
suiziq 发表于 2025-5-7 20:44 | 显示全部楼层
中断安全,FLASH操作期间禁用中断,防止任务切换导致写入失败。
穷得掉渣大侠 发表于 2025-5-8 08:29 | 显示全部楼层
这个方法虽然可以解决字节写入的问题,但是效率和寿命损耗是不得不考虑的问题。在实际应用中,我们是否还有其他更好的解决方案呢?
ewyu 发表于 2025-5-8 09:12 | 显示全部楼层
写入失败,未解锁FLASH/地址越界/正在擦除
tiakon 发表于 2025-5-8 11:25 | 显示全部楼层
数据错误,写入前未擦除/对齐错误,确保擦除后写入,按最小单位对齐
teaccch 发表于 2025-5-8 13:16 | 显示全部楼层
操作超时,FLASH忙或电源不稳定,增加超时检测,使用稳定电源
魔法森林精灵 发表于 2025-5-8 13:27 | 显示全部楼层
这个方法确实可以在一定程度上实现字节级别的操作,但正如你所说,这样做会大大增加FLASH的擦写次数,影响其使用寿命。在实际应用中,我们还是应该尽量避免频繁的擦写操作。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

50

主题

1627

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部