ESP32 Restart loop after successful flash: "invalid magic byte (nothing flashed here?)"

Hello,

I am using VSCode and PIO to develop with esp-idf on a DevkitC device (ESP32-WROOM-32D). I have been able to flash & run code successfully for a while now, but today it has stopped working. The issue appeared after flashing espressif’s hello_world example manually with idf.py on the command line. The hello_world example flashes and runs fine, but now when I use platformIO’s toolchain, this happens:

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:7768
load:0x40078000,len:17328
load:0x40080400,len:4216
entry 0x400806e0
I (52) boot: ESP-IDF 4.3.2 2nd stage bootloader
I (52) boot: compile time 17:28:38
I (53) boot: chip revision: 1
I (56) boot_comm: chip revision: 1, min. bootloader chip revision: 0
I (63) boot.esp32: SPI Speed      : 40MHz
I (68) boot.esp32: SPI Mode       : DIO
I (72) boot.esp32: SPI Flash Size : 4MB
I (77) boot: Enabling RNG early entropy source...
I (82) boot: Partition Table:
I (86) boot: ## Label            Usage          Type ST Offset   Length
I (93) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (100) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (108) boot:  2 factory          factory app      00 00 00010000 00100000
I (116) boot: End of partition table
E (120) esp_image: image at 0x10000 has invalid magic byte (nothing flashed here?)
E (128) boot: Factory app partition is not bootable
E (134) boot: No bootable app partitions in the partition table
ets Jun  8 2016 00:22:57

This continues in a loop … it looks like the program has not actually been flashed to the correct location. However, PIO uploads just fine:

> Executing task: platformio run --target upload --environment esp32dev <

