Zephyr Settings 子系统 使用说明

Settings 子系统提供持久性存储配置和运行时状态的方法。开发人员能够根据实际状况灵活地选择后端将配置和状态存储在指定的永久性存储器上。简单的说就是 Setting 子系统可以用来存储诸音量,时间长度等数据,并在重开机后也有效。具体的存储位置可以由开发者指定 Setting 的后端。Settings 支持 FCB、NVS 和文件后端,开发人员也可以注册自己实现的 Setting 后端。

基本概念 链接到标题

Setting 系统在逻辑上使用 key-value 结构,key 是字符串,value 是由用户决定的任意类型数据。例如 “vol”:36, key 是"vol", vaule 是 36。 通常情况下 key 可以按照 package/subtree 的逻辑组织。例如:

"id/serial"
"id/serial/baudrate"
"id/serial/hwflow"
"id/spi"
"id/spi/speed"

id 为包名,下面由 serial 和 spi 两个 subtree

使用方法 链接到标题

初始化 链接到标题

使用 Setting 子系统前,需要先调用 settings_subsys_init() 进行子系统初始化,该 API 允许多次调用,内部会进行判断是否重复初始化。

读写 链接到标题

Setting 子系统在设计理念上可以将 package/subtree 作为整体进行操作。在进行读写前需要先对 package/subtree 进行 callback 的注册, 通过 callback 来完成subtree的读写。

读写 handler 链接到标题

静态注册 handler 使用下列宏,在编译期就决定了 package/subtree 的 callback

# define SETTINGS_STATIC_HANDLER_DEFINE(_hname, _tree, _get, _set, _commit, _export )

动态注册 handler 使用下面函数,在运行时通过下面函数指定 package/subtree 的 callback

struct settings_handler {

    const char *name;
    int(*h_get )(const char *key, char *val, int val_len_max);
    int(*h_set )(const char *key, size_t len, settings_read_cb read_cb, void *cb_arg);
    int(*h_commit )(void);
    int(*h_export )(int(*export_func )(const char *name, const void *val, size_t val_len )) ;

    sys_snode_t node;
};

int settings_register(struct settings_handler *cf);

需要特别注意的是 settings_register 的使用者需要分配并保留 cf 空间,该空间的生命周期在settings使用过程都要有效。

静态和动态注册 callback 的参数有如下对应关系,后面就以 struct settings_handler 进行参数细节说明

  • _hname: 静态 callback handle 名称
  • _treename
  • _get: h_get
  • _set: h_set 将 value 设置给应用
  • _commit: h_commit
  • _export: h_export

const char *name; package/subtree 的名称,表示这个 handler 是处理这个子树的,例如可以是"id/serial"

int(*h_get )(const char *key, char *val, int val_len_max);

Settings 通过 h_get 从应用获取 namekey 的值,将应用的值保存在 val 指向的内存中,val 最大允许保存 val_len_max 个字节。

int(*h_set )(const char *key, size_t len, settings_read_cb read_cb, void *cb_arg);

Settings 调用 h_set,通知 namekey 的数据长度为 len,并提供后端读取函数 read_cbnamekey 的值读取到应用内存中

int(*h_commit )(void);

Settings 调用 h_commit 通知应用 namekey 的数据已经通过 h_set 全部读取到应用内存中

int(*h_export )(int(*export_func )(const char *name, const void *val, size_t val_len )) ;

Settings 调用 h_export 将所有应用内存中的值通过 cb 保存到非易失存储器上。

下面是注册 handler 的示例

int spi_handle_set(const char *name, size_t len, settings_read_cb read_cb,
          void *cb_arg);
int spi_handle_commit(void);
int spi_handle_export(int(*cb )(const char *name,
                   const void *value, size_t val_len )) ;

int serial_handle_set(const char *name, size_t len, settings_read_cb read_cb,
          void *cb_arg);
int serial_handle_commit(void);
int serial_handle_export(int(*cb )(const char *name,
                   const void *value, size_t val_len )) ;
int serial_handle_get(const char *name, char *val, int val_len_max);

SETTINGS_STATIC_HANDLER_DEFINE(serial, "id/serial", serial_handle_get,
                   serial_handle_set, serial_handle_commit,
                   serial_handle_export);

struct settings_handler spi_handler = {
        .name = "id/spi",
        .h_get = NULL,
        .h_set = spi_handle_set,
        .h_commit = spi_handle_commit,
        .h_export = spi_handle_export
};

settings_register(&spi_handler);

SETTINGS_STATIC_HANDLER_DEFINE 用于静态注册 handler,当注册多个 handler 有相同的 _hname 时,编译会报错。 settings_register 用于运行时动态注册 handler,当注册 handler 的 name 已经存在时,会注册失败。Zephyr 默认 CONFIG_SETTINGS_DYNAMIC_HANDLERS=y 支持动态注册的 hander。

对于 handle 里面的 callback,允许设置为 NULL。h_geth_set 的参数 key 是跳过 name 的字符串,例如当处理"id/spi/speed"时 spi_handle_set 得到的 key 就是"speed"。

