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 touch controller on the Kode Dot

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 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:

Specifications

The board

ThingValue
Address0x15 (KBSP_I2C_ADDR_TOUCH)
IdentityRegisters 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
FingersOne. The frame carries 12 bits per axis
InterruptDSI_TP_INT to expander pin P6 (KBSP_IOEXP_BIT_TP_INT), active low
ResetGPIO7 (KBSP_PIN_DSP_TP_RST), shared with the panel
Poll while pressed10 ms (KODE_TOUCH_POLL_MS)
Defaults measured on a Kode Dotirq_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 sheetNone 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.

RegFieldWhat it is
0x01gestureLast gesture: 0 none, 1 up, 2 down, 3 left, 4 right, 5 click, 0x0B double click, 0x0C long press. Read only
0x02fingersFingers down, 0 or 1. Read only
0x03event [7:6], x [3:0]Event flag: 0 down, 1 up, 2 contact. High nibble of x. Read only
0x04xLow byte of x. Read only
0x05y [3:0]High nibble of y. Read only
0x06yLow byte of y. Read only
0xA7chip_idThe part. Read only, cached
0xA8proj_idThe panel supplier's project. Read only, cached
0xA9fw_verThe controller's firmware. Read only, cached

The configuration registers

The registers 0xEC to 0xFE: gestures, the interrupt, low-power scanning and sleep.

RegFieldWhat it is
0xECmotion_maskGestures reported: en_dclick [0] double click, en_con_ud [1] continuous up/down slide, en_con_lr [2] continuous left/right slide
0xEDirq_pulse_widthHow long INT stays low, in 0.1 ms
0xEEnor_scan_perScan period while awake, in 10 ms
0xEFmotion_sl_angleThe angle that separates a horizontal slide from a vertical one
0xF0, 0xF1lp_scan_raw1Low-power scan, channel 1, raw, high byte then low. Read only
0xF2, 0xF3lp_scan_raw2Channel 2, same layout. Read only
0xF4lp_auto_wake_timeHow often the low-power scan recalibrates, in minutes
0xF5lp_scan_thLow-power wake threshold
0xF6lp_scan_winLow-power scan window
0xF7lp_scan_freqLow-power scan frequency
0xF8lp_scan_idacLow-power scan current
0xF9auto_sleep_timeIdle seconds before the chip sleeps and stops answering
0xFAirq_ctlWhat 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
0xFBauto_resetSeconds of a touch with no gesture before the chip resets itself; 0 off
0xFClong_press_timeSeconds a press is held before it is a long press; 0 off
0xFDio_ctlsoft_rst [2] hold the chip in reset, iic_od [1] open-drain I2C pins, en_1v8 [0] I/O at 1.8 V
0xFEdis_auto_sleepNon-zero keeps the chip awake. Writing 0 puts it to sleep

The driver

CallWhat 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

FileWhat it does
kdrv/cst820/include/kdrv_cst820.hThe field enum, the field description, the calls
kdrv/cst820/src/kdrv_cst820.cThe field table, bring-up, the frame burst, get, set and dump
kaos/touch/src/touch_ao.cAO_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