Zephyr Settings Subsystem User Guide

The Settings subsystem provides methods for persistent storage configuration and runtime status. Developers can flexibly choose the backend to store configuration and status in specified permanent storage based on actual conditions. In simple terms, the Settings subsystem can be used to store data such as volume and duration, which remain effective even after rebooting. The specific storage location can be specified by the developer in the Settings backend. Settings support FCB, NVS, and file backends, and developers can also register their own implemented Settings backend.

Basic Concepts

The Settings system logically uses a key-value structure, where the key is a string and the value is any type of data determined by the user. For example, “vol”:36, where the key is “vol” and the value is 36. Generally, keys can be organized according to the package/subtree logic. For example:

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

Here, “id” is the package name, with “serial” and “spi” as two subtrees.

Usage

Initialization

Before using the Settings subsystem, you need to call settings_subsys_init() to initialize the subsystem. This API allows multiple calls, and it will internally check for repeated initialization.

Read and Write

The Settings subsystem is designed to operate on the package/subtree as a whole. Before performing read and write operations, you need to register a callback for the package/subtree, which will be used to complete the read and write for the subtree.

Read and Write Handler

Static registration of handlers uses the following macro, which determines the callback for the package/subtree at compile time:

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

Dynamic registration of handlers uses the following function, which specifies the callback for the package/subtree at runtime:

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);

It is important to note that the user of settings_register needs to allocate and retain space for cf, and this space must remain valid throughout the usage of settings.

The parameters for static and dynamic registration of callbacks have the following correspondence, which will be detailed later using struct settings_handler:

  • _hname: Static callback handle name
  • _tree: name
  • _get: h_get
  • _set: h_set sets the value for the application
  • _commit: h_commit
  • _export: h_export

const char *name; is the name of the package/subtree, indicating that this handler processes this subtree, for example, it can be “id/serial”.

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

The Settings subsystem uses h_get to obtain the value of key under name from the application, storing the application’s value in the memory pointed to by val, which can hold a maximum of val_len_max bytes.

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

The Settings subsystem calls h_set to notify that the data length for key under name is len, providing the backend read function read_cb to read the value of key under name into application memory.

int(*h_commit )(void);

The Settings subsystem calls h_commit to notify the application that the data for key under name has been fully read into application memory via h_set.

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

The Settings subsystem calls h_export to save all values in application memory to non-volatile storage using cb.

Below is an example of registering a 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 is used for static registration of handlers. If multiple handlers are registered with the same _hname, a compilation error will occur. settings_register is used for dynamic registration of handlers at runtime. If a handler with the same name is already registered, it will fail to register. Zephyr defaults to CONFIG_SETTINGS_DYNAMIC_HANDLERS=y, supporting dynamically registered handlers.

For callbacks in the handler, NULL is allowed to be set. The parameters key for h_get and h_set skip the name string, for example, when processing “id/spi/speed”, the key received by spi_handle_set will be “speed”.

Writing

Setting writes key-value, which means writing data held by the Settings user into non-volatile storage. You can write a specified key or all keys. To write a specified key, use the following API:

// Change the value of key "name" to "value", with length "val_len"
int settings_save_one(const char *name, const void *value, size_t val_len);

For example:

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

The Settings subsystem can also use the following API to write to all package/subtrees:

int settings_save(void);

settings_save() traverses both static and dynamic handlers, calling h_export to complete the operation. Below is an implementation example of h_export, which shows that each h_export uses the cb provided by Settings to write the values of its subtree members:

int spi_handle_export(int(*cb )(const char *name,
                   const void *value, size_t val_len ))
{
    // In the application, the value of "id/spi/speed" is stored in the variable "speed_val". When executing h_export, use cb to write "speed_val" to non-volatile storage.
    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");
    // In the application, the values of "id/serial/baudrate" and "id/serial/hwflow" are stored in the variables "baudrate_val" and "hwflow_val" respectively. When executing h_export, use cb to write "baudrate_val" and "hwflow_val" to non-volatile storage.
(void)cb("id/serial/baudrate", &baudrate_val, sizeof(baudrate_val));
(void)cb("id/serial/hwflow", &hwflow_val, sizeof(hwflow_val));

    return 0;
}

Reading

Setting reads key-value, which refers to reading data from non-volatile storage into memory held by the Settings user. You can read all data at once through the following API:

int settings_load(void);

You can also read data from a specified package/subtree using the following API:

int settings_load_subtree(const char *subtree);

For example:

// Read all data under "id/serial", which includes "baudrate" and "hwflow"
settings_load_subtree("id/serial");

// Read data for "id/spi/speed"
settings_load_subtree("id/spi/speed");

The data reading method is non-display, and internally, Settings ultimately completes the operation through h_set. When h_set has loaded all data, it will notify the application through h_commit. Therefore, at least h_set must be implemented, while h_commit can be implemented and registered as needed.

