kodeOS internals

Building and flashing

How kodeOS builds into an image, where the board settings come from, and the two ways to flash a Kode Dot: esptool or OpenOCD.

What it is

kodeOS is an ESP-IDF project. idf.py build at the root of the repository produces build/kodeos.bin, and idf.py flash writes it to a Kode Dot over the USB-C cable. The first build of any project, and the generic flash, are on build/setup, build/first-build and build/flash. This page covers what is particular to the kodeOS tree: how the root build file is wired, which file holds which setting, the storage image, and the JTAG route.

There are two independent ways to write the flash. One is esptool over the serial port. The other is OpenOCD over JTAG. Both use the same USB-C cable, because the ESP32-P4 has a USB-Serial/JTAG peripheral that presents a serial port and a JTAG interface together. Use the serial way unless your editor is set to flash through JTAG.

How it is connected

The root CMakeLists.txt does four jobs before ESP-IDF takes over. It adds the SDK's components, reads two default files, sets the version and the chip, and stages the FAT image that goes into the storage partition.

flowchart LR
subgraph settings["Settings"]
  board["kbsp<br/>sdkconfig.board"] --> cfg["sdkconfig<br/>generated"]
  defaults["sdkconfig.defaults"] --> cfg
end
subgraph firmware["Firmware"]
  comps["sdk/components"] -- "EXTRA_COMPONENT_DIRS" --> bin["build/kodeos.bin"]
end
subgraph fatimg["FAT image"]
  pack["kui/kody/pack"] --> stage["build/storage_stage"]
  img["storage_image"] --> stage
  stage -- "fatfs_create_<br/>spiflash_image" --> fat["build/storage.bin"]
end
cfg --> bin
class bin,fat store
From the sources to the images

The SDK is a git submodule at sdk/, with the ESP32-C5's firmware nested inside it. Fresh clones need git submodule update --init --recursive. components/ in the kodeOS tree holds only kgui, the screens that sit on top of the SDK.

The board facts (flash size, PSRAM mode, cache, console, the SDIO wiring to the C5) live in sdkconfig.board next to the partition table in sdk/components/kbsp, so every project reads one copy. See sdk/kbsp.

Specifications

The commands

CommandWhat it does
idf.py buildBuilds build/kodeos.bin, the bootloader, the partition table and build/storage.bin
idf.py -p PORT flashWrites the bootloader at 0x2000, the partition table at 0x8000, otadata at 0x1B000 and kodeOS at 0x20000. Skips images unchanged since the last flash
idf.py -p PORT flash -aWrites every image, changed or not. A blank or erased chip needs it
idf.py -p PORT storage-flashReplaces the whole storage partition with build/storage.bin
tools/jtag_flash.sh [build-dir]Writes every image of the build over JTAG with one OpenOCD that exits afterwards. The build directory defaults to build
tools/check_layers.shThe include rules of every layer. Must exit 0 before a change is done
tools/host_tests.shThe SDK's host suites; see internals/host-tests

PORT is the serial port of the Kode Dot. On macOS it looks like /dev/cu.usbmodem*. idf.py flash compares each image with the *_flashed.bin files that the previous flash left in the build directory, verifies the result, and falls back to a full write when the check fails.

The root build file

ThingValue
EXTRA_COMPONENT_DIRSsdk/components/. The folder sdk/c5/ is an ESP-IDF project, not a component, so it must stay out of any scanned directory
SDKCONFIG_DEFAULTSsdk/components/kbsp/sdkconfig.board first, then the root sdkconfig.defaults. Later files win
PROJECT_VER00.00.23 unless -DPROJECT_VER=... is on the command line. This is the version app_list, info and the boot banner report. A release passes the version of its tag
IDF_TARGETesp32p4, set before project()
Partition tableCONFIG_PARTITION_TABLE_CUSTOM_FILENAME="sdk/components/kbsp/partitions.csv" in the root sdkconfig.defaults
Storage stagebuild/storage_stage, made at configure time: Kody's pack from sdk/components/kui/kody/pack/ into kode/, then storage_image/ without its README.md and *.commit files
Stampstorage_image/c5_slave.commit, when present, is copied to build/ and stays out of the FAT
storage.binfatfs_create_spiflash_image(storage ...), not flashed by idf.py flash

