SDK

Design rules

The nine rules every line of Kode Dot firmware follows, what checks each one, and the three places where "everything is an event" gives way.

What it is

The SDK follows nine rules, and each one has a check. The check is a script, a test at boot, an error at build time, or a way of writing the code that leaves no room for the mistake. A change that breaks a rule is wrong until someone shows the rule does not apply.

Read this page before you change SDK code.

The rules speak of Active Objects, owners, events, posts and workers. If any of those words is new to you, read "The ideas behind it" in sdk/overview first: it explains each one in plain words.

If you write an app, the include checks (rule 2) do not read your files, because the script scans the components/ folder of the SDK. Your Active Objects still follow rules 1, 3, 5 and 7.

How it is connected

flowchart LR
dev["You write code"]
subgraph pr["Before every merge"]
  script["tools/check_layers.sh"]
  build["Compiler<br/>#error, _Static_assert"]
  gates["check_ram.sh<br/>check_misra.sh"]
end
subgraph run["At boot"]
  owner["Every Active Object<br/>kaos_claim()"]
  table["kbsp_owners.c"]
end
dev --> script
dev --> build
dev --> gates
owner --> table
script -- "rule 2: includes" --> ok["exit 0"]
build -- "rule 8: #error" --> ok
gates -- "rule 8, style" --> ok
table -- "rule 1: two owners<br/>stop the boot" --> ok
class dev app
class owner ao
class table store
Where each rule is checked

Nothing can check rule 1 from outside, because a script does not run the system. Every Active Object calls kaos_claim() for its devices in its initial transition. A second claim on the same resource prints which two owners met and stops the boot with Q_ERROR().

The other rules are held by the compiler or by a script that runs before every merge (internals/test-pipeline). tools/check_layers.sh reads .c, .h, .cpp and .hpp files, so C++ code follows the same rules as C.

Specifications

The rules

The nine rules, with the check or the habit that holds each one.

#RuleHow it is held
1One owner per device. An Active Object claims its devices at startup. A bus is shared, a device is owned whole. Everyone else asks the owner by event. One gate breaks it, belowkbsp_claim() is an atomic compare-and-swap on one slot per resource. Two claimants stop the boot, naming both (sdk/kbsp)
2Each layer includes only the headers its place allows. ESP-IDF stops at kdrv, QP/C stops at kqp, pins and addresses live in kbsptools/check_layers.sh, 13 checks, below
3Everything is an event, with three exceptions. Requests wait in the queue of the ownerThe three exceptions are listed below. A fourth needs the same written justification. The boot gate is a documented break of rules 1 and 3, below, because no queue exists when it runs
4Public headers of khal and kdrv return kode_status_t, never esp_err_t. Some calls answer with a bool or a pointer. None leaks an ESP-IDF typeLayer check 1 keeps ESP-IDF headers out of everything above kdrv. No script looks for esp_err_t itself: review does, and no public header of khal or kdrv names it today
5Long work is cut into steps, runs at low priority, or both. One step of an Active Object stays short, because its queue has a fixed size and QP/C stops the system when an event does not fitAO_AppMgr writes an image in 32 KB chunks and waits 10 ms between them. kscreen arms its render tick as a one-shot after each pass, never as a periodic timer
6The system says why before it stops. AO_Power logs before the write that switches the board off, and AO_LowPower logs the state it enters and why before it stops the tick. A destructive action asks, then confirmsAO_Power logs ship mode: opening the BATFET before the write. AO_LowPower logs entering OFF (<why>) before deep sleep. The Turn off card of kodeOS asks first
7An event carries a copy, not a pointer into a table of the sender. A list travels as one event per entry and an end markerThree documented exceptions for data too large to copy: FlushEvt.pixels, StorageWriteEvt.data and DisplayGlyphsEvt.strip, the digits of the clock that AO_Display draws while the board is asleep. The comment of each says what keeps the memory alive
8Internal RAM is taken by name. The ESP32-P4 has 640 KB of it against 32 MB of PSRAM. A buffer is khal_alloc_psram(), never a static, unless it must be internaltools/check_ram.sh fails a data symbol of 2048 bytes or more in internal RAM that has no line in the project's budget file. #error lines in khal_system_esp.c, kdrv_radio.c, kdrv_ble.c and kdrv_net.c stop a build that lacks the board's memory options (sdk/kbsp)
9A worker runs one job and decides nothing. Waiting work leaves its Active Object for that Active Object's own worker task, which posts the result back as one KqpWorkerDoneEvtkqp_worker_submit() is the only way in. The state machine, timeouts, retries, order of jobs and the device stay in the Active Object

The layer checks

tools/check_layers.sh ends with exit code 0 when no check failed. Its comments number the checks 1, 2, 3, 4, 5, 5b, 5c, 6, 7, 8, 9, 10 and 11.

When you add an #include, look up its row.

