SDK

KTD2026 LED driver

The chip behind the Kode Dot's one RGB LED: how the colours are wired, the registers the driver writes, the value that keeps it lit, and who may call it.

What it is

The Kode Dot has one RGB LED and no wire from the P4 to it. A KTD2026 current-sink driver on the shared I2C bus sets how much current flows through each of the three colours, and an 8-bit level per colour is all your code says. The driver turns a level into a current, remembers the last colour, and offers on, off and toggle.

The chip can also blink and breathe on its own. The firmware uses neither. A blink in kodeOS is a timer in the LED's owner, AO_LedMgr, that toggles the LED, and the chip only ever sees a steady colour.

How it is connected

flowchart LR
subgraph board["On the Kode Dot"]
  ao["AO_LedMgr"] --> p4["ESP32-P4"]
  p4 -- "I2C, 0x30" --> ktd["KTD2026 (U8)"]
  subgraph d13["LED D13"]
    g["green die"]
    b["blue die"]
    r["red die"]
  end
  ktd -- "OUT1" --> g
  ktd -- "OUT2" --> b
  ktd -- "OUT3" --> r
  vsys["+VSYS"] --> g
  vsys --> b
  vsys --> r
end
class ao ao
class p4,ktd chip
class vsys store
The LED on the Kode Dot

The chip is U8 and answers at 0x30 on the shared bus (KBSP_I2C_ADDR_LED). It has no identity register, so the driver confirms it by an acknowledge. D13 is a three-die LED with a common anode on +VSYS. The three cathodes go to the chip's current sinks. The chip is supplied from 3.3 V. It has no enable pin and no interrupt line: reset and enable are register writes. Only C411, 100 nF, decouples it.

Which sink is which colour

Which sink is which colour. OUT1 lights the green die, OUT2 the blue and OUT3 the red. The schematic does not settle this, and a bench test did. The mapping lives in one function, led_drive(), and nowhere else. test component=led is what catches a board that differs.

The owner

The owner. AO_LedMgr claims KBSP_RES_LED and is the only caller of the driver. That is why the driver needs no lock: an Active Object finishes one event before it takes the next. Everyone else publishes a signal, and the facade does it for apps: kode_led_set(), kode_led_toggle() and kode_led_rgb(). A call answers KODE_NO_RESOURCES when the event pool is dry.

AO_LedMgr runs at priority 7 with a queue of 16. It starts the LED dark and subscribes to five signals: LED_SET_SIG, LED_TOGGLE_SIG, LED_RGB_SIG, LED_BLINK_PERIOD_SIG and LED_HEARTBEAT_ENABLE_SIG.

The heartbeat is a periodic timer that toggles the LED, 500 ms by default, off until a request enables it. A period below 10 ms (KODE_LED_BLINK_MIN_MS) is refused with a warning. Disabling the heartbeat turns the LED off.

AO_Power publishes LED_RGB_SIG itself while a flash write runs, so the LED pulses blue for 500 ms and goes dark for 500 ms, screen on or off. Outside a write it publishes nothing: the battery has no colour.

The test row

The row. test component=led is a substate of AO_LedMgr. It lights red, green and blue in turn at level 96 (KODE_LED_TEST_LEVEL) and waits for the pad: the top button confirms the colour, the bottom button refuses it, and 10 s of silence is a timeout. The row ends at the first colour not confirmed.

For its whole length AO_LedMgr drops every signal that would put a colour on the chip, so the operator never answers for a colour the row did not ask about. A heartbeat or period request that arrives meanwhile is recorded and applied when the row exits. The exit turns the LED off.

Bring-up and a colour

Bring-up. kdrv_led_init() probes 0x30 for 100 ms and attaches the device. It writes 0x07 to register 0x00, a full reset, which the chip does not acknowledge, so the status is ignored. After 1 ms it writes 0x18 to the same register. If that write fails, the driver forgets the device and the next call runs the whole sequence again. Then it clears all channels, stores white as the default colour and puts the chip to sleep with 0x08. The first LED call of any kind runs init itself.

A colour. led_drive() wakes the chip with 0x18 if anything is to be lit, writes the three current registers, writes the channel-enable register, and, if everything is now dark, puts the chip back to sleep. Channels are switched off in the enable register before the sleep write, which is the datasheet's order. The wake write is the SDA toggling that wakes the chip, so it is retried once if the first is not acknowledged.

