Skip to content

PartitionsAndFlashLayout

Rob Dobson edited this page May 3, 2026 · 1 revision

Partitions and Flash Layout

The flash partition table determines where firmware, configuration, OTA images and the filesystem live on the device. Raft projects ship a partitions.csv per SysType so each board variant can size partitions to fit its flash chip and feature set.

ESP-IDF reads the partition table from a CSV file at build time, lays each partition out at fixed offsets, and embeds the table in flash so the bootloader and OTA system can find them at runtime. Raft makes two contributions to this:

  • RaftProject.cmake copies the SysType's partitions.csv into the build directory, so each variant can have its own table without changes elsewhere.
  • The build sets CONFIG_PARTITION_TABLE_CUSTOM=y and CONFIG_PARTITION_TABLE_CUSTOM_FILENAME in sdkconfig.defaults to point at the copy.

This page covers the partition layouts Raft uses and the trade-offs between them.

OTA-capable layout (the standard Raft layout)

The default Raft project ships a layout designed for in-the-field OTA updates and a filesystem for Web UI assets and runtime data:

# Name,   Type, SubType, Offset,    Size,      Flags
nvs,      data, nvs,     0x009000,  0x015000,            #  84 KB  Non-volatile storage
otadata,  data, ota,     0x01e000,  0x002000,            #   8 KB  OTA boot selector
app0,     app,  ota_0,   0x020000,  0x1b0000,            # 1.6875 MB  App slot 0
app1,     app,  ota_1,   0x1d0000,  0x1b0000,            # 1.6875 MB  App slot 1
fs,       data, 0x83,    0x380000,  0x080000,            # 512 KB  LittleFS / Web UI

Total: 4 MB flash. Two equal-sized app partitions plus a small otadata are required by esp_ota — see OTA Update Flow. nvs is used by RaftJson for runtime-mutable configuration. fs (subtype 0x83 = LittleFS) holds the bundled Web UI and any data files placed under FS_IMAGE_PATH.

This layout is the right starting point for any device that:

  • Has 4 MB or more of flash.
  • Will receive OTA firmware updates after manufacture.
  • Serves a Web UI or bundles data files.

For 8 MB or 16 MB flash chips, simply enlarge the app partitions and/or fs. Keep app0 and app1 the same size; the layout is otherwise insensitive to flash size.

Single-app layout (small or fixed-firmware devices)

Some examples (e.g. RaftWebServer/examples/basic/) use a non-OTA layout:

nvs,      data, nvs,     0x9000,    0x6000,              #  24 KB
phy_init, data, phy,     0xf000,    0x1000,              #   4 KB
fs,       data, spiffs,  0x10000,   0xF0000,             # 960 KB
factory,  app,  factory, 0x100000,  2M,                  #   2 MB

This is acceptable for tethered or factory-flashed-only devices. It frees up the second app partition (saving ~1.7 MB) at the cost of being unable to OTA-update — ESPOTAUpdate will refuse the update because there is no ota_0/ota_1 to swap into. Pick this only if you are sure OTA will never be needed.

Partition rules and constraints

A few hard rules apply to every Raft partition table:

  • Offsets must be 4 KB-aligned (0x1000) for data and 64 KB-aligned (0x10000) for app partitions.
  • The first partition must start at or after 0x9000 — the bootloader and partition table itself live below that. ESP-IDF will warn if you start nvs earlier.
  • app0 and app1 (when present) must be the same size; OTA writes whichever is currently inactive and will refuse if the new image does not fit.
  • The otadata partition is mandatory if any ota_x partition exists. It is exactly 0x2000 (8 KB) — it stores two redundant copies of the boot selector.
  • The fs partition's subtype tells the firmware which FS to mount: spiffs for SPIFFS, 0x83 for LittleFS. Match the value of FS_TYPE in your SysType's features.cmake (see WebUI Build Pipeline).

Sizing

Rules of thumb:

Partition Typical size When to grow
nvs 24–84 KB Increase when storing many runtime-mutable config keys, paired BLE bond data, or large key/value blobs. 84 KB suits most apps; 24 KB is the bootloader minimum.
otadata 8 KB Fixed by ESP-IDF.
app0 / app1 1.5 MB–3 MB each Increase when adding many SysMods, BLE + WiFi simultaneously, large ML/DSP libraries, or -Og debug builds. 1.6875 MB is enough for typical Raft apps with WebUI handling code. Halve flash usage with -Os and CONFIG_COMPILER_OPTIMIZATION_SIZE=y.
fs 256 KB–4 MB Drives Web UI bundle size + any data files in FS_IMAGE_PATH. 512 KB fits a parcel-built React + MUI starter. Audio / image assets push this up quickly.

When a build runs out of room you will see one of:

  • Error: app partition is too small for binary — the linked firmware exceeds the ota_0 size.
  • LittleFS image creation failed: too large — the FS image bigger than the fs partition.
  • Partition table … unaligned — the offsets and sizes do not line up.

For each, edit partitions.csv to grow the affected partition (and shrink another, since total ≤ flash size).

Where the table comes from

Each SysType has its own partitions.csv next to its sdkconfig.defaults:

systypes/RaftI2CBox/
├── features.cmake
├── partitions.csv          ← copied by RaftProject.cmake
├── sdkconfig.defaults      ← references the file via CONFIG_PARTITION_TABLE_CUSTOM_FILENAME
└── SysTypes.json

sdkconfig.defaults for the OTA-capable layout typically contains:

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="systypes/<SYSTYPE>/partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y    # optional, see "Rollback" below

RaftProject.cmake then runs:

add_custom_command(
    OUTPUT ${_partitions_csv_file}
    COMMAND ${CMAKE_COMMAND} -E copy "${BUILD_CONFIG_DIR}/partitions.csv" ${_partitions_csv_file}
    DEPENDS  "${BUILD_CONFIG_DIR}/partitions.csv"
)

— so editing partitions.csv triggers a rebuild and the new table is flashed on the next idf.py flash.

OTA partition selection at runtime

The first OTA image in flash is always picked from the factory partition if one exists; otherwise the bootloader chooses ota_0. After a successful OTA write, esp_ota_set_boot_partition() flips the next-boot pointer in otadata to the inactive slot. The mechanics live in OTA Update Flow; the relevant point for the partition table is just that both ota_0 and ota_1 must exist and be the same size for OTA to work at all.

Rollback

ESP-IDF supports a "pending verify" boot mode where a freshly-OTA'd image must mark itself valid within a configured window or the bootloader rolls back to the previous slot. Enable with:

CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y

Then call esp_ota_mark_app_valid_cancel_rollback() from the application after sufficient self-test passes. Raft's ESPOTAUpdate does not call this for you — the new image is unconditionally marked bootable on a clean esp_ota_end(). Apps that want rollback need to either implement the check themselves or set CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y for stronger version-monotonic guarantees.

NVS encryption

For products that handle PII, keys or BLE bond data, consider enabling NVS encryption:

CONFIG_NVS_ENCRYPTION=y
CONFIG_NVS_ENC_KEY_SOURCE_FROM_FLASH_PARTITION=y

This requires an additional nvs_keys partition. Add to partitions.csv (sample):

nvs_keys, data, nvs_keys, 0x01a000, 0x004000, encrypted

See the NVS Encryption guide. RaftJson's NVS reads/writes are unaffected — the encryption is transparent.

Inspecting the running device

espefuse.py summary and idf.py partition-table show the active table. At runtime, esp_partition_find_first() and the SysManager status REST API report partition usage. The boot log prints the chosen app partition on every boot:

I (492) boot: Loaded app from partition at offset 0x20000

See also

Clone this wiki locally