231 lines
10 KiB
Markdown
231 lines
10 KiB
Markdown
|
---
|
|||
|
title: STM32 Flash的读写
|
|||
|
tags:
|
|||
|
- Flash读写
|
|||
|
categories:
|
|||
|
- STM32
|
|||
|
comments: true
|
|||
|
abbrlink: 31184d7e
|
|||
|
date: 2018-03-07 23:26:39
|
|||
|
images:
|
|||
|
---
|
|||
|
这里记一下STM32F4板子FLASH的读写操作以及需要注意的地方。
|
|||
|
## FLASH的写操作
|
|||
|
1. FLASH由 ‘1’ 变为 ‘0’ 不能由 ‘0’ 变为 ‘1’ ,所以在写入之间需要检测是否为 ‘1' ,并且擦除flash只能按照一个扇区来删除,由上可知,我们写入之前必须确保我们需要写入的地址读回来的值是0XFF,如果不是0XFF就需要将整个扇区擦除(扇区变为全 ‘1’ )。
|
|||
|
2. 写入操作(包括擦除)之前需要将FLASH解锁 `FLASH_Unlock()`,操作完成之后需要将FLASH上锁 `FLASH_Lock()` 。
|
|||
|
<!---more--->
|
|||
|
|
|||
|
## [ 正点原子](http://www.alientek.com/)的例子
|
|||
|
- 写入流程:
|
|||
|
- 解锁FLASH `FLASH_Unlock()`;
|
|||
|
- 禁止数据缓存`FLASH_DataCacheCmd(DISABLE)`,根据你传入的起始地址 `WriteAddr` 和写入的字节数 `NumToWrite` 计算出结束地址 `endaddr`;
|
|||
|
- 遍历整个写入范围,一旦发现非 ’1‘ 数据的出现就擦除整个扇区,他这里是字(Word)来检测的,所以地址 `addrx` 每次加4;
|
|||
|
- 检测完毕并且擦除之后,进入写操作,每次写入一个字(4byte)的数据。所以 `WriteAddr` 每次加4, `u32 *pBuffer` 每次加1 ;
|
|||
|
- 使能数据缓存`FLASH_DataCacheCmd(ENABLE)`,上锁FLASH `FLASH_Lock()`。
|
|||
|
|
|||
|
```c
|
|||
|
//从指定地址开始写入指定长度的数据
|
|||
|
//特别注意:因为STM32F4的扇区实在太大,没办法本地保存扇区数据,所以本函数
|
|||
|
// 写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
|
|||
|
// 写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
|
|||
|
// 没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
|
|||
|
//该函数对OTP区域也有效!可以用来写OTP区!
|
|||
|
//OTP区域地址范围:0X1FFF7800~0X1FFF7A0F
|
|||
|
//WriteAddr:起始地址(此地址必须为4的倍数!!)
|
|||
|
//pBuffer:数据指针
|
|||
|
//NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
|
|||
|
void STMFLASH_Write(u32 WriteAddr, u32 *pBuffer, u32 NumToWrite)
|
|||
|
{
|
|||
|
FLASH_Status status = FLASH_COMPLETE;
|
|||
|
u32 addrx = 0;
|
|||
|
u32 endaddr = 0;
|
|||
|
if(WriteAddr < STM32_FLASH_BASE || WriteAddr % 4)return; //非法地址
|
|||
|
FLASH_Unlock(); //解锁
|
|||
|
FLASH_DataCacheCmd(DISABLE);//FLASH擦除期间,必须禁止数据缓存
|
|||
|
|
|||
|
addrx = WriteAddr; //写入的起始地址
|
|||
|
endaddr = WriteAddr + NumToWrite * 4; //写入的结束地址
|
|||
|
if(addrx < 0X1FFF0000) //只有主存储区,才需要执行擦除操作!!
|
|||
|
{
|
|||
|
while(addrx < endaddr) //扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
|
|||
|
{
|
|||
|
if(STMFLASH_ReadWord(addrx) != 0XFFFFFFFF) //有非0XFFFFFFFF的地方,要擦除这个扇区
|
|||
|
{
|
|||
|
status = FLASH_EraseSector(STMFLASH_GetFlashSector(addrx), VoltageRange_3); //VCC=2.7~3.6V之间!!
|
|||
|
if(status != FLASH_COMPLETE)break; //发生错误了
|
|||
|
}
|
|||
|
else addrx += 4;
|
|||
|
}
|
|||
|
}
|
|||
|
if(status == FLASH_COMPLETE)
|
|||
|
{
|
|||
|
while(WriteAddr < endaddr) //写数据
|
|||
|
{
|
|||
|
if(FLASH_ProgramWord(WriteAddr, *pBuffer) != FLASH_COMPLETE) //写入数据
|
|||
|
{
|
|||
|
break; //写入异常
|
|||
|
}
|
|||
|
WriteAddr += 4;
|
|||
|
pBuffer++;
|
|||
|
}
|
|||
|
}
|
|||
|
FLASH_DataCacheCmd(ENABLE); //FLASH擦除结束,开启数据缓存
|
|||
|
FLASH_Lock();//上锁
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
- 测试和分析
|
|||
|
经过实验,擦除扇区的时间是很久的,擦除128KByte的扇区大概需要1s多。并且,不解锁虽然可以通过擦除函数并且返回 `FLASH_COMPLETE` 但是实际并没有擦除成功。我开始以为他这个耗时主要体现在读和判断,擦除操作比较快,😒但其实读120KByte的数据只需要3.9ms加上判断的时间也没多少主要的时间耗费在擦除操作上。😆另一个体现就是如果在 Keil 的工程选项里把擦除全部flash勾选,下载前的擦除的时间是很长的。下面是测试代码和解释结果。测试结果可以看出,擦除的时间并没有很大变化,因为都是需要一次性操作一个扇区,只要不超过一个扇区的长度都不会变化很大。写入和读出的时间保持线性变化。所有的测试都是用原子官方例程按照WORD(32bit)来写入的。
|
|||
|
|
|||
|
```c
|
|||
|
int main(void)
|
|||
|
{
|
|||
|
...上续代码
|
|||
|
STMFLASH_Write(FLASH_SAVE_ADDR, (u32 *)&txbuf, SIZE);//写入一些数据保证flash不全为'1'
|
|||
|
uprintf(USART1, "扇区预先不为全 '1'时写入:\r\n");
|
|||
|
uprintf(USART1, "写入长度:%d\r\n", TEXT_LENTH);
|
|||
|
//原子的例子擦除flash再写入数据
|
|||
|
STMFLASH_Write(FLASH_SAVE_ADDR, (u32 *)&txbuf, SIZE);
|
|||
|
memset(readbuf,0,TEXT_LENTH);//清空数据读取
|
|||
|
StartTiming();
|
|||
|
STMFLASH_Read(FLASH_SAVE_ADDR, (u32 *)&readbuf, SIZE);
|
|||
|
timems = GetTimeMs();
|
|||
|
uprintf(USART1, "读出耗时:%.3fms\r\n", timems);
|
|||
|
uprintf(USART1, "读出长度:%d\r\n----\r\n", TEXT_LENTH, strlen((const char *)readbuf));
|
|||
|
...下续代码
|
|||
|
}
|
|||
|
|
|||
|
void STMFLASH_Write(u32 WriteAddr, u32 *pBuffer, u32 NumToWrite)
|
|||
|
{
|
|||
|
...上续代码
|
|||
|
FLASH_DataCacheCmd(DISABLE);//FLASH擦除期间,必须禁止数据缓存
|
|||
|
StartTiming();//开始计时擦除扇区耗时
|
|||
|
.....上续代码
|
|||
|
timems = GetTimeMs();
|
|||
|
uprintf(USART1,"擦除扇区耗时 :%.3fms\r\n",timems);
|
|||
|
StartTiming();//开始写入计时
|
|||
|
if(status == FLASH_COMPLETE)
|
|||
|
{
|
|||
|
...上续代码
|
|||
|
FLASH_Lock();//上锁
|
|||
|
timems = GetTimeMs();//获取写入耗时
|
|||
|
uprintf(USART1, "写入耗时:%.3fms\r\n", timems);
|
|||
|
}
|
|||
|
/*测试结果 测试120、12K、120K(Byte)的数据*/
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:120
|
|||
|
擦除扇区耗时 :1046.720ms
|
|||
|
写入耗时:0.440ms
|
|||
|
读出耗时:0.020ms
|
|||
|
读出长度:120
|
|||
|
----
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:12288
|
|||
|
擦除扇区耗时 :1050.220ms
|
|||
|
写入耗时:45.700ms
|
|||
|
读出耗时:0.380ms
|
|||
|
读出长度:12288
|
|||
|
----
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:122880
|
|||
|
擦除扇区耗时 :1051.860ms
|
|||
|
写入耗时:458.700ms
|
|||
|
读出耗时:3.880ms
|
|||
|
读出长度:122880
|
|||
|
----
|
|||
|
```
|
|||
|
## 改进擦除方式
|
|||
|
- 改进的一点想法
|
|||
|
根据正点原子的测试结果来看,擦除扇区的时间是不可避免的,无非就是暴力一点不检测直接擦除(因为通常来说,如果你存储在固定的flash地址除了第一次之后都会有数据)。这样搞了之后从测试的结果来看提升并不明显。写120KByte的数据下擦除时间少了4ms左右😔(少了读取和判断的时间)。所以擦除的方式并没有很好的改进方法(标题党😈)。
|
|||
|
|
|||
|
## 通用性考虑
|
|||
|
- 通过正点原子的例子熟悉了Flash的读写,但是只支持4字节对齐(WORD)的操作,我看了一下库的函数是可以支持 BYTE,HALFWORD,WORD, 和 DOUBLEWORD 的,不过 DOUBLEWORD 需要外部Vpp。所以想改进一些支持库函数里的各种byte(1-4) 的操作。
|
|||
|
- 修改好之后测试一下测试代码和上面原版的差不多。只是写入函数换成了修改之后的`STMFLASH_WriteWithErase`,测试结果如下,测试的时候用的是WORD方式写入。和原子基本没有差别。
|
|||
|
```c
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:120
|
|||
|
擦除扇区耗时 :1043.500ms
|
|||
|
写入耗时:0.460ms
|
|||
|
读出耗时:0.000ms
|
|||
|
读出长度:120
|
|||
|
----
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:12288
|
|||
|
擦除扇区耗时 :1046.580ms
|
|||
|
写入耗时:45.700ms
|
|||
|
读出耗时:0.400ms
|
|||
|
读出长度:12288
|
|||
|
----
|
|||
|
扇区预先不为全 '1'时写入:
|
|||
|
写入长度:122880
|
|||
|
擦除扇区耗时 :1050.860ms
|
|||
|
写入耗时:459.140ms
|
|||
|
读出耗时:3.900ms
|
|||
|
读出长度:122880
|
|||
|
----
|
|||
|
```
|
|||
|
## 其他ByteSize方式写入
|
|||
|
相对于用WORD来说用其他方式写入flash是要慢一些的。采用 BYTE 方式需要 大概`1832ms`,采用HALFWORD方式需要`920ms`左右。因为擦除都是采用的`VoltageRange_3`也就是WORD方式所以擦除时间差不多。
|
|||
|
|
|||
|
## 分离操作的方式
|
|||
|
- 分离操作的意义
|
|||
|
把擦除和写入分开。考虑到一个扇区是非常大的,一个扇区可能会存储多种数据。如果写入之前有数据就会导致整个扇区被擦除,数据就会丢失。所以采用分离的办法,把数据的写入和擦除分隔开,根据需要选择是否擦除扇区内的数据,保证在擦除的操作之前有相应的备份操作就OK了。这样即使忘记擦除那么也只是新的数据没法正确写到flash里面,其他部分的数据就不会被擦除,这样有利于更好的数据完整性。
|
|||
|
- 测试结果和测试代码,经过测试分离之后的操作和没分离的时间上基本没什么差别,
|
|||
|
```c
|
|||
|
分离操作方式写入flash:
|
|||
|
擦除扇区耗时:1023.320ms
|
|||
|
写入长度:120 byte
|
|||
|
写入耗时:0.440ms
|
|||
|
读出耗时:0.000ms
|
|||
|
读出长度:120 byte
|
|||
|
****
|
|||
|
分离操作方式写入flash:
|
|||
|
擦除扇区耗时:1015.140ms
|
|||
|
写入长度:12288 byte
|
|||
|
写入耗时:45.940ms
|
|||
|
读出耗时:0.400ms
|
|||
|
读出长度:12288 byte
|
|||
|
****
|
|||
|
合并操作方式写入flash:
|
|||
|
写入长度:122880
|
|||
|
擦除扇区耗时 :1010.480ms
|
|||
|
写入耗时:461.420ms
|
|||
|
读出耗时:3.900ms
|
|||
|
读出长度:122880
|
|||
|
****
|
|||
|
int main(void)
|
|||
|
{
|
|||
|
...上续代码
|
|||
|
uprintf(USART1, "分离操作方式写入flash:\r\n");
|
|||
|
StartTiming();
|
|||
|
status = StmEraseFlashSector(FLASH_SAVE_ADDR, TEXT_LENTH);
|
|||
|
//FLASH_DataCacheCmd(DISABLE);//FLASH擦除期间,必须禁止数据缓存
|
|||
|
//status = FLASH_EraseSector(STMFLASH_GetFlashSector(FLASH_SAVE_ADDR), VoltageRange_3); //VCC=2.7~3.6V之间!!
|
|||
|
if(status != FLASH_COMPLETE)
|
|||
|
{
|
|||
|
uprintf(USART1, "擦除扇区失败:%d\r\n",status); //发生错误了
|
|||
|
while(1) {};
|
|||
|
}
|
|||
|
timems = GetTimeMs();
|
|||
|
uprintf(USART1, "擦除扇区耗时:%.3fms\r\n", timems);
|
|||
|
uprintf(USART1, "写入长度:%d byte\r\n", TEXT_LENTH);
|
|||
|
StartTiming();
|
|||
|
//不需要擦除falsh直接写数据
|
|||
|
STMFLASH_WriteNoErase(FLASH_SAVE_ADDR, txbuf,TEXT_LENTH,ByteSize);
|
|||
|
timems = GetTimeMs();
|
|||
|
uprintf(USART1, "写入耗时:%.3fms\r\n", timems);
|
|||
|
memset(readbuf,0,TEXT_LENTH);//清空数据读取
|
|||
|
StartTiming();
|
|||
|
STMFLASH_Read(FLASH_SAVE_ADDR,readbuf, TEXT_LENTH,ByteSize);
|
|||
|
timems = GetTimeMs();
|
|||
|
uprintf(USART1, "读出耗时:%.3fms\r\n", timems);
|
|||
|
uprintf(USART1, "读出长度:%d byte\r\n****\r\n", TEXT_LENTH, strlen((const char *)readbuf));
|
|||
|
}
|
|||
|
```
|
|||
|
## 后记
|
|||
|
搞了几天才把这个写完整。中途也进了好多的坑,浪费了好长时间。不过还是写完了。说一点注意的地方
|
|||
|
- flash的写入地址不是偶数就可以,得是4的倍数。
|
|||
|
- 无论是擦除还是写入都需要先解锁flash。读取则不需要。
|
|||
|
- 对于同一个扇区的建议用分离的操作,并且在操作前根据需要把其他的数据读出来备份一下在写进去。
|
|||
|
|
|||
|
关于本文的代码详见于[Coding项目](https://coding.net/u/BycCoding/p/stm32f4_flash/git)。
|