SDK

khal

The hardware services the Kode Dot's drivers share: I2C, console, time, GPIO, PWM, expansion header, SD card, NVS, PSRAM and identity, behind kode_status_t.

What it is

khal is the hardware abstraction layer of the SDK. It holds the services that several drivers share, and it is where ESP-IDF stops: above kdrv, no file includes esp_*, driver/*, freertos/*, soc/* or hal/*. The public headers speak kode_status_t for a result and opaque handles for an object. None uses esp_err_t.

Two kinds of code live in khal and nothing else:

A module is a header and a backend. A call goes from the caller to the header, to the backend file khal_<module>_esp.c, and then to ESP-IDF. A chip driver calls khal_i2c_write_reg(), and the ESP-IDF call that does the work happens inside khal_i2c_esp.c.

How it is connected

flowchart TB
subgraph above["Layers that call khal"]
  kdrv["kdrv<br/>chip drivers"]
  kaos["kaos<br/>Active Objects"]
  kqp["kqp<br/>QP tick, workers"]
end
khal["khal<br/>16 modules"]
subgraph below["What khal uses"]
  kbsp["kbsp<br/>pins, addresses"]
  klog["klog<br/>the log"]
  kdefs["kdefs<br/>kode_status_t"]
  idf["ESP-IDF drivers"]
end
kdrv --> khal
kaos --> khal
kqp --> khal
khal --> kbsp
khal --> klog
khal --> kdefs
khal --> idf
class kaos ao
Where khal sits

khal requires kdefs, klog and kbsp, and all three are public, so an app that requires kode also sees the khal headers and kbsp.

The ESP-IDF drivers are private requirements: esp_adc, esp_driver_gpio, esp_driver_i2c, esp_driver_ledc, esp_driver_sdmmc, esp_driver_tsens, esp_driver_uart, esp_driver_usb_serial_jtag, esp_hw_support, esp_psram, esp_system, spi_flash, esp_timer, fatfs, nvs_flash, sdmmc and vfs.

A module that drives a device is not thread-safe. It has no lock, and two tasks must not call it at the same time. That is safe because each device has one owner, and only the owner calls the module (sdk/design-rules). The one exception is PWM, whose LEDC serves two owners, so khal_pwm has a lock.

A header never includes an ESP-IDF header. khal_i2c.h forward-declares ESP-IDF's two handle structures and hands out pointers to them, so a compiler can read every khal header without ESP-IDF and the host tests run on a computer. One vendor library, the codec component, needs the raw bus, and khal_i2c_bus() gives it out.

kode_init() brings khal up in a fixed order:

  1. kode_log_init()
  2. khal_heap_watch()
  3. khal_identity_init()
  4. khal_pwm_init()
  5. khal_exp_init()

The I2C bus is created by the first driver that needs it, in the initial transition of its Active Object. khal_nvs_init() runs on first use.

Two build details live in the component's CMakeLists.txt:

Specifications

The modules

One row per module, with the files that make it.

ModuleFilesWhat it does
i2ci2c/include/khal_i2c.h, i2c/src/khal_i2c_esp.cThe one shared bus, I2C0, and a handle per device
consoleconsole/include/khal_console.h, console/src/khal_console_esp.cConsole bytes in and out, the reader task, raw mode, and the dock port
timetime/include/khal_time.h, time/src/khal_time_esp.cTime since boot, the wall clock, a delay and a periodic timer
syncsync/include/khal_sync.h, sync/src/khal_sync_esp.cA mutex and a binary semaphore
gpiogpio/include/khal_gpio.h, gpio/src/khal_gpio_esp.cInput pins, output pins and one-shot pin interrupts
pwmpwm/include/khal_pwm.h, pwm/src/khal_pwm.c, pwm/src/khal_pwm_port.h, pwm/src/khal_pwm_esp.cThe LEDC by channel. khal_pwm.c is the portable table, khal_pwm_port.h the private seam it calls, khal_pwm_esp.c the ESP-IDF side
expexp/include/khal_exp.h, exp/src/khal_exp.c, exp/src/khal_exp_port.h, exp/src/khal_exp_esp.cThe expansion header J3 by position. Same three-file split as pwm; the port covers the ADC
clkclk/include/khal_clk.h, clk/src/khal_clk_esp.cWhich clock the RTC runs on, and what a clock really measures
sdmmcsdmmc/include/khal_sdmmc.h, sdmmc/src/khal_sdmmc_esp.cThe microSD: slot, FAT volume and raw sectors
systemsystem/include/khal_system.h, system/src/khal_system_esp.cRestart, panic, random numbers, heap, tasks, the supervisor task, PSRAM allocation and the start hook
temptemp/include/khal_temp.h, temp/src/khal_temp_esp.cThe die temperature
workerworker/include/khal_worker.h, worker/src/khal_worker_esp.cA task that runs one job at a time from a queue
nvsnvs/include/khal_nvs.h, nvs/src/khal_nvs_esp.cSmall persistent key and value storage
identityidentity/include/khal_identity.h, identity/src/khal_identity_esp.cThe board's serial, derived from its C5's MAC
crc32crc32/include/khal_crc32.h, crc32/src/khal_crc32_esp.cThe zlib CRC-32, from the ROM
zlibzlib/include/khal_zlib.h, zlib/src/khal_zlib_esp.cOne whole zlib stream inflated, with the ROM's decoder
forceforce/khal_task_psram.hForce-included into a library so that every xTaskCreate() in it takes its stack from PSRAM. Used by kui for LVGL

The component also carries Kconfig, idf_component.yml and CMakeLists.txt. idf_component.yml pins espressif/esp_tinyusb ==2.2.0, fetched only with the dock console.

i2c

I2C0 on SDA GPIO4 and SCL GPIO5 at 400 kHz, with the internal pull-ups on and a glitch filter of 7 clock cycles. Every transaction times out after 100 ms.

The ESP-IDF driver serialises transactions, so two owners that talk to two chips need no coordination. A run of several transactions with one chip stays safe because its owner does the whole run inside one step.

CallWhat it does
khal_i2c_init()Starts the bus. Idempotent, takes no lock. KODE_ERROR when the controller will not start
khal_i2c_bus()The raw bus handle for a vendor component. NULL before a successful init
khal_i2c_probe(addr, timeout_ms)Whether anything acknowledges a 7-bit address. It probes twice and counts the address only if both answer, because one probe of an empty address can come back answered (espressif/esp-idf#13547)
khal_i2c_add(addr, &dev)Attaches a device at KBSP_I2C_FREQ_HZ
khal_i2c_remove(dev)Detaches and frees a handle. Owed on every path that leaves after a successful add. NULL is accepted
khal_i2c_write_reg(dev, reg, val)Writes one register
khal_i2c_read_reg(dev, reg, buf, len)Reads len bytes from reg
khal_i2c_write_regs(dev, reg, buf, len)Writes 1 to KHAL_I2C_BURST_MAX (64) bytes in one transaction. A FIFO register that does not auto-increment takes them all at one address. KODE_DENIED for a bad length

console

The console is the native USB-Serial/JTAG of the USB-C port. With CONFIG_KHAL_CONSOLE_DOCK it is also a TinyUSB CDC-ACM device on the magnetic pads (GPIO26 and GPIO27), and nothing above khal tells the two ports apart. The backend also builds for a plain UART.

Writes go through stdout and share newlib's stream lock. Reads never go through stdin: a select() on a VFS descriptor walks the file-system table that unmounting a FAT volume frees. The reader asks the transport driver for its bytes.

The calls of the console module:

CallWhat it does
khal_console_init()Installs the transport driver and sets input to verbatim, so a binary payload keeps its CR bytes. With the dock option it starts the dock port
khal_console_start_reader(on_byte, prio)Starts the khal.rd task, a plain task and not an Active Object, which blocks on the console and calls on_byte for each byte. The callback posts an event and returns. KODE_ERROR for NULL, KODE_NO_RESOURCES when the task cannot be created
khal_console_raw_begin(pump, on_armed)Asks the reader to hand the port to pump instead of reporting bytes. KODE_DENIED for NULL or when raw mode is armed or requested
khal_console_read_raw(buf, want, timeout_ms)Reads up to want bytes. Short reads are normal. 0 on timeout, -1 for bad arguments or no transport
khal_console_raw_end()Returns the reader to reporting bytes
khal_console_write(buf, len)Writes to every port the console has

The sizes and times the console module is built with:

ConstantValue
RX ring of the driver8192 bytes, about 16 ms of a payload at the line rate of USB. A full ring drops bytes with no sign
TX ring of the driver512 bytes
Reader task stack4096 bytes
Reader timeout50 ms, the longest it waits before it looks for a raw-mode request
Dock write waitCONFIG_KHAL_CONSOLE_DOCK_TX_WAIT_MS, default 20, range 0 to 200. A host that does not drain makes the write drop the rest

Raw mode streams a firmware image in over the port that carried the command. One event per byte could not do it: a 500 KB image would be half a million events. The steps run in order:

  1. A caller requests raw mode.
  2. At its next pass the reader drains the pending input, promotes the request and calls on_armed.
  3. Only then does the board tell the host to start the payload.
  4. The reader calls pump, which reads chunks and ends the mode itself.

With the dock option, raw mode reads the one port that spoke last. The reader does the drain and the caller does not, because two readers of the driver's single-consumer ring would hang the console for good.

time

CallWhat it does
khal_time_us(), khal_time_ms()Time since boot. Monotonic; the millisecond count wraps after about 49 days
khal_delay_ms(ms)Sleeps and yields the CPU. Never from an Active Object
khal_time_wall_us()Microseconds since the Unix epoch, UTC. At or near 0 on a board never told the time
khal_time_wall_set(unix_us)Sets the wall clock. It rides on settimeofday(), which carries the RTC correction, so a date survives a reset
khal_time_wall_known()True when the reading is later than KODE_CLOCK_KNOWN_AFTER_S (2026-01-01). A threshold decides, so no flag has to agree with the clock
khal_timer_periodic(cb, arg, period_us)A periodic callback in an esp_timer task, until the next reset. It runs in task context, above every Active Object except AO_Rfid. kqp drives the QP tick with it. KODE_NO_RESOURCES or KODE_ERROR when the timer cannot be had

The board keeps UTC and no time zone. The constant lives in the header of klog because klog needs it for the timestamp and khal already depends on klog.

sync

A lock marks something that no Active Object owns, since an Active Object handles one event at a time. The mutex protects the few shared places:

The semaphore serves the screen flush and the waits of the blocking facade calls.

CallWhat it does
khal_mutex_create(), khal_mutex_lock(m), khal_mutex_unlock(m)A non-recursive mutex. create returns NULL if there is no memory. lock waits as long as it takes; there is no timed take
khal_sem_create()A binary semaphore, not signalled
khal_sem_wait_timeout(s, ms)The only wait, and it always has a limit. 0 takes a pending signal without waiting. False when the time ran out
khal_sem_signal(s)Signals from a task or from an interrupt, deciding by itself which with xPortCanYield(). It lives in IRAM, because the DSI interrupt calls it with the flash cache off

gpio

CallWhat it does
khal_gpio_input(pin, pull)A pin as an input with KHAL_GPIO_PULL_NONE, _UP or _DOWN
khal_gpio_output(pin, level)A push-pull output, driven to level at once
khal_gpio_write(pin, level), khal_gpio_level(pin)Drive an output; read any pin
khal_gpio_release(pin)Detaches the interrupt and returns the pin to high impedance with both internal pulls off. Safe on a pin that was never configured
khal_gpio_irq_attach(pin, kind, cb, arg)Arms KHAL_GPIO_IRQ_LOW_LEVEL, _HIGH_LEVEL or _ANY_EDGE. The pin must already be an input. KODE_DENIED for a NULL callback or a pin the P4 lacks
khal_gpio_irq_enable(pin)Arms the interrupt again after a trigger was served

A pin that does not exist answers KODE_DENIED, and a configuration the driver refuses answers KODE_ERROR.

The interrupt service is installed without the IRAM flag. While the cache is off, for example during an image write, ESP-IDF masks the interrupt, which arrives late and not lost: a level is still there and an edge is latched.

pwm

The LEDC has 8 channels (KHAL_PWM_CHANNELS) and 4 timers (KHAL_PWM_TIMERS). A timer sets frequency and resolution, a channel sets duty and drives one pin.

Two users need it, the expansion header and the 125 kHz RFID carrier, and one table hands out both. All four timers run from PLL_F80M (80 MHz), because ESP-IDF refuses a timer whose source differs from one in use.

CallWhat it does
khal_pwm_init()Creates the lock. A second call is a no-op
khal_pwm_open(&cfg, &ch)Starts a channel. Takes the lowest free channel. A shared request joins a timer at the same frequency and resolution, or takes a free one. own_timer always takes a free one. On a refusal nothing changes and ch is KHAL_PWM_NONE
khal_pwm_set(ch, hz, duty_x10)Changes frequency and duty, never another channel's. A new frequency joins a shared timer, retunes the channel's own timer if it is the only user, or moves to a free timer. KODE_NO_RESOURCES leaves the channel as it was
khal_pwm_pause(ch), khal_pwm_resume(ch)Stops and restarts the channel's own timer. Only for a channel opened with own_timer. They take no lock
khal_pwm_close(ch)Frees the channel, and its timer with its last user. The pin is left a plain GPIO driven low

The duty is in tenths of a percent, 0 to KHAL_PWM_DUTY_MAX (1000). KHAL_PWM_BITS_WIDEST asks for the widest resolution at the frequency: the largest n with the frequency times 2^n at most 80 MHz, so 1000 Hz gets 16 bits and 125 kHz gets 9.

A refusal is one of three statuses:

Each logs its reason at debug level on the tag khal.pwm.

exp

The expansion header by position, 1 to 14 (KHAL_EXP_POS_MIN to KHAL_EXP_POS_MAX), never by GPIO number. What each position can become is in KBSP_EXP[] (sdk/kbsp). A position is in exactly one mode.

The six modes:

ModeMeaning
KHAL_EXP_FREEHigh impedance, held by nobody
KHAL_EXP_INDigital input, with or without a pull
KHAL_EXP_OUTDigital output
KHAL_EXP_ADCAnalog input on ADC1
KHAL_EXP_PWMLEDC output, from khal_pwm
KHAL_EXP_LENTHanded to a peripheral driver under a tag

The calls of the exp module:

CallWhat it does
khal_exp_init()Puts every position FREE and high impedance, closing its mode and voiding its loans. Idempotent but not harmless on a table in use: the owner calls it at its own start only
khal_exp_input(pos, pull), khal_exp_output(pos, level), khal_exp_write(pos, level), khal_exp_level(pos)Digital use. input on an OUT position, or output on an ADC position, changes the mode. level answers false in any other mode
khal_exp_adc(pos), khal_exp_adc_read(pos, &raw, &mv, &cal)ADC1 one-shot, 12 bits, 12 dB attenuation. With three non-NULL pointers all three outputs are written on every path, refusals included. Without the chip's calibration scheme, mv scales the count to a nominal 3100 mV full scale. The scheme is created per channel
khal_exp_pwm(pos, hz, duty_x10)A PWM output, or a retune. hz 1 to 40000, duty 0 to 1000. A request that does not fit answers KODE_NO_RESOURCES and the position keeps its mode
khal_exp_irq_attach(pos, kind, cb, arg), khal_exp_irq_enable(pos)khal_gpio's contract, on an IN position
khal_exp_release(pos)Back to FREE. KODE_DENIED when lent
khal_exp_lend(pos[], n, tag, gpio[])Lends positions to a peripheral driver, all or none, writing back the GPIO numbers. Every position must be FREE
khal_exp_return(tag)Frees every position and controller lent under tag and returns the pins to high impedance. A tag that holds nothing is not an error
khal_exp_ctrl(bus, tag, &id)Takes the lowest free controller of that kind from KBSP_EXP_CTRL and writes IDF's number. Only the books: the caller installs ESP-IDF's driver. KODE_NO_RESOURCES when all are held
khal_exp_ctrl_info(idx, &out), khal_exp_info(pos, &out)What exp and ps print

Eight positions can be PWM at once, and four distinct frequencies, because the LEDC has eight channels and four timers. While the RFID field is up, its carrier holds one channel and one timer of its own, which leaves seven and three.

Every refusal logs its reason once at debug level on the tag khal.exp. khal_exp cannot tell who calls it. KBSP_RES_EXP is claimed by the console or by the facade, and that claim stops two owners from using one table.

clk

CallWhat it does
khal_clk_main_hz()The nominal frequency of the main crystal, the reference of every measurement. 0 if the chip reports none
khal_clk_slow_src()Which source the RTC slow-clock mux is physically switched to: RC, the 32.768 kHz crystal, the 32 kHz RC, or unknown
khal_clk_measure(src, cycles, &milli_hz)Counts the crystal or the slow RC against the main crystal. A busy wait of about 31 µs per cycle at 32.768 kHz, so 1024 cycles holds the core for about 31 ms. KODE_TIMEOUT when the source produces no edges
khal_clk_tree_hz(clk, &hz)Reads the CPU, APB, RTC slow or LP fast clock in hertz

The module has no call that asks whether the 32 kHz crystal is enabled. ESP-IDF's version reads a bit of a power domain, so it returns true with the crystal desoldered.

If the crystal does not start at boot, ESP-IDF tries three times, then switches to the internal RC with one warning before the console exists and no flag. The mux read and the measurement together show it. Everything is counted against the main crystal, so an error in that crystal cannot be seen from the board (sdk/kdrv/clocks).

sdmmc

The P4 has one SDMMC controller with two slots. Slot 0 is the microSD card and slot 1 is the link to the ESP32-C5.

CallWhat it does
khal_sdmmc_mount(width, max_khz, &card)Powers the IO rail (LDO channel KBSP_SD_LDO_CHAN, 10 ms to settle), identifies the card and mounts its FAT volume at /sdcard. Never formats. KODE_DENIED when something is mounted or attached
khal_sdmmc_unmount(), khal_sdmmc_mounted(), khal_sdmmc_mount_point()Unmount (safe with nothing mounted), query, and the path
khal_sdmmc_attach(width, max_khz, &card), khal_sdmmc_detach(), khal_sdmmc_attached()The card identified with no volume on it, for a computer that owns what is on it. A card cannot be mounted and attached at once
khal_sdmmc_set_hosted(on), khal_sdmmc_hosted()Whether the next mount reuses the controller esp-hosted owns. True at boot whenever esp-hosted is compiled in. Call with the card unmounted
khal_sdmmc_info(&total, &free)Size of the volume and free bytes. Asked every time
khal_sdmmc_format()A fresh empty FAT volume. See the gotcha
khal_sdmmc_read_sectors(first, count, dst), khal_sdmmc_write_sectors(first, count, src)Whole sectors, past the filesystem. A write on a mounted card overwrites what the filesystem holds without telling it

The volume keeps at most 5 files open and is made with 64 KB clusters.

khal_sdmmc_card_t is a copy of the card's CID, CSD, SCR and SSR registers and of the link as negotiated, taken once per mount, because ESP-IDF's sdmmc_card_t is a live handle and giving it out would put driver/sdmmc_host.h above khal.

The card's own supply is a pin that kdrv_storage switches before every mount (KBSP_PIN_SD_PWR). This module powers only the IO domain.

system

CallWhat it does
khal_restart()A clean reset through esp_restart(). otadata and the boot request stay as they were
khal_panic(why)Stops through the panic handler, leaving a core dump with why. Q_onError and hcf end here
khal_random()32 bits from the hardware generator. Not cryptography: it is fully seeded only once the radio or the SAR ADC runs
khal_panic_print(fmt, ...)Prints through the ROM with no lock, for a QP assert and an interrupt. Only %s %d %u %x %c
khal_heap_free(), khal_psram_free(), khal_psram_largest()Free bytes in the general heap (mostly PSRAM), in PSRAM, and the largest PSRAM block. Ask the last before a big allocation
khal_heap_internal_free(), khal_heap_internal_largest(), khal_heap_internal_low()The internal DMA-capable pool: free, largest block, and the lowest since boot (the sum of each region's own low mark)
khal_heap_watch(), khal_heap_alloc_failures()Counts every allocation the heap refuses and prints the first 4 with khal_panic_print(): size, capabilities and task. ps shows alloc_fail
khal_task_list(out, max, &total)A snapshot of the scheduler's tasks. Room for 32; a board with more gets no entries and total says why. The snapshot runs with interrupts off
khal_task_stack_free(), khal_task_name()The calling task's unused stack and its name. Both are safe where a snapshot is not; the name is safe from a critical section
khal_watch_start(pass, period_ms)Starts the supervisor task kqp.watch at the highest application priority, with a static 3072-byte stack in internal RAM, and subscribes it to the task watchdog set to panic. KODE_DENIED for a second call, NULL, a period under one tick or a build without the task watchdog
khal_chip_info(&info)Model, silicon revision (302 reads 3 and 2), flash size and PSRAM size, for the boot banner
khal_alloc_psram(align, size), khal_realloc_psram(p, size), khal_free(p)PSRAM only, zeroed on allocation, never a fallback to internal RAM. The realloc follows realloc() and does not zero new bytes

khal has no allocator that tries internal RAM first. The 640 KB of internal RAM are shared with every DMA buffer, interrupt and task stack, and an allocator that took it when it happened to fit would leave the board with none for the radio's HCI commands. A build that does not carry the board's memory policy stops with an #error in khal_system_esp.c (sdk/kbsp).

The start hook. The hook port_start_app_hook() stalls core 1 while core 0 starts the scheduler, then a one-shot task core1_go releases it.

Without it, core 1's wait loop races the stack fills of the new tasks and can raise a store fault on core 0 before app_main. A boot that faults never confirms a new OS slot. On one build 25 of 80 boots faulted without the hook and none of 97 with it.

worker

CallWhat it does
khal_worker_create(&w, name, stack_bytes, prio, depth, job_size, run, psram_stack)A task pinned to no core and a queue that copies job_size bytes per job, created once and never released. psram_stack puts the stack, queue and job copy in PSRAM, and the control block stays internal. KODE_DENIED for a NULL argument or a zero size or depth
khal_worker_submit(w, job)Copies a job into the queue. Never blocks. KODE_NO_RESOURCES when the queue is full
khal_worker_stack_free(w)The stack never used, at the lowest point so far
khal_worker_is_self(w)Whether the caller is the worker's own task

This is the task half. The Active Object half, KqpWorker, is in sdk/kqp. A worker knows nothing about events and decides nothing.

nvs

Small values in the nvs partition, behind one interface: strings, one byte, 64-bit values and blobs. Namespaces and keys have at most 15 characters. Every setter commits before it returns, and set_blob exists for records whose fields must agree, because one commit either lands or does not.

CallWhat it does
khal_nvs_init()Idempotent, and every other call runs it on first use. A partition of another NVS version or out of pages is erased whole rather than failing the boot
khal_nvs_get_str, _get_u8, _get_u64, _get_blobRead. KODE_DENIED when the key or the namespace is not there. That is the answer "nothing was stored"
khal_nvs_set_str, _set_u8, _set_u64, _set_blobWrite and commit. KODE_ERROR when the store refuses
khal_nvs_erase(ns, key)Missing is success
khal_nvs_key(ns, idx, out, cap)The name of stored key number idx, so a store can be listed. No order is promised, each key appears once across 0 to N-1 while nothing is written, and KODE_DENIED past the last. Listing a namespace costs O(n²). A name that does not fit is refused, never cut

identity

The serial is computed and never stored. It comes from the base MAC of the ESP32-C5, because the identity key and the certificate live on that chip. A C5 replaced in a repair gives the board a new serial. The MAC is cached in NVS (namespace kident, key c5mac) because the C5 is not always reachable.

CallWhat it does
khal_identity_init()Names the board from the cached MAC. KODE_ERROR when none was ever cached
khal_identity_set_mac(mac)Called by the owner of the C5 when the link comes up. Another MAC renames the board. KODE_DENIED for NULL or six zero bytes (an unburned eFuse); KODE_IO_ERROR when the cache could not be written, and the name lasts this boot
khal_identity_serial(), khal_identity_pretty()KODE plus 10 symbols plus 1 check symbol, and the printed form KODE-XXXX-XXXX-XXX. Empty string when unknown
khal_identity_valid(), khal_identity_mac(mac)Whether the board has a name, and the MAC behind it
khal_identity_encode(mac, out, cap)Pure encoder, public so a test can check it against a fixed answer

The encoding: the 6 MAC bytes, most significant first, are left-padded to 50 bits and read 5 bits at a time as 10 symbols of Crockford Base32 (no I, L, O or U), so the first symbol is always 0 to 7. One check symbol follows, Luhn mod 32 over the ten. It catches every single-symbol error and every swap of neighbours except 0 with Z.

KHAL_ID_LEN is 16 and KHAL_ID_PRETTY_LEN is 19, both with the terminator, and both pinned by _Static_assert to the length of KBSP_ID_PREFIX.

The C5 firmware builds the same serial from the same bytes for its certificate request, so the two encoders must stay identical. The serial identifies a board and proves nothing: it is advertised and printed, so anyone can claim it.

temp, crc32 and zlib

CallWhat it does
khal_temp_read(&deci_c)Die temperature in tenths of a degree Celsius, so 415 is 41.5 °C. The first call installs and starts the sensor, set up for -10 to 80 °C. It reads the chip, not the air. Takes no lock; AO_Console is its only caller in the SDK
khal_crc32(crc, buf, len)The reflected CRC-32 of zlib and Ethernet, from the ROM. Chainable: the result seeds the next chunk, starting from KHAL_CRC32_INIT (0). Over a whole payload it equals Python's zlib.crc32(data). NULL or empty input returns crc unchanged
khal_zlib_decompress(src, src_len, dst, dst_cap, &out_len)Inflates one whole zlib stream with the ROM's tinfl_decompress(). KODE_OK also means the Adler-32 matched. KODE_ERROR for a damaged or cut stream, trailing bytes or a bad checksum. KODE_DENIED for NULL, a zero length, or a result bigger than dst_cap. KODE_NO_RESOURCES when PSRAM has no room for the decoder's state, about 11 KB, taken and freed inside the call

The decoder runs with HAS_MORE_INPUT, so a stream cut short stops and does not decode zeros. AO_AppMgr, AO_Radio and AO_Storage checksum their transfers with khal_crc32(). kdrv_apps.c calls the ROM routine itself with the seed UINT32_MAX, and the ROM inverts the seed, so its values differ from this module's for the same bytes.

Gotchas

Example code

Log the CRC-32 of the standard test text, then the free PSRAM and the state of the internal pool. The CRC reads 0xCBF43926, the known check value of CRC-32.

/*
 * khal_probe.c - 2026-10-28 - from Kode with love
 *
 * Calls three khal services from an app: the CRC-32 of "123456789", the free
 * PSRAM, and the internal RAM pool. On a Kode Dot the log shows the check
 * value 0xCBF43926 of CRC-32, then two lines of byte counts for the PSRAM
 * and the internal pool.
 */
#include "kode.h"
#include "khal_crc32.h"
#include "khal_system.h"
 
#define PROBE_TAG "probe"
 
/* Bring the system up, then log what khal reports. */
void app_main(void)
{
    static const char sample[] = "123456789";
 
    kode_init();
 
    uint32_t crc = khal_crc32(KHAL_CRC32_INIT, sample, sizeof(sample) - 1U);
    KLOG_N(PROBE_TAG, "crc32 of %s is 0x%08X", sample, (unsigned)crc);
 
    KLOG_N(PROBE_TAG, "free PSRAM %u bytes, largest block %u bytes",
           (unsigned)khal_psram_free(), (unsigned)khal_psram_largest());
    KLOG_N(PROBE_TAG, "internal pool: %u free, %u at its lowest",
           (unsigned)khal_heap_internal_free(), (unsigned)khal_heap_internal_low());
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK