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()
From a pin edge to the subscribers

AO_IoExp in numbers:

ThingValue
Priority17
Queue16 events
Stack4096 bytes
ClaimsKBSP_RES_IOEXP: the chip at 0x20 and its INT on GPIO6
Periodic timerNone: 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.

What it publishes

IoexpInputEvt carries three fields:

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

SignalEventFrom
IOEXP_INT_SIGnone (static)The GPIO6 interrupt, through kqp_post_from_isr()
IOEXP_RETRY_SIGnone (timer)Itself: the one-shot after a failed read
IOEXP_READ_REQ_SIGnoneioexp_read_request(): AO_Buttons for the settle, and btn_read
IOEXP_FIELD_REQ_SIGIoexpFieldReqEvt (who, op, field, value)ioexp_field_request(): AO_Display for P7, AO_Console for ioexp
IOEXP_TEST_REQ_SIGnoneioexp_test_request(), for test component=ioexp
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIGLowPowerRoundEvt, LowPowerStateEvtAO_LowPower, subscribed with CONFIG_KODE_LOWPOWER: arm the wake, and take it away on AWAKE

Signals out

SignalEventRangeTo
IOEXP_INPUT_SIGIoexpInputEvt (levels, changed, ok)publishAO_Buttons and AO_Touch, and AO_LowPower, which reads BOT from it
IOEXP_FIELD_RSP_SIGIoexpFieldEvtpostThe asker
TEST_RESULT_SIGTestResultEvt named ioexppublishAO_Autotest

The pins

PinUseOwner of the meaning
P0Bottom buttonAO_Buttons
P1 to P4Pad: up, left, down, rightAO_Buttons
P5IMU INT1. Driven by the IMU; nothing in kodeOS uses itNone
P6Touch controller interruptAO_Touch
P7Panel 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.

  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