SDK
CST820 touch
The capacitive touch controller of a Kode Dot: its registers, how it sleeps, how a finger becomes a panel pixel, and the driver and owner over it.
What it is
The glass of a Kode Dot senses fingers. Behind the front surface a grid of thin electrodes feels the small electric change a fingertip causes, and a CST820 reads that grid. The chip is Hynitron's, from the CST816 family. It reports one finger at a time as two 12-bit coordinates, plus the last gesture it recognised: a slide, a click, a long press.
The chip counts in its own units. Two layers turn its numbers into something an app can use. The driver kdrv_cst820 reads the chip and decodes it. The owner AO_Touch clamps the point to the glass and publishes a TouchEvt in panel pixels. The controller's pixels are the panel's, and nothing is calibrated.
The CST820 sits on the panel module from supplier A. The module from supplier B carries a different controller, a CST916E at 0x5A with another register map, which kodeOS does not drive. A unit with that module boots without touch. The display driver finds a panel by asking for the CST820 at 0x15, so a module without one is also a unit without a screen: the display driver reports no panel and the board runs with its console only.
How it is connected
flowchart LR chip["CST820"] -- "I2C 0x15" --> bus["shared I2C bus"] chip -- "INT, DSI_TP_INT" --> p6["TCA6408A<br/>P6"] p6 --> ioexp["AO_IoExp"] ioexp -- "IOEXP_INPUT_SIG" --> touch["AO_Touch"] touch -- "kdrv_cst820_read()" --> chip gpio7["GPIO7"] -- "reset, shared<br/>with the panel" --> chip touch -- "TOUCH_EVT_SIG" --> gui["AO_Gui or<br/>your AO"] class chip chip class p6 chip class ioexp ao class touch ao class gui app
The chip answers at 0x15 (KBSP_I2C_ADDR_TOUCH) on the shared I2C bus, which runs at 400 kHz. The chip moves to the next register by itself after each byte, so the driver reads the six-byte frame as one transaction and the three identity bytes as another.
Shared wires
Two wires are shared with other parts:
- The reset is GPIO7, the same net as the panel's reset (
DSP_&_TP_RST). The display driver pulses it while it starts the panel, so the touch controller always starts after the panel. AO_Touch subscribes toDISPLAY_STATE_SIGand callskdrv_cst820_init()only when that event says the panel is up. No code above the driver resets the chip alone. The only software control is thesoft_rstfield, and nothing uses it. - The interrupt is pin P6 of the expander. No pin of the ESP32-P4 carries it. P6 is an input and the pull-up is on the module. AO_IoExp reads the expander on the expander's own interrupt and publishes all eight pin levels as
IOEXP_INPUT_SIG. Seesdk/kdrv/tca6408a.
The owner
AO_Touch owns the chip (KBSP_RES_TOUCH) at priority 14. Touch_idle waits for the panel and refuses field requests. Touch_armed and Touch_tracking share a parent, Touch_up, which serves field requests and the guided test. Touch_armed reads nothing at all, so an idle board costs no I2C. When P6 falls it moves to Touch_tracking, which polls every 10 ms (KODE_TOUCH_POLL_MS) until the finger lifts.
It polls instead of following edges because some CST816-family firmwares pulse INT once per report and others once per touch-and-lift, and polling only while a finger is down is correct for both. An edge with no finger behind it gets three polls (TOUCH_EMPTY_POLLS) and then the state falls back to armed, so a glitch cannot start a poll that never ends.
AO_Touch publishes only on change: a press, a release, or movement of more than 2 px (TOUCH_MOVE_EPSILON) from the last published point, not from the last sample. That keeps a slow drag from walking its reference under the threshold and ending far from where the GUI last heard. A failed read counts as a release with the last position, so the GUI is never left pressed.
No calibration
The controller's pixels are the panel's. The point AO_Touch publishes is the one the chip reported, pinned to the 410 by 502 glass. No scale or offset is stored. The guided test, test component=tp, asks for two taps, one on each side of the glass, and judges each within 64 px (KODE_TOUCH_TEST_TOL_PX). It gives each tap 20 s (KODE_TOUCH_TEST_STEP_MS) and three tries. It proves that the panel answers where a finger lands and fits nothing.
Sleep
The chip sleeps, and the driver is written around it. After auto_sleep_time seconds without a finger the chip enters a low-power scan and stops answering the bus: every read and write gets a NAK. A finger or a reset of the panel wakes it, and a reset puts every register back to the supplier's defaults. The software draws three consequences:
- The driver reads the identity once, right after the panel's reset when the chip is known to be awake, and serves
chip_id,proj_idandfw_verfrom that copy. A check that read the chip at check time could fail on a unit that happens to be asleep. - A failed read is an error. It is never a value and never a release. The driver logs the bus state on its edges only, one line when the chip stops answering and one when it answers again, because the owner polls every 10 ms while a finger is down.
dis_auto_sleepis a bench setting and the board's rest. Only AO_Touch's rest writes it (withauto_sleep_timeandirq_ctl), and it writes the values it read at bring-up back on wake (sdk/kaos/touch).
Specifications
The board
| Thing | Value |
|---|---|
| Address | 0x15 (KBSP_I2C_ADDR_TOUCH) |
| Identity | Registers 0xA7, 0xA8, 0xA9. chip_id reads 0xB7 (KDRV_CST820_CHIP_ID) on this part; kdrv_cst820_init() accepts any 0xB0 to 0xBF, the family nibble, and refuses the rest |
| Fingers | One. The frame carries 12 bits per axis |
| Interrupt | DSI_TP_INT to expander pin P6 (KBSP_IOEXP_BIT_TP_INT), active low |
| Reset | GPIO7 (KBSP_PIN_DSP_TP_RST), shared with the panel |
| Poll while pressed | 10 ms (KODE_TOUCH_POLL_MS) |
| Defaults measured on a Kode Dot | irq_ctl 0x60 (touch and change pulses), dis_auto_sleep 1, auto_sleep_time 0, irq_pulse_width 1; every register answers on an idle screen |
| Data sheet | None for the CST820 itself. The map is the public map of the CST816 family; a bench test shows which configuration registers this part answers. TBD: what a write to the low-power and gesture registers does on this part |
The frame and identity registers
The driver gives every register in the two register tables as a field, whole and bit by bit. [7:6] is bits 7 to 6. This table lists the registers that carry a touch and the chip's identity.
| Reg | Field | What it is |
|---|---|---|
0x01 | gesture | Last gesture: 0 none, 1 up, 2 down, 3 left, 4 right, 5 click, 0x0B double click, 0x0C long press. Read only |
0x02 | fingers | Fingers down, 0 or 1. Read only |
0x03 | event [7:6], x [3:0] | Event flag: 0 down, 1 up, 2 contact. High nibble of x. Read only |
0x04 | x | Low byte of x. Read only |
0x05 | y [3:0] | High nibble of y. Read only |
0x06 | y | Low byte of y. Read only |
0xA7 | chip_id | The part. Read only, cached |
0xA8 | proj_id | The panel supplier's project. Read only, cached |
0xA9 | fw_ver | The controller's firmware. Read only, cached |
The configuration registers
The registers 0xEC to 0xFE: gestures, the interrupt, low-power scanning and sleep.
| Reg | Field | What it is |
|---|---|---|
0xEC | motion_mask | Gestures reported: en_dclick [0] double click, en_con_ud [1] continuous up/down slide, en_con_lr [2] continuous left/right slide |
0xED | irq_pulse_width | How long INT stays low, in 0.1 ms |
0xEE | nor_scan_per | Scan period while awake, in 10 ms |
0xEF | motion_sl_angle | The angle that separates a horizontal slide from a vertical one |
0xF0, 0xF1 | lp_scan_raw1 | Low-power scan, channel 1, raw, high byte then low. Read only |
0xF2, 0xF3 | lp_scan_raw2 | Channel 2, same layout. Read only |
0xF4 | lp_auto_wake_time | How often the low-power scan recalibrates, in minutes |
0xF5 | lp_scan_th | Low-power wake threshold |
0xF6 | lp_scan_win | Low-power scan window |
0xF7 | lp_scan_freq | Low-power scan frequency |
0xF8 | lp_scan_idac | Low-power scan current |
0xF9 | auto_sleep_time | Idle seconds before the chip sleeps and stops answering |
0xFA | irq_ctl | What pulses INT: en_test [7] periodic test pulse, en_touch [6] every report, en_change [5] a change of state, en_motion [4] a gesture, once_wlp [0] one pulse per long press |
0xFB | auto_reset | Seconds of a touch with no gesture before the chip resets itself; 0 off |
0xFC | long_press_time | Seconds a press is held before it is a long press; 0 off |
0xFD | io_ctl | soft_rst [2] hold the chip in reset, iic_od [1] open-drain I2C pins, en_1v8 [0] I/O at 1.8 V |
0xFE | dis_auto_sleep | Non-zero keeps the chip awake. Writing 0 puts it to sleep |
The driver
| Call | What it does |
|---|---|
kdrv_cst820_init() | Starts the bus, probes 0x15 up to three times 20 ms apart, reads the three identity bytes in one burst and keeps them. Refuses a first byte outside 0xB0 to 0xBF with KODE_ERROR. Writes nothing. Must run after the panel starts |
kdrv_cst820_present() | true after a successful init |
kdrv_cst820_identity(&chip, &proj, &fw) | The cached bytes. KODE_DENIED before init. Uses no bus |
kdrv_cst820_read(&pressed, &x, &y) | The six-byte frame in one burst. x and y are written only while pressed, in the controller's own pixels, uncalibrated. KODE_ERROR and nothing written when the chip does not answer |
kdrv_cst820_get(f, &value) | One field. Two registers in one burst for the wide ones. value is untouched on a failed read, so a sleeping chip never produces a value that looks like a setting |
kdrv_cst820_set(f, value) | One field. KODE_DENIED before any bus access for a read-only field or a value out of range. Otherwise read, replace the field's bits, write, read back and compare those bits only: another bit of the register may be a status bit the chip changes on its own. KODE_ERROR when the chip did not keep it |
kdrv_cst820_field_info(f), kdrv_cst820_field_by_name(name, &f) | Name, unit, range and step of a field; look a field up by the name the console prints. Neither uses the bus |
kdrv_cst820_dump() | Logs the identity, the frame and the block 0xEC to 0xFE, read in two bursts. A sleeping chip gives one line instead of twenty |
The driver takes no decisions. Whether to let the chip sleep belongs to AO_Touch.
The files
| File | What it does |
|---|---|
kdrv/cst820/include/kdrv_cst820.h | The field enum, the field description, the calls |
kdrv/cst820/src/kdrv_cst820.c | The field table, bring-up, the frame burst, get, set and dump |
kaos/touch/src/touch_ao.c | AO_Touch: states, the poll, publishing, field requests, the guided test |
Gotchas
Example code
Log every touch sample as panel pixels. With the app running, touch the glass: each press, move and release prints a line such as x=205 y=251 pressed=1, and the release repeats the last position with pressed=0.
/*
* touch_log.c - 2026-10-28 - from Kode with love
*
* An Active Object of its own subscribes to the touch samples that AO_Touch
* publishes and logs each one on the serial console, in panel pixels. Touch
* the glass of the Kode Dot and lines such as "x=205 y=251 pressed=1" appear.
*/
#include "kode_ao.h"
static QEvtPtr l_queue[KQP_AO_QUEUE_LEN];
static StackType_t l_stack[KQP_AO_STACK_BYTES / sizeof(StackType_t)];
static QActive l_ao;
/* The one state: log every touch sample. */
static QState touch_running(QActive * const me, QEvt const * const e)
{
(void)me;
if (e->sig == TOUCH_EVT_SIG) {
TouchEvt const * const t = (TouchEvt const *)e;
KLOG_I("touch", "x=%d y=%d pressed=%d", t->x, t->y, (int)t->pressed);
return Q_HANDLED();
}
return Q_SUPER(&QHsm_top);
}
/* The initial transition: subscribe, then run. */
static QState touch_initial(QActive * const me, void const * const par)
{
(void)par;
QActive_subscribe(me, TOUCH_EVT_SIG);
return Q_TRAN(&touch_running);
}
/* Bring the system up, then start the Active Object. */
void app_main(void)
{
kode_init();
QActive_ctor(&l_ao, Q_STATE_CAST(&touch_initial));
QACTIVE_START(&l_ao, KODE_PRIO_USER_LOW, l_queue, Q_DIM(l_queue),
l_stack, sizeof(l_stack), (void *)0);
}Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.
In the SDK
components/kdrv/cst820: the driver and its field table.kdrv_cst820.h: every field and call.components/kaos/touch: AO_Touch, the owner.