SDK

AO_Touch

Owns the touch controller of a Kode Dot: how a finger becomes a TOUCH_EVT_SIG, the polling rules, the two-target test and the rest of the controller.

What it is

AO_Touch turns a finger on the glass into events. It publishes each press, each release and each move of more than 2 px as TOUCH_EVT_SIG, with the position pinned to the glass. AO_Gui in kodeOS and the render pass of sdk/kscreen in an app both feed on it.

It publishes changes only, so a finger resting on the screen costs nobody any work. The position is the controller's own, clamped to the 410 by 502 glass: nothing is calibrated, and no setting is stored. A guided test of two taps proves that the panel answers where a finger lands.

The console reaches every register of the chip through AO_Touch (tp). The chip, its registers and its habit of falling asleep are on sdk/kdrv/cst820.

How it is connected

flowchart LR
chip["CST820<br/>INT line"] -- "falls" --> tca["TCA6408A<br/>pin P6"]
tca --> ioexp["AO_IoExp"]
ioexp -- "IOEXP_INPUT_SIG" --> ao["AO_Touch"]
ao -- "kdrv_cst820_read()<br/>I2C 0x15" --> chip
ao -- "TOUCH_EVT_SIG" --> gui["AO_Gui<br/>or kscreen"]
disp["AO_Display"] -- "DISPLAY_STATE_SIG" --> ao
class chip,tca chip
class ao,ioexp,disp ao
From a finger to TOUCH_EVT_SIG

Priority and the interrupt line

AO_Touch runs at priority 14 (KODE_PRIO_TOUCH), with a 16-event queue and a 4096-byte stack. The polls are short and need low jitter, so the interface follows the finger. It claims KBSP_RES_TOUCH, and kode_init() starts it right before AO_Display.

The controller's interrupt line does not reach a pin of the P4. It goes to P6 of the TCA6408A expander, which AO_IoExp owns. AO_IoExp reads the expander on its own interrupt and publishes all eight pins as IOEXP_INPUT_SIG. AO_Touch subscribes to that signal and reads nothing from the controller until P6 falls.

States

stateDiagram-v2
[*] --> Touch_idle: claim, subscribe
Touch_idle --> Touch_armed: DISPLAY_STATE_SIG up, CST820 answers
Touch_armed --> Touch_tracking: IOEXP_INPUT_SIG, P6 changed and low
Touch_tracking --> Touch_tracking: TOUCH_POLL_SIG, sample and publish on change
Touch_tracking --> Touch_armed: release published, or 3 empty polls
AO_Touch states

Idle

The touch controller and the panel share one reset line on GPIO7. Starting the panel resets the controller, so AO_Touch waits for DISPLAY_STATE_SIG. If the panel is not up it logs no panel; touch shares its reset line, so no touch and stays idle. A unit with the other supplier's panel does this. If the panel is up but kdrv_cst820_init() fails, it logs no CST820 and stays idle. In idle, a field request is answered with KODE_DENIED so the console does not wait for its watchdog, and a test request fails with no prompt.

AO_Touch does not call display_state_request() in its initial transition. kode_init() starts it before AO_Display, so the announcement is still ahead, and a request would post to an AO with no queue.

Armed and tracking

Touch_up is the parent of both and serves the field and test requests. In armed, AO_Touch reads nothing. On the falling edge it enters tracking, which arms a periodic 10 ms timer (KODE_TOUCH_POLL_MS) and leaves it disarmed on exit. Each TOUCH_POLL_SIG reads the chip and does one of three things:

Position and pressed flag move only with an event that went out. If the event pool is dry, the next poll says the same thing again, so a release is never lost.

The polling exists because CST816-family firmwares differ: some pulse INT once per report, others once per touch. A board lying on a desk uses no I2C at all.

No calibration

A published point is the controller's own, pinned to the glass: a finger on the bezel can read past the last pixel of the 410 by 502 panel, so a coordinate below 0 becomes 0 and one past the edge becomes the last pixel. Nothing else is applied. Rotation and mirroring are design facts of the panel and are not stored. A kset/tcal blob that an older kodeOS left in the store is never read.

The guided test

test component=tp needs a person. It checks the chip's identity from the copy the driver took at start-up, then shows two targets, one on each side of the glass in the clear band between the printed zones of the screen protector:

Each prompt is a published TOUCH_TEST_SIG. The tap is the first release after the prompt, and AO_Touch judges the average of every raw sample while the finger was down, within 64 px (KODE_TOUCH_TEST_TOL_PX) of the target on each axis. A step has 20 s (KODE_TOUCH_TEST_STEP_MS).

A tap off the target is a slip and is forgiven: TP_TEST_MISS names where it landed and the same target is asked again. The third tap off one target (KODE_TOUCH_TEST_TRIES) fails the step with TP_TEST_FAIL, and the run goes on to the next target. When both targets have been judged the verdict is published. Nothing is fitted or stored: the test proves the panel answers, and it does not calibrate it.

The origin of a run. touch_test_request_from(origin) starts a run for a client other than AO_Autotest. Every TOUCH_TEST_SIG step and the verdict carry the origin of the run (TEST_ORIGIN_AUTOTEST, 0, or the client's own number), so AO_Autotest and the console count only a run they asked for. A request that arrives while a run is up is refused with a failing verdict in its own origin, and the run in flight keeps its origin.

Resting with the board

With CONFIG_KODE_LOWPOWER, AO_Touch joins the rounds of AO_LowPower as touch (sdk/kaos/lowpower). Right after the bring-up, the one moment the chip is known to answer, it reads the three settings the rest changes: the interrupt control, the auto-sleep switch and the auto-sleep time. If any of them does not read, the rest leaves the chip alone.

On a round, AO_Touch switches the interrupt off and auto-sleep on, with a time of KODE_TOUCH_REST_SLEEP_S (1 s), so the chip stops scanning a second after the last touch. The writes may be refused by a chip that is already asleep, which is the rest itself, so their results are not judged. Once asleep, the chip NAKs every access until a finger lands or the panel is reset. The reset line is the panel's too, so there is no reset.

On LOWPOWER_STATE_SIG with state AWAKE, AO_Touch writes the awake settings back. A chip that NAKs the write is tried again every KODE_TOUCH_RESTORE_RETRY_MS (200 ms), on TOUCH_RESTORE_SIG, until it answers, which it does as soon as a finger lands. So the first touch after a wake can be lost and the next one is not. The guided test makes AO_Touch answer a round busy (touch test). A touch does not wake the board from ASLEEP or OFF.

Specifications

Signals

SignalEventDirectionMeaning
DISPLAY_STATE_SIGDisplayStateEvtin, subscribedGate out of idle
IOEXP_INPUT_SIGIoexpInputEvt: levels, changed, okin, subscribedP6 low with its bit in changed is the edge. Every other one is ignored
TOUCH_FIELD_REQ_SIGTouchFieldReqEvt: who, op, field, valuein, posted by AO_ConsoleGet, set or dump a CST820 field. After a set AO_Touch reads back
TOUCH_TEST_REQ_SIG, TOUCH_TEST_REQ_FROM_SIGnone, or TestReqEvt: originin, posted by AO_Autotest or another clientStart the guided test. A second request during a run is refused with a failing verdict in the asker's origin
TOUCH_POLL_SIGnoneself, periodicOnly while tracking
TOUCH_TEST_TMO_SIGnoneself, one-shotOne per prompt
TOUCH_EVT_SIGTouchEvt: x, y, pressed, t_msout, publishedA sample, pinned to the glass. t_ms is the read time, so a subscriber can measure a drag
TOUCH_TEST_SIGTouchTestEvt: phase, step, total, tx, ty, gx, gy, originout, publishedTP_TEST_PROMPT, _PASS, _MISS, _FAIL, _TIMEOUT, _DONE. gx, gy are where the tap read
TOUCH_FIELD_RSP_SIGTouchFieldEvt: op, field, rc, valueout, posted to whoKODE_DENIED for a read-only field, a value out of range or no controller. KODE_ERROR when the chip did not answer, which on this part usually means asleep
TEST_RESULT_SIGnamed tp, with the origin of the runout, publishedThe verdict
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIGLowPowerRoundEvt, LowPowerStateEvtin, subscribedWith CONFIG_KODE_LOWPOWER: rest the controller, and put it back on state AWAKE
TOUCH_RESTORE_SIGnoneself, one-shotWith CONFIG_KODE_LOWPOWER: the retry of a write-back the sleeping chip refused

Calls

CallWhat it does
touch_field_request(who, op, field, value)op is FIELD_OP_GET, FIELD_OP_SET or FIELD_OP_DUMP; field is a kdrv_cst820_field_t. A NULL who is served and not answered
touch_test_request(), touch_test_request_from(origin)Start the guided test, for AO_Autotest or for the client with that origin

Numbers, verbs and settings

ThingValue
Poll period10 ms (KODE_TOUCH_POLL_MS); the QP tick is 1 ms
Move hysteresis2 px per axis (TOUCH_MOVE_EPSILON)
Empty polls before giving up an edge3 (TOUCH_EMPTY_POLLS)
Test2 targets (KODE_TOUCH_TEST_STEPS), 20 s a tap (KODE_TOUCH_TEST_STEP_MS), 3 tries a target (KODE_TOUCH_TEST_TRIES); console budget 125 s (KODE_TOUCH_TEST_BUDGET_MS)
Test tolerance64 px on each raw axis (KODE_TOUCH_TEST_TOL_PX)
RestAuto-sleep after 1 s (KODE_TOUCH_REST_SLEEP_S); the write-back retries every 200 ms (KODE_TOUCH_RESTORE_RETRY_MS)
Consoletp, tp field="fingers", test component=tp. touch, tap and swipe inject fingers but never pass through AO_Touch, so they cannot pass the test
SettingNone. A kset/tcal blob left by an older kodeOS is ignored
KconfigNone for the touch itself, which is always built. The rest needs CONFIG_KODE_LOWPOWER

Gotchas

Example code

This is an Active Object of your own that subscribes to the touch events and logs them. Touch the glass and the console prints one line per press, move and release. The log level must be i or lower: config key="log" value=i.

/*
 * touch_log.c - 2026-10-28 - from Kode with love
 * Logs every TOUCH_EVT_SIG of the Kode Dot. Touch the glass and the console
 * prints the position of each press, move and release.
 */
#include "kode_ao.h"
static QActive     l_ao;
static QEvtPtr     l_queue[KQP_AO_QUEUE_LEN];
static StackType_t l_stack[KQP_AO_STACK_BYTES / sizeof(StackType_t)];
 
/* The one state: log each touch sample, pass the rest to the top state. */
static QState TouchLog_running(QActive * const me, QEvt const * const e)
{
    QState status = Q_SUPER(&QHsm_top);
    (void)me;
    if (e->sig == TOUCH_EVT_SIG) {
        TouchEvt const * const t = (TouchEvt const *)e;
        KLOG_I("touch_log", "x=%d y=%d %s", (int)t->x, (int)t->y,
               t->pressed ? "down" : "up");
        status = Q_HANDLED();
    }
    return status;
}
 
/* Subscribe to the touch events, then run. */
static QState TouchLog_initial(QActive * const me, void const * const par)
{
    (void)par;
    QActive_subscribe(me, TOUCH_EVT_SIG);
    return Q_TRAN(&TouchLog_running);
}
 
/* Bring the system up, then start the Active Object. */
void app_main(void)
{
    kode_init();
    QActive_ctor(&l_ao, Q_STATE_CAST(&TouchLog_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