Skip to main content

Flash Partitions

Flash is divided by partitions.csv, and every line is a decision. The full table with offsets is in the partition reference; this chapter is the reasoning.

The map

0x000000 bootloader + partition table
0x009000 nvs 64 KB settings, favorites, cards, schedules, token
0x019000 phy_init 4 KB radio calibration
0x01A000 otadata 8 KB which app slot boots (A/B state)
0x020000 ota_0 4.5 MB app slot A (USB flashes land here)
0x4A0000 ota_1 4.5 MB app slot B
0x920000 storage 2 MB LittleFS: dashboard files, TLS certs
0xB20000 coredump 64 KB post-mortem crash dumps
0xB30000 nvs_key 4 KB key material for NVS encryption

Production builds swap this table for partitions_production.csv, which moves the partition table to 0x10000 and nvs_key to 0xB50000. The offsets above are the development layout.

The reasoning, entry by entry

Two 4.5 MB app slots, no factory. The table originally carried a factory slot plus two 3 MB OTA slots, but under A/B OTA the factory image is never rewritten and never booted again after the first update: it was 3 MB of dead weight while the firmware grew past 2.4 MB. Removing it grew both OTA slots to 4.5 MB with every data partition staying at its old offset, so the one-time migration was a USB reflash that preserved NVS and storage. With otadata blank the bootloader boots ota_0 ("No factory image, trying OTA 0"), USB flashes land in ota_0 at the same 0x20000 offset as before, and updates alternate between the slots so the previous version physically persists for rollback. Always flash ota_data_initial.bin alongside a full image so a stale slot selection cannot point at an empty partition.

NVS at 64 KB. Generous for the actual data (a few KB) because NVS wear-levels across its pages: more pages, longer flash life for the 30-second resume writes. The nvs_key partition holds the encryption key when NVS encryption is enabled in the production profile.

Storage as LittleFS, 2 MB. The dashboard is ~170 KB; the rest is margin for certificates and future assets. LittleFS over FAT for power-cut safety (it is copy-on-write by design) and because it mounts read-mostly content with tiny RAM cost.

Coredump, 64 KB. Enough for task stacks and registers of every thread at crash time. The observability chapter shows this partition paying for itself.

The missing-otadata lesson

For most of the project's life this table had the three app slots but no otadata entry, and OTA silently could not work: every update downloaded, verified, wrote a slot perfectly, and then failed at the final step with ESP_ERR_NOT_FOUND when asking the system to boot from it.

The mechanism: otadata is a tiny two-sector partition where esp_ota_set_boot_partition() records which slot the bootloader should choose (two copies with counters, so a power cut mid-write cannot corrupt the decision). Without the partition, the write has nowhere to go; with it absent, every boot had also been logging esp_ota_ops: not found otadata, a warning that scrolled past unread for months because nothing user-visible failed until the first real OTA attempt.

Two lessons got institutionalized:

  1. Boot warnings are a to-do list, not weather. The boot report exists partly because of this: failures that reach a screen get fixed; failures that scroll by do not.
  2. Exercise the whole path, not the parts. Download, verify, and write all had been tested; "reboot into the new image" had not, and the failure lived precisely in the gap.

The fix was one CSV line, placed in a pre-existing 24 KB gap between phy_init and factory so no other partition moved: fielded devices needed only a partition-table reflash, with NVS, storage, and both app slots untouched at their old offsets. Leaving alignment gaps in a first layout is exactly what makes such surgical fixes possible later.

Rules when touching the table

  • Never move nvs or storage on devices in the field; their content is their offset.
  • App partitions must stay 64 KB aligned (bootloader requirement).
  • After any table change, a full idf.py flash (not app-flash) is required once, and the change deserves a release note shouting about it.