文章目录
- Env 小型KV数据库,支持 写平衡(磨损平衡) 及掉电保护模式
- 一、功能:
- 二、介绍
- 1. 资源占用
- 2. 与直接读写 Flash 的对比
- 3. 支持平台
- 4. 磨损平衡机制
- 5. 性能与寿命测试
- 6. 函数简洁
- 7. 使用方式
- 三、 常见错误与排查
- 1. 禁止在中断服务中使用!
Env 小型KV数据库,支持 写平衡(磨损平衡) 及掉电保护模式
让Flash变为NoSQL(非关系型数据库)模型的小型键值(Key-Value)存储数据库。在产品上,能够更加简捷的实现 设定参数 或掉电保存的功能。
一、功能:
- 简易设置KEY和VAL,自动垃圾回收。至少需要占用两页FLASH空间。
- 平衡flash读写,提高flash擦写寿命
二、介绍
1. 资源占用
Code (inc. data) | RO Data | RW Data | ZI Data | Debug | Object Name| |--|--|--|--|--|--| | 498 | 16 | 0 | 0 | 33 | 3252 | kv_sys.o| RAM 16 字节 ROM 约500字节2. 与直接读写 Flash 的对比
| 指标 | 直接读写 Flash | 本 KV 系统 |
|---|---|---|
| 擦写次数 | 每次更新都擦写同一页,寿命短 | 页轮换 + 垃圾回收,各页擦写均匀,寿命长 |
| RAM 占用 | 几乎为 0 | 约 16 字节 |
| ROM 占用 | 几乎为 0 | 约 500 字节 |
| 写入耗时 | 快(直接擦写) | 略慢(需查空白页,满时触发 GC) |
| 数据管理 | 需自行维护地址与有效性 | 按 KEY 自动管理,支持删除与校验 |
简要说明:直接读写 Flash 虽然省资源、速度快,但每次更新都擦写同一页,会迅速耗尽该页寿命;本 KV 系统以约 16 字节 RAM 和 500 字节 ROM 的代价,换来磨损平衡与自动垃圾回收,显著延长 Flash 使用寿命,适合频繁保存参数的场景。
3. 支持平台
4. 磨损平衡机制
Flash 的擦写寿命有限,若总是写同一页会很快损坏。本系统通过「垃圾回收 + 页轮换」实现磨损平衡:写入时优先找空白页,写满后触发kv_gc_env()垃圾回收,把有效数据搬运到备份区,再擦除旧页并轮换使用,使各页擦写次数趋于均匀,从而延长 Flash 寿命。
各类单片机、MCU,通用性强5. 性能与寿命测试
以常见的 SPI NOR Flash 型号 W25Q64(容量 8MB,页大小 256 字节,扇区 4KB,擦写寿命约 10 万次)为例,对比直接读写与本 KV 系统的寿命与性能表现。
| 指标 | 直接读写 Flash | 本 KV 系统 |
|---|---|---|
| 单次写入耗时 | 约 0.5~3 ms(页编程) | 约 0.5~3 ms(页编程)+ 查空白页开销(微秒级) |
| 触发垃圾回收时单次写入耗时 | 无 | 约 60~120 ms(含擦除 4KB 扇区 + 搬运有效数据) |
| 垃圾回收触发频率 | 无 | 与 KV 区页数成反比,页数越多触发越少 |
| 擦写寿命(单页) | 约 10 万次即报废 | 页轮换后各页均匀磨损,整体寿命可提升数倍至数十倍 |
| 典型寿命估算(每秒写 1 次) | 约 27 小时耗尽单页 | 按 8 页轮换估算,约 9 天以上 |
估算说明:W25Q64 单页擦写寿命约 10 万次。直接读写若固定写同一页,每秒写 1 次约 27 小时即耗尽该页;本 KV 系统通过页轮换把写入分散到多页,假设 KV 区配置 8 页,则等效寿命约为单页的 8 倍,且垃圾回收仅在空白页耗尽时触发,频率远低于每次写入,整体寿命显著延长。
6. 函数简洁
void *kv_get_env(uint8_t key_id);
bool kv_del_env(uint8_t key_id);
bool kv_set_env(uint8_t key_id, void *data, uint8_t len);
7. 使用方式
// 存入kv_set_env(BS_KV_KEY_TEMP_INFO,"ABCDAD",strlen("ABCDAD"));// 读取uint8_t*p;uint8_tbuff[10];p=kv_get_env(BS_KV_KEY_TEMP_INFO);if(p!=NULL){memcpy(buff,p,10);}三、 常见错误与排查
| 现象 | 可能原因 | 解决方法与调试建议 |
|---|---|---|
kv_set_env返回false | key_id为 0 或 255,超出合法范围 [1,254] | 检查传入的key_id宏定义,确保在 1~254 之间;可在调用前打印key_id定位 |
kv_set_env返回false | len超过KV_BUFF_MAX_SIZE(即BS_FLASH_KV_ONE_PAGE_BYTE - 4) | 核对写入长度,必要时拆分存储或增大单页字节数;调试时打印len与KV_BUFF_MAX_SIZE |
kv_set_env返回false | 上次写入未完成,kv_set_state仍为true(如被中断打断) | 确认未在中断中调用;若怀疑状态残留,可复位后重试,或增加超时保护 |
kv_get_env返回NULL | key_id为 0 或 255 | 同上,检查key_id合法性 |
kv_get_env返回NULL | 该 KEY 从未写入,或已被kv_del_env删除 | 先调用kv_set_env写入再读取;调试时遍历打印各 KEY 的is_enabled状态 |
kv_get_env返回NULL | Flash 中数据校验和(sum)不匹配,数据损坏 | 检查 Flash 驱动写入是否完整、地址是否越界;可重新写入该 KEY 覆盖旧数据 |
| Flash 写入失败 / 数据丢失 | 写入时 Flash 忙、电压不稳或地址越界 | 确认bsp_flash_write_nbyte_s返回值;检查BS_KV_BASE_ADDR与页大小配置是否匹配 |
| 频繁写入后系统变慢 | 空白页耗尽,频繁触发kv_gc_env()垃圾回收 | 适当增大 KV 区页数;减少高频写入,或对相同数据先比较再写入(系统已内置该优化) |
调试建议:在kv_set_env与kv_get_env入口处打印key_id、len与返回值,结合kv_sys_t结构体中的is_enabled与sum字段,可快速定位是参数问题、数据损坏还是 Flash 驱动问题。
1. 禁止在中断服务中使用!
/******************************************************************************** * @file kv_sys.c * @author jianqiang.xue * @version V1.0.0 * @date 2021-11-03 * @brief KV键值最小系统 ********************************************************************************//* Includes ------------------------------------------------------------------*/#include<stdint.h>#include<stdio.h>#include<string.h>#include<stdbool.h>#include"bsp_flash.h"#include"log.h"/* Private Includes ----------------------------------------------------------*/#include"kv_sys.h"#include"business_function.h"/* Private Define ------------------------------------------------------------*/// KV系统总共可以使用N字节#defineKV_SUM_SIZE(BS_FLASH_PAGE_SIZE*(BS_FLASH_KV_PAGE-1))// KV系统总共使用键值数量#defineKV_SUM_NUM(KV_SUM_SIZE/BS_FLASH_KV_ONE_PAGE_BYTE)// KV系统备份区使用键值数量#defineKV_BACK_SUM_NUM(BS_FLASH_PAGE_SIZE/BS_FLASH_KV_ONE_PAGE_BYTE)// KV系统中buff最大长度值#defineKV_BUFF_MAX_SIZE(BS_FLASH_KV_ONE_PAGE_BYTE-4)#ifBOOT_SUPPORT#include"os_api.h"#definedelayos_delay#else#include"main.h"#definedelaydelay_ms#endifkv_sys_tkv_sys_temp={0};bool kv_set_state=false;// flash--free true--bus/* Private Function Prototypes -----------------------------------------------*/staticuint8_tcompute_checksum(uint8_t*data,uint8_tlen){uint16_tsum=0;for(uint8_ti=0;i<len;i++){sum+=*(data+i);}return(uint8_t)(sum&0x00FF);}staticvoid*find_kv_addr(uint8_tkey_id){kv_sys_t*kv;uint8_tsum=0;for(uint8_ti=0;i<KV_SUM_NUM;i++){kv=(kv_sys_t*)(BS_KV_BASE_ADDR+BS_FLASH_KV_ONE_PAGE_BYTE*i);if(kv->key_id!=key_id){continue;}if(kv->is_enabled!=0xFF){continue;}sum=compute_checksum((uint8_t*)kv,sizeof(kv_sys_t)-1);if(kv->sum==sum){return(void*)kv;}}returnNULL;}staticvoid*find_blank_addr(void){kv_sys_t*kv;for(uint8_ti=0;i<KV_SUM_NUM;i++){kv=(kv_sys_t*)(BS_KV_BASE_ADDR+BS_FLASH_KV_ONE_PAGE_BYTE*i);if(kv->key_id==0xFF&&kv->is_enabled==0xFF){return(void*)kv;}}returnNULL;}/* Public Function Prototypes ------------------------------------------------*/voidkv_gc_env(void){bsp_flash_erase_page(BS_KV_BACK_ADDR,1);delay(50);kv_sys_t*kv;uint8_tsum=0;uint8_tkv_page_tick=0;uint8_tback_tick=0;// KV_BACK_SUM_NUMfor(uint8_ti=0;i<KV_SUM_NUM;i++){kv=(kv_sys_t*)(BS_KV_BASE_ADDR+BS_FLASH_KV_ONE_PAGE_BYTE*i);if(kv->is_enabled!=0xFF){continue;}// 判断数据的完整性sum=compute_checksum((uint8_t*)kv,sizeof(kv_sys_t)-1);if(kv->sum!=sum){continue;}// 搬运有效数据bsp_flash_write_nbyte_s(BS_KV_BACK_ADDR+back_tick*BS_FLASH_KV_ONE_PAGE_BYTE,(uint8_t*)kv,sizeof(kv_sys_t));back_tick++;if(back_tick==KV_BACK_SUM_NUM){bsp_flash_carry(BS_KV_BASE_ADDR+kv_page_tick*BS_FLASH_PAGE_SIZE,BS_KV_BACK_ADDR,BS_FLASH_PAGE_SIZE);kv_page_tick++;back_tick=0;}}if(back_tick!=0){bsp_flash_carry(BS_KV_BASE_ADDR+kv_page_tick*BS_FLASH_PAGE_SIZE,BS_KV_BACK_ADDR,BS_FLASH_PAGE_SIZE);kv_page_tick++;back_tick=0;}// 清理未使用的空间for(uint8_ti=kv_page_tick;i<BS_FLASH_KV_PAGE-1;i++){bsp_flash_erase_page(BS_KV_BASE_ADDR+kv_page_tick*BS_FLASH_PAGE_SIZE,1);}delay(50);}/** * @brief 从FLASH中获取KV值 * @param key_id: KEY ID * @retval 数据指针 */void*kv_get_env(uint8_tkey_id){if(key_id==0||key_id==255){returnNULL;}kv_sys_t*kv=(kv_sys_t*)find_kv_addr(key_id);if(kv!=NULL){returnkv->buff;}returnNULL;}boolkv_del_env(uint8_tkey_id){bool state;kv_sys_t*kv=(kv_sys_t*)find_kv_addr(key_id);uint32_ttemp_addr=(uint32_t)kv+(uint8_t)BS_FLASH_KV_ONE_PAGE_BYTE-2;if(kv!=NULL){// 将之前值标记为无效state=bsp_flash_write_byte(temp_addr,0x00);}returnstate;}/** * @brief KV值写入Flash * @param key_id: KEY ID * @param *data: 数组指针 * @param len: 数据长度 */boolkv_set_env(uint8_tkey_id,void*data,uint8_tlen){// 检测ID是否异常if(key_id==0||key_id==255){returnfalse;}// 检测参数和当前状态是否异常if((len>KV_BUFF_MAX_SIZE)||(kv_set_state)||(KV_SUM_SIZE==0)){returnfalse;}kv_set_state=true;// 检测KEY_ID是否存在kv_sys_t*kv=NULL;// 判断数据是否相同uint8_t*old=kv_get_env(key_id);if(old!=NULL){if(memcmp(data,old,len)==0){// 数据一致,直接返回kv_set_state=false;returntrue;}}kv_del_env(key_id);// 得到空白块kv=find_blank_addr();// 如果数据满了,则进行垃圾回收处理if(kv==NULL){kv_gc_env();kv=find_blank_addr();}// 填充数据kv_sys_temp.key_id=key_id;memset(kv_sys_temp.buff,0,KV_BUFF_MAX_SIZE);memcpy(kv_sys_temp.buff,data,len);kv_sys_temp.len=len;kv_sys_temp.is_enabled=0xFF;kv_sys_temp.sum=compute_checksum((uint8_t*)&kv_sys_temp,sizeof(kv_sys_t)-1);bsp_flash_write_nbyte_s((uint32_t)kv,(uint8_t*)&kv_sys_temp,sizeof(kv_sys_t));kv_set_state=false;returntrue;}/******************************************************************************** * @file kv_sys.h * @author jianqiang.xue * @version V1.0.0 * @date 2021-11-03 * @brief KV键值系统 ********************************************************************************/#ifndef__KV_SYS_H__#define__KV_SYS_H__/* Includes ------------------------------------------------------------------*/#include<stdint.h>#include<stdbool.h>#include"business_function.h"/* Public Struct -------------------------------------------------------------*/typedefstruct{uint8_tkey_id;// KEY ID [1,254] 0和255不能使用uint8_tbuff[BS_FLASH_KV_ONE_PAGE_BYTE-4];// 实际数据uint8_tlen;// 实际长度uint8_tis_enabled;// 是否有效 0--无效 FF--有效uint8_tsum;// 校验和}kv_sys_t;/* Public Function Prototypes -----------------------------------------------*/void*kv_get_env(uint8_tkey_id);boolkv_del_env(uint8_tkey_id);boolkv_set_env(uint8_tkey_id,void*data,uint8_tlen);#endif[单片机框架] [kv_sys] 实现一个简易KV键值系统
[单片机框架] [kv_sys] 实现一个简易KV键值系统(升级版)