SDK
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
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()
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:
- 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_INPUTSis0x7F, 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. - It publishes one reading, so subscribers start from a known byte.
- It arms the interrupt. If
kdrv_tca6408a_int_attach()refuses, it logsthe INT could not be armed; inputs will only be read on requestand still entersIoExp_ready. When the buttons are silent andioexp 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 = falseand 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 forKODE_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
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.
- Type
ioexp field="input". The answer is one record with the level of each pin. A pressed button reads low. - Type
log_level tag="kaos.ioexp" level=d. From now on AO_IoExp logs each interrupt and each good read. - Press a pad button. The log shows
INT, theninput=0xFD changed=0x02for the up button (P1), and again on release withchanged=0x02. - Type
test component=ioexp. Three checks run and the record>|test=ioexp result=passends the row.
In the SDK
components/kaos/ioexp:ioexp_ao.c, the two states and the interrupt service.components/kdrv/tca6408a: the driver, its field table and the INT line (sdk/kdrv/tca6408a).kaos.h:ioexp_read_request(),ioexp_field_request(),ioexp_test_request().