Plain UBI Workflow¶
What this page covers: how to wire plain (non-encrypted) UBI into your application — when to use it, the lifecycle of a UBI handle, volume management, the LEB read/write path, the garbage-collection contract, error and degraded-mode handling, and clean teardown.
Prerequisites: Concepts at a Glance for the vocabulary (PEB, LEB, EC, VID, EBA) and Quick Start if you want a 5-minute hands-on session first.
After reading this: you will know exactly what each public call does at the right level of abstraction to integrate UBI into a real application — without having to reverse-engineer the API page or the internals.
1. When to use plain UBI¶
Plain UBI is the right choice when:
you store application data on raw NOR or NAND flash and need wear-leveling and crash-safe metadata;
the data does not require confidentiality or authenticity beyond CRC integrity;
the platform has no PSA Crypto / mbedTLS, or the secure cost (~20 KB extra flash) is unjustified;
you want a deterministic, dependency-free volume layer that pairs cleanly with a higher-level filesystem (LittleFS, FAT) or with application-managed records.
If any of those conditions does not hold and you need authenticated encryption of every on-flash record, read Secure UBI Workflow instead.
2. Prerequisites checklist¶
Before your first plain UBI build:
Prerequisite |
Where it lives |
How to verify |
|---|---|---|
|
Kconfig |
Build pulls in |
Flash partition declared in DeviceTree ( |
|
|
Static or heap memory backend chosen |
Kconfig ( |
See Configuration § Memory Backend |
Pool sizes scaled to the partition |
|
|
Garbage-collection trigger (workqueue, periodic timer, or app-level call site) |
Application code |
Dirty-PEB count stays bounded under sustained writes |
3. Lifecycle of a UBI handle¶
Every interaction starts with ubi_device_init() and ends with
ubi_device_deinit(). Between those two calls the returned
struct ubi_device * is the single handle through which all
operations on that flash partition are routed.
#include <ubi.h>
static struct ubi_device *ubi;
int storage_init(void)
{
struct ubi_flash_desc flash = {
.partition_id = FIXED_PARTITION_ID(ubi_partition),
};
int ret = ubi_device_init(&flash, NULL, &ubi);
if (ret != 0) {
return ret;
}
return 0;
}
void storage_deinit(void)
{
ubi_device_deinit(ubi);
ubi = NULL;
}
Notes:
The third argument to
ubi_device_init()is the runtime selector for the secure backend. PassNULLfor plain UBI.A single partition admits one active handle at a time; a second
ubi_device_init()for the samepartition_idreturns-EBUSY.ubi_device_init()is the only call that performs the full PEB scan. Subsequent operations work entirely from the in-RAM cache.On a brand-new partition, init also performs a one-time format: it writes the device header to the reserved PEBs and an EC header with
ec=0to every data PEB.
4. Creating, using, removing volumes¶
A UBI device hosts up to CONFIG_UBI_MAX_NR_OF_VOLUMES independent
volumes. Each volume is a named, sized container of LEBs.
struct ubi_volume_config cfg = {
.name = "settings",
.type = UBI_VOLUME_TYPE_DYNAMIC,
.leb_count = 4,
};
int vol_id;
int ret = ubi_volume_create(ubi, &cfg, &vol_id);
if (ret == 0) {
/* vol_id is the durable identifier; persist or rediscover it. */
}
Static volumes (
UBI_VOLUME_TYPE_STATIC) freeze theirleb_countat create time. Best for fixed assets like firmware slots in an A/B layout.Dynamic volumes (
UBI_VOLUME_TYPE_DYNAMIC) accept laterubi_volume_resize(). Best for application data whose size changes over time.ubi_volume_create()is idempotent when called with the samenameand the same configuration — it returns0and the existingvol_id. With the samenamebut a different config it returns-EEXIST.Volume IDs are monotonic and never reused. After
ubi_volume_remove()the slot is freed but the ID is gone for good. The 32-bitvol_id_watermarkis large enough that this is only relevant in pathological create/remove loops.
Discovery after reboot:
struct ubi_device_info di;
ubi_device_get_info(ubi, &di);
for (int i = 0; i < di.vol_count; i++) {
/* enumerate by index — see Volume Management in the API ref */
}
Most applications instead persist the vol_id of each known volume
in their own settings store and look up ubi_volume_get_info(ubi, vol_id, &cfg, NULL) on the next boot.
5. Reading and writing LEBs¶
UBI exposes raw LEB-level I/O. There is no notion of files, offsets across LEBs, or partial-LEB metadata — that is the next layer up.
Write¶
int ret = ubi_leb_write(ubi, vol_id, /*lnum=*/0, payload, payload_len);
What happens internally:
The least-worn free PEB is selected (
rb_get_min(free_pebs)).The new PEB is fully written: EC header → user data → VID header. The VID header is the commit point — the new mapping is visible only after that final write succeeds.
The old PEB (if any) is moved to the dirty pool for later reclaim.
On flash failure the new PEB is marked bad and step 1 retries with the next-best free PEB; the old mapping is untouched.
This copy-on-write contract guarantees: a write either takes effect in full or leaves the previous content intact. There is no “half-written LEB” state visible to the caller.
Read¶
int ret = ubi_leb_read(ubi, vol_id, /*lnum=*/0, /*offset=*/0,
buf, sizeof(buf));
Reads bypass the dirty / bad pools and go directly through the EBA
table. An unmapped LEB returns -ENOENT. Reads do not advance
sequence numbers or touch flash beyond the data area.
Map / unmap¶
ubi_leb_map() writes only the EC + VID headers and leaves the data
area erased — useful when you want to reserve a LEB without payload.
ubi_leb_unmap() releases the mapping (the underlying PEB becomes
dirty). Unmap is forbidden on static volumes and returns
-EACCES.
6. Garbage collection¶
UBI does not run an internal background thread. The application
drives reclaim by calling ubi_device_erase_peb(). One PEB is
processed per call:
pick the dirty PEB with the lowest erase counter;
erase it on flash;
bump its EC, write a fresh EC header, return it to the free pool;
on erase failure, mark the PEB bad and return
0.
A typical wiring is a periodic Zephyr work item — see Cookbook § Periodic garbage collection in a workqueue for the runnable recipe. The right cadence is application-specific:
Bursty writes (config snapshots, journal flushes): call
ubi_device_erase_peb()immediately after the burst.Steady-state writes (sensor logs): a 1 Hz workqueue is usually enough and is cheap when there is nothing to do (the call returns
0instantly whendirty_pebsis empty).Tight free-PEB headroom: trigger reclaim from the
-ENOSPCerror path of your write code as a fallback.
The cost of one erase_peb() call is dominated by the underlying
flash erase latency (milliseconds on NOR, tens of milliseconds on
NAND). Schedule accordingly.
7. Error handling¶
All public functions return 0 on success or a negative errno.
The full enumeration is in Error Codes; the codes
that drive normal application logic are:
Code |
Where you will see it |
What it tells you |
|---|---|---|
|
reads, info queries |
volume or LEB does not exist |
|
|
name reused with different config |
|
writes, create, resize |
run |
|
every mutator |
device is in degraded read-only mode (see § 8); reads still work |
|
any flash-touching call |
physical flash error; affected PEB will be marked bad |
|
every public function |
programmer error — fix the caller, do not retry |
-EBUSY is reserved for ubi_device_init() and signals that
another handle on the same partition is already alive.
8. Degraded read-only mode¶
UBI keeps two active reserved PEBs (dual-bank) and up to two cold
spares (CONFIG_UBI_DEV_HDR_NR_OF_RES_PEBS, default 2 active + 0
spares = total 2). When a reserved PEB fails permanently and no
spare is available, the device drops into degraded read-only
mode. From that point on, every mutator returns -EROFS. Reads,
info queries, and ubi_device_erase_peb() still work.
Recovery is automatic: each ubi_device_erase_peb() call attempts
to rewrite the bad reserved bank from the surviving active PEB. On
success the read_only_degraded flag clears and writes resume.
Wiring the periodic GC work-item is therefore enough — no separate
recovery code is needed.
To check the flag explicitly:
struct ubi_device_info di;
ubi_device_get_info(ubi, &di);
if (di.read_only_degraded) {
LOG_WRN("UBI is in read-only mode, "
"self-healing on next erase_peb() cycle");
}
9. Tear-down¶
ubi_device_deinit() acquires the per-device mutex, releases all
in-RAM structures (the static slabs are returned to their pools, the
heap structures are freed), and clears the partition guard so a
later ubi_device_init() can re-attach.
Make sure no other thread starts a new operation after you have called deinit. Operations already in flight will complete before deinit proceeds — they hold the mutex.
10. What’s next¶
Cookbook — six runnable recipes (STM32U5, nRF5340, A/B firmware slots, GC workqueue, key rotation, freshness store).
Configuration — Kconfig sizing, DeviceTree partition layout, log levels.
Architecture Guide — internals, header byte layouts, RBT data structures, recovery rules.
API Reference — per-function Doxygen reference.
Error Codes — full error-code enumeration.