kodeOS internals

Boot model

How a Kode Dot picks the firmware to start after a reset: the partition table, otadata, the boot request, the mailbox and the bootloader override.

What it is

After every reset the Kode Dot has to decide which firmware to run: a kodeOS image, or an app. Three records in the flash decide, and each has exactly one owner.

RecordWhereWhat it saysOwner
otadatapartition otadata, 0x1B000Which kodeOS slot startsAO_Boot. Only a kodeOS update writes it
The boot requestsector at offset 0x2000 of the apps regionWhich installed app starts, and for how many resetsAO_Boot writes it; the bootloader override advances it
The mailboxpartition app_pending, 0x1A00000An image a computer left for kodeOS to adoptAO_AppMgr reads it at boot

An app never writes otadata. That is why any reset brings the board back to kodeOS: the next boot finds no request that still stands and falls to the slot otadata names. A sketch uploaded through the mailbox is the exception, and only for the resets listed under Specifications.

kodeOS keeps three slots for itself: factory, ota_0 and ota_1. factory is the rescue copy, written only over the cable. The other two take updates in turn. The apps region is not a slot, but it also holds firmware, and the bootloader starts an app from it in place, without copying it.

How it is connected

flowchart TD
rom["ROM bootloader"] --> bl["Bootloader override"]
bl --> q{"Boot request stands?<br/>NEW, ALWAYS or ARMED"}
q -- "yes" --> app["App in the<br/>apps region"]
q -- "no" --> od{"otadata names<br/>a slot?"}
od -- "yes" --> slot["ota_0 or ota_1"]
od -- "no" --> factory["factory"]
class rom chip
class app app
class slot,factory store
What the bootloader decides at every reset

The bootloader is the standard ESP-IDF one with four additions, in bootloader_components/main/bootloader_start.c. Three run at every boot:

The fourth is the question in the diagram. If no request stands, the standard path runs.

The request changes before the jump

The bootloader moves NEW to TRIED and ALWAYS to ARMED with one 4-byte write, then jumps. A one-shot app that crashes or resets finds TRIED at the next boot, so the standard path starts kodeOS. The same order means a failed write blocks the jump: a request that cannot advance would otherwise jump on every reset.

An ARMED request is judged on every reset

It stands only if the reset cause is one a working app or its host produces (the list under Specifications) and the mailbox is empty. If it does not stand, the bootloader writes TRIED and starts kodeOS without verifying the app. The first boot of an ALWAYS request never consults the list, because kodeOS launches an app with a software reset and that reset must not cancel its own launch.

sequenceDiagram
autonumber
participant OS as kodeOS<br/>AO_Boot
participant BL as Bootloader<br/>override
participant App as App
OS->>OS: write the request: NEW, or ALWAYS from the mailbox
OS->>BL: reset, after the leave
BL->>BL: check magic, CRC, bounds and esp_image_verify
BL->>BL: NEW becomes TRIED, ALWAYS becomes ARMED
BL->>App: jump in place
App->>BL: any reset
Note over BL: TRIED, so otadata names kodeOS
BL->>OS: start kodeOS
OS->>OS: req_tried() gives boot reason app, then req_clear()
One launch and the way home

AO_Boot decides what kodeOS does about it

Its behaviour depends on the firmware's role, a Kconfig choice (CONFIG_KODEOS_BOOT_ROLE_OS or _APP). In an app (the default) it claims otadata and the request sector and writes neither, so a firmware that crashes on its first line still returns to kodeOS. In kodeOS it runs three steps at every boot:

    1. If the running image is still PENDING_VERIFY, kdrv_slot_confirm_self() clears the mark. This is mandatory: an unconfirmed image is rolled back at the next boot, and esp_ota_begin() refuses to open an update while the running slot is unconfirmed.
    2. It calls kdrv_apps_init() and then kdrv_apps_req_tried(). A TRIED request proves the bootloader jumped, so the reason is app and the request is cleared. Any other request still present is cleared with a warning, because the bootloader's retire write did not stick, and the reason is cold.
    3. If kodeOS runs from factory while otadata is not blank, a kodeOS update died before it confirmed. The reason is unexpected and AO_Boot clears otadata, so the board stays on factory.

The reset reason of the chip, read after those steps, classes how the app before it ended: panic, watchdog, brownout, or clean for everything else. An unknown reset reason never reads as a crash.

A restart leaves in an order

Every deliberate restart (an app launch, a kodeOS update, kode_exit_to_os(), the bottom button held) goes through the same short leave in AO_Boot:

  1. AO_Net stops the Kode channel cleanly (at most 200 ms).
  2. AO_Radio hangs up Bluetooth and parks the ESP32-C5 so the Wi-Fi link survives the jump (at most 250 ms).
  3. The console drains for 150 ms.
  4. The chip resets.

Each step ends on its owner's answer or on its bound, so a silent neighbour never holds the jump. A second restart request during the leave is dropped.

The C5 across the jump