写入 链接到标题

Setting 写入 key-value,是将 Settings 用户持有的数据写入到非易失存储器中。可以写指定 key 和所有 key。 写指定 key 使用下方 API:

//将 key 为 name 的值更改为 vaule,长度为 val_len
int settings_save_one(const char *name, const void *value, size_t val_len);

例如

settings_save_one("id/serial/baudrate", &baudrate_val, sizeof(baudrate_val )) ;

Setting 也可以使用下方 API,对所有 package/subtree 进行写入

int settings_save(void);

settings_save() 是对静态和动态的 handler 进行遍历,调用 h_export 来完成,下面是 h_export 的实现示例,可以看到每个 h_export 都使用 Settings 提供的 cb,将自己 subtree 下的成员值写入

int spi_handle_export(int(*cb )(const char *name,
                   const void *value, size_t val_len ))
{
    //应用程序中 id/spi/speed 的值被存储在变量 speed_val 中,当执行 h_export 时,使用 cb 将 speed_val 写入到非易失存储器中
    printk("export keys under <spi> handler\n");
(void)cb("id/spi/speed", &speed_val, sizeof(speed_val )) ;

    return 0;
}

int serial_handle_export(int(*cb )(const char *name,
                   const void *value, size_t val_len ))
{
    printk("export keys under <serial> handler\n");
    //应用程序中 id/serial/baudrate 和 id/serial/hwflow 的值分别被存储在变量 baudrate_val 和 hwflow_val 中,当执行 h_export 时,使用 cb 将 baudrate_val 和 hwflow_val 写入到非易失存储器中
(void)cb("id/serial/baudrate", &baudrate_val, sizeof(baudrate_val )) ;
(void)cb("id/serial/hwflow", &hwflow_val, sizeof(hwflow_val )) ;

    return 0;
}

读出 链接到标题

Setting 读出 key-value,是指将数据从非易失存储器读出到 Settings 用户持有的内存中。读出通过下面 API 可以一次性读出所有的数据。

int settings_load(void);

也可以通过下面 API 读出指定 package/subtree 的数据

int settings_load_subtree(const char *subtree);

例如

//读出 id/serial 下的所有数据,也就是 baudrate 和 hwflow
settings_load_subtree("id/serial");

//读出 id/spi/speed 的数据
settings_load_subtree("id/spi/speed");

读出数据的方式是非显示的,Settings 内部最终都是通过 h_set 来完成,当 h_set 将所有数据加载完后就会通过 h_commit 通知应用。因此至少要实现 h_seth_commit 可以按需求实现和注册。

//serial_handle_set 是"id/serial" subtree 的处理 handler callback,这里 name 只会送入 id/serial 下的成员,也就是 baudrate 和 hwflow
int serial_handle_set(const char *name, size_t len, settings_read_cb read_cb,
          void *cb_arg )
{
    const char *next;
    size_t name_len;
    int rc;

    name_len = settings_name_next(name, &next);
    //检查 baudrate 是否还有下一级成员,没有才能做动作。这是为了避免 baudrate 还有下一级,导致误访问,例如 `id/serial/baudrate/high`
    if(!next){
        //确认是 baudrate,通过 setting 提供的 read_cb 将数据从非易失性存储中读到 baudrate_val 内
        if(!strncmp(name, "baudrate", name_len )) {
            rc = read_cb(cb_arg, &baudrate_val, sizeof(baudrate_val )) ;
            if(rc < 0){
                return rc;
 } else if(rc > 0){
                printk("<id/serial/baudrate> = %u\n", baudrate_val);
            }
            return 0;
        }

        //确认是 hwflow,通过 setting 提供的 read_cb 将数据从非易失性存储中读到 hwflow_val 内
        if(!strncmp(name, "hwflow", name_len )) {
            rc = read_cb(cb_arg, &hwflow_val, sizeof(hwflow_val )) ;
            if(rc < 0){
                return rc;
 } else if(rc > 0){
                printk("<id/serial/hwflow> = %d\n", hwflow_val);
            }
            return 0;
        }
    }

    return -ENOENT;
}

//spi_handle_set"id/spi" subtree 的处理 handler callback,这里 name 只会送入 id/spi 下的成员,也就是 speed
int spi_handle_set(const char *name, size_t len, settings_read_cb read_cb,
          void *cb_arg )
{
    const char *next;
    size_t next_len;
    int rc;

    //检查 speed 是否还有下一级成员,没有才能做动作。这是为了避免 speed 还有下一级,导致误访问,例如 `id/serial/speed/high`
    if(settings_name_steq(name, "speed", &next)&& !next){
        if(len != sizeof(speed_val )) {
            return -EINVAL;
        }

        //通过 setting 提供的 read_cb 将数据从非易失性存储中读到 speed_val 内
        rc = read_cb(cb_arg, &speed_val, sizeof(speed_val )) ;
        printk("<id/spi/speed> = %u\n", speed_val);
        return 0;
    }

    return -ENOENT;
}