// serial_handle_set is the processing handler callback for the "id/serial" subtree. Here, "name" will only be passed the members under "id/serial", which are "baudrate" and "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);
    // Check if "baudrate" has any next-level members. If not, we can proceed. This avoids accessing "baudrate" with a next level, such as "id/serial/baudrate/high".
    if(!next){
        // Confirm it is "baudrate". Use the read_cb provided by settings to read data from non-volatile storage into "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;
        }

        // Confirm it is "hwflow". Use the read_cb provided by settings to read data from non-volatile storage into "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 is the processing handler callback for the "id/spi" subtree. Here, "name" will only be passed the members under "id/spi", which is "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;

    // Check if "speed" has any next-level members. If not, we can proceed. This avoids accessing "speed" with a next level, such as "id/serial/speed/high".
    if(settings_name_steq(name, "speed", &next) && !next){
        if(len != sizeof(speed_val)) {
            return -EINVAL;
        }

        // Use the read_cb provided by settings to read data from non-volatile storage into "speed_val".
        rc = read_cb(cb_arg, &speed_val, sizeof(speed_val));
        printk("<id/spi/speed> = %u\n", speed_val);
        return 0;
    }

    return -ENOENT;
}

// After h_set has loaded all data under "id/serial", serial_handle_commit will be called.
int serial_handle_commit(void )
{
    printk("loading all settings under <serial> handler is done\n");
    return 0;
}

// After h_set has loaded all data under "id/spi", spi_handle_commit will be called.
int spi_handle_commit(void )
{
    printk("loading all settings under <spi> handler is done\n");
    return 0;
}

Another way is to call settings_load_subtree_direct to complete the task:

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

settings_load_subtree_direct does not rely on handlers and will use cb to read data from the specified subtree from non-volatile storage into the variable provided by the application. This allows for explicit data retrieval, and this API does not call h_commit after data retrieval.

typedef int(*settings_load_direct_cb)(
    const char      *key,    // Skip the subtree specified by settings_load_subtree_direct
    size_t           len,    // Length of data in non-volatile storage
    settings_read_cb read_cb,    // The read_cb provided by Settings for reading data from non-volatile storage
    void            *cb_arg,    // cb_arg is passed by settings to read_cb
    void            *param);    // param is user-defined parameter

Example:

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;
}

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

Deletion

You can delete a key-value pair using int settings_delete(const char *name);. Example:

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

Note that settings_delete will only delete the final package/subtree and cannot delete the package itself. For example, after settings_delete("id/serial"), "id/serial/baudrate" and "id/serial/hwflow" will not be deleted.

Notification

In addition to calling h_commit after settings_load() to notify, the Settings subsystem also provides the following two APIs for notification:

// Calls `h_commit` for all handlers
int settings_commit(void);

// Calls `h_commit` for the handler corresponding to the subtree
int settings_commit_subtree(const char *subtree);

Runtime API

When CONFIG_SETTINGS_RUNTIME=y, the Settings subsystem supports the following APIs for operating on specified subtrees:

// Set the value of name to data, with length len, effectively calling the corresponding h_set for name
int settings_runtime_set(const char *name, const void *data, size_t len);

// Get the value of name and set it to data, with length len, effectively calling the corresponding h_get for name
int settings_runtime_get(const char *name, void *data, size_t len);

// Commit for name, effectively calling h_commit
int settings_runtime_commit(const char *name);

Backends

Zephyr supports three backends by default:

  • CONFIG_SETTINGS_FCB Flash Circular Buffer
  • CONFIG_SETTINGS_FILE File
  • CONFIG_SETTINGS_NVS NVS. Zephyr’s three supported backends can only choose one to use.

Configuration Files

Configuration using FCB

CONFIG_SETTINGS=y
CONFIG_SETTINGS_FCB=y

Configuration using NVS

CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y

Configuration using Files

CONFIG_SETTINGS_FILE_PATH is used to specify which file to use for saving settings data.

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

Device Tree

For FCB and NVS backends, it will automatically look for fixed partitions labeled “storage”. For example, writing it like this will be automatically recognized by the Settings subsystem as the storage partition for NVS and FCB backends:

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

You can also explicitly specify which partition to use as the settings storage partition using zephyr,settings-partition, for example:

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

Shell

When using the Settings subsystem, we will write several key-value pairs. To facilitate debugging, Zephyr provides a set of dedicated settings shell commands for operations:

  • settings list: List all key-value pairs
  • settings read: Read the value of the specified key
  • settings write: Write key-value pairs
  • settings delete: Delete key-value pairs

The image below shows how to use the settings shell commands:

Zephyr Settings Subsystem User Guide

References

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

Leave a Comment