目录
在嵌入式系统中,flash存储经常用于保存配置数据或关键信息,但flash操作容易出问题,比如掉电或干扰导致数据损坏。flash的每个扇区有擦写次数限制,频繁擦写会缩短寿命,下面介绍嵌入式领域安全存储的一种方案,在实际项目中可以借鉴使用,供大家学习参考。
设计思路
- 双区备份设计:
- 主区与备份区:将Flash划分64KB做为主区,文件系统创建一个4KB的文件用来做备份区,数据存储时需同时写入两个区域,并通过CRC32校验保证一致性。
- 读写流程:
- 写入:先擦除主区,写入数据并计算CRC,再同步至备份区。
- 读取:优先读取主区数据并校验CRC,校验失败则数据无效;若主区数据不存在读取备份区并校验CRC,校验失败则数据无效。
- 磨损均衡设计:
- 扇区间均衡:主区64KB空间共16个扇区,扇区间轮换写入,避免单扇区过载
- 扇区分块:每个扇区分成16块,每个块可以保存一条用户数据。
单条数据结构
typedef struct
{
uint8_t index;
uint8_t version;
uint16_t data_len;
uint32_t crc;
uint8_t data[0];
}St_SecData;
- index,标识数据块的逻辑或物理位置,常用于磨损均衡算法中的地址映射
- version,实现多版本数据管理,支持AB分区备份与回滚机制
- data_len,动态数据长度标识,避免存储冗余。结合柔性数组
data[0]实现变长数据高效存储 - crc,数据完整性校验,防止因Flash写入异常或读取干扰导致的数据错误。校验失败时触发备份区恢复流程
- data[0],柔性数组,动态分配实际数据空间,适用于非固定长度的数据
单扇区数据块分类管理
typedef struct {
uint8_t index; // 数据块索引
uint16_t max_len; // 最大允许长度
} St_MaxDataLen;
const St_MaxDataLen SecDataInfo[MAX_SECUREDATA_COUNT + 1] = {
{0, 0},
{1, 52}, {2, 52}, ..., {8, 52}, // 索引1-8:最大52字节
{9, 100}, ..., {12, 100}, // 索引9-12:最大100字节
{13, 500}, {14, 500}, // 索引13-14:最大500字节
{15, 1000}, {16, 1000} // 索引15-16:最大1000字节
};
将不同索引的数据块按最大长度分级管理(52/100/500/1000字节),通过预定义最大长度,确保数据写入时不会溢出。
//注意4字节对齐
typedef struct
{
St_SecData datainfo1;
uint8_t data1[52];
St_SecData datainfo2;
uint8_t data2[52];
St_SecData datainfo3;
uint8_t data3[52];
St_SecData datainfo4;
uint8_t data4[52];
St_SecData datainfo5;
uint8_t data5[52];
St_SecData datainfo6;
uint8_t data6[52];
St_SecData datainfo7;
uint8_t data7[52];
St_SecData datainfo8;
uint8_t data8[52];
St_SecData datainfo9;
uint8_t data9[100];
St_SecData datainfo10;
uint8_t data10[100];
St_SecData datainfo11;
uint8_t data11[100];
St_SecData datainfo12;
uint8_t data12[100];
St_SecData datainfo13;
uint8_t data13[500];
St_SecData datainfo14;
uint8_t data14[500];
St_SecData datainfo15;
uint8_t data15[1000];
St_SecData datainfo16;
uint8_t data16[1000];
}St_4K_MAP;
- St_SecData,单条数据块的页表管理结构,存储每条数据CRC、有效标志等
- data,用来存储单条有效数据,地址紧跟在页表管理后面
关键技术实现
双区数据读写接口
写入接口
int32_t ql_securedata_store(uint8_t index, uint8_t *pdata, uint32_t len)
{
if(0 == SECUREDATA_AREA_START_ADDR)
{
return SECURE_DATA_RET_ERR_FATAL;
}
if((index < 1) || (index > MAX_SECUREDATA_COUNT) || (0 == len) || (NULL == pdata) || (false == CheckIFDatalenOK(index, len)))
{
return SECURE_DATA_RET_ERR_PARAM;
}
{
St_SecData *SecureData = NULL;
SecureData = malloc(sizeof(St_SecData) + SecDataInfo[index].max_datalen);
if(NULL == SecureData)
{
return SECURE_DATA_RET_ERR_FULL;
}
//清FF,避免无效数据损伤FLASH
memset(SecureData, 0xFF, sizeof(St_SecData) + SecDataInfo[index].max_datalen);
SecureData->index = index;
SecureData->data_len = len;
SecureData->version = SECUREDATA_VALID;
memcpy(SecureData->data, pdata, len);
SecureData->crc = crc32_calc(SecureData, sizeof(St_SecData) - sizeof(SecureData->crc));
SecureData->crc = crc32_update(SecureData->crc, SecureData->data, len);
ql_securedata_flashwrite(index, (uint8_t *)SecureData, sizeof(St_SecData) + len);
free(SecureData);
}
return SECURE_DATA_RET_OK;
}
读取接口
int32_t ql_securedata_read(uint8_t index, uint8_t* pBuffer, uint32_t len)
{
if(0 == SECUREDATA_AREA_START_ADDR)
{
return SECURE_DATA_RET_ERR_FATAL;
}
uint8_t i = 0;
int32_t ret = len;
if((index < 1) || (index > MAX_SECUREDATA_COUNT) || (0 == len) || (NULL == pBuffer))
{
SECDAT_TRACE(g_sw_SECDAT, "[SECDAT] para invalid index:%d, pBuffer:%p, len:%d", index, pBuffer, len);
return SECURE_DATA_RET_ERR_PARAM;
}
uint32_t addr = ql_securedata_flashaddr_calc(index, READ);
SECDAT_TRACE(g_sw_SECDAT, "[SECDAT] %08x", addr);
if(addr != 0)
{
St_SecData * datainfo = malloc(sizeof(St_SecData) + SecDataInfo[sizeof(SecDataInfo)/sizeof(SecDataInfo[0]) - 1].max_datalen);
hal_SpiFlashRead((uint32_t)addr, datainfo, sizeof(St_SecData) + SecDataInfo[index].max_datalen);
if(ql_securedata_crcCheck(index, (St_SecData *)datainfo))
{
if(datainfo->data_len >= len)
{
memcpy(pBuffer, datainfo->data, len);
}
else
{
memcpy(pBuffer, datainfo->data, datainfo->data_len);
ret = datainfo->data_len;
}
free(datainfo);
}
else
{
SECDAT_TRACE(g_sw_SECDAT, "[SECDAT] crc unmatch");
free(datainfo);
return SECURE_DATA_RET_ERR_ERROR;
}
}
//Flash中数据丢失,则看文件系统数据是否存在
else
{
FILE_ID hdl = FDI_fopen(SECDAT_FILENAME, "rb");
if(hdl == 0)
{
return SECURE_DATA_RET_ERR_ERROR;
}
//文件系统文件存在,判断对应index的数据是否完整,crc是否正确
else
{
uint8_t *Max_data_buffer = NULL;
Max_data_buffer = malloc(sizeof(St_SecData) + SecDataInfo[MAX_SECUREDATA_COUNT].max_datalen);
if(NULL == Max_data_buffer)
{
return SECURE_DATA_RET_ERR_FULL;
}
memset(Max_data_buffer, 0, sizeof(St_SecData) + SecDataInfo[MAX_SECUREDATA_COUNT].max_datalen);
St_SecData *secdat_tmp = (St_SecData *)Max_data_buffer;
for(i = 1; i <= MAX_SECUREDATA_COUNT; ++i)
{
//ssize_t readlen = vfs_read(hdl, secdat_tmp, sizeof(St_SecData) + SecDataInfo[i].max_datalen);
size_t readlen = FDI_fread((void *)secdat_tmp, 1, sizeof(St_SecData) + SecDataInfo[i].max_datalen, hdl);
//找到对应index的数据
if(i == index)
{
//若文件系统的数据正常则恢复,否则不做操作
if(readlen == (sizeof(St_SecData) + SecDataInfo[i].max_datalen) && ql_securedata_crcCheck(i, secdat_tmp))
{
if(secdat_tmp->data_len >= len)
{
memcpy(pBuffer, secdat_tmp->data, len);
}
else
{
memcpy(pBuffer, secdat_tmp->data, secdat_tmp->data_len);
ret = secdat_tmp->data_len;
}
ql_securedata_flashwrite(i, (uint8_t *)secdat_tmp, sizeof(St_SecData) + secdat_tmp->data_len);
}
else
{
ret = SECURE_DATA_RET_ERR_ERROR;
}
break;
}
memset(secdat_tmp, 0, sizeof(St_SecData) + SecDataInfo[i].max_datalen);
}
free(Max_data_buffer);
FDI_fclose(hdl);
}
}
return ret;
}
备份还原核心设计
- 检查备份文件是否存在
- 存在
- 检查主区有效数据CRC校验,不正确则检查备份区CRC校验,如果正确将备份文件对应数据还原到主区,若备份区数据CRC校验失败,则数据丢失。
- 不存在
- 创建新的备份文件
- 检查主区有效数据CRC校验,CRC校验通过,将对应有效数据拷贝到备份文件,否则不操作
void ql_securedata_init(void)
{
if(0 == SECUREDATA_AREA_START_ADDR){
return;
}
uint8_t i = 0;
uint32_t flashaddr = 0;
St_4K_MAP *Flash_4K = NULL;
Flash_4K = malloc(sizeof(St_4K_MAP));
if(NULL == Flash_4K)
{
return;
}
memset(Flash_4K, 0, sizeof(St_4K_MAP));
//打开文件系统中备份的文件
FILE_ID hdl = FDI_fopen(SECDAT_FILENAME, "rb");
//不存在则创建一个
if(hdl == 0)
{
uint32_t offset = 0;
hdl = FDI_fopen(SECDAT_FILENAME, (char *)"wb");
memset(Flash_4K, 0xFF, sizeof(St_4K_MAP));
for(i = 1; i <= MAX_SECUREDATA_COUNT; ++i)
{
flashaddr = ql_securedata_flashaddr_calc(i, READ);
if(flashaddr != 0)
{
St_SecData datainfo = {0};
hal_SpiFlashRead((uint32_t)flashaddr, &datainfo, sizeof(datainfo));
hal_SpiFlashRead(flashaddr, ((uint8_t *)Flash_4K) + offset, sizeof(St_SecData) + datainfo.data_len);
}
offset = offset + sizeof(St_SecData) + SecDataInfo[i].max_datalen;
}
FDI_fwrite((const void *)Flash_4K, sizeof(St_4K_MAP), 1, hdl);
}
//若已存在该文件,判断FLASH中的数据是否有损坏
else
{
St_SecData *secdat_tmp = (St_SecData *)Flash_4K;
for(i = 1; i <= MAX_SECUREDATA_COUNT; ++i)
{
size_t readlen = FDI_fread((void *)secdat_tmp, 1, sizeof(St_SecData) + SecDataInfo[i].max_datalen, hdl);
flashaddr = ql_securedata_flashaddr_calc(i, READ);
//FLASH中数据损坏则使用文件系统中的数据重新填写
if(0 == flashaddr)
{
//若文件系统的数据正常则恢复,否则不做操作
if(readlen == (sizeof(St_SecData) + SecDataInfo[i].max_datalen) && ql_securedata_crcCheck(i, secdat_tmp))
{
ql_securedata_flashwrite(i, (uint8_t *)secdat_tmp, sizeof(St_SecData) + secdat_tmp->data_len);
}
else
{
//do nothing
}
}
memset(secdat_tmp, 0, sizeof(St_4K_MAP));
}
}
FDI_fclose(hdl);
if(Flash_4K)
{
free(Flash_4K);
}
return;
}
磨损均衡算法实现
主区大小划分了64KB,有效数据只有4KB,这样设计主要是为了实现flash磨损均衡。
当向主区写数据时,例如写index为1的数据,主区16个扇区中的datainfo1初始化为一个环形链表,首先会轮训链表中的datainfo1有效标志是否置位,datainfo1有效标志均未被置位,那就向环形链表的头结点也就是扇区1的datainfo1中记录数据。若下次想更改datainfo1里面的内容时,就向这个扇区的下一个扇区的datainfo1中写入当前数据,写完后把上一个扇区中的有效标志清零。
当在写完datainfo1后清除前一个扇区的有效标志位时模块被意外掉电,有效标志清除失败,下次上电时主区datainfo1中就存在两个有效标志被置位。这里需要判断这两个扇区是否是连续的,如果是连续的两个扇区,则认为后一个扇区中的数据是有效的数据,则向这两个扇区中的后一个的下一个扇区写数据,然后把这两个有效标志清零。

如果这两个扇区不连续,则向第一个查到的扇区的下一个扇区写数据,写完把这两个不连续的扇区有效标志清零。
当向主区读数据时,正常情况是只有一个扇区中datainfo的数据有效标志被置位,CRC校验通过,把数据返回。若主区中存在两个datainfo有效标志被置位,若两个扇区是连续的,则认为后一个为最新的数据,检查数据CRC校验值,正确则把数据返回。

如果扇区不连续,则认为前一个扇区中的数据为有效数据。

这种设计每次向环形链表的下一个结点记录数据,实现了flash磨损均衡防止一个结点被反复擦写,延长flash寿命。
文章中只是部分源码,希望对你有帮助。想获取完整源码,请到个人主页资源中付费下载,博主码字不易,感谢!
923

被折叠的 条评论
为什么被折叠?