A changed pack or image file configures again by itself. A pack regenerated by kodygen.py reaches the image on the next configure.

The settings files

FileWhat it isWhat you do
sdkconfig.boardThe Kode Dot's settings, shared by every projectNever edit it, never copy it
sdkconfig.defaultsThis project's defaultsEdit it
sdkconfigGenerated from the two aboveDelete it to apply a changed default

A value already in the generated sdkconfig beats a new default, every time. A project spells out two board facts itself, because ESP-IDF needs them before it can fetch the shared file:

SettingWhereValue
CONFIG_PARTITION_TABLE_CUSTOM_FILENAMEsdkconfig.defaultsRoot: sdk/components/kbsp/partitions.csv. An app next to its SDK submodule: ../sdk/components/kbsp/partitions.csv. An SDK example: managed_components/kodediy__kbsp/partitions.csv
set(IDF_TARGET esp32p4)CMakeLists.txt, before project()Without it ESP-IDF guesses esp32 and caches the guess in CMakeCache.txt

The storage image

ThingValue
Partitionstorage, FAT, 0x1800000, 2 MB, wear levelled. About 2,016 KB are usable
Ships withkode/ (Kody's pack, under 100 KB) and c5_slave.bin (the release's C5 image, about 1,310 KB)
Bench filesAny other file in storage_image/. A release refuses to package one
Git*.bin, *.kfw and *.commit in storage_image/ are ignored, so the folder looks empty on GitHub
c5_slave.binBuilt by tools/build_c5.sh from sdk/c5/ with ESP-IDF v6.0.2
*.kfwBuilt by tools/mkkfw.py from the .bin of the same name
kodeos.binDoes not go in the volume: at about 2.7 MB it does not fit, even alone

A new kodeOS reaches a board in one of four ways:

See internals/bench-tools and internals/command-set.

Flashing over JTAG

tools/jtag_flash.sh does three things, in order.

StepWhat happens
Free the adapterWhatever listens on port 3333 owns the JTAG interface. The script prints that process, sends kill, then kill -9, and aborts with exit 2 if the port stays held
Read the imagesOffsets and file names come from build/flasher_args.json, in ascending offset order, so a moved partition table needs no change in the script. Each image becomes one program_esp <file> <offset> verify
Run and leaveOne OpenOCD with board/esp32p4-builtin.cfg (override with OPENOCD_CFG; add a scripts folder with OPENOCD_SCRIPTS), then reset run and shutdown

Exit codes:

A real flash of a 1.25 MB image takes about 13 s, and a no-op verify about 1 s. JTAG and serial are separate paths: one can work while the other fails.

The serial port

FactConsequence
The Kode Dot re-enumerates after every resetThe port name changes. Identify the board by its MAC address with python -m esptool --chip esp32p4 -p PORT --no-stub read-mac
Opening the console port resets the boardClearing DTR and RTS before the open does not prevent it. Anything that builds state must run inside one session
A busy port is almost always a monitor still openlsof /dev/cu.usbmodem*, kill the holder, retry. macOS has a tty.* and a cu.* node for each device, and a program holding one makes the other fail with EBUSY

Gotchas

Example code

The commands in the order you run them, from the root of the kodeOS repository. Each step ends in something you can see.

  1. Load ESP-IDF v6.1 in the shell, for example . ~/.espressif/v6.1/esp-idf/export.sh. The path depends on how you installed it.
  2. Fetch the SDK and the C5 firmware once: git submodule update --init --recursive. sdk/c5/CMakeLists.txt now exists.
  3. Build: idf.py build. It ends with the size of build/kodeos.bin.
  4. Find the port with ls /dev/cu.usbmodem*, then flash: idf.py -p PORT flash. It ends with one Hash of data verified per image.
  5. Seed the storage volume when you need Kody's pack and the C5 image on the board: idf.py -p PORT storage-flash.
  6. To use JTAG instead of the serial steps, run tools/jtag_flash.sh. It prints flashed over JTAG; nothing left listening.
  7. Check the layers before you call a change done: tools/check_layers.sh exits 0.

In the SDK