# AO_IoExp

The Active Object that owns the TCA6408A expander and its interrupt: one read and one publish per edge, for the buttons, the touch and the panel.

## What it is

An expander is a chip that gives the processor more pins. The TCA6408A has eight, P0 to P7, on the shared I2C bus (`sdk/kdrv/tca6408a`). Four parts of the Kode Dot hang on them: five buttons, the interrupt of the touch controller, the INT1 output of the IMU, which nothing in kodeOS uses, and the enable pin of the panel's supply.

The chip has one input register and one interrupt line, INT. When an input changes it pulls INT low, and a read of the input register releases it, whoever does the read. So exactly one Active Object reads that register, and **AO_IoExp** is it. It owns the whole chip and its INT line.

The pins are not split among their users because of that release: if AO_Buttons read for the buttons and AO_Touch for P6, a touch would pull INT low, the button read would release it, and AO_Touch would never learn that its pin moved.

AO_IoExp serves the interrupt and publishes what the eight pins read, so AO_Buttons and AO_Touch learn of a change without using the bus. It reads the chip when another AO asks for a fresh reading. And it reads or writes one field of the chip for whoever asks and answers only that asker: AO_Display asks for P7 before it touches the panel, and the console verb `ioexp` can ask for any field.

## How it is connected

```mermaid
sequenceDiagram
  autonumber
  participant Chip as TCA6408A
  box "On the P4"
    participant ISR as on_int, GPIO6
    participant AO as AO_IoExp
    participant Subs as AO_Buttons, AO_Touch
  end
  Chip->>ISR: an input moved, INT low
  ISR->>AO: IOEXP_INT_SIG, static event
  AO->>Chip: read the input register, INT released
  AO->>Subs: IOEXP_INPUT_SIG
  AO->>ISR: kdrv_tca6408a_int_rearm()
```

*From a pin edge to the subscribers*

AO_IoExp in numbers:

| Thing | Value |
| --- | --- |
| Priority | 17 |
| Queue | 16 events |
| Stack | 4096 bytes |
| Claims | `KBSP_RES_IOEXP`: the chip at `0x20` and its INT on GPIO6 |
| Periodic timer | None: the chip tells it when a pin changed |

The priority is above AO_Display and everything that draws, because a button press or a touch changes what the screen must show and its edge must not wait behind a repaint. It can sit that high because it never does anything longer than one I2C read and one publish.

### Two states, chosen once

The initial transition claims the chip and starts the driver. If nothing answers at `0x20` it enters `IoExp_dead` and logs `no expander; the buttons behind it, the touch IRQ and the panel's supply are out of reach`. In `IoExp_dead` every read request publishes `ok = false`, every field request is answered `KODE_ERROR` and the self-test fails. Otherwise it enters `IoExp_ready`. It stays in that state until the next boot, and every signal it handles is a self-transition.

In `IoExp_ready` the initial transition does three things in order:

1. It puts the pins that must be inputs back to inputs. The chip's RESET is tied high on the Kode Dot, so its registers keep whatever the last firmware left. `KBSP_IOEXP_DIR_INPUTS` is `0x7F`, P0 to P6 in and P7 out, and each bit that is an output is written back to input, with a log line naming the pin. P7 is left as found: it is the panel's supply, and AO_Display owns the decision.
2. It publishes one reading, so subscribers start from a known byte.
3. It arms the interrupt. If `kdrv_tca6408a_int_attach()` refuses, it logs `the INT could not be armed; inputs will only be read on request` and still enters `IoExp_ready`. When the buttons are silent and `ioexp field="input"` answers, that line in the boot log is the cause.

### The interrupt

The callback calls `kqp_post_from_isr()` with a static event and nothing else. AO_IoExp reads the register, publishes `IOEXP_INPUT_SIG` and re-arms INT. The read is the acknowledgement. A change that arrives between the read and the re-arm leaves INT low, so it fires again at once.

- **A failed read.** AO_IoExp publishes the reading with `ok = false` and does not re-arm. INT is still low after a failed read, and an interrupt on a low level that is re-armed on a low line fires again the moment it returns. At this priority every failed read would hold the shared I2C bus for its whole timeout while the PMIC, the gauge, the LED and the touch controller wait on it. So it arms a one-shot timer for `KODE_IOEXP_RETRY_MS` (500 ms) instead, and looks at the line again when it fires.
- **A dry pool.** It drops the sample and does not assert. The read already released the line, but `last`, the last reading that was published, stays as it was, so the next event still carries the bit that changed.

### What it publishes

`IoexpInputEvt` carries three fields:

- `levels`: bit n is pin Pn.
- `changed`: the bits that differ from the last reading **published**, so a failed read or a dropped sample loses nothing.
- `ok`: false when the read failed.

Before the first reading it assumes `0xFF`, every input at rest, so a button held through the boot arrives as a change in the first event. With `last` at `0xFF`, a press on P0 gives `levels` `0xFE` and `changed` `0x01`.

The event carries the levels and not their meaning: AO_Buttons decodes "low means pressed" and AO_Touch decodes "P6 fell".

### The requests

A read request makes AO_IoExp read and publish the same event, changed or not. AO_Buttons uses it for the second look of its debounce (`sdk/kaos/buttons`). A field request carries `who`, an op (`FIELD_OP_GET`, `_SET` or `_DUMP`), a field of `kdrv_tca6408a_field_t` and a value. After a set it does a get, so the answer holds what the chip kept.

It posts the answer to `who` and does not publish it, since AO_Display and the console both ask and neither wants the other's answer. A read-only field or a value out of range is refused with `KODE_DENIED` and the bus is not used. With `who` NULL the request is served and no answer is sent.

### The self-test

`test component=ioexp` runs three checks and logs each: P0 to P6 are inputs, the input register reads, and INT is high after that read. Its read goes through the driver and not the publish path, so it releases INT without telling a subscriber, which is harmless for the reason a dropped sample is.

### The wake of the board

With `CONFIG_KODE_LOWPOWER`, AO_IoExp is what wakes the Kode Dot. It joins the rounds of AO_LowPower as `ioexp` (`sdk/kaos/lowpower`). On a round it arms the INT line as a light-sleep wake with `kdrv_tca6408a_sleep_wake()`, so a press of BOT reaches a P4 that is asleep, and it answers ready. On `LOWPOWER_STATE_SIG` with state AWAKE it takes the wake away again.

The driver withdraws the wake inside the interrupt, before it calls the callback, and puts it back when the input register has been read and `kdrv_tca6408a_int_rearm()` runs (`sdk/kdrv/tca6408a`). A line that stays low cannot wake the chip in a loop. A read that failed and waits for its retry makes AO_IoExp answer the round busy (`expander retry`), because the line is still low and nothing would release it. A wake line that would not arm is busy too (`no wake line`). In `IoExp_dead` there is no chip and no line, and the round is answered ready.

In OFF the same line, GPIO6, is the one wake source of deep sleep (`ext1`, low). The gate at the top of `kode_init()` reads the expander before AO_IoExp exists (`sdk/kaos/lowpower`).

## Specifications

### Signals in

| Signal | Event | From |
| --- | --- | --- |
| `IOEXP_INT_SIG` | none (static) | The GPIO6 interrupt, through `kqp_post_from_isr()` |
| `IOEXP_RETRY_SIG` | none (timer) | Itself: the one-shot after a failed read |
| `IOEXP_READ_REQ_SIG` | none | `ioexp_read_request()`: AO_Buttons for the settle, and `btn_read` |
| `IOEXP_FIELD_REQ_SIG` | `IoexpFieldReqEvt` (`who`, `op`, `field`, `value`) | `ioexp_field_request()`: AO_Display for P7, AO_Console for `ioexp` |
| `IOEXP_TEST_REQ_SIG` | none | `ioexp_test_request()`, for `test component=ioexp` |
| `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG` | `LowPowerRoundEvt`, `LowPowerStateEvt` | AO_LowPower, subscribed with `CONFIG_KODE_LOWPOWER`: arm the wake, and take it away on AWAKE |

### Signals out

| Signal | Event | Range | To |
| --- | --- | --- | --- |
| `IOEXP_INPUT_SIG` | `IoexpInputEvt` (`levels`, `changed`, `ok`) | publish | AO_Buttons and AO_Touch, and AO_LowPower, which reads BOT from it |
| `IOEXP_FIELD_RSP_SIG` | `IoexpFieldEvt` | post | The asker |
| `TEST_RESULT_SIG` | `TestResultEvt` named `ioexp` | publish | AO_Autotest |

### The pins

| Pin | Use | Owner of the meaning |
| --- | --- | --- |
| P0 | Bottom button | AO_Buttons |
| P1 to P4 | Pad: up, left, down, right | AO_Buttons |
| P5 | IMU INT1. Driven by the IMU; nothing in kodeOS uses it | None |
| P6 | Touch controller interrupt | AO_Touch |
| P7 | Panel supply enable (an output) | AO_Display |

The timer is `KODE_IOEXP_RETRY_MS`, 500 ms. AO_IoExp has no stored setting and no option. The console verbs are `ioexp`, `ioexp_help` and `test component=ioexp`; the field table is on `sdk/kdrv/tca6408a`.

## Gotchas

> **WARNING**
>
> **Nothing posts to AO_IoExp before it has a queue.** `kode_init()` starts it before AO_Buttons, AO_Touch and AO_Display, which post to it or subscribe to it from their initial transitions. Posting to an AO with no queue stops the system with a FreeRTOS assertion and starts a boot loop.

> **NOTE**
>
> **The console cannot fire the interrupt path by itself.** Only a real edge on a button pin or on the touch pin does. P5 is driven by the IMU, and nothing else is free. To watch it, run `log_level tag="kaos.ioexp" level=d` and press a pad button: the log shows `INT` and then `input=0x.. changed=0x..` with the button's bit.

> **NOTE**
>
> **A change of a polarity bit does not fire INT.** The chip compares the levels of the pins and not the inverted register value.

> **NOTE**
>
> **The absent-chip path runs only on a board.** The host tests start AO_IoExp once per test binary, with a chip that answers.

## Example code

An app has no call into AO_IoExp: the buttons and the touch reach you through their own owners. Read the expander from the console instead, and watch an edge.

1. Type `ioexp field="input"`. The answer is one record with the level of each pin. A pressed button reads low.
2. Type `log_level tag="kaos.ioexp" level=d`. From now on AO_IoExp logs each interrupt and each good read.
3. Press a pad button. The log shows `INT`, then `input=0xFD changed=0x02` for the up button (P1), and again on release with `changed=0x02`.
4. Type `test component=ioexp`. Three checks run and the record `>|test=ioexp result=pass` ends the row.

## In the SDK

- [`components/kaos/ioexp`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/ioexp): `ioexp_ao.c`, the two states and the interrupt service.
- [`components/kdrv/tca6408a`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/tca6408a): the driver, its field table and the INT line (`sdk/kdrv/tca6408a`).
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): `ioexp_read_request()`, `ioexp_field_request()`, `ioexp_test_request()`.
