Zephyr NVS文件系统原理及应用
Posted 17岁boy想当攻城狮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr NVS文件系统原理及应用相关的知识,希望对你有一定的参考价值。
目录
概念
Zephyr NVS是建立在Flash存储器之上的一个简易文件系统,它的目的是为了解决Flash擦除的寿命,因为Flash每次覆盖写入时都需要擦除扇区区域,NVS的作用是让用户每次覆盖写入时不会去擦除块区域,有效降低Flash的擦写寿命
实现原理
Flash特性
Flash在每次写入数据时需要将Flash按扇区(不同的Flash类型划分单位不一样)擦除,也就是将所有的存储单元置为1(0xFFFF),因为Flash的特性它只能将bit从1变为0,而不能将0变为1,所以它不能对同一块内存区域进行重复写入,每次写入之前需要对块区域进行擦除然后在写入。
这里需要提到为什么Flash不能将0变为1,首先是电路上的设计,Flash是EEPROM,注意它是ROM改良而来的,而ROM本身就是设计为不可写的,只可读的,所以它里面的总线以及电子架构都是按只读方式来的,Flash是在ROM架构之上增加的写的方式,它在内部设计的时候对每个电容增加了放电的电路,每个电容都有一个单独的电路控制,一个电容在内存单元里对应一个BIT位,所以电容越多也就意味着电路越多,放电与充电的电路是分开的。
有的人肯定会问,为什么不能设计成一个?在放电的时候不给电容充电不就可以了?
这个原因是因为电容充完电之后如果此时断开VCC电源充电,那么电容也有一定的真空期,而且Flash里面使用的是一些类似磁信号的电容是不丢失的,如果要放电是需要电容的负极产生电能来推动电容里的电子向外流通,将电容里存储的电子导出,这是需要单独设计电路的,而充电只需要正负极接好,从正极给正向电流使电子流入电容,电容是一个闭环设计,正极可以流向负极而负极也可以流向正极,当正极流如足够电子时也就是电容无法存储的情况下额外的电子就从负极流出。
如果只是接了充电的电路,没有放电的电路,如果此时想要放电,那么不充电的情况下即便是动态电容它也会隔一段时间也能让把电放完,它是有一个真空期的,不是瞬间放电,如果要瞬间放电需要从负极产生能量把里面的电子导出。
Flash里的电路设计可以看成这样:
从上图可以看到放电电路在芯片控制器里面有单独的线接在上面,而充电电路是一根线分接在所有的电容上面,也就是说如果我要充电,只需要给充电这根线一个正向电流就可以了,如果要对一个bit放电则根据解析地址总线里的地址然后找到接在电容放电端的那根线开始放电就可以了,读根放电一样都是单独的电路,只有充电不是,这一点和我们主机PC上面的DDR这一类内存不一样,这一类的比较贵,并且DDR内部还有内存寻址算法这些,相较于Flash就高端了很多,Flash的优点就是廉价,所以它基本上是用在单片机这一类微型MCU上的。
NVS实现原理
概念
nvs巧妙的利用的Flash不能往重复地址写的特性,NVS每次写入时会规定一个ID,这个ID是唯一的,当写入数据时NVS会往FLASH里写一组数据并在开头字节里做好ID标记,当再次写入时NVS会往后面的地址插入数据,它不会覆盖原始ID,也就是说第二次写入时Flash里其实有两组ID相同的数据,但是当使用Read函数去读的时候读取记录分配表里的ID信息找到有效的数据位置,NVS实则就是利用了空间来换取擦写寿命,当某个扇区写满了以后在想往里面写NVS就需要擦除这个扇区了。
特性
- 使用ID作为数据索引
- ID使用16bit(2字节)存储,所以最大只能有655336条数据
- 屏蔽Flash写入时的对齐,NVS会自动对齐
- 不需要每次重复写入时都擦除
扇区
Flash一般管理内存是会将内存划分为块、扇区、页,如:W25Q16会将2MB内存划分为:32个块、每块为16个扇区,每个扇区的大小是4096,最后每个扇区划分为16页,每页大小为256字节
而NVS则会在此基础上建立一层自己的文件系统映射:
NVS以扇区为单位,一个扇区对应Flash里至少两个以上的页,这个取决于你Flash页的大小,同时擦除写入等动作都由Zephyr调用Flash提供的功能完成。
记录分配表
NVS在存储时会向ate(记录分配表)里写入一组数据,记录了这组数据的信息,结构如下:
struct nvs_ate
uint16_t id; /* 数据的ID */
uint16_t offset; /* 数据在扇区内的偏移 */
uint16_t len; /* 数据的长度 */
uint8_t part; /* */
uint8_t crc8; /* crc8 check of the entry */
__packed;
ate在扇区中从后向前增长,而数据在扇区的开头,向后增长
当当前扇区被写满后会切换到下一个扇区继续开始写,每次读取时NVS会首先去ate里寻找最后的一个ID,即有效ID,然后取出它指向的地址与数据,NVS就是以这种方式,将新数据向后插入而不覆盖数据,这样就能避免擦除,但浪费时间与空间。
垃圾回收
同时为了避免多余的ID数据浪费,因为nvs里永远是最后一条ID是有效的,这就意味在进行多次写入的情况下会有大量的重复ID,nvs引入了垃圾回收:
触发垃圾回收的条件是最后一个C扇区里的剩余空间大于A扇区里的ID数据(不包含重复ID),当达到这一条件的时候C扇区会去A扇区里遍历ate,比如寻找到了ID1,它会去A扇区寻找,发现ID1最新一条记录就在A扇区,那么搬运到C扇区,如果A扇区没有,那么在B扇区遍历到了,则丢弃A扇区里所有ID为1的数据,遍历完成之后把A扇区清空,当C扇区写满以后开始往A扇区里写,当A扇区剩余空间大于B扇区里的ID数据(不包含重复ID)时开始重复刚刚的垃圾回收步骤,遍历B扇区里的eta,有效数据搬运到A扇区,无效则清空,当A扇区写满了以后则开始往B扇区里写,反复进行这个过程,达到清理多余的ID的目的,每次完成垃圾回收的扇区都会写入一条ate,id=gc
同样地,如果删除了一个ID,nvs会在ate里插入一条id=delete的记录信息,当nvs找到这条记录时就知道这个id是被删除的了
ATE结构
-
last ate
- 最近一次写入的ate
-
close ate
-
id=0xFFFF
-
len=0
-
offset
指向该扇区最后写入的ate -
close ate固定位于扇区的最末尾,不指向任何数据,用于标识扇区已被关闭
-
-
gc ate
-
id=0xFFFF
-
len=0
-
offset
指向gc ate自己 -
gc ate固定位于close ate前,不指向任何数据,用于标识该扇区的数据是被垃圾回收过的
-
-
delete ate
-
id
为被删除数据的id -
len=0
-
offset=0
-
delete ate用于标识某个id的数据被删除
-
通过如上状态,可以分为如下状态:
-
open sector 不含有有效close ate的扇区,表示开放扇区
-
close sector 含有有效close ate的扇区
-
write sector 当前可写入的扇区
-
empty sector 被擦除后,内容为全FF的扇区
重复写入
在写入时nvs会首先去寻找这个id最新数据,看下数据是否重复,如果重复则不写,否则则写入
NVS恢复
如果Flash之前已经被NVS初始化过一次之后,NVS会对它进行一次恢复,恢复过程如下:
- 遍历每个扇区的ate,找到open sector标识(即没有close sector eta)的扇区,并将其设置为write sector(活动写入扇区)
- 如果全部都是open sector,那么第一个扇区作为写入write sector
- 检查write sector是否为empty sector:
- 如果不是则检查是否有gc ate,如果没有gc ate则对当前扇区进行擦除,并进行垃圾回收
- 如果已经有gc ate,则往后扇区去遍历
NVS初始化
初始化就更简单了,NVS初始化时只需要告知NVS Flash页与扇区对应关系以及NVS应该放入Flash的哪个位置,即从哪个位置开始,一般首地址,注意开发者需要为这个Flash实现驱动模型
NVS API
nvs_init
函数原型
int nvs_init(struct nvs_fs *fs, const char *dev_name);
函数作用
在Flash上初始化NVS
参数介绍
fs:
nvs描述结构体
dev_name:
Flash驱动名称
返回值:
0成功,errno失败
nvs_clear
函数原型
int nvs_clear(struct nvs_fs *fs);
函数作用
清楚Flash上的NVS文件系统
参数介绍
fs:
nvs描述结构体
返回值:
0成功,errno失败
nvs_write
函数原型
ssize_t nvs_write(struct nvs_fs *fs, uint16_t id, const void *data, size_t len);
函数作用
写入数据
参数介绍
fs:
nvs描述结构体
id:
记录的ID
data:
要写入的数据
len:
数据长度
返回值:
返回写入数据长度,如果返回为0则找到了重复数据,无需写入,否则返回errno
nvs_read
函数原型
ssize_t nvs_read(struct nvs_fs *fs, uint16_t id, void *data, size_t len);
函数作用
读取数据
参数介绍
fs:
nvs描述结构体
id:
记录的ID
data:
要存放的数据的空间地址
len:
数据长度
返回值:
返回读取的数据长度,如果长度大于len则意味着数据没有读完,失败返回errno
nvs_delete
函数原型
int nvs_delete(struct nvs_fs *fs, uint16_t id);
函数作用
删除指定条目
参数介绍
fs:
nvs描述结构体
id:
记录的ID
返回值:
0表示删除成功,失败返回errno
nvs_read_hist
函数原型
ssize_t nvs_read_hist(struct nvs_fs *fs, uint16_t id, void *data, size_t len, uint16_t cnt);
函数作用
读取历史数据
参数介绍
fs:
nvs描述结构体
id:
记录的ID
data:
要存放的数据的空间地址
len:
数据长度
cnt:
0表示最近的一个数据,其它数值则表示重复次数,如这个值为3,则寻找ID三次后返回
返回值:
返回读取的数据长度,如果长度大于len则意味着数据没有读完,失败返回errno
nvs_calc_free_space
函数原型
ssize_t nvs_calc_free_space(struct nvs_fs *fs);
函数作用
计算NVS可用剩余空间
参数介绍
fs:
nvs描述结构体
返回值:
返回NVS可用空间,以字节为单位,失败返回errno
struct nvs_fs fs
结构体原型:
struct nvs_fs fs
off_t offset;
uint32_t ate_wra;
uint32_t data_wra;
uint16_t sector_size;
uint16_t sector_count;
bool ready;
struct k_mutex nvs_lock;
const struct device * flash_device;
const struct flash_parameters * flash_parameters;
;
成员介绍
成员名 | 作用 |
---|---|
offset | FLASH中的文件系统偏移量 |
ate_wra | 分配表条目写入地址。地址存储为uint32_t:高2字节对应扇区,低2字节对应扇区中的偏移量 |
data_wra | 数据写入地址 |
sector_size | Flash总共多少扇区,每个扇区的大小,必须是页面大小的倍数 |
sector_count | 将Flash里的页划分为多少个扇区 |
ready | 指示文件系统是否已初始化的标志 |
nvs_lock | 互斥锁 |
flash_device | Flash设备运行时结构 |
flash_parameters | Flash参数结构 |
使用前准备
使用前需要在prj里开启它,默认nvs是关闭的
# 启用NVS
CONFIG_NVS=y
# NVS依赖Flash,需要启用Flash驱动
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
# 当使用的flash地址空间被MPU保护时,需要配置允许对应地址空间被写入
CONFIG_MPU_ALLOW_FLASH_WRITE=y
实战
如下代码记录了每次重启与开机的记录
#include <fs/nvs.h>
#include <drivers/flash.h>
//定义ID
#define REBOOT_COUNT_ID 0
#define POWER_NORMAL_ID 1
//Flash驱动
#define FLASH_LABLE "FLASH_ESP32C3"
//nvs结构体
struct nvs_fs fs;
struct flash_pages_info info;
uint16_t reboot_cnt = 0;
bool power_normal = false;
int main()
//binbang驱动
const struct device *flash_dev = device_get_binding(FLASH_LABLE);
//获取flash信息
flash_get_page_info_by_offs(flash_dev, fs.offset, &info);
// nvs放到flash的0x250000处,nvs总计3个扇区
fs.offset = 0x250000;
fs.sector_size = info.size;
fs.sector_count = 3U;
// 初始化nvs
int rc = nvs_init(&fs, FLASH_LABLE);
if (rc)
printk("Flash Init failed\\n");
return -1;
// 读出上一次reboot count,如果存在做一次加1
rc= nvs_read(&fs, REBOOT_COUNT_ID, &reboot_cnt, sizeof(reboot_cnt));
if(rc == sizeof(reboot_cnt))
reboot_cnt++;
else
reboot_cnt = 0;
// 更新重启的次数
nvs_write(&fs, REBOOT_COUNT_ID, &reboot_cnt, sizeof(reboot_cnt));
//读出关机记录
rc= nvs_read(&fs, POWER_NORMAL_ID, &power_normal, sizeof(power_normal));
if(rc == sizeof(power_normal))
//删除关机记录
nvs_delete(&fs, POWER_NORMAL_ID);
return 0;
以上是关于Zephyr NVS文件系统原理及应用的主要内容,如果未能解决你的问题,请参考以下文章