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
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.
| # | Rule | How it is held |
|---|---|---|
| 1 | One 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, below | kbsp_claim() is an atomic compare-and-swap on one slot per resource. Two claimants stop the boot, naming both (sdk/kbsp) |
| 2 | Each layer includes only the headers its place allows. ESP-IDF stops at kdrv, QP/C stops at kqp, pins and addresses live in kbsp | tools/check_layers.sh, 13 checks, below |
| 3 | Everything is an event, with three exceptions. Requests wait in the queue of the owner | The 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 |
| 4 | Public 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 type | Layer 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 |
| 5 | Long 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 fit | AO_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 |
| 6 | The 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 confirms | AO_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 |
| 7 | An event carries a copy, not a pointer into a table of the sender. A list travels as one event per entry and an end marker | Three 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 |
| 8 | Internal 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 internal | tools/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) |
| 9 | A 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 KqpWorkerDoneEvt | kqp_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.
- Lines that look like prose (a comment starting with
*or//) are skipped. - The vendored
qpcandrfalfolders are skipped.
When you add an #include, look up its row.
| Check | What it checks | Why |
|---|---|---|
| 1 | esp_*, driver/, freertos/, soc/ and hal/ only in kbsp, khal, kdrv and klog | Above kdrv the code is portable C. soc/ and hal/ reach registers and do not start with esp_ |
| 2 | qpc.h only in kqp | Everyone else gets QP/C through kqp.h, which kode_ao.h passes on to an app |
| 3 | KBSP_PIN_*, KBSP_I2C_ADDR_* and KBSP_IOEXP_* defined only in kbsp | A pin or an address has one definition |
| 4 | <dirent.h> and <sys/stat.h> only in kdrv_storage.c | AO_Storage owns the media. A second file that opens files is a second reader of the card |
| 5 | driver/rmt_* nowhere | The 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 |
| 5b | esp_adc/* only in khal_exp_esp.c and kdrv_rfid_lf.c | The expansion header has one ADC table. The RFID driver reads its own fixed pin |
| 5c | driver/ledc.h only in khal_pwm_esp.c | The LEDC timers are shared, so a second file could retune another user's timer |
| 6 | esp-hosted, esp_wifi and the four NimBLE include roots only in kdrv_radio.c, kdrv_wifi.c, kdrv_ble.c and kdrv_c5.c | These are one RPC channel to one device, the C5, and AO_Radio owns it |
| 7 | lwip/, ping/, esp_http_client, esp_tls, esp_crt_bundle, mqtt_client and the SNTP headers only in kdrv_net.c | Check 1 misses them, because their names do not start with esp_ |
| 8 | driver/i2s_*, esp_codec_dev, audio_codec_* and es8311_* only in kdrv_es8311.c | The I2S controller is bound to five pins that already have an owner |
| 9 | lv_* calls only in kui and kscreen | LVGL has no lock, because one task drives it |
| 10 | miniz.h only in khal_zlib_esp.c and kdrv_kfw.c | The ROM inflater. One seam for whole streams, one more that feeds a chunk at a time |
| 11 | cJSON.h only in kui_pack.c | cJSON 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:
lv_*only incomponents/kgui.cJSON.hnowhere.
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.
| Exception | Why a queue cannot serve | What it needs |
|---|---|---|
| The end of a screen flush | The 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 posting | khal_sem_signal() and everything the interrupt touches live in IRAM |
| The rescue path of the log | Inside 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 restart | esp_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 time | kdrv_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 network one-shots (
kode_net_ping(),kode_http_get()) - the card on loan after
kode_sd_lend() - the audio stream after
kode_audio_stream_open() - the display flush wait of the screens
The style rules
The four rules about how the code is written.
| Rule | How it is held |
|---|---|
| All C follows MISRA C | tools/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 @return | Written in the header. The contract lives in the .h |
A number that means something is a named #define with a one-line comment | Review. 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 bug | Review |
Gotchas
Example code
Run the checks before you call a change done. Each step ends with a result you can read.
- From the root of the SDK, run
tools/check_layers.sh. Every line starts withok, and the last line readsAll layer rules hold. - Run
tools/host_tests.sh. If you changed a header, delete thetests/host*/buildfolders first, because stale mocks fake compile errors. - Build your firmware, then run
tools/check_ram.sh build/<project>.elf <budget file>. It prints the internal RAM used against the budget and saysall listedfor the large statics. - 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
tools/check_layers.sh: the thirteen include checks, each with the reason in its comment.components/kbsp/owners: the ownership table that holds rule 1.tools/check_ram.sh: the internal RAM gate for rule 8.