Processing esp32dev (platform: espressif32; board: esp32dev; framework: espidf)
-------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32dev.html
PLATFORM: Espressif 32 (3.5.0) > Espressif ESP32 Dev Module
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (esp-prog) External (esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES: 
 - framework-espidf 3.40302.0 (4.3.2) 
 - tool-cmake 3.16.4 
 - tool-esptoolpy 1.30100.210531 (3.1.0) 
 - tool-mkspiffs 2.230.0 (2.30) 
 - tool-ninja 1.9.0 
 - toolchain-esp32ulp 1.22851.191205 (2.28.51) 
 - toolchain-riscv32-esp 8.4.0+2021r2-patch2 
 - toolchain-xtensa-esp32 8.4.0+2021r2-patch2 
 - toolchain-xtensa-esp32s2 8.4.0+2021r2-patch2
Reading CMake configuration...
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ off
Found 2 compatible libraries
Scanning dependencies...
No dependencies
Building in debug mode
Retrieving maximum program size .pio/build/esp32dev/firmware.elf
Checking size .pio/build/esp32dev/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   3.9% (used 12872 bytes from 327680 bytes)
Flash: [==        ]  15.5% (used 163049 bytes from 1048576 bytes)
Configuring upload protocol...
AVAILABLE: esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Auto-detected: /dev/cu.usbserial-120
Uploading .pio/build/esp32dev/firmware.bin
esptool.py v3.1
Serial port /dev/cu.usbserial-120
Connecting........_____..
Chip is ESP32-D0WD (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: e8:68:e7:30:af:f8
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Flash will be erased from 0x00001000 to 0x00008fff...
Flash will be erased from 0x00010000 to 0x00010fff...
Flash will be erased from 0x00020000 to 0x00047fff...
Compressed 29408 bytes to 17609...
Writing at 0x00001000... (50 %)
Writing at 0x00007bf4... (100 %)
Wrote 29408 bytes (17609 compressed) at 0x00001000 in 0.9 seconds (effective 251.7 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00010000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00010000 in 0.1 seconds (effective 339.7 kbit/s)...
Hash of data verified.
Compressed 163440 bytes to 85264...
Writing at 0x00020000... (16 %)
Writing at 0x0002af9f... (33 %)
Writing at 0x00030675... (50 %)
Writing at 0x00035dff... (66 %)
Writing at 0x0003e633... (83 %)
Writing at 0x000469a7... (100 %)
Wrote 163440 bytes (85264 compressed) at 0x00020000 in 2.5 seconds (effective 518.3 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
================================= [SUCCESS] Took 20.92 seconds =================================

I’m not really sure what’s going on here… any ideas? If I flash the basic hello_world example with idf.py like so:

idf.py -p /dev/tty.usbserial-320 flash

It works just fine. So it seems to me there is an issue with my PIO configuration.

Here is my platformio.ini:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = espidf
monitor_speed = 115200
monitor_filters = colorize
monitor_flags = --raw
monitor_rts = 0
monitor_dtr = 0
lib_compat_mode = off
build_flags = -DCONFIG_MBEDTLS_CMAC_C
build_type = debug
debug_tool = esp-prog
debug_speed = 500
debug_init_break =

[platformio]
description = Core firmware for ESP32 architecture.

Any ideas as to what might have happened here? I’ve tried erasing flash before uploading as well, with no luck. Here’s how I erased the flash:

esptool.py --chip esp32 --port /dev/tty.usbserial-320 erase_flash

Thanks in advance :slight_smile:

Is ESP-IDF 4.3.2 which was released 5 days ago in PlatformIO (Releases · platformio/platform-espressif32 · GitHub) the intended target framework version?

Using platform = espressif32@3.4.0 changes something?

Thank you for your quick response - no, that change makes no difference. Same issue:

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:7432
load:0x40078000,len:16576
load:0x40080400,len:4216
entry 0x400806e4
I (52) boot: ESP-IDF 4.3.1 2nd stage bootloader
I (52) boot: compile time 19:04:29
I (52) boot: chip revision: 1
I (56) boot_comm: chip revision: 1, min. bootloader chip revision: 0
I (63) boot.esp32: SPI Speed      : 40MHz
I (68) boot.esp32: SPI Mode       : DIO
I (72) boot.esp32: SPI Flash Size : 4MB
I (77) boot: Enabling RNG early entropy source...
I (82) boot: Partition Table:
I (86) boot: ## Label            Usage          Type ST Offset   Length
I (93) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (100) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (108) boot:  2 factory          factory app      00 00 00010000 00100000
I (116) boot: End of partition table
E (120) esp_image: image at 0x10000 has invalid magic byte (nothing flashed here?)
E (128) boot: Factory app partition is not bootable
E (134) boot: No bootable app partitions in the partition table
ets Jun  8 2016 00:22:57

I’ll note that I haven’t made any changes to IDF version before this issue appeared.

Also, turns out the IDF version on that hello_world example was:

$ idf.py --version
ESP-IDF v5.0-dev-1452-g93106c9348

Which raises the question… does flashing with esp-idf v5+ firmware cause breaking changes that make it impossible to revert to 4.3.x?

That would be baffling. The ESP32 has a read-only-memory with a first-stage bootloader (should be unmodifyable) that loads the second-stage bootloader from flash and executes it, which in turn executes the firmware. Everything starting from the second stage bootloader is on the flash chip which should be completely erasable and reprogrammable.

Execute the project task Advanced → Verbose Upload. What is the invocation for esptool.py?

Thank you for this explanation. I am still new to this architecture, coming over from nrf52.

Here is the invocation for esptool.py:

"/Users/ztaylor/.platformio/penv/bin/python" "/Users/ztaylor/.platformio/packages/tool-esptoolpy/esptool.py" --chip esp32 --port "/dev/cu.usbserial-120" --baud 460800 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 /Users/ztaylor/dev/core-esp32/.pio/build/esp32dev/bootloader.bin 0x10000 /Users/ztaylor/dev/core-esp32/.pio/build/esp32dev/partitions.bin 0x20000 .pio/build/esp32dev/firmware.bin

This looks all good. The bootlaoder is flashed at 0x1000, the partition table at 0x10000 and 0x10000 after that (0x20000) the firmware.bin is flashed. This flash command agrees with the partition table that the ESP32 reports.

Can you erase the chip again and program an Arduino blinky example on the chip? (platform-espressif32/examples/arduino-blink at develop · platformio/platform-espressif32 · GitHub). Does it also not boot?

I am unfamiliar with running Arduino code on this particular chip, so I couldn’t make it happy… I was able to get the esp-idf blink example to work, though:

> Executing task: platformio run --target upload <

Processing esp32dev (platform: espressif32; framework: espidf; board: esp32dev)
------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32dev.html
PLATFORM: Espressif 32 (3.5.0) > Espressif ESP32 Dev Module
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (esp-prog) External (esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES: 
 - framework-espidf 3.40302.0 (4.3.2) 
 - tool-cmake 3.16.4 
 - tool-esptoolpy 1.30100.210531 (3.1.0) 
 - tool-mkspiffs 2.230.0 (2.30) 
 - tool-ninja 1.9.0 
 - toolchain-esp32ulp 1.22851.191205 (2.28.51) 
 - toolchain-riscv32-esp 8.4.0+2021r2-patch2 
 - toolchain-xtensa-esp32 8.4.0+2021r2-patch2 
 - toolchain-xtensa-esp32s2 8.4.0+2021r2-patch2
Reading CMake configuration...
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 0 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Retrieving maximum program size .pio/build/esp32dev/firmware.elf
Checking size .pio/build/esp32dev/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   3.9% (used 12904 bytes from 327680 bytes)
Flash: [==        ]  15.8% (used 165913 bytes from 1048576 bytes)
Configuring upload protocol...
AVAILABLE: esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Auto-detected: /dev/cu.usbserial-120
Uploading .pio/build/esp32dev/firmware.bin
esptool.py v3.1
Serial port /dev/cu.usbserial-120
Connecting........_____.
Chip is ESP32-D0WD (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: e8:68:e7:30:af:f8
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Flash will be erased from 0x00001000 to 0x00007fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x00010000 to 0x00038fff...
Compressed 26272 bytes to 16027...
Writing at 0x00001000... (100 %)
Wrote 26272 bytes (16027 compressed) at 0x00001000 in 0.9 seconds (effective 239.9 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.1 seconds (effective 309.3 kbit/s)...
Hash of data verified.
Compressed 166304 bytes to 86341...
Writing at 0x00010000... (16 %)
Writing at 0x0001b2c7... (33 %)
Writing at 0x00020a4a... (50 %)
Writing at 0x00026377... (66 %)
Writing at 0x0002fb46... (83 %)
Writing at 0x00036f67... (100 %)
Wrote 166304 bytes (86341 compressed) at 0x00010000 in 2.6 seconds (effective 516.8 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
========================================== [SUCCESS] Took 19.99 seconds ==========================================

And from the monitor:

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:7580
ho 0 tail 12 room 4
load:0x40078000,len:14912
load:0x40080400,len:3688
entry 0x4008067c
I (29) boot: ESP-IDF 4.3.2 2nd stage bootloader
I (29) boot: compile time 19:59:39
I (29) boot: chip revision: 1
I (32) boot_comm: chip revision: 1, min. bootloader chip revision: 0
I (39) boot.esp32: SPI Speed      : 40MHz
I (43) boot.esp32: SPI Mode       : DIO
I (48) boot.esp32: SPI Flash Size : 4MB
I (52) boot: Enabling RNG early entropy source...
I (58) boot: Partition Table:
I (61) boot: ## Label            Usage          Type ST Offset   Length
I (69) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (76) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (84) boot:  2 factory          factory app      00 00 00010000 00100000
I (91) boot: End of partition table
I (95) boot_comm: chip revision: 1, min. application chip revision: 0
I (102) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=0726ch ( 29292) map
I (122) esp_image: segment 1: paddr=00017294 vaddr=3ffb0000 size=02990h ( 10640) load
I (126) esp_image: segment 2: paddr=00019c2c vaddr=40080000 size=063ech ( 25580) load
I (138) esp_image: segment 3: paddr=00020020 vaddr=400d0020 size=13a98h ( 80536) map
I (168) esp_image: segment 4: paddr=00033ac0 vaddr=400863ec size=04e9ch ( 20124) load
I (176) esp_image: segment 5: paddr=00038964 vaddr=50000000 size=00010h (    16) load
I (182) boot: Loaded app from partition at offset 0x10000
I (182) boot: Disabling RNG early entropy source...
I (199) cpu_start: Pro cpu up.
I (199) cpu_start: Starting app cpu, entry point is 0x40081354
I (0) cpu_start: App cpu up.
I (213) cpu_start: Pro cpu start user code
I (213) cpu_start: cpu freq: 160000000
I (213) cpu_start: Application information:
I (218) cpu_start: Project name:     espidf-blink
I (223) cpu_start: App version:      1
I (227) cpu_start: Compile time:     Feb  4 2022 19:59:02
I (234) cpu_start: ELF file SHA256:  d35e93bb4aa7f03a...
I (239) cpu_start: ESP-IDF:          4.3.2
I (245) heap_init: Initializing. RAM available for dynamic allocation:
I (252) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (258) heap_init: At 3FFB3268 len 0002CD98 (179 KiB): DRAM
I (264) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
I (270) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
I (277) heap_init: At 4008B288 len 00014D78 (83 KiB): IRAM
I (284) spi_flash: detected chip: generic
I (288) spi_flash: flash io: dio
I (292) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
Turning off the LED
Turning on the LED
<loop continues>

The only change I made from the base example was adding

monitor_filters = colorize
monitor_flags = --raw
monitor_rts = 0
monitor_dtr = 0

So I could see the boot information.

I am terribly confused as to why an example like this might work just fine, but my custom project fails. They have very similar setups… the main difference is build_type = debug, yes?

To add: I have also tried building in release mode. No luck.

You would just need to create a new project in the PIO Home wizard, use ESP32 Dev as the board and select Arduino as the framework, use the project environment switcher to switch to the new project and use the code from here (or slightly adapted to e.g. do a Serial.begin(9600); Serial.println("hello");).

Have you made modifications to the default sdkconfig via the menuconfig? Can you delete the sdkconfig.* files (or move them somewhere else) so that the defaults ones are regenerated and flash that?

That seems to have worked and gotten the project booting again!! But now I want to understand whygit to the rescue:

$ git diff --unified=0 sdkconfig.esp32dev 
diff --git a/sdkconfig.esp32dev b/sdkconfig.esp32dev
index a237891..e9cf1c2 100644
--- a/sdkconfig.esp32dev
+++ b/sdkconfig.esp32dev
@@ -66 +65,0 @@ CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0
-CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y
@@ -93,2 +92,2 @@ CONFIG_ESPTOOLPY_FLASHFREQ="40m"
-# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
-CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
+CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
+# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
@@ -97 +96 @@ CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
-CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
+CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
@@ -125 +124 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv"
-CONFIG_PARTITION_TABLE_OFFSET=0x10000
+CONFIG_PARTITION_TABLE_OFFSET=0x8000
@@ -462 +460,0 @@ CONFIG_ESP_SYSTEM_PD_FLASH=y
-# CONFIG_ESP_SYSTEM_FLASH_LEAKAGE_WORKAROUND is not set
@@ -582,4 +579,0 @@ CONFIG_FMB_PORT_TASK_PRIO=10
-# CONFIG_FMB_PORT_TASK_AFFINITY_NO_AFFINITY is not set
-CONFIG_FMB_PORT_TASK_AFFINITY_CPU0=y
-# CONFIG_FMB_PORT_TASK_AFFINITY_CPU1 is not set
-CONFIG_FMB_PORT_TASK_AFFINITY=0x0
@@ -595,2 +588,0 @@ CONFIG_FMB_TIMER_INDEX=0
-CONFIG_FMB_MASTER_TIMER_GROUP=0
-CONFIG_FMB_MASTER_TIMER_INDEX=0

Did I accidentally make a change in the menuconfig that changed any of these options? Would these options cause the issue I was seeing before deleting the sdkconfig.* files and recreating them?

…Is the new, refreshed sdkconfig deleting the 4MByte flash setting and setting 2Mbyte flash setting? That would be bad considering you have a 4Mbyte flash chip if

is correct.

Ah, yes - you’re right. There are two options I’d updated intentionally with menuconfig… the partition table offset (0x8000 → 0x10000), and the flash size (2MB → 4MB).

Annndd I think I’ve found the culprit/ Changing the partition table offset to 0x10000 again caused the boot failure. Changing it back to 0x8000 fixes it… does that make any sense? I don’t quite understand why that would cause a failure, it should just mean that the program goes to a different location, right?

Is there another configuration item where I need to tell the first-stage bootloader where to read the program from / what the offset is?

Setting the partition table offset to 0x8000 will cause the second-stage bootloader (which itself is a 0x1000) to attempt to read a partition table at 0x8000. Magically it still found a partition table there (remnants from a previous partition table flash?) and read that the application is at offset 0x10000, which however given the initial 0x8000 partition table offset ends up with a total offset of 0x18000. Since the application was flashed at 0x20000, it will find nothing there.

The first-stage bootlaoder (which in in static ROM) is not configurable, see documentation. It always loads the piece of code sitting at 0x1000 in flash. This is where the second stage bootloader comes in and tries to read the partition table to figure out which application to boot. You can control the partition table through the aforementioned menuconfig by giving it the filename of custom partition table file

which describes to the second-stage bootloader the layout of the flash, including the offset to the actual user application. See Espressif 32 — PlatformIO latest documentation.

Thanks for this great information - things are starting to make a little more sense.

Let’s see if I understand. With the default partition table:

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     ,        0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,

and partition table offset CONFIG_PARTITION_TABLE_OFFSET=0x10000, PIO uploads in the following manner:

Writing at 0x00001000... (50 %)
Writing at 0x00007bee... (100 %)
Wrote 28320 bytes (16900 compressed) at 0x00001000 in 0.9 seconds (effective 255.7 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00010000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00010000 in 0.1 seconds (effective 280.5 kbit/s)...
Hash of data verified.
Compressed 227728 bytes to 120235...
Writing at 0x00020000... (12 %)
Writing at 0x0002ce95... (25 %)
Writing at 0x00032afe... (37 %)
Writing at 0x000395bd... (50 %)
Writing at 0x0003f33c... (62 %)
Writing at 0x00046759... (75 %)
Writing at 0x0004fe87... (87 %)
Writing at 0x000559f6... (100 %)
Wrote 227728 bytes (120235 compressed) at 0x00020000 in 3.2 seconds (effective 567.8 kbit/s)...
Hash of data verified.

so we end up with the program at 0x20000. This agrees with the output of idf.py partition-table:

# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x11000,24K,
phy_init,data,phy,0x17000,4K,
factory,app,factory,0x20000,1M,

Though I’m not sure I understand why the nvs partition starts at 0x11000 yet esptool.py is writing at 0x10000…

This also doesn’t agree with the serial logs during boot:

I (82) boot: Partition Table:
I (86) boot: ## Label            Usage          Type ST Offset   Length
I (93) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (100) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (108) boot:  2 factory          factory app      00 00 00010000 00100000
I (116) boot: End of partition table
E (120) esp_image: image at 0x10000 has invalid magic byte (nothing flashed here?)
E (128) boot: Factory app partition is not bootable
E (134) boot: No bootable app partitions in the partition table

It looks like the bootloader is still looking for the nvs partition at 0x9000, as if the partition table had a 0x8000 offset, yes? This is after erasing flash, so there wouldn’t be any leftover memory from a previous flash.

The NVS partition is not written / uploaded by PlatformIO, it’s just a partition declaration for the firmware to put stuff into. The only flashed things are the 2nd stage bootloader, the partition table, and the firmware. Of course when uploading the firmware, to get to the absolute address of the firmware, one has to add up the partition table offset plus the offset mentioned for the factory,app. At absolute address 0x10000 is the partition table.

In my understanding the offset it to be understood as an additional offset to the partition table, not as an absolute address or offset from the start of flash.