The C5 has its own reset line, so a P4 restart does not reset it. The firmware that leaves writes a handover note into a register of the low-power domain, only after the C5 confirmed it parked. The firmware that starts reads the note at its first link, takes the Wi-Fi association over and erases the note. After a panic, a watchdog or a power cut there is no note, and the C5 is reset to a fresh start.

Specifications

The partition table

The bootloader is at 0x2000 and the table at 0x8000 (CONFIG_PARTITION_TABLE_OFFSET, where the Arduino core also looks for it). The file is components/kbsp/partitions.csv. nvs starts at 0xB000, so the two sectors after the table stay free and a table that grows never moves it.

NameTypeSubTypeOffsetSizeHolds
nvsdatanvs0xB0000x10000 (64 KB)Settings of kodeOS and the Arduino Preferences of a sketch
otadatadataota0x1B0000x2000 (8 KB)The two entries that say which slot starts
phy_initdataphy0x1D0000x1000 (4 KB)Radio calibration
factoryappfactory0x200000x400000 (4 MB)The rescue copy of kodeOS
ota_0appota_00x4200000x400000 (4 MB)An update slot of kodeOS
ota_1appota_10x8200000x400000 (4 MB)An update slot of kodeOS
coredumpdatacoredump0xC200000x20000 (128 KB)The last panic, in ELF format
appsdata0x400xC400000xBC0000 (11.75 MB)Directory, boot request and installed apps (internals/apps-region)
storagedatafat0x18000000x200000 (2 MB)The internal FAT volume, wear-levelled
app_pendingdata0x410x1A000000x600000 (6 MB)The mailbox. Never booted from

From factory to the end of the flash each area starts where the one before ends, up to app_pending at 28 MB. The flash is 32 MB.

RuleWhy
apps starts at 0xC40000 and its base never movesInstalled apps keep their addresses across a table change
app_pending is always at 0x1A00000Every host tool and every esptool line quotes the address
Every image in apps starts on a 64 KB boundaryCONFIG_MMU_PAGE_SIZE_64KB: the MMU maps flash in pages of that size
Code runs from apps across the 16 MB markCONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH: the cache maps 32-bit addresses
coredump sits below 16 MBThe panic handler writes it with the cache off
Each slot is 4 MBA table cannot be changed over the air, so the slots have room to grow
An image through the mailbox is at most 0x5FF000 bytes, and an OS image also fits its 4 MB slotThe last 4 KB sector of the mailbox holds the intent word
factory is never an OTA destinationesp_ota_begin() refuses it, so nothing running on the board can overwrite the rescue copy

The coredump= field of info says whether a dump waits. owner=kodeos means the dump's ELF SHA-256 matches the running image. owner=other is any other dump: a sketch's, another build's, or one this build cannot decode (the Arduino core's libraries checksum with CRC32 and kodeOS checks SHA-256 only, so it prints task=- pc=-). Read a dump with idf.py coredump-info.

The boot request

kapps_req_t is 20 bytes: magic ("KRUN", 0x4E55524B), offset, size, crc, then state. The CRC covers magic to size. state sits after it so the bootloader can advance it with a 4-byte write that only clears bits. The byte formats are on sdk/kdrv/apps.

StateWordWritten by
EMPTY0xFFFFFFFFErased flash
NEW0xAAAAAAAAAO_Boot: the launcher, app_run and install-and-launch. One run
ALWAYS0x55555555AO_Boot, for mailbox adoption only. A sticky run, not started yet
ARMED0x00005555The bootloader, on the first boot of an ALWAYS request
TRIED0x00000000The bootloader, when it retires a request

The bootloader reads the request and checks, in order, the magic, the CRC (esp_rom_crc32_le), a state of NEW, ALWAYS or ARMED, and the geometry (kapps_req_bounds_ok()). A request that fails the first three is ignored without a write. A request that fails the geometry check is ignored too and stays where it is, so AO_Boot clears it when kodeOS starts. Then one of five things happens:

RequestWhat the bootloader does
NEWVerify the image, write TRIED, jump
ALWAYSVerify the image, write ARMED, jump. The reset cause is not read
ARMED, and it standsVerify the image, jump. Nothing is written, so the next reset asks again
ARMED, and it does not standWrite TRIED and start kodeOS. No image verification
Any state, image fails esp_image_verify()Write TRIED and start kodeOS

If the write that advances the state fails, the bootloader does not jump. If the array of images it may load is full, it returns to the standard path. The standard loader falls back to the earlier images (ota_1, ota_0, factory) when the jump itself fails.

Resets that keep an ARMED request

The list is closed (reset_keeps_request() is a switch whose default answers no). The numbers are the ROM's soc_reset_reason_t, not the esp_reset_reason_t that kodeOS logs at boot. The request also needs an empty mailbox: the bootloader reads the first word of app_pending and counts a bare image (0xE9) or a .kfw container (0x3157464B) as full, and a failed read counts as full.