//在 h_set 加载完 `id/serial` 下所有的数据后,会调用 serial_handle_commit
int serial_handle_commit(void )
{
    printk("loading all settings under <serial> handler is done\n");
    return 0;
}

//在 h_set 加载完 `id/spi` 下所有的数据后,会调用 spi_handle_commit
int spi_handle_commit(void )
{
    printk("loading all settings under <spi> handler is done\n");
    return 0;
}

另外还有一种就是调用 settings_load_subtree_direct 来完成

int settings_load_subtree_direct (
    const char             *subtree,
    settings_load_direct_cb cb,
    void                   *param);

settings_load_subtree_direct 不依赖 handler,会通过 cbsubtree 数据从非易失存储器读取数据放到应用程序提供的变量中,可以做到显示的获取数据,该 API 获取数据完成后也不会去调用 h_commit

typedef int(*settings_load_direct_cb)(
    const char      *key,    //跳过 settings_load_subtree_direct 指定的 subtree
    size_t           len,    //非易失存储器中数据的长度
    settings_read_cb read_cb,    //Setting 提供的 read_cb 用于从非易失存储器读取数据
    void            *cb_arg,    //cb_arg,是由 settings 送给 read_cb
    void            *param);    //param 是用户参数

示例如下:

static int serial_baudrate_loader(const char *name, size_t len, settings_read_cb read_cb,
              void *cb_arg, void *param )
{
    const char *next;
    int rc;

    printk("direct load id/serial/baudrate: ");

    settings_name_next(name, &next);

    if(!next){
        rc = read_cb(cb_arg, param, sizeof(uint32_t )) ;
        printk("<id/serial/baudrate> %u\n", * (( uint32_t*)param )) ;
        return 0;
    }

    return -ENOENT;
}

//读取 id/serial/baudrate
settings_load_subtree_direct("id/serial/baudrate", serial_baudrate_loader, &baudrate_val);

删除 链接到标题

通过 int settings_delete(const char *name); 可以将 key-value 删除,示例如下

int settings_delete("id/serial/baudrate");

注意 settings_delete 只会删除最终的 package/subtree,不能做到删 package,例如做 settings_delete("id/serial" )"id/serial/baudrate""id/serial/hwflow" 并不会被删除。

通知 链接到标题

除了 settings_load() 完成后会调用 h_commit 进行通知外,Setting 也提供下面两个 API 来通知

//会遍历调用所有 handler 下的 `h_commit`
int settings_commit(void);

//调用 subtree 对应 handler 的 `h_commit`
int settings_commit_subtree(const char *subtree);

运行时 API 链接到标题

CONFIG_SETTINGS_RUNTIME=y 时 Setting 支持下面几个 API,用于操作指定 subtree

//将 name 的值设置为 data,长度为 len,实际是调用 name 对应的 h_set 完成
int settings_runtime_set(const char *name, const void *data, size_t len);

//获取 name 的值设置放到 data,长度为 len,实际是调用 name 对应的 h_get 完成
int settings_runtime_get(const char *name, void *data, size_t len);

//对 name 进行提交,实际调用的是 h_commit
int settings_runtime_commit(const char *name);

后端 链接到标题

Zephyr 默认支持三个后端

  • CONFIG_SETTINGS_FCB Flash 循环缓冲区
  • CONFIG_SETTINGS_FILE 文件
  • CONFIG_SETTINGS_NVS NVS Zephyr 的默认支持的三个后端,只能选择一个使用。

配置文件 链接到标题

使用 FCB 的配置 链接到标题

CONFIG_SETTINGS=y
CONFIG_SETTINGS_FCB=y

使用 NVS 的配置 链接到标题

CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y

使用文件的配置 链接到标题

CONFIG_SETTINGS_FILE_PATH 用于指定使用哪个文件保存 settings 的数据

CONFIG_SETTINGS=y
CONFIG_SETTINGS_FILE=y
CONFIG_SETTINGS_FILE_PATH="/lfs/settings"

设备树 链接到标题

对于 FCB 和 NVS 的后端会自动去找带有标签「storage」的固定分区,例如写成下面这样会被 Setting 的 NVS 和 FCB 后端自动作为存储分区

        storage_partition: partition@250000 {
            label = "storage";
            reg = <0x00250000 0x00026000>;
        };

也可以通过 zephyr,settings-partition 显示的指定使用哪个分区作为 settings 的存储分区,例如

/ {
    chosen {
        zephyr,settings-partition = &storage_partition;
    };
};

Shell 链接到标题

我们在使用 settings 子系统时会写入若干个 key-value 对,为了方便调试 Zephyr 提供一组专用的 settings shell 命令对 settings 进行操作

  • settings list: 列出所有的 key-value 对
  • settings read: 读取指定 key 的 value
  • settings write: 写入 key-value 对
  • settings delete: 删除 key-value 对

下图展示 settings shell 命令的使用方法

alt text

参考 链接到标题

https://docs.zephyrproject.org/latest/services/settings/index.html