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
From a finger to a button event

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.

EnumButtonWherePull-up
KDRV_BTN_UPD-pad upExpander P1R39, 10 kΩ
KDRV_BTN_DOWND-pad downExpander P3R41, 10 kΩ
KDRV_BTN_LEFTD-pad leftExpander P2R40, 10 kΩ
KDRV_BTN_RIGHTD-pad rightExpander P4R42, 10 kΩ
KDRV_BTN_TOPThe upper side buttonGPIO35 (KBSP_PIN_BUTTON_TOP)R38, 10 kΩ, and the P4's internal pull-up
KDRV_BTN_BOTThe lower side buttonExpander P0None 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:

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.

SignalPayloadWhen
BUTTON_EVT_SIGButtonEvt: id (a kdrv_button_t), pressed, t_ms (khal_time_ms() at confirmation)Each confirmed press and release
BUTTON_HOLD_SIGButtonHoldEvt: id, top or bottom600 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_SIGButtonDoubleEvt: id (always top), t_msRight 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.

TurnUp publishesDown publishesLeft publishesRight publishes
0updownleftright
90rightleftupdown
180downuprightleft
270leftrightdownup

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.

Specifications

The driver

CallWhat 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

CallWhat 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

ConstantValueMeaning
KODE_BUTTONS_SETTLE_MS30 msTime from a change to its confirming sample
KODE_BUTTON_HOLD_MS600 msA press this long is a hold
KODE_BUTTON_DOUBLE_MS800 msLongest gap from the first release to the second press
KODE_BUTTONS_TEST_STEP_MS20 sTime allowed for each press in the guided test
KODE_BUTTONS_TEST_RELEASE_MS3 sTime allowed to let a judged press go, or the button is stuck
KODE_PRIO_BUTTONS13AO_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