CauseValueWho produces it
RESET_REASON_CORE_USB_UART0x17A host opens the USB-Serial/JTAG port, or esptool's hard reset
RESET_REASON_CORE_USB_JTAG0x16OpenOCD through the USB bridge
RESET_REASON_CPU_JTAG0x18OpenOCD's reset halt
RESET_REASON_CORE_SW0x03OpenOCD's first reset, and the ESP-IDF brownout interrupt, which ends in a software reset
RESET_REASON_CPU_SW0x0Cesp_restart(), a panic's reboot, and every launch and home gesture of kodeOS
RESET_REASON_CORE_DEEP_SLEEP0x05A deep-sleep wake

Any other cause retires the request: power-on (0x01, which BOT and LEFT held together produce), every hardware watchdog, the hardware brownout reset 0x0F, the glitch detectors, a CPU lockup, and a number nobody named. A full mailbox retires it as well, so a second upload works without touching the board.

What resets the boardOne-shot request (NEW, then TRIED)Mailbox request (ARMED)
A program opens the portkodeOSThe same app
The debugger resets the chipkodeOSThe same app
kode_exit_to_os(), esp_restart(), a panic, BOT heldkodeOSThe same app
The ESP-IDF brownout interruptkodeOSThe same app
A deep-sleep wakekodeOSThe same app
BOT and LEFT together (power-on reset)kodeOSkodeOS
A hardware watchdogkodeOSkodeOS
A hardware brownout resetkodeOSkodeOS
A new image waits in the mailboxkodeOSkodeOS

kodeOS's own OFF state ends in deep sleep too, and its wake is the row of the deep-sleep wake with no request standing: a normal boot. Before AO_Log starts, kode_boot_gate() sends the board back to deep sleep when a button other than BOT woke it (sdk/kaos/lowpower).

AO_Boot

SignalWhat AO_Boot does
BOOT_OS_SWITCH_SIGRefuses when the inactive slot is absent or holds no valid image. Otherwise kdrv_otadata_select_slot() verifies the image and writes otadata, then the leave and a reset
BOOT_APP_RUN_SIGWrites a fresh request: NEW for KBOOT_RUN_ONCE, ALWAYS for KBOOT_RUN_STICKY. These are the only two states kodeOS writes. If the write fails it logs and does not reset
BOOT_INFO_REQ_SIGPublishes BOOT_STATE_SIG and logs the image in the inactive slot
BOOT_EXIT_REQ_SIGA plain reset after the leave. Nothing is written
HOME_SIG and the holdIn an app with CONFIG_KODEOS_HOME_HOLD (default on), BOT held 600 ms publishes HOME_SIG and resets 200 ms later. The app may ask for more time with kode_home_defer(), up to 3000 ms from the hold, and say go with kode_home_now()

boot_info answers one record: running=<slot> addr=0x... size_kb=<n> otadata=blank|slot reason=cold|app|unexpected app_end=none|clean|panic|watchdog|brownout. After a clean install, running=factory, otadata=blank and app_list answers nothing.

Flash addresses a host tool uses

A host tool contains these as constants and never computes them.

AddressWhatUsed by
0x0A merged image starts hereFull install
0x2000bootloader.binRecovery
0x8000partition-table.binRecovery
0x1B000ota_data_initial.binRecovery
0x20000kodeos.binRecovery
0xC40000apps.bin, the apps region with the welcome app installedFactory pack
0x1800000storage.bin, the FAT with Kody's pack and the C5 imageFactory pack
0xC20000The coredump partitionCrash read-back
0x1A00000The mailboxApp push, OS update
0x1FFF000The mailbox's last sector, the OS intent word KOTAOS update through the mailbox

Four ways to put firmware on the board use these addresses:

WayWhat it does
Full installErases the whole chip and writes one merged image at 0x0
RecoveryWrites the same four regions with no erase. It brings back the bootloader and factory, blanks otadata, and leaves apps, storage and app_pending alone
Push an appOne write to the mailbox. At the next boot AO_AppMgr verifies it, installs it (evicting the oldest app if the region is full, replacing a same-named app) and launches it with a sticky request
An OS update through the mailboxWrites the 4-byte word KOTA at 0x1FFF000 first, then the image. Without the word the image installs as an app

Nothing infers the intent from the image's bytes, because the author of an image controls its name field. Every consume of the mailbox erases both sectors, so an old word never describes a later upload.

Gotchas

Example code

Push a sketch or an app through the mailbox and come back to kodeOS. Each step ends in something you can see.

    1. Write the image to the mailbox: python -m esptool --chip esp32p4 -p <port> write-flash 0x1A00000 app.bin. The board resets, kodeOS adopts the image and the app starts. The glass shows the app.
    2. Open a serial monitor on the port. The port opening resets the board, and the same app starts again, with its console answering.
    3. Hold BOT and LEFT together for about one second. The board resets as a power-on and the request is retired.
    4. kodeOS counts the boot as the return from an app, so Apps is on the glass with the app in focus, listed first (internals/gui/launcher).

TBD: these steps have not been run on a Kode Dot from this page.

In the SDK