Zephyr Services

Data Passing

Zephyr provides various ways to pass data between threads and ISRs, the following tables summerizes the differences between them:

Kernel Services for Data Passing

Generally, both the producing and consuming sides of the data passing methods provided by the kernel can be accross multiple contexts, i.e., threads and ISRs, as the them utilizes locks in both the producing and consuming sides. In addition, since this methods are also kernel objects, thay have the same functionalites such as object cores, object tracing, etc. as other kernel objects.

Object

Data size

Data storage

Data passed by

Features

FIFO/LIFO

Arbitrary

User managed

Pointer

Intrusive structure

Stack

Word

Array

Copy

For passing pointers

Pipe

Byte

Ring buffer

Copy

For passing raw bytes

Message Queue

Fixed

Ring buffer

Copy

For passing structs

Mailbox

Arbitrary

User managed

Pointer

Destined transfer

Refer to the data passing section of the kernel service documentation for more details.

Kernel Data Structures for Data Passing

Two data structures provided by the kernel are specifically designed for data passing, namely single producer single consumer (SPSC) and multiple producer single consumer (MPSC) packet buffers. And there are also lockless versions of them that is intrusive and requires the user to manage the data storage.

Note that as the name suggests, SPSC is designed for passing data between a single producing context and a single consuming context, and MPSC is designed for multiple producing contexts and a single consuming context, unlike the kernel services for data passing that allows multiple producing and consuming contexts.

Object

Data size

Data storage

Data passed by

Features

SPSC/MPSC

Arbitrary

Ring Buffer

Copy

Arbitrary-sized packets

Lockless SPSC/MPSC

Arbitrary

User managed

Pointer

Lockless

Winstream

Byte

Ring buffer

Copy

Lockless SPSC

Refer to the kernel data structures for more details.

Note

The Winstream data structure is not documented in the kernel data structures documentation, but the API documentation is available in the file reference.

OS Services for Data Passing

Data passing methods provided by OS services are built on top of the kernel ones and have additional features such as sublisher/subscriber model, callbacks, etc.

Object

Data size

Data storage

Data passed by

Features

Zbus

Fixed

Queue

Pointer/Copy

Callbacks

Refer to their respective documentations for more details.

Pooled Parallel Preemptible Priority-based Work Queues (P4WQ)

Aside from the kernel work queue that runs only in one thread, Zephyr provides a more advanced work queue system called P4WQ that provides parallel execution of works using thread pool based on its priority, with the tradeoff not having delayable works and thread pool control API. Additionally, it is not documented in the Zephyr documentation, and its API documentation is only available in its file reference. P4WQ can be enabled by Kconfig option CONFIG_SCHED_DEADLINE.

k_p4wq_work is used to define a work item, where k_p4wq_work.priority and k_p4wq_work.deadline are what the priority and the deadline of the thread will be when the work is executed. This however means that when all threads are busy, newly submitted works with higher priority will not preempt the lower priority works that are already being executed [1].

Note

Thread deadline in Zephyr is only considered when scheduling threads with the same priority using earliest deadline first scheduling [2].

References

Logging

Zephyr provides a comprehensive logging system that provides various logging backends (i.e., where these log messages will be outputed to) such as console or file system.

File system backend enabled by Kconfig option CONFIG_LOG_BACKEND_FS is particularly handy since it will only log message only after the file system is mounted.

Tracing

Zephyr provides a tracing system that allows you to trace the execution of kernel objects such as threads, work queues and mutexes, etc. Here we use Segger SystemView as the tracing backend.

Custom SystemView Description

Though SystemView provides description table for Zephyr, it’s not complete as mentioned in the tracing subsystem documentation. However, to add the proper description for Zephyr, the correct description file, i.e. zephyr/supsys/tracing/sysview/SYSVIEW_Zephyr.txt, should be placed at /opt/SEGGER/SystemView/Description [3] for Linux and Mac OS systems, or at C:\Program Files\SEGGER\SystemView\Description for Windows systems.

Interrupt Service Routine (ISR) Number

When ISR is executed, it does not have a name that is easily recognizable. Instead a number is used to identify the ISR. For cortex-M series, the number is the first nine bits of Interrupt Control and State Register (ICSR) register of the System Control Block (SCB) in the CPU [4]. Which corresponds to the number of the ISR to be called in the interrupt vector table.

For STM32 microcontrollers, this table is listed in Interrupt and exception vectors in the Nested vectored interrupt controller (NVIC) section in the reference manual. For example, if the ISR number is 102, it corresponds to address 0x198 (102*4 in decimal) and for STM32G4 series, it is fired from FDCAN2_IT0 line.

Reference

LittleFS

LittleFS support both non-volatile memory (NVM) such as internal flash or external SPI flash and block device such as SD cards or USB drives. However, since there is little to no example for the latter, some quirks are worth noting here, and a SD card block device is used here as an example.

Though LittleFS provides device tree bindings for configuring the file system, it is mainly designed for NVM. For block devices, LittleFS will determine the block size when mounting the device, and set other parameters such as the read and program size the same as the block size and lookhead size four times the block size [5]. Since it does not know how big the block size will be, it simply uses melloc() to allocate the read, program, and lookhead buffers for the block device [6], so be sure to enable melloc() and set the heap size to at least six times the size of the block size.

