prerequisite

Flash, RAM, and partitions

Where code and data live on an ESP32: flash that survives power-off, RAM that does not, PSRAM, and the partition table that divides flash into regions.

Before this

This page assumes you are comfortable with:

Why you need this

Every flashing step names an offset, every out-of-memory error names a kind of RAM, and every lost setting after a power cut is a question of which memory it lived in. Before you can follow flashing and the ROM bootloader, you need a picture of the memories on the board and how flash is carved up.

The idea

An ESP32 board has three kinds of memory. This page uses 1 KB = 1024 bytes and 1 MB = 1024 KB, the way memory and flash sizes are counted.

Memory Where Survives power-off? Typical size Speed
SRAM (static RAM) Inside the chip No Hundreds of KB Fastest
Flash A separate chip on the module or board, wired over SPI Yes 4 MB to 32 MB Slow to write, read through a cache
PSRAM (pseudo-static RAM) A second chip, or inside the chip's package No 2 MB to 32 MB Slower than SRAM

RAM (random-access memory) is where a running program keeps its variables, stack, and buffers. It forgets everything when power goes away. Flash is the program's home: the bootloader, your app, settings, and files stay there with the power off. PSRAM is extra RAM on a separate memory die, for big buffers such as a display frame or downloaded files.

The boards on the workbench, from the author's board notes and the Inspector's catalog:

Board Chip Internal SRAM Flash PSRAM
CYD, CYDE, WROOM-32 DevKit ESP32 520 KB 4 MB none
Super Mini ESP32-S3 ESP32-S3 512 KB 4 MB 2 MB
YD-ESP32-S3 (workbench unit, S3-N16R8 module) ESP32-S3 512 KB 16 MB 8 MB
ESP32-C6-DevKitM clone ESP32-C6 512 KB (0x40800000 to 0x4087FFFF) 4 MB none
Waveshare ESP32-P4 Core DevKit ESP32-P4 768 KB (0x4FF00000 to 0x4FFBFFFF) 32 MB 32 MB

The classic ESP32's 520 KB comes from ESP-IDF's memory-types guide. The S3's 512 KB is from the author's Super Mini notes; the other S3 board uses the same chip. Not all of it is free for your program: the boot ROM, the radio stack, and the operating system take their share first.

Flash is erased in sectors

Flash can be read one byte at a time, but writing is lopsided. A write can only clear bits from 1 to 0. Setting them back to 1 needs an erase, and erasing works only on a whole sector. On the flash chips ESP32 boards use, a sector is 4096 bytes (0x1000); esptool's documentation calls it "the smallest erasable unit." That is why every flash offset on these pages is a multiple of 0x1000.

Running code straight from flash

Copying a 1 MB program into a few hundred KB of RAM is impossible, so ESP32 chips run most code in place from flash. A hardware unit called the MMU (memory management unit) maps a window of flash into the chip's address space, and a cache keeps recently used parts in SRAM. ESP-IDF's guide says accessing cached flash "is as fast as accessing other types of internal memory" once it is in the cache. Code that must run when the cache is off, such as an interrupt handler that can fire during a flash write, goes in instruction RAM instead.

You can see both styles in a C6 boot log the Inspector annotates: a segment marked map at 0x42000020 is code read in place from flash, and a segment marked load at 0x40800000 is data copied into SRAM.

The partition table

Flash is one long array of bytes, so ESP-IDF divides it into named regions listed in a partition table. ESP-IDF writes the table at offset 0x8000 by default. Its documentation gives the rules:

  • The table is 0xC00 bytes long, allowing up to 95 entries plus a checksum, and fills one 4 KB sector.
  • Partitions of type app start on a 64 KB boundary (a multiple of 0x10000).
  • Data partitions start on a 4 KB boundary.
  • A blank offset means "right after the previous partition", rounded up to the required boundary.

Common entries:

Name Type Holds
nvs data Non-volatile storage: small key-value settings that survive power-off
phy_init data Radio calibration data
otadata data Which app slot to boot, for over-the-air updates
factory app The app flashed over USB
ota_0, ota_1 app Two app slots that an update writes to in turn
a filesystem partition data Files, such as MicroPython's main.py and CircuitPython's code.py

Below 0x8000 sits the second-stage bootloader, at an offset that depends on the chip.

Worked example

ESP-IDF ships a built-in table called "Factory app, two OTA definitions" for 4 MB boards. This is its CSV file from ESP-IDF 5.4.1 (official framework file, offsets left blank for the tool to fill):

# Name,   Type, SubType, Offset,   Size, Flags
nvs,      data, nvs,     ,        0x4000,
otadata,  data, ota,     ,        0x2000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
ota_0,    app,  ota_0,   ,        1M,
ota_1,    app,  ota_1,   ,        1M,

Fill in the offsets by the rules. The table ends at 0x8000 + 0x1000 = 0x9000, so the first partition starts there.

Partition Offset Size Ends at How the offset was chosen
(bootloader) depends on chip below 0x8000 fixed per chip
(partition table) 0x00008000 4 KB 0x00009000 ESP-IDF default
nvs 0x00009000 16 KB 0x0000D000 right after the table
otadata 0x0000D000 8 KB 0x0000F000 right after nvs
phy_init 0x0000F000 4 KB 0x00010000 right after otadata
factory 0x00010000 1024 KB 0x00110000 already on a 64 KB boundary
ota_0 0x00110000 1024 KB 0x00210000 right after factory
ota_1 0x00210000 1024 KB 0x00310000 right after ota_0

A 4 MB chip ends at 0x00400000, so 0x400000 - 0x310000 = 0xF0000 bytes, which is 960 KB, are left over for a filesystem. ESP-IDF's own documentation lists the same offsets.

The Inspector's echo payloads use the simpler single-app default instead: nvs at 0x9000 (24 KB), phy_init at 0xF000 (4 KB), and one 1 MB factory app at 0x10000. The classic ESP32 echo app is 151,760 bytes, about 148 KB, so it fills under 15% of its slot. Almost all of that is ESP-IDF's runtime; the echo itself is 55 bytes.

In an ESP32 project

Partitions decide stage 3. Flashing a program means writing the bootloader, the table, and the app to the right offsets (flashing and the ROM bootloader), and booting means the bootloader reading the table to find the app (the boot sequence). They matter again in stage 5: over-the-air updates need the two-slot layout. Settings live in nvs. The author's USB keyboard bridge, for instance, keeps its key-remap profiles and radio peers there, so erasing that partition resets them to the defaults.

Common mistakes

  • Flashing the app to an offset the partition table does not name. The bootloader looks where the table says, finds no valid app there, and prints an error on every boot.
  • An app bigger than its partition. The ESP-IDF build stops with a size error. Choose a larger table or shrink the app.
  • Expecting a variable to survive a reboot. Anything in RAM is gone after a reset or power cut. Save it to nvs or a file.
  • Erasing all of flash to fix a bad app. That works, but it also wipes nvs and the filesystem, so saved settings and uploaded files disappear.
  • Assuming the board's flash size from the chip name. The same S3 chip ships with 4 MB, 8 MB, or 16 MB beside it. Read the module marking or ask esptool.

Cost

Flash is cheap in money and space but slow and finite to write: every write needs a 4 KB erase first, so a program that saves a tiny setting often should batch its writes. Two OTA slots double the flash spent on apps (2 MB of a 4 MB chip in the worked example) to buy a safe update. SRAM is the scarce resource: a few hundred KB shared by the stack, the radio, and your buffers, which is why boards with PSRAM are chosen for displays and large downloads.

Going further

  • ESP-IDF Programming Guide, "Partition Tables" and "Memory Types".
  • The esptool documentation on flash erase and read-flash.
  • Over-the-air updates, for how the two OTA slots and otadata are used.
  • Memory maps and registers, for the address ranges where flash and RAM appear.

Leads to

Back to ESP32 development: assembly, C, MicroPython, and CircuitPython