# 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.

| Record | Where | What it says | Owner |
| --- | --- | --- | --- |
| `otadata` | partition `otadata`, `0x1B000` | Which kodeOS slot starts | AO_Boot. Only a kodeOS update writes it |
| The boot request | sector at offset `0x2000` of the `apps` region | Which installed app starts, and for how many resets | AO_Boot writes it; the bootloader override advances it |
| The mailbox | partition `app_pending`, `0x1A00000` | An image a computer left for kodeOS to adopt | AO_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

```mermaid
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 IO_MUX clock goes back to the crystal.
- Every GPIO interrupt a previous firmware left armed is disarmed.
- The panel's reset line is pulsed low for 2 ms.

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.

```mermaid
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.

| Name | Type | SubType | Offset | Size | Holds |
| --- | --- | --- | --- | --- | --- |
| `nvs` | data | nvs | `0xB000` | `0x10000` (64 KB) | Settings of kodeOS and the Arduino `Preferences` of a sketch |
| `otadata` | data | ota | `0x1B000` | `0x2000` (8 KB) | The two entries that say which slot starts |
| `phy_init` | data | phy | `0x1D000` | `0x1000` (4 KB) | Radio calibration |
| `factory` | app | factory | `0x20000` | `0x400000` (4 MB) | The rescue copy of kodeOS |
| `ota_0` | app | ota_0 | `0x420000` | `0x400000` (4 MB) | An update slot of kodeOS |
| `ota_1` | app | ota_1 | `0x820000` | `0x400000` (4 MB) | An update slot of kodeOS |
| `coredump` | data | coredump | `0xC20000` | `0x20000` (128 KB) | The last panic, in ELF format |
| `apps` | data | `0x40` | `0xC40000` | `0xBC0000` (11.75 MB) | Directory, boot request and installed apps (`internals/apps-region`) |
| `storage` | data | fat | `0x1800000` | `0x200000` (2 MB) | The internal FAT volume, wear-levelled |
| `app_pending` | data | `0x41` | `0x1A00000` | `0x600000` (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.

| Rule | Why |
| --- | --- |
| `apps` starts at `0xC40000` and its base never moves | Installed apps keep their addresses across a table change |
| `app_pending` is always at `0x1A00000` | Every host tool and every `esptool` line quotes the address |
| Every image in `apps` starts on a 64 KB boundary | `CONFIG_MMU_PAGE_SIZE_64KB`: the MMU maps flash in pages of that size |
| Code runs from `apps` across the 16 MB mark | `CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH`: the cache maps 32-bit addresses |
| `coredump` sits below 16 MB | The panic handler writes it with the cache off |
| Each slot is 4 MB | A 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 slot | The last 4 KB sector of the mailbox holds the intent word |
| `factory` is never an OTA destination | `esp_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`.

| State | Word | Written by |
| --- | --- | --- |
| `EMPTY` | `0xFFFFFFFF` | Erased flash |
| `NEW` | `0xAAAAAAAA` | AO_Boot: the launcher, `app_run` and install-and-launch. One run |
| `ALWAYS` | `0x55555555` | AO_Boot, for mailbox adoption only. A sticky run, not started yet |
| `ARMED` | `0x00005555` | The bootloader, on the first boot of an `ALWAYS` request |
| `TRIED` | `0x00000000` | The 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:

| Request | What the bootloader does |
| --- | --- |
| `NEW` | Verify the image, write `TRIED`, jump |
| `ALWAYS` | Verify the image, write `ARMED`, jump. The reset cause is not read |
| `ARMED`, and it stands | Verify the image, jump. Nothing is written, so the next reset asks again |
| `ARMED`, and it does not stand | Write `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.

| Cause | Value | Who produces it |
| --- | --- | --- |
| `RESET_REASON_CORE_USB_UART` | `0x17` | A host opens the USB-Serial/JTAG port, or esptool's hard reset |
| `RESET_REASON_CORE_USB_JTAG` | `0x16` | OpenOCD through the USB bridge |
| `RESET_REASON_CPU_JTAG` | `0x18` | OpenOCD's `reset halt` |
| `RESET_REASON_CORE_SW` | `0x03` | OpenOCD's first reset, and the ESP-IDF brownout interrupt, which ends in a software reset |
| `RESET_REASON_CPU_SW` | `0x0C` | `esp_restart()`, a panic's reboot, and every launch and home gesture of kodeOS |
| `RESET_REASON_CORE_DEEP_SLEEP` | `0x05` | A 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 board | One-shot request (`NEW`, then `TRIED`) | Mailbox request (`ARMED`) |
| --- | --- | --- |
| A program opens the port | kodeOS | The same app |
| The debugger resets the chip | kodeOS | The same app |
| `kode_exit_to_os()`, `esp_restart()`, a panic, BOT held | kodeOS | The same app |
| The ESP-IDF brownout interrupt | kodeOS | The same app |
| A deep-sleep wake | kodeOS | The same app |
| BOT and LEFT together (power-on reset) | kodeOS | kodeOS |
| A hardware watchdog | kodeOS | kodeOS |
| A hardware brownout reset | kodeOS | kodeOS |
| A new image waits in the mailbox | kodeOS | kodeOS |

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

| Signal | What AO_Boot does |
| --- | --- |
| `BOOT_OS_SWITCH_SIG` | Refuses 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_SIG` | Writes 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_SIG` | Publishes `BOOT_STATE_SIG` and logs the image in the inactive slot |
| `BOOT_EXIT_REQ_SIG` | A plain reset after the leave. Nothing is written |
| `HOME_SIG` and the hold | In 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.

| Address | What | Used by |
| --- | --- | --- |
| `0x0` | A merged image starts here | Full install |
| `0x2000` | `bootloader.bin` | Recovery |
| `0x8000` | `partition-table.bin` | Recovery |
| `0x1B000` | `ota_data_initial.bin` | Recovery |
| `0x20000` | `kodeos.bin` | Recovery |
| `0xC40000` | `apps.bin`, the apps region with the welcome app installed | Factory pack |
| `0x1800000` | `storage.bin`, the FAT with Kody's pack and the C5 image | Factory pack |
| `0xC20000` | The `coredump` partition | Crash read-back |
| `0x1A00000` | The mailbox | App push, OS update |
| `0x1FFF000` | The mailbox's last sector, the OS intent word `KOTA` | OS update through the mailbox |

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

| Way | What it does |
| --- | --- |
| Full install | Erases the whole chip and writes one merged image at `0x0` |
| Recovery | Writes 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 app | One 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 mailbox | Writes 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

> **WARNING**
>
> **After a mailbox upload, opening the serial port starts the app again, not kodeOS.** The adopted image holds an `ARMED` request, and the port opening is a reset on the keep list. The console that answers is the app's. Hold BOT and LEFT together for about one second to clear the request, or read `boot_info` first to see what runs.

> **WARNING**
>
> **`idf.py flash` right after a sketch starts the sketch, not the kodeOS you flashed.** The flash never writes the apps region, so the armed request survives, and esptool's final reset is a USB reset. BOT and LEFT together, or a power cycle, clears it.

> **WARNING**
>
> **A board whose partition table is not at `0x8000` cannot be moved by an update.** `install dst=os` writes a slot and `otadata`, and the new image looks for the table at `0x8000`, finds none and never confirms. The bootloader then rolls back to the other `otadata` entry or to `factory`. One full cable flash (`idf.py -p <port> flash`) is the whole migration.

> **DANGER**
>
> **A full install erases everything on the chip.** The host erases the flash before it writes, so settings, Bluetooth bonds, Wi-Fi credentials and every installed app go. Use recovery when you want to keep them. If the install stops between the erase and the write, the board is blank and recoverable: connect the cable and run both again.

## 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

- [`bootloader_components/main/bootloader_start.c`](https://github.com/kodediy/KodeOS/blob/main/bootloader_components/main/bootloader_start.c): the bootloader override, about 300 lines.
- [`components/kaos/boot/src/boot_ao.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/boot/src/boot_ao.c): AO_Boot, the owner of `otadata` and the boot request.
- [`components/kbsp/partitions.csv`](https://github.com/kodediy/kode-sdk/blob/main/components/kbsp/partitions.csv): the partition table.
