1、简介
在项目中,单片机与电脑的上位机通过串口通信,使用RS232连接时,经常会遇到比如说电脑休眠了,电脑关机了,线断开了,造成的上位机与单片机失联,但是单片机还在正常的运行程序,你的运行数据发上去结果并没有收到,也不考虑实验停止,毕竟实验品是不能这么浪费的,比如说医疗器械,总不能因为这么个问题叫患者重新采血吧。所以单片机也需要存储运行的试验数据。
但是这里有一个严重的问题,Flash存储十万次有效存储次数大多为十万次,我们有一个仪器,一般一次运行2个小时,一次运行存储45次数据(最多可达99次,最少也有30次),前几年每天运行24个小时,那么一天平均值就540次,有那么我们存储一年不到,以后的内存就不能保证了。所以延长Flash寿命是一个很大的问题。
2、磨损均衡算法
使用Flash时都知道,写入前要此页先擦除然后再写入,不然会报错,因为Flash只能将1改为0,无法直接将0改为1。如果开发者直接尝试写入数据而未擦除扇区,写入操作要么失败,要么导致数据错误。但是如果你能够保证你写入的地址区域为1,那么你不擦除也可以。其实这个肯定不用想都可以,因为ISP升级中,就是先整体擦除页,然后分多次写入的,如果一定要擦除一页才能写入那么ISP根本无法实现。
所以为了提高Flash的利用率和寿命,我们使用了磨损均衡算法,降低Flash擦写次数,提高单页的空间的利用率,动态的调节各扇区的使用率。比如动态和静态均衡策略,动态分配冷数据到高磨损块 :将高频更新的日志分配到低擦除次数的块,静态数据分配到高擦除次数的块。静态磨损 均衡:将长期未修改的数据移动到高磨损块,平衡整体损耗。然后是减少写入开销的方法,比如缓冲写入、数据压缩、差分日志,或者将日志存储在RAM中再批量写入。
另外,文件系统的选择也很重要,比如使用专为Flash设计的文件系统如YAFFS2、JFFS2或SPIFFS,它们内置了磨损均衡机制。
3、CRC校验
当你怀疑存储的寿命的时候你也需要对数据的正确性进行一定的考虑,所以CRC校验就像是给数据上了把锁。当你把数据写入Flash时,同时计算一个校验值一起存储。下次读取时重新计算校验值,如果和存储的不一致,就说明数据可能损坏了。
4、扇区管理
延长Flash寿命的关键在于不要每次写入都擦除扇区,比如说你一页2kb的扇区,你的每次写入都是0.1kb,所以可以将扇区分成20个单元
以往操作
- 你的第一次前擦除然后写入了0.1kb的数据到扇区0的地址,
- 第二次擦除,写入0.2kb的数据到扇区0的地址
- 依次往下重复。
优化后的
- 第一次写入前擦除,然后写入0.1kb的数据到扇区0的地址,
- 第二次写入0.1kb的数据到扇区1的地址
这样一页本来写满要擦除20次的数据只需要擦除1次,使用寿命一下子就可以从10万次变成200万次了
5、代码实现
这里模拟写入,一次存储的数据总字节数:ID+长度+数据+CRC
#ifndef __FLASH_WEAR_LEVELING_H
#define __FLASH_WEAR_LEVELING_H
#include "stm32f10x.h"
#include <stdint.h>
#include <stdbool.h>
// 配置区:根据实际芯片型号调整
#define FLASH_START_ADDR 0x08000000 // Flash起始地址
#define FLASH_PAGE_SIZE 0x800 // 扇区大小,STM32F103为2KB
#define FLASH_TOTAL_PAGES 128 // 总扇区数(以512KB Flash为例)
#define DATA_SECTOR_COUNT 20 // 用于磨损均衡的扇区数量
#define DATA_SECTOR_SIZE FLASH_PAGE_SIZE
#define DATA_PER_SECTOR (DATA_SECTOR_SIZE / 4) // 每扇区可存储的32位数据个数
// 存储区起始地址(避开程序区,通常放在Flash末尾)
#define STORAGE_START_ADDR (FLASH_START_ADDR + (FLASH_TOTAL_PAGES - DATA_SECTOR_COUNT) * FLASH_PAGE_SIZE)
// 错误码定义
typedef enum {
FLASH_WL_OK = 0,
FLASH_WL_ERROR,
FLASH_WL_FULL,
FLASH_WL_INVALID_ADDR,
FLASH_WL_ERASE_FAILED,
FLASH_WL_WRITE_FAILED
} FlashWL_Status;
// 数据结构体(示例)
typedef struct {
uint16_t id; // 数据ID
uint16_t len; // 数据长度(字节)
uint8_t data[]; // 可变长数据(柔性数组)
} __attribute__((packed)) DataItem;
// 函数声明
void FlashWL_Init(void);
FlashWL_Status FlashWL_Write(uint16_t id, uint8_t *data, uint16_t len);
FlashWL_Status FlashWL_Read(uint16_t id, uint8_t *buffer, uint16_t *len);
FlashWL_Status FlashWL_Format(void);
uint32_t FlashWL_GetWearCount(uint8_t sector);
#endif
java
#include "flash_wear_leveling.h"
#include <string.h>
// 私有全局变量
static uint32_t current_sector = 0; // 当前活跃扇区索引
static uint32_t write_offset = 0; // 当前扇区内的写入偏移
static uint32_t sector_wear_count[DATA_SECTOR_COUNT] = {0}; // 磨损计数
// 私有函数声明
static uint32_t GetSectorAddr(uint8_t sector_idx);
static FlashWL_Status EraseSector(uint8_t sector_idx);
static FlashWL_Status WriteToFlash(uint32_t addr, uint8_t *data, uint16_t len);
static uint32_t FindNextWriteAddr(void);
static uint32_t FindDataById(uint16_t id, uint8_t *sector_idx);
// 初始化磨损均衡模块
void FlashWL_Init(void) {
uint32_t min_wear = 0xFFFFFFFF;
uint8_t target_sector = 0;
// 扫描所有扇区,找到磨损最少的扇区作为当前活跃扇区
for (int i = 0; i < DATA_SECTOR_COUNT; i++) {
uint32_t addr = GetSectorAddr(i);
uint32_t wear_marker = *(volatile uint32_t*)(addr);
// 检查扇区是否已使用(第一个字作为磨损计数标记)
if (wear_marker != 0xFFFFFFFF) {
sector_wear_count[i] = wear_marker;
} else {
sector_wear_count[i] = 0;
}
// 查找磨损最少的扇区
if (sector_wear_count[i] < min_wear) {
min_wear = sector_wear_count[i];
target_sector = i;
}
}
current_sector = target_sector;
write_offset = FindNextWriteAddr();
}
// 写入数据
FlashWL_Status FlashWL_Write(uint16_t id, uint8_t *data, uint16_t len) {
if (len == 0 || len > 1024)
return FLASH_WL_ERROR;
uint16_t i = 0;
uint16_t val = 0;
uint32_t write_addr = 0;
uint32_t crc = 0; // 实际应用中应计算CRC
uint32_t wear_addr = GetSectorAddr(current_sector);
// 计算需要存储的总字节数(ID + 长度 + 数据 + CRC)
uint16_t total_len = sizeof(uint16_t) + sizeof(uint16_t) + len + sizeof(uint32_t);
bool isEraseSector = false;
// 检查当前扇区剩余空间
if (write_offset + total_len > DATA_SECTOR_SIZE) {
isEraseSector = true;
}else{
//判断此地址后此页是否全为0,不是的话需要擦除
for(i = write_offset; i < DATA_SECTOR_SIZE; )
{
val = *(volatile uint16_t*)(wear_addr + i);
if (val != 0xFFFF) {
isEraseSector = true;
break; // 找到空闲位置
}
i+=2;
}
}
if(isEraseSector){
current_sector = (current_sector + 1) % DATA_SECTOR_COUNT;
write_offset = 0;
// 擦除新扇区
if (EraseSector(current_sector) != FLASH_WL_OK) {
return FLASH_WL_ERASE_FAILED;
}
// 更新磨损计数
sector_wear_count[current_sector]++;
uint32_t wear_value = sector_wear_count[current_sector];
WriteToFlash(wear_addr, (uint8_t*)&wear_value, sizeof(uint32_t));
write_offset = sizeof(uint32_t); // 跳过磨损计数标记
}
// 准备写入数据
write_addr = wear_addr + write_offset;
// 写入ID
if (WriteToFlash(write_addr, (uint8_t*)&id, sizeof(id)) != FLASH_WL_OK)
return FLASH_WL_WRITE_FAILED;
write_addr += sizeof(id);
// 写入长度
if (WriteToFlash(write_addr, (uint8_t*)&len, sizeof(len)) != FLASH_WL_OK)
return FLASH_WL_WRITE_FAILED;
write_addr += sizeof(len);
// 写入数据
if (WriteToFlash(write_addr, data, len) != FLASH_WL_OK)
return FLASH_WL_WRITE_FAILED;
if(len % 2 == 1){
len++;
total_len++;
}
write_addr += len;
// 写入CRC
if (WriteToFlash(write_addr, (uint8_t*)&crc, sizeof(crc)) != FLASH_WL_OK)
return FLASH_WL_WRITE_FAILED;
write_offset += total_len;
return FLASH_WL_OK;
}
// 读取数据
FlashWL_Status FlashWL_Read(uint16_t id, uint8_t *buffer, uint16_t *len) {
uint8_t sector_idx;
uint32_t data_addr = FindDataById(id, §or_idx);
if (data_addr == 0)
return FLASH_WL_ERROR;
// 读取长度
uint16_t data_len = *(volatile uint16_t*)(data_addr);
data_addr += sizeof(uint16_t);
// 检查缓冲区大小
// if (*len < data_len) {
// *len = data_len;
// return FLASH_WL_ERROR;
// }
// 读取数据
for (int i = 0; i < data_len; i++) {
buffer[i] = *(volatile uint8_t*)(data_addr + i);
}
*len = data_len;
return FLASH_WL_OK;
}
// 格式化所有数据扇区
FlashWL_Status FlashWL_Format(void) {
for (int i = 0; i < DATA_SECTOR_COUNT; i++) {
if (EraseSector(i) != FLASH_WL_OK) {
return FLASH_WL_ERASE_FAILED;
}
sector_wear_count[i] = 0;
}
current_sector = 0;
write_offset = 0;
return FLASH_WL_OK;
}
// 获取扇区磨损计数
uint32_t FlashWL_GetWearCount(uint8_t sector) {
if (sector >= DATA_SECTOR_COUNT) return 0;
return sector_wear_count[sector];
}
// ========== 私有函数实现 ==========
// 获取扇区物理地址
static uint32_t GetSectorAddr(uint8_t sector_idx) {
return STORAGE_START_ADDR + sector_idx * DATA_SECTOR_SIZE;
}
// 擦除指定扇区
static FlashWL_Status EraseSector(uint8_t sector_idx) {
FLASH_Status status;
uint32_t sector_addr = GetSectorAddr(sector_idx);
// 解锁Flash
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
// 擦除扇区
status = FLASH_ErasePage(sector_addr);
// 上锁Flash
FLASH_Lock();
return (status == FLASH_COMPLETE) ? FLASH_WL_OK : FLASH_WL_ERASE_FAILED;
}
// 写入Flash(按半字写入)
static FlashWL_Status WriteToFlash(uint32_t addr, uint8_t *data, uint16_t len) {
FLASH_Status status;
uint16_t halfword_len = (len + 1) / 2; // 向上取整
// 解锁Flash
FLASH_Unlock();
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
// 写入数据
for (int i = 0; i < halfword_len; i++) {
uint16_t value;
if (i * 2 + 1 < len) {
value = (data[i * 2 + 1] << 8) | data[i * 2];
} else {
value = 0xFF00 | data[i * 2]; // 奇数长度时高位补0xFF
}
status = FLASH_ProgramHalfWord(addr + i * 2, value);
if (status != FLASH_COMPLETE) {
FLASH_Lock();
return FLASH_WL_WRITE_FAILED;
}
}
FLASH_Lock();
return FLASH_WL_OK;
}
// 查找当前扇区内的下一个写入地址
static uint32_t FindNextWriteAddr(void) {
uint32_t sector_addr = GetSectorAddr(current_sector);
uint32_t offset = sizeof(uint32_t); // 跳过磨损计数标记
while (offset < DATA_SECTOR_SIZE) {
uint16_t id = *(volatile uint16_t*)(sector_addr + offset);
if (id == 0xFFFF)
break; // 找到空闲位置
// 跳过当前数据项
uint16_t len = *(volatile uint16_t*)(sector_addr + offset + sizeof(uint16_t));
if(len % 2 == 1){
len++;
}
offset += sizeof(uint16_t) * 2 + len + sizeof(uint32_t);
}
return offset;
}
// 根据ID查找数据地址
static uint32_t FindDataById(uint16_t id, uint8_t *sector_idx) {
// 从当前扇区向前搜索(最新数据优先)
for (int s = 0; s < DATA_SECTOR_COUNT; s++) {
int sector = (current_sector - s + DATA_SECTOR_COUNT) % DATA_SECTOR_COUNT;
uint32_t sector_addr = GetSectorAddr(sector);
uint32_t offset = sizeof(uint32_t); // 跳过磨损计数
while (offset < DATA_SECTOR_SIZE) {
uint16_t stored_id = *(volatile uint16_t*)(sector_addr + offset);
if (stored_id == 0xFFFF)
break; // 空闲位置,停止搜索本扇区
if (stored_id == id) {
if (sector_idx)
*sector_idx = sector;
return sector_addr + offset + sizeof(uint16_t); // 返回数据长度字段地址
}
// 跳转到下一个数据项
uint16_t len = *(volatile uint16_t*)(sector_addr + offset + sizeof(uint16_t));
if(len % 2 == 1){
len++;
}
offset += sizeof(uint16_t) * 2 + len + sizeof(uint32_t);
}
}
return 0; // 未找到
}
java
#include "stm32f10x.h"
#include "flash_wear_leveling.h"
#include <stdio.h>
#include <string.h>
int main(void) {
// 示例数据
uint8_t data1[] = {0x01, 0x02};
uint8_t data2[] = "Hello world";
uint8_t read_buffer[128] = {0};
uint16_t read_len = 0;
int i = 0;
// 初始化磨损均衡模块
FlashWL_Init();
// 获取磨损计数
for (int i = 0; i < DATA_SECTOR_COUNT; i++) {
uint32_t wear = FlashWL_GetWearCount(i);
// 记录或显示磨损信息
}
while (1) {
// 主循环
for(int i = 0;i < 2000;i++){
FlashWL_Write(0x1000+i, data2, strlen((const char*)data2));
if (FlashWL_Read(0x1000+i, read_buffer, &read_len) == FLASH_WL_OK) {
// 处理读取的数据
// read_buffer 现在包含 "Hello world"
}
}
}
}
做了一个简易的Flash存储添加磨损均衡,大部分的代码和逻辑也是Deepseek生成的,我只是调试了一下,这里有一个注意的点就是Flash的写入必须是双数的寄存器地址,是不允许单数的写入的,如果你的数据携带长度,那么你的数据是必然会有单双数的,这个时候需要对数据之后的地址写入进行一个判断。然后代码设计你的id应该是唯一的。
这里大部分的功能都实现了,缺少了一个静态磨损均衡,但是这个其实也是很好实现,比如这里擦除次数到达一万次以后,就给使用扇区的地址改为其它没啥使用的扇区地址就行了,其它的都不需要改