Specifications

The board

ThingValue
PartKTD2026EWE-TR, U8
Address0x30, found by acknowledge, because the chip has no identity register
LEDD13, three dies, common anode on +VSYS. Cathodes: OUT1 green, OUT2 blue, OUT3 red
CurrentI = 0.125 mA × (code + 1) per channel. The chip reaches 24 mA (0xBF); the driver never sends more than 5.0 mA (KTD_FULL_SCALE_MA), because this is a status indicator inside a case
Levels0 to 255 per colour map linearly onto 0 to 5 mA. Level 255 is full drive; the production row uses 96, about 1.9 mA
Supply current190 µA with the chip on and every channel off; 0.1 µA asleep (datasheet)
Wake from sleepBy traffic on SDA. Other devices' bus traffic wakes it for as long as it lasts plus 600 µs; the registers keep their values
Bus400 kHz, shared with the IMU, expander, codec and the rest

The registers

The driver writes registers 0x00, 0x04 and 0x06 to 0x08. The others are in the datasheet and used by the board's validation program for a breathing demo; the firmware never writes them.

RegNameBits and meaning
0x00Enable and reset[2:0] reset and timer slot, [4:3] enable control, [6:5] ramp scale
0x01Flash period[6:0] period of the breathing curve, [7] linear ramp
0x02PWM1Duty of PWM1, 256 steps
0x03PWM2Duty of PWM2
0x04Channel enableTwo bits per channel, OUT1 in bits 1:0, OUT2 in bits 3:2, OUT3 in bits 5:4: 00 off, 01 on, 10 PWM1, 11 PWM2
0x05Ramp[3:0] rise time, [7:4] fall time
0x06ISET1OUT1 current, 0.125 mA per code, plus one step
0x07ISET2OUT2 current
0x08ISET3OUT3 current

The values the driver writes to register 0x00:

ValueNameMeaning
0x07KTD_RESET_FULLFull register reset. The chip needs about 200 µs afterwards; the driver waits 1 ms
0x18KTD_EN_NORMALTimer slot 1, enable control 11 (device always on), ramp scale 1x. Held while anything is lit
0x08KTD_EN_SLEEPTimer slot 1, enable control 01 (on while SDA toggles). Held while nothing is lit

The driver

CallWhat it does
kdrv_led_init()Brings the chip up, dark and asleep. Safe to call again
kdrv_led_rgb(r, g, b)Sets the colour. (0, 0, 0) means off and keeps the last real colour
kdrv_led_set(on)On at the last colour set, or white if none; or off
kdrv_led_toggle()Inverts on and off

The signals

SignalEventWhat AO_LedMgr does
LED_SET_SIGLedEvt.onkdrv_led_set(on)
LED_TOGGLE_SIGLedEvtkdrv_led_toggle()
LED_RGB_SIGLedRgbEvt (r, g, b)kdrv_led_rgb(r, g, b)
LED_BLINK_PERIOD_SIGLedBlinkEvt.period_msStores the period, at least 10 ms, and re-arms a running heartbeat
LED_HEARTBEAT_ENABLE_SIGLedEvt.onStarts the toggling timer, or stops it and turns the LED off
Consoleled_on, led_off, led_toggle, led_blink, led_hb, led_rgb, test component=led

Gotchas

Example code

Cycle the LED through red, green and blue, one second each. You see the colour change every second, dim enough to look at.

/*
 * led_cycle.c - 2026-10-28 - from Kode with love
 *
 * Cycles the Kode Dot's RGB LED through red, green and blue, one second
 * each, by asking AO_LedMgr, the owner of the KTD2026, for each colour. The
 * level is 32 of 255 on purpose: this LED is bright. You see the colour
 * change every second.
 */
#include "kode.h"
 
#define LEVEL 32U
 
/* Show one colour, then wait a second. */
static void show(uint8_t red, uint8_t green, uint8_t blue)
{
    (void)kode_led_rgb(red, green, blue);
    kode_delay_ms(1000);
}
 
/* Bring the system up, then cycle the three colours for ever. */
void app_main(void)
{
    kode_init();
 
    while (1) {
        show(LEVEL, 0U, 0U);
        show(0U, LEVEL, 0U);
        show(0U, 0U, LEVEL);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK