# 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

```mermaid
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 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 to `DISPLAY_STATE_SIG` and calls `kdrv_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 the `soft_rst` field, 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`. See `sdk/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_id` and `fw_ver` from 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_sleep` is a bench setting and the board's rest. Only AO_Touch's rest writes it (with `auto_sleep_time` and `irq_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

> **WARNING**
>
> **Reads fail while the screen is untouched.** A chip with auto-sleep on stops answering after `auto_sleep_time` idle seconds, and `tp field="x"` then answers `>err code=5` until a finger lands. This does not happen on an unchanged unit, where auto-sleep is off. Keep it that way outside the board's rest: kodeOS writes `dis_auto_sleep` only for the rest and puts the old value back on wake.

> **WARNING**
>
> **Writing `dis_auto_sleep` to 0 cannot be undone in that boot.** With `auto_sleep_time` at 0 the chip sleeps within seconds, and from then on every read and every write gets a NAK, including the write that would set the bit back. Only a finger or a reset of the panel wakes it.

> **NOTE**
>
> **The driver's coordinates are not clamped.** `kdrv_cst820_read()` and the `x` and `y` fields return the controller's own units, which are the panel's pixels, but a finger on the bezel can read past the last pixel. The `x` and `y` in a `TouchEvt` are the same numbers pinned to the glass. Subscribe to `TOUCH_EVT_SIG` and leave the chip to AO_Touch.

> **NOTE**
>
> **Do not start the controller before the panel.** The panel's start-up pulses GPIO7 and undoes an earlier start. A NAK right after the panel starts is a sleeping chip first and a dead chip second.

## 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`.

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/cst820): the driver and its field table.
- [`kdrv_cst820.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/cst820/include/kdrv_cst820.h): every field and call.
- [`components/kaos/touch`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/touch): AO_Touch, the owner.
