news 2026/9/14 11:08:31

[单片机框架] [kv_sys] 实现一个简易KV键值系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[单片机框架] [kv_sys] 实现一个简易KV键值系统

文章目录

  • Env 小型KV数据库,支持 写平衡(磨损平衡) 及掉电保护模式
    • 一、功能:
    • 二、介绍
      • 1. 资源占用
      • 2. 与直接读写 Flash 的对比
      • 3. 支持平台
      • 4. 磨损平衡机制
      • 5. 性能与寿命测试
      • 6. 函数简洁
      • 7. 使用方式
    • 三、 常见错误与排查
      • 1. 禁止在中断服务中使用!

Env 小型KV数据库,支持 写平衡(磨损平衡) 及掉电保护模式

让Flash变为NoSQL(非关系型数据库)模型的小型键值(Key-Value)存储数据库。在产品上,能够更加简捷的实现 设定参数 或掉电保存的功能。

一、功能:

  1. 简易设置KEY和VAL,自动垃圾回收。至少需要占用两页FLASH空间。
  2. 平衡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 寿命。

写入新 KV

存在空白页?

写入空白页

垃圾回收: 搬运有效数据到备份区

擦除旧页并轮换

各类单片机、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返回falsekey_id为 0 或 255,超出合法范围 [1,254]检查传入的key_id宏定义,确保在 1~254 之间;可在调用前打印key_id定位
kv_set_env返回falselen超过KV_BUFF_MAX_SIZE(即BS_FLASH_KV_ONE_PAGE_BYTE - 4核对写入长度,必要时拆分存储或增大单页字节数;调试时打印lenKV_BUFF_MAX_SIZE
kv_set_env返回false上次写入未完成,kv_set_state仍为true(如被中断打断)确认未在中断中调用;若怀疑状态残留,可复位后重试,或增加超时保护
kv_get_env返回NULLkey_id为 0 或 255同上,检查key_id合法性
kv_get_env返回NULL该 KEY 从未写入,或已被kv_del_env删除先调用kv_set_env写入再读取;调试时遍历打印各 KEY 的is_enabled状态
kv_get_env返回NULLFlash 中数据校验和(sum)不匹配,数据损坏检查 Flash 驱动写入是否完整、地址是否越界;可重新写入该 KEY 覆盖旧数据
Flash 写入失败 / 数据丢失写入时 Flash 忙、电压不稳或地址越界确认bsp_flash_write_nbyte_s返回值;检查BS_KV_BASE_ADDR与页大小配置是否匹配
频繁写入后系统变慢空白页耗尽,频繁触发kv_gc_env()垃圾回收适当增大 KV 区页数;减少高频写入,或对相同数据先比较再写入(系统已内置该优化)

调试建议:在kv_set_envkv_get_env入口处打印key_idlen与返回值,结合kv_sys_t结构体中的is_enabledsum字段,可快速定位是参数问题、数据损坏还是 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键值系统(升级版)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 10:59:14

在 Windows 上安装与管理 Vector 可观测性数据管道

在 Windows 上安装与管理 Vector 可观测性数据管道 【免费下载链接】vector A high-performance observability data pipeline. 项目地址: https://gitcode.com/GitHub_Trending/vect/vector 本指南面向需要在 Microsoft Windows 环境中部署 Vector 的运维与开发人员&am…

作者头像 李华
网站建设 2026/9/14 10:58:07

企业级AI编程平台选型落地指南:安全合规与效能度量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 10:57:56

OpenGL多光源渲染实现与优化技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华