SDK
Buttons
The Kode Dot's D-pad and two side buttons: which pin each one is on, how a press is debounced, and the press, hold and double-press events your code receives.
What it is
The Kode Dot has six buttons: a D-pad of four (up, down, left, right) and two side buttons, top and bottom. The driver names each one by what it is for, not by where it is wired, and turns the state of all six into one number, the button mask, with one bit per button.
Your code never sees the wiring or the bounce. A press arrives as one BUTTON_EVT_SIG event that says which button changed and whether it is down, already filtered. Two gestures are timed for you as well: a hold of the top or bottom button, and a double press of the top button. If you prefer to poll, kode_buttons_read() returns the mask.
How it is connected
flowchart LR subgraph board["On the Kode Dot"] pad["pad and<br/>bottom button"] --> tca["TCA6408A<br/>P0 to P4"] tca -- "INT, GPIO6" --> ioexp["AO_IoExp"] ioexp -- "IOEXP_INPUT_SIG" --> btn["AO_Buttons"] top["top button<br/>GPIO35"] -- "edge" --> btn end btn -- "BUTTON_EVT_SIG" --> app["your AO"] class pad chip class tca chip class top chip class ioexp ao class btn ao class app app
The wiring
Five buttons hang off the TCA6408A expander (sdk/kdrv/tca6408a) and one is on a pin of the P4. All six switches are SKRMAAE010 and all are active low: a pressed button pulls its net to ground and reads 0.
| Enum | Button | Where | Pull-up |
|---|---|---|---|
KDRV_BTN_UP | D-pad up | Expander P1 | R39, 10 kΩ |
KDRV_BTN_DOWN | D-pad down | Expander P3 | R41, 10 kΩ |
KDRV_BTN_LEFT | D-pad left | Expander P2 | R40, 10 kΩ |
KDRV_BTN_RIGHT | D-pad right | Expander P4 | R42, 10 kΩ |
KDRV_BTN_TOP | The upper side button | GPIO35 (KBSP_PIN_BUTTON_TOP) | R38, 10 kΩ, and the P4's internal pull-up |
KDRV_BTN_BOT | The lower side button | Expander P0 | None of its own: the charger's internal QON pull-up, 200 kΩ |
The bottom button shares its net with the QON pin of the BQ25896 (sdk/kdrv/bq25896), so it is the button that wakes the board from ship mode, and the one that wakes it from OFF. A tap does not wake it from OFF: the boot gate wants BOT held for about a second (sdk/kaos/lowpower). Two hardware behaviours act with no software. The bottom button and D-pad left held together for about a second reset the P4. The bottom button held 16 to 23 s with no USB power-cycles the board. A script that tests buttons must never hold those two together.
Owners and debounce
Two owners. The expander belongs to AO_IoExp, which reads it on its interrupt and publishes all eight pins. AO_Buttons owns the driver, claims KBSP_RES_BTN_TOP (GPIO35), runs at priority 13 and reads nothing while nobody presses. An expander publish and an edge on GPIO35 each count as one sample of the six buttons.
The driver itself holds only GPIO35: it sets the input with the pull-up, arms an any-edge interrupt, and decodes a sample. The interrupt disables itself before its callback runs, so a bouncing contact posts once per arming.
The debounce is AO_Buttons'. A change arms a one-shot of 30 ms (KODE_BUTTONS_SETTLE_MS). When it fires, AO_Buttons re-arms GPIO35 and asks AO_IoExp for a fresh reading. That reading is the only sample that can confirm a change, for all six buttons, and only when it agrees with the sample before it. Earlier samples, which arrive microseconds apart, never confirm. There are two reasons:
- GPIO35 is a strapping pin that has carried a false level across a reset.
- The bottom button, held up only by
QON's 200 kΩ, rings as it is let go. Measured on a Kode Dot, with two close samples enough, one release published a release, a press nobody made and a second release within 1 ms.
A failed expander read is not a sample, since decoding a dead bus as "every button released" would publish releases nobody made.
The events
AO_Buttons publishes on change only, so a resting finger costs nothing downstream. Every confirmed edge logs one line at INFO: the button, pressed or released, and which sample confirmed it.
| Signal | Payload | When |
|---|---|---|
BUTTON_EVT_SIG | ButtonEvt: id (a kdrv_button_t), pressed, t_ms (khal_time_ms() at confirmation) | Each confirmed press and release |
BUTTON_HOLD_SIG | ButtonHoldEvt: id, top or bottom | 600 ms (KODE_BUTTON_HOLD_MS) after a press was confirmed, if the button is still down. Once per press. Never armed during the guided test |
BUTTON_DOUBLE_SIG | ButtonDoubleEvt: id (always top), t_ms | Right after the second press's BUTTON_EVT_SIG, when it is confirmed within 800 ms (KODE_BUTTON_DOUBLE_MS) of the first press's release |
A press that reached hold does not start a double, and the second press of a double does not start another, so three quick taps make one double. The detector is fed only by real presses; console-injected ones are left out on purpose.
The pad follows the glass
An app that rotates its screen with kscreen_set_rotation() gets the D-pad rotated with it when CONFIG_KSCREEN_ROTATE_PAD is on, which is the default. kscreen calls buttons_rotation_request(quarter_turns), and each pad button then publishes the direction it points on the turned glass. The top and bottom buttons never turn. A turn that arrives while a pad button is down waits for its release, so a press and its release always name the same button.
| Turn | Up publishes | Down publishes | Left publishes | Right publishes |
|---|---|---|---|---|
| 0 | up | down | left | right |
| 90 | right | left | up | down |
| 180 | down | up | right | left |
| 270 | left | right | down | up |
The guided test
test component=buttons asks for six presses: left, bottom, down, top, right, up. A short between two D-pad tracks or a pin mapped to the wrong button shows as a wrong name.
- The first confirmed sample with a button down is the answer: exactly the asked button passes. A wrong button, or the asked button with another, is forgiven and asked again; the third wrong press on a step ends the test as a failure, and the second double fails the step. Each names what was pressed.
- Each prompt waits until every button is released, so the operator is never asked for the second half of the reset combination.
- A step times out after 20 s, a button still held 3 s after being judged is reported stuck, and the run goes on.
- The verdict is
TEST_RESULT_SIGnamedbuttons.
Specifications
The driver
| Call | What it does |
|---|---|
kdrv_buttons_init() | Makes GPIO35 an input with the P4's pull-up |
kdrv_buttons_top_attach(cb, arg) | Arms GPIO35's any-edge interrupt. cb runs in interrupt context and may post one static event and nothing else. KODE_DENIED for a NULL callback |
kdrv_buttons_top_rearm() | Enables the interrupt again after the owner has sampled the pin |
kdrv_buttons_top_pressed() | True when GPIO35 reads low |
kdrv_buttons_decode(exp_levels, top_pressed) | Pure function. Takes the expander's input byte (bit n is Pn, high at rest) and the top button's state; returns the mask with bit 1 << kdrv_button_t set for each button that is down. P5 to P7 are ignored |
kdrv_button_name(b) | pad-up, pad-down, pad-left, pad-right, top, bottom; ? out of range. The pad- prefix keeps a log line such as pad-down pressed from reading as a state |
The driver does no debouncing. A caller that used it directly would see every bounce.
The owner
| Call | What it does |
|---|---|
kode_buttons_read() | The mask of buttons down, as the BUTTON_EVT_SIG stream last described them, so a polling app and a subscribing one agree. A tap shorter than the 30 ms settle shows nowhere; compare with the previous call to find an edge. 0 before kode_init() |
buttons_down() | The same word, from kaos. Never blocks, any task may call it |
buttons_inject(id, pressed) | Publishes one change as if AO_Buttons had confirmed it; what the console's btn uses. False for an id out of range or a dry event pool |
buttons_dump_request() | Asks for one raw reading, answered by BTN_STATE_SIG; the console's btn_read |
buttons_test_request() | Starts the guided test |
buttons_rotation_request(quarter_turns) | Turns the D-pad, 0 to 3. Anything else is ignored with a warning |
The timings
| Constant | Value | Meaning |
|---|---|---|
KODE_BUTTONS_SETTLE_MS | 30 ms | Time from a change to its confirming sample |
KODE_BUTTON_HOLD_MS | 600 ms | A press this long is a hold |
KODE_BUTTON_DOUBLE_MS | 800 ms | Longest gap from the first release to the second press |
KODE_BUTTONS_TEST_STEP_MS | 20 s | Time allowed for each press in the guided test |
KODE_BUTTONS_TEST_RELEASE_MS | 3 s | Time allowed to let a judged press go, or the button is stuck |
KODE_PRIO_BUTTONS | 13 | AO_Buttons' priority |
Gotchas
Example code
Poll the buttons and show them on the LED: green while the top button is down, red for the bottom button, blue for any D-pad button. You see the colour follow your thumb.
/*
* buttons_led.c - 2026-10-28 - from Kode with love
*
* Reads the Kode Dot's six buttons every 20 ms and shows them on the RGB
* LED: green while the top button is down, red for the bottom button and
* blue for any D-pad button. You see the colour follow your thumb. The
* bottom button held for 600 ms returns to kodeOS, so tap it.
*/
#include "kode.h"
#define LEVEL 32U
#define PAD_MASK ((1UL << (uint32_t)KDRV_BTN_TOP) - 1UL)
/* Bring the system up, then map the button mask to a colour forever. */
void app_main(void)
{
kode_init();
while (1) {
uint32_t const down = kode_buttons_read();
uint8_t red = 0U;
uint8_t green = 0U;
uint8_t blue = 0U;
if ((down & (1UL << (uint32_t)KDRV_BTN_TOP)) != 0UL) {
green = LEVEL;
}
if ((down & (1UL << (uint32_t)KDRV_BTN_BOT)) != 0UL) {
red = LEVEL;
}
if ((down & PAD_MASK) != 0UL) {
blue = LEVEL;
}
(void)kode_led_rgb(red, green, blue);
kode_delay_ms(20);
}
}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/buttons: the driver,kdrv_buttons.c.kdrv_buttons.h: the button enum and the decode call.components/kaos/buttons: AO_Buttons, the owner, with the debounce, hold, double press and guided test.