Additionally, LittleFS uses k_heap_alloc() for allocating file caches [7] using a memory pool controlled by Kconfig option CONFIG_FS_LITTLEFS_CACHE_SIZE, so also make sure to set it to values greater than block size.

Since the automount feature is not available for block devices, they must be mounted manually. The following code snippet shows how to do so:

static struct fs_littlefs lfsfs;
static struct fs_mount_t mp = {
    .type = FS_LITTLEFS,
    .fs_data = &lfsfs,
    .flags = FS_MOUNT_FLAG_USE_DISK_ACCESS,
    .storage_dev = CONFIG_SDMMC_VOLUME_NAME,
    .mnt_point = "/" CONFIG_SDMMC_VOLUME_NAME ":",
};

fs_mount(&mp);

Reference

Storage Systems

Aside from file systems, Zephyr also provides lower-level storage systems that are designed to store data on the limited internal flash, such as Flash Circular Buffer (FCB) that stores data in FIFO manner, as well as Non-Volatile Storage (NVS) and Zephyr Memory Storage (ZMS) that store using key-value pairs (refer to the documentation of ZMS for detailed comparison between them).

Note

NVS stores the flash page size (the unit of an erase operation) in a uint16_t [8], so it can only support flash page size up to 32KB (page size is typically in power of 2 and uint16_t is at most 65535 so 32KB is the maximum). Though ZMS does not have such limit, since it have to go through the entire all pages for every write operation [9], it is not suitable for large total size. And since flash with large page sizes such as STM32F4 and STM32H7 series will inevitably have large total size, both technologies are not suitable for such use cases. Instead, external SPI flash have to be used.

Settings

Zephyr provides a settings system that built on top of the storage or file systems to store and load settings and its backend can be selected by Kconfig option CONFIG_SETTINGS_BACKEND.

Note

For FCB, NVS, and ZMS backends, the settings system will automatically chose storage_partition if zephyr,settings_partition is not chosen in the device tree [10] [11] [12].

References

Sensing Subsystem

The sensing subsystem provides a high level of accessing sensors, such as scheduling sampling for multiple clients that requests data at different rates and resoultons (since currently most sensor drivers are designed for single client only [13]), and fusing multiple sensors to provide a new kind of data (e.g. fusing two IMUs on the lid and the base of a foldable phone to calculate the hinge angle).

However, dispite being merged to the mainline in October 2023, the sensing subsystem is still only a minimum viable product as of Zephyr 4.0.0 lacking some important features such as batch reading and error reporting [14]. And currently seemed to be not in active development anymore.

Yet it is still a good starting point for managing multiple sensors with a single interface.

Some issues of the sensing subsystem should be considered or addressed before being adopted:

Duplicated Sensor Data Types

In the documentation of sensing subsystem, sensing_sensor_value_q31 is the data type for sensor data, which is almost the same as sensor_q31_data used for the sensor driver. The only difference is that the timestamp is in micro seconds for the former and in nano seconds for the latter [15] [16].

This is probably due to sensor_q31_data being introduced later than the initial proposal of the sensing subsystem. However, since sensor_q31_data is widely adopted by the sensor driver, it should be used as the sensor data type in the sensing subsystem instead of sensing_sensor_value_q31.

Use of HID Sensor Type (instead of existing sensor_channel)

Intel (the author of the sensor subsystem) uses HID sensor types instead of the existing sensor_channel of sensor types from CHRE (context hub runtime environmrnt) since they claimed that it is the only cross-OS standard for sensor types [17]. However, they did not implement any other part of the HID standard, making the choice of using HID sensor type that’s incompatible with the existing sensor_channel existing sensor drivers depends on some what abrubtly. Additionally, the sensing subsystem configures the sensor using sensing_set_config() that requires sensor_channel as the parameter as mentioned in the next section, it is better to stick to sensor_channel for sensor types.

Confusing Device API

During development, the sensing subsystem sensor device API is required to be the same as the sensor driver sensor_driver_api as mentioned in this RFC. As as result, when setting required interval and senstivity using sensing_set_config(), it sets SENSOR_ATTR_SAMPLING_FREQUENCY and SENSOR_ATTR_HYSTERESIS attributes, respectively, of the channel corresponding to that HID sensor type via sensor_attr_set() [18] [19], which is somewhat confusing, but have to bare in mind when developing sensor subsystem drivers.

Sensor Scheduling

Sample scheduling is done internally by first setting SENSOR_ATTR_SAMPLING_FREQUENCY sensor attribute based on the minimum requested sampling intervals of all clients, and then downsample the data via a timer that only passes the data to the clients when the time elapsed from the last sample is greater than the period [20]. Which means that the actual sampling interval will be longer and unpredictable if the underlying sensor driver sampling jitters. Both of which are not ideal for real-time applications.

A better approach would be to set the sensor driver sampling rate based on the greatest common factor of all clients’ requested sampling intervals with some kind of tolerence for the jitter.

References