CheckWhat it checksWhy
1esp_*, driver/, freertos/, soc/ and hal/ only in kbsp, khal, kdrv and klogAbove kdrv the code is portable C. soc/ and hal/ reach registers and do not start with esp_
2qpc.h only in kqpEveryone else gets QP/C through kqp.h, which kode_ao.h passes on to an app
3KBSP_PIN_*, KBSP_I2C_ADDR_* and KBSP_IOEXP_* defined only in kbspA pin or an address has one definition
4<dirent.h> and <sys/stat.h> only in kdrv_storage.cAO_Storage owns the media. A second file that opens files is a second reader of the card
5driver/rmt_* nowhereThe infrared block is wired to the ESP32-C5, so its RMT runs in the C5 firmware. kdrv_ir.c asks the C5 over the link
5besp_adc/* only in khal_exp_esp.c and kdrv_rfid_lf.cThe expansion header has one ADC table. The RFID driver reads its own fixed pin
5cdriver/ledc.h only in khal_pwm_esp.cThe LEDC timers are shared, so a second file could retune another user's timer
6esp-hosted, esp_wifi and the four NimBLE include roots only in kdrv_radio.c, kdrv_wifi.c, kdrv_ble.c and kdrv_c5.cThese are one RPC channel to one device, the C5, and AO_Radio owns it
7lwip/, ping/, esp_http_client, esp_tls, esp_crt_bundle, mqtt_client and the SNTP headers only in kdrv_net.cCheck 1 misses them, because their names do not start with esp_
8driver/i2s_*, esp_codec_dev, audio_codec_* and es8311_* only in kdrv_es8311.cThe I2S controller is bound to five pins that already have an owner
9lv_* calls only in kui and kscreenLVGL has no lock, because one task drives it
10miniz.h only in khal_zlib_esp.c and kdrv_kfw.cThe ROM inflater. One seam for whole streams, one more that feeds a chunk at a time
11cJSON.h only in kui_pack.ccJSON recurses as deep as the text nests, on the caller's stack. kui_pack.c bounds the depth first

kodeOS adds two checks of its own for the code above the SDK:

Its tools/check_layers.sh runs the SDK script after them, and prints skipped for the SDK part when the submodule is missing.

The three exceptions to rule 3

Each row is a place where a queue cannot serve, and what the exception requires.

ExceptionWhy a queue cannot serveWhat it needs
The end of a screen flushThe DSI interrupt that ends a flush can fire with the flash cache off (CONFIG_LCD_DSI_ISR_CACHE_SAFE), so it signals a semaphore instead of postingkhal_sem_signal() and everything the interrupt touches live in IRAM
The rescue path of the logInside an interrupt and inside Q_onError() stdio takes a recursive lock, and ESP-IDF answers that with a bare abort()Print with esp_rom_printf through khal_panic_print() or kode_log_emergency(), never printf or KLOG_x (sdk/klog)
The panel reset at a restartesp_restart() runs the shutdown handler in the task that asked for the restart, which can be esp-hosted's SDIO task. No queue is served in timekdrv_display registers one handler that drives the reset line low and touches nothing else

The gate at the top of kode_init()

With CONFIG_KODE_LOWPOWER, kode_init() calls kode_boot_gate() right after the log starts, before any Active Object exists. A boot that wakes from OFF must go on only when BOT is down, and any of the buttons on the expander raises the line that wakes the board from deep sleep. Only the expander's input register says which button it was. So the gate calls kdrv_tca6408a_init() and kdrv_tca6408a_get() itself: it breaks rule 1, because AO_IoExp owns that chip, and rule 3, because it reads the chip with no event. The reason is that no queue, no event pool and no AO_IoExp exist yet. If BOT is not down, the board goes back to deep sleep. The break ends when the function returns, and from then on AO_IoExp is the only code that touches the expander (sdk/kaos/lowpower).

A GPIO interrupt is not a fourth exception. Its callback posts a static event with kqp_post_from_isr() and nothing more.

A few calls also reach a driver from the task of the caller, with no event, because the owner has handed the thing over or the thing has no owner:

The style rules

The four rules about how the code is written.

RuleHow it is held
All C follows MISRA Ctools/check_misra.sh runs cppcheck over the compile database. tools/misra_deviations.txt lists the rules the code base deviates from, each with its reason. The project's baseline file counts the other findings per file, and a file may not exceed its count
Every public name has a Doxygen comment with @brief, @param and @returnWritten in the header. The contract lives in the .h
A number that means something is a named #define with a one-line commentReview. Layouts that two programs share carry sizes and offsets as names and a _Static_assert
The simplest solution wins. Two mechanisms for one job means one is a bugReview

Gotchas

Example code

Run the checks before you call a change done. Each step ends with a result you can read.

  1. From the root of the SDK, run tools/check_layers.sh. Every line starts with ok, and the last line reads All layer rules hold.
  2. Run tools/host_tests.sh. If you changed a header, delete the tests/host*/build folders first, because stale mocks fake compile errors.
  3. Build your firmware, then run tools/check_ram.sh build/<project>.elf <budget file>. It prints the internal RAM used against the budget and says all listed for the large statics.
  4. Run tools/check_misra.sh build/compile_commands.json <baseline file> components, from the folder the baseline's paths are relative to. A file with more findings than its baseline count fails.

In the SDK