STM32内部Flash的写寿命大约是1万次,假如我们在其Flash中存储数据,每天100次写操作,100天后Flash就无法继续可靠使用了;外部FLASH,比如说W25Q32,擦写次数也只有十万次,在高频率读写下也支撑不了多久, 本文采取了一种非常简单的方法,将Flash的使用寿命无限延长,取决于你为它分配的存储区大小。
主要思想就是将FLASH 分配一块区域给我们的管理机,然后用索引的方式累积写FLASH,中途不进行擦写,在存满整个分区时进行统一擦写,读取根据ID进行读取,并且加上了数据校验,异常回调。主要用于存储系统配置,运行记录等。支持多个存储管理机管理不同的区域。
FLASH存储模型如下:
管理者内存结构如下:
typedef struct{
unsigned int PageSize; //页大小
unsigned int Pages; //写的总页数
unsigned int StartAddr; //起始地址
unsigned int DriftAddr; //偏移地址需要大于四字节
unsigned int Number; //总共有效条数
unsigned char * Buffer; //数据缓冲区地址
unsigned int ReadAddr; //读地址,用于节省查找时间提高效率
unsigned int WriteAddr; //写地址,用于节省时间提高查找空区效率
void (* Error_Handle)(FERROR err, unsigned char * src, unsigned int len);
}FlashConfig;
PageSize:用户为该管理机提供的存储分区业大小。同时也最小擦除单位。
Pages:存储分区页大小。
StartAddr:存储分区起始地址。
DriftAddr:需要存储数据的数据宽度
Number:需要存储的数据条数,也就是最大ID,这里如果你相同类型的数据有多条有效数据,就通过ID来进行区分。打个比方:周一到周五的菜单,每天固定三个菜,要进行存储的话,就需要提供五组数据,那么日期就是ID,用来区分周几。
Buffer:这个是你数据的内存首地址,这个在后面不同的接口会有介绍
ReadAddr:读地址,由于查找最后一块有效数据地址随着分区的大小,查找时间会相应的变长,所以在系统上电初始化的时候将最后一块有效数据块首地址记录,之后就不用再从子分区0查找了,提高系统执行效率,这个保留在这,暂未实现,后面的版本会加入。
WriteAddr:写地址,与上面一样。
Error_Handle:错误回调,如未实现,请赋值为NULL
存储器接口:
void open_service()
{
//这里可提供线程安全防止中断flash操作,由close_service释放安全锁
eeplog("%s", "open service\n");
}
void close_service()
{
eeplog("%s", "close service\n");
}
/**
* @brief 写储存器接口。
* @param addr:写首地址
sd:写入的数据首地址
len:写入的长度
* @retval None
*/
void write_flash(unsigned int addr, const unsigned char * sd, unsigned int len){
unsigned short * pt = (unsigned short *)sd;
HAL_FLASH_Unlock();
for(int i = 0; i < len/2; i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr+i*2, *(pt+i));
}
HAL_FLASH_Lock();
}
/**
* @brief 擦除储存器接口
* @param addr:擦除地址
* @param 擦写长度,暂时未用到,保留,接口默认擦除一页,
* @retval None
*/
void erase_flash(unsigned int addr, unsigned int len){
uint32_t PageError = 0;
FLASH_EraseInitTypeDef pEraseInit;
pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
pEraseInit.PageAddress = addr;
pEraseInit.NbPages = 1;
HAL_FLASH_Unlock();
HAL_StatusTypeDef s = HAL_FLASHEx_Erase(&pEraseInit, &PageError);
HAL_FLASH_Lock();
}
/**
* @brief 读储存器接口.
* @param addr:读起始地址
sd:读数据缓存区首地址
len:读的长度
* @retval None
*/
void read_flash(unsigned int addr, unsigned char * sd, unsigned int len){
unsigned short * pt = (unsigned short *)sd;
for(int i = 0; i < len / 2; i++){
pt[i] = *((__IO uint16_t*)(addr+i*2));
}
}
erase_flash:这里的长度其实无效,这里需要提供的是擦除一页的接口,这里也许会有人说NAND Flash,最小擦除单位为1块,也就是64页,怎么办呢?这里你就需要在配置管理类的时候,将页数量配置为1,然后页大小不超过一块的大小就可以。
下面是全部代码:
eeprom.c
/*********************************************************************************
*Copyright(C) -
*FileName: eeprom.c
*Author: 我不是阿沸
*Version: 6.1.0
*Date: 2023.08.20
*Description: 用于嵌入式系统保存数据至存储器,兼容大多数存储器,经内部算法优化,擦写寿命可达:正常寿命*100倍,这个取决于你分配的空间大小,空间越大寿命越长。
*Others: 互斥需自己实现
*History:
1.Date:2023/04/03
Author:我不是阿沸
Modification:增加自定义互斥接口,由用户实现
2.Date:2023/05/16
Author:我不是阿沸
Modification:修改相关接口实现方式
3.Date:2023/07/18
Author:我不是阿沸
Modification:修改读写数据实现方式
4.Date:2023/08/20
Author:我不是阿沸
Modification:删除冗余成分
4.Date:2023/010/24
Author:我不是阿沸
Modification:增加查找分区地址记录,只需上电查找一次,提高效率
5.其他修改
**********************************************************************************/
#include "eeprom.h"
void open_service()
{
//HAL_FLASH_Unlock();
//这里可提供线程安全防止中断flash操作,由close_service释放安全锁
eeplog("%s", "open service\n");
}
void close_service()
{
eeplog("%s", "close service\n");
//HAL_FLASH_Lock();
}
//ProgramGroup programGroups[10];
/**
* @brief 写储存器接口。
* @param addr:写首地址
sd:写入的数据首地址
len:写入的长度
* @retval None
*/
void write_flash(epmu32 addr, const epmu8 * sd, epmu32 len){
//注意,这里传入的sd必须是偶数地址,不然在转换为16位时会出现字节对齐的情况
uint64_t * pt = (uint64_t *)sd;
HAL_FLASH_Unlock();
//rt_kprintf("\nlen:%d\n", len);
Flash_If_Write((uint8_t*)sd, addr, len);
// for(int i = 0; i < len/8; i++)
// {
// HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr+i*8, *(pt+i));
// }
HAL_FLASH_Lock();
}
/**
* @brief 擦除储存器接口
* @param addr:擦除地址
* @retval None
*/
void erase_flash(epmu32 addr, epmu32 len){
epmu32 PageError = 0;
//FLASH_EraseInitTypeDef pEraseInit;
HAL_FLASH_Unlock();
Flash_If_Erase(addr);
HAL_FLASH_Lock();
}
/**
* @brief 读储存器接口.
* @param addr:读起始地址
@param sd:读数据缓存区首地址
@param len:读的长度
* @retval None
*/
void read_flash(epmu32 addr, epmu8 * sd, epmu32 len){
uint64_t * pt = (uint64_t *)sd;
// for(int i = 0; i < len / 8; i++){
// pt[i] = *((__IO uint64_t*)(addr+i*8));
// }
Flash_If_Read(sd, addr, len);
}
/**
* @brief 校验数据接口
* @param src:需要校验的数据首地址
len:要校验的长度
* @retval epmbool:返回校验成功或者失败
*/
epmbool check_flash(epmu8 *src, epmu32 len){
eeplog("start check data\n");
epmu32 src_ckeck = epm_crc32(src, len -4);
epmu32 dec_ckeck = epmu32_buffer_max((src+ len - 4));
if(src_ckeck != dec_ckeck) {
eeplog("data check error!\n");
return EPMFALSE;
}
eeplog("data check success!\n");
return EPMTRUE;
}
/**
* @brief 判断第一块存储区是否进行初始化
* @param flg:分区信息
* @retval epmbool:true:存储区已初始化 false:未初始化
*/
epmbool isinit_first_block(FlashConfig flg){
epmu8 ch[10];
read_flash(flg.start_addr, ch, 8);
int i = 0;
for(; i < 8; i++){
if(ch[i] != 0xA5) break;
}
if(i != 8) return EPMFALSE;
return EPMTRUE;
}
/**
* @brief 第一块存储区初始化,写入初始化信息
* @param flg:分区信息
* @retval epmbool:true:存储区初始化成功 false:分区损坏
*/
epmbool first_block_init(FlashConfig flg){
eeplog("Is check epm init?\n");
epmu8 ch[10];
memset(ch, 0xA5, 8);
eeplog("Sector not initialized, about to initialize...\n");
for(int m = 0; m < flg.pages; m++){
erase_flash(flg.start_addr+m*flg.page_size, flg.page_size);
}
write_flash(flg.start_addr, ch, 8);
read_flash(flg.start_addr, ch, 8);
for(int i = 0; i < 8; i++){
if(ch[i] != 0xA5) {
eeplog("Sector initialization default!!!!!!!!!!!!!!!\n");
return EPMFALSE;
}
}
eeplog("Sector initialization successful\n");
return EPMTRUE;
}
/**
* @brief 校验flash读出的数据是否为空
* @param data:需要校验的数据首地址
len:数据的长度
* @retval epmbool:未被写入过数据返回true
*/
epmbool is_eeprom_null(FlashConfig flg, epmu8 * data, int len){
int n = 0;
eeplog("Is flash empty?\n");
for(n = 0; n < len; n++){
if(data[n] != 0xff){
eeplog("flash isn`t empty\n");
return EPMFALSE;
}
}
eeplog("flash is empty\n");
return EPMTRUE;
}
/**
* @brief CRC32校验
* @param data:需要校验的数据首地址
len:数据的长度
* @retval CRC32校验值
*/
#define POLY 0xEDB88320UL
epmu32 epm_crc32(epmu8 * data, epmu32 datalen)
{
const uint8_t *bytes = data;
uint32_t crc = 0xFFFFFFFFUL;
// 循环处理每个字节
for (size_t i = 0; i < datalen; i++) {
crc ^= bytes[i]; // 把当前字节与 crc 的低 8 位进行异或操作
// 处理当前字节的 8 位,每次处理一位
for (int j = 0; j < 8; j++) {
if (crc & 1) { // 如果 crc 的最低位为 1,则右移并与多项式除数进行异或操作
crc = (crc >> 1) ^ POLY;
} else { // 否则,只右移一个比特位
crc >>= 1;
}
}
}
return ~crc; // 取反操作得到最终结果
}
/**
* @brief 初始化分区信息
* @param 分区信息指针
* @param 起始地址
* @param 偏移地址,子存储区大小
* @param 有效数据条数
* @param 分区占用存储器页数
* @param 存储器叶大小
* @param 数据缓存区指针
* @param 错误回调接口
* @retval none
*/
void eeprom_init(FlashConfig * flg, epmu32 start_addr, epmu8 drift_addr, epmu16 number, epmu16 pages, epmu32 page_size, epmu8 * buffer, error_handle error)
{
flg->find_flag = EPMFALSE;
flg->find_addr = start_addr;
flg->start_addr = start_addr;
flg->drift_addr = drift_addr;
flg->number = number;
flg->pages = pages;
flg->page_size = page_size;
flg->buffer = buffer;
flg->error = error;
}
/**
* @brief 顺序查找flash空区
* @param flg:分区信息
* @retval int:返回空区首地址
*/
epmu32 epm_seq_search_addr(FlashConfig * flg){
int findCount = flg->page_size * flg->pages / (flg->drift_addr+4);
epmu32 start_addr = flg->start_addr;
epmu32 find_addr = start_addr;
epmu8 data[200];
int i;
open_service();
eeplog("epy find start!\n");
//这里注意,首地址为初始化校验区,无有效数据
if(flg->find_flag == EPMFALSE)
{
epmbool bl = isinit_first_block(*flg);
if(bl == EPMFALSE) {
close_service();
flg->find_flag = EPMTRUE;
flg->find_addr = start_addr;
return start_addr;
}
for(i = 1; i < findCount; i++){
read_flash(start_addr + i*(flg->drift_addr+4), data, flg->drift_addr+4);
if(is_eeprom_null(*flg, data, flg->drift_addr+4) == EPMTRUE){
find_addr = start_addr + i*(flg->drift_addr+4);
eeplog("epy find end, return emp addr!\n");
break;
}
}
if(i == findCount)
{
find_addr = start_addr + findCount*(flg->drift_addr+4);
}
else
{
}
flg->find_addr = find_addr;
flg->find_flag = EPMTRUE;
}
close_service();
return flg->find_addr;
}
/**
* @brief 写入数据
* @param flg:分区信息
data:需要写入的数据
* @retval int:返回对应的空区首地址
*/
FERROR epm_write_data(FlashConfig * flg, const epmu8 * data){
epmu8 ch[200] = {0};
epmu8 chb[200] = {0};
epmu32 addr = flg->start_addr;
int findCount = flg->page_size * flg->pages / (flg->drift_addr+4);
addr = epm_seq_search_addr(flg);
if(flg->find_addr >= flg->start_addr + findCount* (flg->drift_addr+4))
{
flg->find_addr = flg->start_addr;
addr = flg->start_addr;
}
open_service();
if(addr == flg->start_addr){
epmbool b = first_block_init(*flg);
if(b == EPMFALSE){
flg->error(InitError, NULL, 0);
close_service();
return InitError;
}
for(int i = 0; i < flg->number; i++){
memcpy(ch, flg->buffer+i*flg->drift_addr, flg->drift_addr);
epmu32 src_ckeck = epm_crc32(ch, flg->drift_addr);
buffer_epmu32_max(src_ckeck, (ch+flg->drift_addr));
write_flash(addr +(i+1)*(flg->drift_addr+4), ch, flg->drift_addr+4);
//这里可适当加入回读错误处理
}
flg->find_addr = addr + (flg->drift_addr+4)*(flg->number+1);
eeplog("addr:%08X\n", addr);
close_service();
return Success;
}
memcpy(ch, data, flg->drift_addr);
epmu32 src_ckeck = epm_crc32(ch, flg->drift_addr);
buffer_epmu32_max(src_ckeck, (ch+flg->drift_addr));
write_flash(addr, ch, flg->drift_addr+4);
read_flash(addr, chb, flg->drift_addr+4);
flg->find_addr += (flg->drift_addr+4);
if(flg->find_addr >= flg->start_addr + findCount* (flg->drift_addr+4))
{
flg->find_addr = flg->start_addr;
}
int m = 0;
for( ; m < flg->drift_addr+4; m++){
if(chb[m] != ch[m]){
break;
}
}
if(flg->drift_addr+4 == m)
{
close_service();
eeplog("%s", "data write succse");
return Success;
}
else
{
eeplog("%s", "data write error, sd have bad block");
}
close_service();
eeplog("%s", "data write error, sd have bad block");
return WriteError;
}
/**
* @brief 读出数据
* @param flg:分区信息
data:需要写入的数据
* @retval NONE
*/
FERROR epm_read_data(FlashConfig *flg, epmu8 * data){
epmu32 addr = epm_seq_search_addr(flg);
epmu8 ch[200];
if(flg->drift_addr+4 >= 200) return IndexError;
if(addr == flg->start_addr){
flg->error(Empty, NULL, 0);
return Empty;
}
else{
addr -= (flg->drift_addr+4);
open_service();
read_flash(addr, ch, flg->drift_addr+4);
close_service();
if(check_flash(ch, flg->drift_addr+4) == EPMFALSE){
flg->error(ReadError, NULL, 0);
return ReadError;
}
memcpy(data, ch, flg->drift_addr);
}
return Success;
}
/**
* @brief 查询全部数据 ,并且通过ID写入对应的缓冲区
* @param flg:分区信息
* @retval void
*/
void epm_select_all_data(FlashConfig * flg)
{
int findCount = (flg->page_size * flg->pages) / (flg->drift_addr+4);
epmu32 start_addr = flg->start_addr;
int i, n;
epmu8 ch[200] = {0};
if(flg->drift_addr+4 >= 200) return;
open_service();
epmbool b = isinit_first_block(*flg);
if(b == EPMFALSE){
flg->error(InitError, NULL, 0);
close_service();
return;
}
for(i = 1; i < findCount; i++){
read_flash(start_addr + i*(flg->drift_addr+4), ch, flg->drift_addr+4);
if(is_eeprom_null(*flg, ch, flg->drift_addr+4) == EPMTRUE)
{
break;
}
if(check_flash(ch, flg->drift_addr+4) == EPMFALSE){
if(flg->error != NULL)
{
flg->error(CheckError, ch, flg->drift_addr);
}
continue;
}
int id = *((epmu16*)ch);
if(id >= flg->number) id = flg->number - 1;
for(n = 0; n < flg->drift_addr; n++){
flg->buffer[id*flg->drift_addr+n] = ch[n];
}
}
close_service();
return;
}
/**
* @brief 通过索引查询多条数据数据
* @param flg:分区信息
start:开始条数,这里换算成地址为:flg->drift_addr* start
num:查寻条数
* @retval int:返回对应的空区首地址
*/
void epm_select_data(FlashConfig * flg, epmu32 start, epmu32 num)
{
epmu32 start_addr = flg->start_addr;
epmu8 ch[200] = {0};
if(flg->drift_addr+ 4 >= 200) return;
open_service();
for(int i = 1; i < num && i < flg->number; i++){
read_flash(start_addr + (i+start)*(flg->drift_addr+4), ch, flg->drift_addr+4);
if(check_flash(ch, flg->drift_addr+4) == EPMFALSE){
flg->error(CheckError, ch, flg->drift_addr);
continue;
}
for(int n = 0; n < flg->drift_addr; n++){
flg->buffer[(i-1)*flg->drift_addr+n] = ch[n];
}
}
close_service();
return;
}
eeprom.h:
/*********************************************************************************
*Copyright(C) -
*FileName: eeprom.h
*Author: 我不是阿沸
*Version: 6.1.0
*Date: 2023.08.20
*Description: 用于嵌入式系统保存数据至存储器,兼容大多数存储器,经内部算法优化,擦写寿命可达:正常寿命*100倍,这个取决于你分配的空间大小,空间越大寿命越长。
*Others: 互斥需自己实现
*History:
1.Date:2023/04/03
Author:我不是阿沸
Modification:增加自定义互斥接口,由用户实现
2.Date:2023/05/16
Author:我不是阿沸
Modification:修改相关接口实现方式
3.Date:2023/07/18
Author:我不是阿沸
Modification:修改读写数据实现方式
4.Date:2023/08/20
Author:我不是阿沸
Modification:删除冗余成分
4.Date:2023/010/24
Author:我不是阿沸
Modification:增加查找分区地址记录,只需上电查找一次,提高效率
5.其他修改
**********************************************************************************/
#ifndef __EEPROMS_H
#define __EEPROMS_H
#include <stdio.h>
#include "if_flash.h"
#include <stdio.h>
#include <string.h>
//#define __EEPDEBUG
#ifdef __EEPDEBUG
#define eeplog(format, ...) \
rt_kprintf("[%s:%d->%s]:"format, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
#else
#define eeplog(format, ...)
#endif
#define FLASH_PAGE_STEP FLASH_PAGE_SIZE
#define START_ADDRESS (uint32_t)0x08000000
#define epmu32_buffer_min(x) (epmu32)(*((x+3))<<24 | *((x)+2)<<16 | *((x)+1)<<8 | *(x))
#define epmu32_buffer_max(x) (epmu32)(*(x)<<24 | *((x)+1)<<16 | *((x)+2)<<8 | *((x)+3))
#define epmu16_buffer_min(x) (epmu16)(*((x)+1)<<8 | *(x))
#define epmu16_buffer_max(x) (epmu16)(*(x)<<8 | *(x+1))
#define buffer_epmu32_min(x, buf) {(buf)[0] = (x); (buf)[1] = (x)>>8; (buf)[2] = (x)>>16; (buf)[3] = (x)>>24;}
#define buffer_epmu32_max(x, buf) {(buf)[3] = (x); (buf)[2] = (x)>>8; (buf)[1] = (x)>>16; (buf)[0] = (x)>>24;}
#define buffer_epmu16_min(x, buf) {(buf)[0] = (x); (buf)[1] = (x)>>8;}
#define buffer_epmu16_max(x, buf) {(buf)[1] = (x); (buf)[0] = (x)>>8;}
typedef unsigned int epmu32;
typedef unsigned short epmu16;
typedef unsigned char epmu8;
typedef enum flash_error{
Success,
WriteError,
ReadError,
IndexError,
CheckError,
InitError,
Empty
}FERROR;
typedef enum{
EPMFALSE,
EPMTRUE
}epmbool;
typedef void (* error_handle)(FERROR err, epmu8 * src, epmu32 len);
typedef struct{
epmu32 page_size; //页大小
epmu16 pages; //写的总页数
epmu32 start_addr; //起始地址
epmu8 drift_addr; //偏移地址需要大于四字节
epmu32 number; //总共有效条数
epmu8 * buffer; //数据缓冲区地址
epmu32 find_addr;
epmbool find_flag;
error_handle error;
}FlashConfig;
void eeprom_init(FlashConfig * flg, epmu32 start_addr, epmu8 drift_addr, epmu16 number, epmu16 pages, epmu32 page_size, epmu8 * buffer, error_handle error);
epmu32 epm_seq_search_addr(FlashConfig * flg);
void epm_select_data(FlashConfig * flg, epmu32 start, epmu32 num);
epmu32 epm_crc32(epmu8 * data, epmu32 datalen);
epmbool check_flash(epmu8 *src, epmu32 len);
void erase_flash(epmu32 addr, epmu32 len);
void write_flash(epmu32 addr, const epmu8 * sd, epmu32 len);
void read_flash(epmu32 addr, epmu8 * sd, epmu32 len);
FERROR epm_read_data(FlashConfig *flg, epmu8 * data);
FERROR epm_write_data(FlashConfig * flg, const epmu8 * data);
void epm_select_all_data(FlashConfig * flg);
#endif
main.c
#include "stdio.h"
#include "eeprom.h"
typedef struct
{
char id;
char depth; //宽度
short cmp;
short angle;
short temp; //温度
short count; //执行次数
short width; //间隔时间
short lc ; //lc开关值
unsigned int freq;
int expand;
int outi;
int kp1;
int ki1;
int kd1;
int power;
int kp2;
int ki2;
int kd2;
int outv;
int kp3;
int ki3;
int kd3;
int y1;
int y2;
int y3;
int ts;
int xx;
}Preset;
Preset preset;
FlashConfig PresetCfg;
void preset_config_ehandle(FERROR err, unsigned char * src, unsigned int len)
{
eeplog("preset_config_ehandle error:%d\n", err);
}
void printf_preset(Preset p)
{
eeplog("freq:%d,cmp:%d,angle:%d,expand:%d,temp:%d,count:%d,width:%d,depth:%d", p.freq, p.cmp, p.angle, p.expand, p.temp, p.count, p.width, p.depth);
}
epmbool write_preset(void)
{
//printf_preset(preset);
epm_write_data(&PresetCfg, (unsigned char *)&preset);
return EPMTRUE;
}
/**
* @brief read_preset 读取预设组
* @param
* @retval bool:读取是否成功
*/
epmbool read_preset(void)
{
epm_read_data(&PresetCfg, (unsigned char * )&preset);
return EPMTRUE;
}
epmbool preset_config_init(void){
eeplog("size:%d\n", sizeof(Preset));
eeprom_init(&PresetCfg, 59*FLASH_PAGE_STEP + START_ADDRESS, sizeof(Preset), 1, 4,
FLASH_PAGE_STEP, (unsigned char * )&preset, preset_config_ehandle);
read_preset();
printf_preset(preset);
return EPMTRUE;
}
如有疑问可以评论区留言,或者联系QQ:2227273007,另外附上eeprom的源文件: