The storage module forwards or stores data from enabled modules.
It is implemented as a small SMF state machine with a parent RUNNING state.
Data types are discovered automatically through iterable sections.
See storage.c, storage.h, and Kconfig.storage for details.
The Storage module implements a state machine with the following states and transitions:
- RUNNING (parent): Initializes backend, handles admin commands (
STORAGE_CLEAR,STORAGE_FLUSH,STORAGE_STATS,STORAGE_SET_THRESHOLD) - STATE_BUFFER_IDLE: Storing incoming data, waiting for commands. Transitions to
STATE_BUFFER_PIPE_ACTIVEonSTORAGE_BATCH_REQUEST. - STATE_BUFFER_PIPE_ACTIVE: Actively serving batch data through the batch interface. Transitions back to
STATE_BUFFER_IDLEwhen batch session ends.
Backends implement the API defined in the app/src/modules/storage/storage_backend.h file and provide init, store, peek, retrieve, count, and clear functionalities.
The storage module supports two backends:
- Characteristics: Fast in-memory storage
- Data persistence: Lost on power loss or device reset
- Use case: Applications that can tolerate data loss
- Characteristics: Persistent flash-based storage using the LittleFS filesystem
- Data persistence: Data survives power loss and device resets
- Use case: Applications requiring data durability and persistence across power cycles
Data producing modules publish sampled data to their respective zbus channel. Data is stored and later emitted by flush or streamed over the batch pipe, using the batch interface described in the following section.
Batch reads use a consume-on-confirm contract so that an item is only removed from the backend after the consumer has confirmed it was processed (for example, successfully sent to the cloud). A typical session looks like this:
- Consumer publishes
STORAGE_BATCH_REQUESTwith a non-zerosession_id. - The storage module responds with
STORAGE_BATCH_AVAILABLE(withdata_lenset to the number of items available) and primes the pipe with the head item. If there is no data, it sendsSTORAGE_BATCH_EMPTY, and you must still close the session. - Consumer calls
storage_batch_read()to read the head item. This call does not remove the item from the backend. - After the item has been processed, the consumer publishes
STORAGE_BATCH_CONSUMEwith the matchingsession_idand thedata_typeof the item. The storage module removes the head item and primes the next one in the pipe. - Steps 3 and 4 repeat until
storage_batch_read()returns-EAGAIN, or until the consumer decides to stop. - Consumer publishes
STORAGE_BATCH_CLOSEto end the session.
If the consumer reads without consuming, storage_batch_read() repeatedly
returns the same head item. If a STORAGE_BATCH_CONSUME arrives with an
unknown or mismatched data_type, the storage module aborts the session with
STORAGE_BATCH_ERROR to avoid silent stalls.
This module allocates RAM from the following places, and understanding these helps you tune it down:
- Built-in batch pipe buffer:
CONFIG_APP_STORAGE_BATCH_BUFFER_SIZEbytes are reserved at boot. - RAM backend ring buffers: For each enabled data type, a ring buffer is declared with capacity
sizeof(type) * CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE. - Message buffers:
struct storage_msgcarries abuffer[STORAGE_MAX_DATA_SIZE], whereSTORAGE_MAX_DATA_SIZEis the max size of any enabled data type. Enabling large types increases this buffer and several temporary buffers. - Subscriber queue: Size is controlled by system zbus configuration.
- Thread stack:
CONFIG_APP_STORAGE_THREAD_STACK_SIZE.
-
Minimize enabled data types
- Disable modules that you do not forward or store (for example,
CONFIG_APP_LOCATION=n), which reduces both slabs and RAM backend ring buffers and shrinksSTORAGE_MAX_DATA_SIZE.
- Disable modules that you do not forward or store (for example,
-
Reduce records per type
- Set
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=1when buffering is not needed. This shrinks both the per-type slabs and RAM ring buffers to a single record each.
- Set
-
Shrink batch pipe buffer
- Set the
CONFIG_APP_STORAGE_BATCH_BUFFER_SIZEKconfig option to a lower value (for example, from 1024 down to 256 bytes), but ensure it can still hold at least one item ofsizeof(header) + max_item_sizeif you use batch mode.
- Set the
-
Reduce thread and queues
- Set the
CONFIG_APP_STORAGE_THREAD_STACK_SIZEKconfig option to a lower value (for example, from 2048 down to 1024) if your application leaves headroom. - Reduce the relevant zbus queue sizes in the system configuration if traffic allows.
- Set the
-
Remove development features
- Disable the
CONFIG_APP_STORAGE_SHELLandCONFIG_APP_STORAGE_SHELL_STATSKconfig option to trim RAM and code footprint.
- Disable the
-
Prefer the LittleFS backend when buffering many records
- Use the LittleFS backend when a large
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPEvalue is needed, because the RAM backend allocates all ring buffers at boot, while the LittleFS backend only uses RAM for the currently stored records.
- Use the LittleFS backend when a large
-
Ready-made Kconfig fragment
- Use
overlay-storage-minimal.confto apply a minimal storage configuration with reduced RAM usage.
- Use
If your application only needs immediate sending (CONFIG_APP_STORAGE_INITIAL_THRESHOLD=1), the following prj.conf excerpt minimizes RAM usage for the storage module.
# Minimal storage configuration
CONFIG_APP_STORAGE=y
CONFIG_APP_STORAGE_BACKEND_RAM=y
# Keep only a single slot per type (no buffering planned)
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=1
# Send a message for every sample
CONFIG_APP_STORAGE_INITIAL_THRESHOLD=1
# Drop development features
CONFIG_APP_STORAGE_SHELL=n
CONFIG_APP_STORAGE_SHELL_STATS=n
Note
For the RAM backend, the actual RAM consumed by the ring buffers scales with which data types are enabled and the value of the CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE Kconfig option.
You must configure the partition size for the LittleFS backend to accommodate the data types in use, their sizes, and the number of records per type. A minimum partition size is required to ensure proper operation.
How to calculate the needed size:
- Per-type block need:
- Total required blocks:
where the +3 accounts for LittleFS metadata and the CoW block.
- Minimum partition size:
Choose a partition size that meets or exceeds flash_size. The LittleFS partition size is set by the littlefs_storage node in app/boards/att_flash_partitions.dtsi, which both board overlays include:
littlefs_storage: partition@4d2000 {
label = "littlefs_storage";
reg = <0x004d2000 0x00100000>; /* 1 MiB */
};
The second reg cell (0x00100000 above) is the partition size in bytes. To make the partition larger, raise that value and shrink the adjacent external_flash_partition by the same amount so the 32 MiB external flash layout stays consistent.
If the requirement is not met, either grow the partition in att_flash_partitions.dtsi as shown above, or reduce storage pressure (fewer records, smaller data types, or fewer enabled types).
Note
The data types are stored in separate files, so the minimum number of flash blocks needed is ∑ data types + 3.
Block size comes from the mounted filesystem (fs_statvfs()), which on ATT targets uses external SPI-NOR with CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 (0x1000). The table below illustrates minimal sizing for a small configuration (three data types, eight records per type); the shipped default is 1 MiB with up to 256 records per type, which needs far more blocks—the LittleFS backend checks sizing at init (see the LittleFS partition size verified log line) or use the formula above.
| Target | Block size | Example blocks needed | Example minimal partition |
|---|---|---|---|
| nrf9151 DK / Thingy:91 X (external SPI-NOR) | 0x1000 (4096 B) | 8 + 3 metadata | 0x2c000 (~176 KiB) |
| Internal flash (not recommended) | 0x1000 | 8 + 3 metadata | 0x2c000 |
The default 1 MiB (0x00100000) partition on both boards leaves ample margin for all enabled data types at CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=256.
LittleFS provides inherent wear leveling at the filesystem level:
- LittleFS automatically distributes writes across available flash blocks, avoiding repeated writes to the same physical location.
- Filesystem metadata is spread across the partition, preventing hotspots on metadata blocks.
- Updates are written to new blocks rather than overwriting existing data, naturally distributing erase cycles.
- As blocks become dirty, LittleFS reclaims and redistributes them, ensuring uniform wear across the entire partition.
- The filesystem tracks block usage patterns and preferentially allocates less-worn blocks for new writes.
The storage module adds an additional wear leveling layer through its ring buffer architecture.
- Entries are distributed across files matched to flash blocks.
- Each data type has its own file, preventing cross-type interference.
- Writes cycle through all available record slots before overwriting.
- Rewrites only modify the affected flash blocks, minimizing unnecessary writes.
The combination of LittleFS wear leveling and the ring buffer architecture provides:
- Temporal distribution: Ring buffer spreads writes over time across record slots.
- Spatial distribution: LittleFS spreads those writes across physical flash blocks.
- Type isolation: Each data type has its own write pattern, preventing interference.
- Automatic wear balancing: No configuration needed—works transparently.
To further optimize flash lifespan:
- Increase partition size: Larger partitions provide more blocks for write distribution.
- Increase record count: Higher
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPEreduces rewrite frequency. - Use ram backend when possible: If data persistence is not critical, use the RAM backend to avoid flash writes entirely.
The following sections showcase various configuration examples.
To enable persistent flash storage:
CONFIG_APP_STORAGE=y
CONFIG_APP_STORAGE_BACKEND_LITTLEFS=y
# Adjust for your needs
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=16
CONFIG_APP_STORAGE_THREAD_STACK_SIZE=4000
The partition size is configured in devicetree (see Minimum partition size above). The default littlefs_storage partition is 1 MiB on both the Thingy:91 X and the nRF9151 DK.
# Storage enabled with persistent backend
CONFIG_APP_STORAGE=y
CONFIG_APP_STORAGE_BACKEND_LITTLEFS=y
# Higher record count reduces rewrite frequency
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=50
To improve wear leveling further, grow the littlefs_storage partition in att_flash_partitions.dtsi so writes are spread across more flash blocks. See Minimum partition size above for the exact devicetree snippet to edit.
The storage module communicates through two zbus channels: storage_chan and storage_data_chan.
All message types are defined in the storage.h file.
Data operations (handled by parent RUNNING state):
-
STORAGE_SET_THRESHOLD: Set the threshold for triggering
STORAGE_THRESHOLD_REACHED. If threshold is1, every sample triggers a message. Higher values enable buffering until the threshold is reached. -
STORAGE_FLUSH: Flushes stored data one item at a time as individual
STORAGE_DATAmessages. Data is sent in FIFO order per type. Available in both operational modes. -
STORAGE_BATCH_REQUEST: Requests access to stored data through batch interface. Responds with
STORAGE_BATCH_AVAILABLE,STORAGE_BATCH_EMPTY,STORAGE_BATCH_BUSY, orSTORAGE_BATCH_ERROR. Available in both operational modes. -
STORAGE_BATCH_CONSUME: Confirms that the head item of an active batch session has been processed (for example, successfully sent to the cloud). The
session_idmust match the active session anddata_typemust identify the type of the item just read withstorage_batch_read(). Storage removes the item from the backend and makes the next item available in the pipe. An unknown or mismatcheddata_typeaborts the session. -
STORAGE_BATCH_CLOSE: Ends a batch session. Must be sent for every session, including sessions that received
STORAGE_BATCH_EMPTYorSTORAGE_BATCH_ERROR. -
STORAGE_CLEAR: Clears all stored data from the backend. Available in both operational modes.
Diagnostics (handled by parent RUNNING state):
- STORAGE_STATS : Requests storage statistics (requires
CONFIG_APP_STORAGE_SHELL_STATS). Statistics are logged to the console. Available in both operational modes.
Data events:
- STORAGE_THRESHOLD_REACHED: Emitted when the number of stored samples for a type reaches the configured threshold. Contains the data type and count that triggered the event.
Data messages:
- STORAGE_DATA: Contains stored data being flushed or forwarded. Includes data type and the actual data payload.
Batch status:
-
STORAGE_BATCH_AVAILABLE: Batch is ready for reading. Message includes total item count available and session ID.
-
STORAGE_BATCH_EMPTY: No stored data available. Batch is empty.
-
STORAGE_BATCH_BUSY: Another module is currently using the batch session.
-
STORAGE_BATCH_ERROR: Error occurred during batch operation.
The message structure used by the storage module is defined in storage.h:
struct storage_msg {
enum storage_msg_type type; /* Message type */
enum storage_data_type data_type; /* Data type for STORAGE_DATA / STORAGE_BATCH_CONSUME */
union {
uint8_t buffer[STORAGE_MAX_DATA_SIZE];
uint32_t session_id; /* Batch session id */
};
uint32_t data_len; /* Length or count */
};The storage module is configurable through Kconfig options in Kconfig.storage.
The following includes the key configuration categories:
-
CONFIG_APP_STORAGE_BACKEND_RAM (default): Uses RAM for storage. Data is lost on a power cycle but provides fast access.
-
CONFIG_APP_STORAGE_BACKEND_LITTLEFS : Uses the LittleFS filesystem for flash storage. Data is persistent across power cycles but provides slower access.
-
CONFIG_APP_STORAGE_MAX_TYPES (default:
3): Maximum number of different data types that can be registered. Affects RAM usage. -
CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE (default:
8for the RAM backend,256for the LittleFS backend): Maximum records stored per data type. Total RAM usage =MAX_TYPES×MAX_RECORDS_PER_TYPE×RECORD_SIZE. -
CONFIG_APP_STORAGE_BATCH_BUFFER_SIZE (default:
512): Size of the internal buffer for batch data access.
The littlefs_storage partition (size and host flash chip) is defined in devicetree. app/boards/att_flash_partitions.dtsi declares the partition on external SPI-NOR and the lfs1 zephyr,fstab,littlefs entry (mount point /att_storage, automount). Board overlays thingy91x_nrf9151_ns.overlay and nrf9151dk_nrf9151_ns.overlay include that file.
To resize the partition, edit the second reg cell of littlefs_storage in att_flash_partitions.dtsi (and shrink external_flash_partition by the same amount). To move it to internal flash, declare a littlefs_storage node under &flash0's partitions instead, note that the nRF9151's 1 MiB internal flash is already heavily utilized by slot0_partition, so external flash is strongly recommended for any non-trivial storage size.
- CONFIG_APP_STORAGE_INITIAL_THRESHOLD (default:
1): Initial threshold for triggeringSTORAGE_THRESHOLD_REACHEDevents. A value of 1 means every sample triggers an event, while higher values enable buffering until the threshold is reached. You can change the threshold at runtime throughSTORAGE_SET_THRESHOLDmessages.
-
CONFIG_APP_STORAGE_THREAD_STACK_SIZE (default:
2048for the RAM backend,4000for the LittleFS backend): Stack size for the storage module's main thread. -
CONFIG_APP_STORAGE_WATCHDOG_TIMEOUT_SECONDS (default:
60): Watchdog timeout for detecting stuck operations. -
CONFIG_APP_STORAGE_MSG_PROCESSING_TIMEOUT_SECONDS (default:
5): Maximum time for processing a single message.
-
CONFIG_APP_STORAGE_SHELL (default:
y): Enable shell commands for storage interaction. -
CONFIG_APP_STORAGE_SHELL_STATS: Enable statistics commands (increases code size).
- RUNNING state: Handles
STORAGE_CLEAR,STORAGE_FLUSH,STORAGE_STATS, andSTORAGE_SET_THRESHOLDmessages. - BUFFER_IDLE: Handles
STORAGE_BATCH_REQUESTto transition toBUFFER_PIPE_ACTIVE. - BUFFER_PIPE_ACTIVE: Populates pipe with
[header + data]items, handles session management.
The storage channel is the primary zbus channel for controlling the storage module and receiving control or status responses.
Input message types:
STORAGE_SET_THRESHOLD- Set threshold forSTORAGE_THRESHOLD_REACHEDeventsSTORAGE_FLUSH- Flush stored data as individual messagesSTORAGE_BATCH_REQUEST- Request batch access to stored dataSTORAGE_CLEAR- Clear all stored dataSTORAGE_STATS- Display storage statistics
Output message types:
STORAGE_THRESHOLD_REACHED- Threshold reached for a data typeSTORAGE_BATCH_AVAILABLE- Batch ready with dataSTORAGE_BATCH_EMPTY- No data availableSTORAGE_BATCH_BUSY- Another session activeSTORAGE_BATCH_ERROR- Error accessing data
This is a dedicated channel for STORAGE_DATA payload messages to avoid self-flooding and race conditions.
The subscribers interested in data should observe this channel.
Output message types:
STORAGE_DATA- Contains stored or forwarded data.
Data types are automatically registered using the DATA_SOURCE_LIST macro in storage_data_types.h. The system currently supports:
- Battery (
CONFIG_APP_POWER): StoresdoublefromPOWER_BATTERY_PERCENTAGE_SAMPLE_RESPONSE - Location (
CONFIG_APP_LOCATION): Storesstruct location_msgfromLOCATION_GNSS_DATA/LOCATION_CLOUD_REQUEST - Environmental (
CONFIG_APP_ENVIRONMENTAL): Storesstruct environmental_msgfromENVIRONMENTAL_SENSOR_SAMPLE_RESPONSE
Each data type registration includes:
- Source channel to subscribe to
- Message type filtering function
- Data extraction function
- Storage data type identifier
Storage backends implement the interface defined in the app/src/modules/storage/storage_backend.h file:
struct storage_backend {
int (*init)(void);
int (*store)(const struct storage_data *type, void *data, size_t size);
int (*peek)(const struct storage_data *type, void *data, size_t size);
int (*retrieve)(const struct storage_data *type, void *data, size_t size);
int (*count)(const struct storage_data *type);
int (*clear)(void);
};The storage module provides a convenience function for reading batch data:
int storage_batch_read(struct storage_data_item *out_item, k_timeout_t timeout);It reads stored data through the batch interface, handling header parsing and data extraction automatically. All other operations (requesting batch access, session management, etc.) go through zbus messages.
It returns 0 on a successful read, -ENODATA when the end-of-batch marker is
reached (all items in the session have been consumed), -EAGAIN if no data
becomes available within timeout, or another negative errno on error.
Important
This function should only be called after receiving a STORAGE_BATCH_AVAILABLE message in response to a STORAGE_BATCH_REQUEST.
When done consuming all items, send STORAGE_BATCH_CLOSE with the same session_id.
Flush: STORAGE_FLUSH emits individual STORAGE_DATA messages. Use for small datasets.
Batch: For bulk access, use STORAGE_BATCH_REQUEST with unique session_id:
struct storage_msg msg = { .type = STORAGE_BATCH_REQUEST, .session_id = 0x12345678 };
err = zbus_chan_pub(&storage_chan, &msg, K_SECONDS(1));
// Wait for STORAGE_BATCH_AVAILABLE, then:
struct storage_data_item item;
while (storage_batch_read(&item, K_SECONDS(1)) == 0) {
switch (item.type) {
case STORAGE_TYPE_BATTERY:
double battery = item.data.BATTERY;
break;
// ... handle other types
}
}
// Close session
struct storage_msg close = { .type = STORAGE_BATCH_CLOSE, .session_id = 0x12345678 };
zbus_chan_pub(&storage_chan, &close, K_SECONDS(1));Responses: STORAGE_BATCH_AVAILABLE (success), STORAGE_BATCH_EMPTY, STORAGE_BATCH_BUSY, STORAGE_BATCH_ERROR.
Subscribe to storage_data_chan to receive forwarded/flushed data:
switch (msg->data_type) {
case STORAGE_TYPE_BATTERY:
double *battery = (double *)msg->buffer;
break;
case STORAGE_TYPE_LOCATION:
struct location_msg *loc = (struct location_msg *)msg->buffer;
break;
/* ... other types */
}/* Clear all stored data */
struct storage_msg msg = { .type = STORAGE_CLEAR };
err = zbus_chan_pub(&storage_chan, &msg, K_SECONDS(1));
/* Show statistics (requires CONFIG_APP_STORAGE_SHELL_STATS) */
struct storage_msg msg = { .type = STORAGE_STATS };
err = zbus_chan_pub(&storage_chan, &msg, K_SECONDS(1));When CONFIG_APP_STORAGE_SHELL is enabled:
att_storage flush # Flush stored data
att_storage clear # Clear all data
att_storage stats # Show statistics (if enabled)- Implement
struct storage_backend(seestorage_backend.h). - Provide
storage_backend_get()function. - Add Kconfig option in
Kconfig.storage.
See backends/ram_ring_buffer_backend.c for reference.
- Zephyr kernel - Core OS functionality
- Zbus messaging system - Inter-module communication
- State Machine Framework (SMF) - State management
- Task watchdog - System reliability monitoring
- Memory slab allocator - FIFO memory management
- Selected storage backend - RAM or flash storage
- Iterable sections - Automatic data type discovery