SDK

AO_Display

Owns the Kode Dot's AMOLED panel: powers it, takes frames from a renderer, sets brightness and lends screenshots and JPEGs.

What it is

AO_Display is the only code that sends commands and pixels to the screen of a Kode Dot. Everything else asks it with an event. It has two jobs. The control path switches the panel on, sets the brightness and tells the rest of the system whether the panel started. The pixel path takes rectangles of a finished picture from a renderer and flushes them to the glass.

A renderer draws into memory and sends one rectangle at a time with DISPLAY_FLUSH_SIG. In kodeOS the renderer is AO_Gui. In your own app it is the render pass of sdk/kscreen, or code of yours. AO_Display never draws anything itself.

It also keeps a shadow copy of what the panel shows. The shadow feeds the console's screenshot and the live mirror (internals/mirror). A JPEG encoder on the same copy serves the screen upload of sdk/kaos/net. The chip itself, its bring-up and the flip that makes a frame tear-free are on sdk/kdrv/co5300.

How it is connected

flowchart LR
gui["AO_Gui or kscreen"] -- "DISPLAY_FLUSH_SIG" --> ao["AO_Display"]
ao -- "kdrv_display_flush()" --> drv["kdrv_display"]
ioexp["AO_IoExp"] -- "IOEXP_FIELD_RSP_SIG" --> ao
ao -- "IOEXP_FIELD_REQ_SIG<br/>for P7" --> ioexp
ao -- "DISPLAY_STATE_SIG" --> subs["AO_Gui, AO_Touch,<br/>kscreen"]
console["AO_Console, AO_Net"] -- "capture, shot, mirror" --> ao
drv -. "DMA done: semaphore" .-> gui
class ao,ioexp,gui,console ao
AO_Display and its neighbours

AO_Display in numbers:

ThingValue
Priority15 (KODE_PRIO_DISPLAY)
Queue16 events
Stack6144 bytes
ClaimsKBSP_RES_PANEL; with KODEOS_MIRROR also KBSP_RES_JPEG and KBSP_RES_PPA, the P4's JPEG codec and its pixel scaler
Subscribes toCONFIG_REQ_SIG, in its initial transition

It sits above AO_Gui, AO_Touch and AO_Buttons on purpose: AO_Gui draws a frame and then waits for AO_Display to send it, so a display that could only run after the renderer finished would deadlock against it.

kode_init() starts it last of all, because bringing the panel up costs about 245 ms of mandated waits. It does them in its own thread, so nobody else waits. A part that starts later subscribes to DISPLAY_STATE_SIG and calls display_state_request().

stateDiagram-v2
[*] --> Display_powering: claim the panel, ask for P7 level
Display_powering --> Display_powering: first IOEXP_FIELD_RSP_SIG, ask for P7 direction
Display_powering --> Display_active: second answer or a refusal, panel up, DISPLAY_STATE_SIG
Display_active --> Display_testing: DISPLAY_TEST_REQ_SIG with the panel up
Display_testing --> Display_active: verdict published
AO_Display states

Powering

The panel's supply, VCI_EN, is pin P7 of the TCA6408A, which AO_IoExp owns (sdk/kaos/ioexp). AO_Display asks for the output level first (o7 = 1) and for the direction after (c7 = 0), so the pin never drives low on its way to high. State requests that arrive meanwhile are deferred into a four-event queue and recalled when Display_active is entered, because the asker cannot tell a late panel from a missing one.

Coming up

panel_up() initialises the driver, then reads kset/bright, clamps it to 5 to 100 and hands it to the driver before the announcement. The glass is still black, and the driver writes the value with the first frame, so the panel lights once at the level where it stays. With the mirror compiled in, the JPEG codec and the scaler are initialised here too; neither is fatal if it fails. AO_Display then publishes DISPLAY_STATE_SIG whether or not the panel came up, since a renderer that hears nothing cannot tell a failed panel from a slow one.

Active

Display_active handles every request in the Specifications tables. Display_testing is a substate of it, so everything Display_active does for other parts keeps working while the production row runs.

The flush is not an event

The flush is the one signal that does not come back as an event. The renderer waits for the end of a transfer inside LVGL's own render call, where it reads no queue. The DMA interrupt signals a semaphore that kdrv_display owns, and the renderer waits on it with kdrv_display_flush_wait(), for 500 ms at most. A flush that fails to start is released by hand with kdrv_display_flush_abort(). This is one of the three documented exceptions to "everything is an event" in sdk/design-rules.

Capture and shot are loans

A screenshot is a 402 KB BMP and a shot is a JPEG of up to 64 KB (KSHOT_JPEG_CAP). Neither fits in an event. The requester gets a response event, reads the buffer through display_capture_buf() or display_shot_buf(), and gives it back with the matching release. A capture freezes the shadow until the release. The shot encodes the front framebuffer in one band into a PSRAM buffer that the first shot allocates and keeps.

A shot request can go unanswered. A request that arrives before the panel is up gets no response, and a pool with no free event block cannot carry one. Whoever asks sets a deadline of its own; AO_Net gives the display 2 s. A lent shot or capture also refuses every other request until it is released.

The rest of the screen

With CONFIG_KODE_LOWPOWER, AO_Display joins the rounds of AO_LowPower as display (sdk/kaos/lowpower). The DSI link and both framebuffers go while the board sleeps, so the renderer is parked before the link goes. AO_Display cannot tell a renderer that draws from one that does not. It knows only that somebody has flushed, and then it waits for the renderer's word that it has stopped.

The contract of the framebuffers is in sdk/kaos/lowpower, and the calls that move the link are on sdk/kdrv/co5300.

Specifications

Signals in

SignalEventFromWhat AO_Display does
DISPLAY_FLUSH_SIGFlushEvt: x1, y1, x2, y2, pixels, lastAO_Gui, kscreenkdrv_display_flush(); on failure kdrv_display_flush_abort(). last marks the final chunk, where the driver flips the framebuffer
DISPLAY_BRIGHTNESS_SIGBrightnessEvt: percent, persistAO_Gui, any appDrives the panel and updates the announced bright. With persist it clamps to 5 to 100 and stores kset/bright. Without it there is no floor and nothing is stored
CONFIG_REQ_SIGKodeConfigReqEvtAO_ConsoleAnswers the key bright; a set applies and stores like a persisted slider release
DISPLAY_STATE_REQ_SIGnoneany part that starts latePublishes DISPLAY_STATE_SIG again. Deferred while powering
DISPLAY_CAPTURE_REQ_SIGDisplayCaptureReqEvt: whoAO_ConsoleFreezes the shadow and posts DISPLAY_CAPTURE_RSP_SIG to who
DISPLAY_CAPTURE_END_SIGnonethe requesterReleases the shadow
DISPLAY_SHOT_REQ_SIGDisplayShotReqEvt: who, qualityAO_NetEncodes the glass as one JPEG and posts DISPLAY_SHOT_RSP_SIG to who
DISPLAY_SHOT_END_SIGnonethe requesterEnds the loan
DISPLAY_MIRROR_START_SIG, _STOP_, _ACK_, _INFO_REQ_MirrorStartEvt, MirrorAckEvtAO_ConsoleThe live mirror (KODEOS_MIRROR only)
DISPLAY_TEST_REQ_SIGnoneAO_AutotestStarts the production row
IOEXP_FIELD_RSP_SIGIoexpFieldEvtAO_IoExpOnly in Display_powering: the two answers about P7
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIGLowPowerRoundEvt, LowPowerStateEvtAO_LowPowerWith CONFIG_KODE_LOWPOWER: rest the glass, and put it back on state AWAKE
DISPLAY_MINUTE_REQ_SIGnoneAO_LowPowerWith CONFIG_KODE_LOWPOWER: write the time into the panel's memory
DISPLAY_GLYPHS_SIGDisplayGlyphsEvtThe renderer, through display_glyphs_hand()With CONFIG_KODE_LOWPOWER: the renderer has stopped, and here are the digits of the time. The strip is a pointer into the renderer's PSRAM and AO_Display never writes it
DISPLAY_GLYPHS_TMO_SIG, DISPLAY_RESTORE_SIGnone (timers)ItselfThe wait for the glyphs, and the retry of a restore that failed

Signals out

SignalEventMeaning
DISPLAY_STATE_SIGDisplayStateEvt: up, brightPublished once when the panel comes up, and again on every state request. A static event, so republishing is safe. bright seeds the settings slider and means nothing when up is false
DISPLAY_CAPTURE_RSP_SIGDisplayCaptureRspEvt: len, w, h, status, paintedPosted to the asker. painted false means nothing was flushed since boot, so the image is black
DISPLAY_SHOT_RSP_SIGDisplayShotRspEvt: len, statusKODE_OK lends the buffer. KODE_DENIED: codec busy or a shot already lent. KODE_NO_RESOURCES: no codec, no framebuffer or no PSRAM. KODE_ERROR: larger than 64 KB
DISPLAY_MIRROR_INFO_SIGMirrorInfoEvtThe mirror's live state
DISPLAY_TEST_SIGDisplayTestEvt: step, total, phase, fieldOne record of the production row
TEST_RESULT_SIGnamed displayThe row's verdict
DISPLAY_REPAINT_SIGnoneOn every exit from the row, and with CONFIG_KODE_LOWPOWER on every exit from a rest of the glass: the link has new framebuffers, and the renderer fetches them again and invalidates the whole screen

Calls

CallWhat it does
display_state_request()Posts DISPLAY_STATE_REQ_SIG. Only after kode_init() returns: earlier, AO_Display has no queue and the post is a FreeRTOS assert and a boot loop
display_brightness_request(pct, persist)Posts DISPLAY_BRIGHTNESS_SIG
display_capture_request(who), display_capture_buf(), display_capture_release()The capture loan. The buffer is valid between a KODE_OK response and the release
display_shot_request(who, quality), display_shot_buf(), display_shot_release()The shot loan. quality is 1 to 100. display_shot_buf() is NULL when nothing is lent
display_mirror_start(scale, quality), _stop(), _ack(seq), _info_request()The mirror. scale is 1 or 2; the quality is capped at 70
display_test_request()Defined in every build. With no panel the row is one record and a failed verdict
display_glyphs_hand(glyphs)With CONFIG_KODE_LOWPOWER: the renderer's hand-over of the time's digits, which also says it has stopped drawing. Posts DISPLAY_GLYPHS_SIG

The production row

test component=display paints five full-screen fields in turn: white, red, green, blue and black (DISPLAY_FIELD_WHITE to DISPLAY_FIELD_BLACK, five in all). Nothing on the board can see the glass, so the person at the bench judges each one: the top button says yes, the bottom button says no.

Each field waits KODE_DISPLAY_TEST_STEP_MS (10 s). The row ends at the first field that is not confirmed, and its console budget is 53 s (KODE_DISPLAY_TEST_BUDGET_MS, five steps plus 3 s).

ThingValue
Brightness during the row100 % (KODE_DISPLAY_TEST_BRIGHT), through the driver, so nothing reaches the store
PaintingBands of 16 lines (KODE_DISPLAY_TEST_BAND_ROWS) from a PSRAM buffer, one band per DISPLAY_TEST_STEP_SIG. 502 lines do not divide by 16, so the last of the 32 bands is short
PhasesDISPLAY_TEST_ASK, _PASS, _FAIL, _TIMEOUT, _DONE, _ABSENT
The padDisplay_testing subscribes to BUTTON_EVT_SIG on entry and unsubscribes on exit. A press while a field is still going up is dropped
The renderer meanwhileEvery DISPLAY_FLUSH_SIG is completed with kdrv_display_flush_abort() and dropped, so the launcher never paints over the field
ExitEvery way out goes through Q_EXIT_SIG: stops the wait timer, frees the pad and the band, restores the user's brightness and publishes DISPLAY_REPAINT_SIG
No panel>|step=1 panel=absent result=absent and a failed verdict

Options, verbs and settings

ThingValue
KODEOS_SCREENSHOTCompiles screenshot
KODEOS_MIRRORDepends on KODEOS_SCREENSHOT. Compiles the mirror, the JPEG codec and the scaler, the two extra claims and the shot path
KODEOS_SHOT_HTTPDepends on the Kode channel and KODEOS_MIRROR. Lets sdk/kaos/net ask for shots
Consoleconfig key="bright" value=N, screenshot, test component=display, mirror_start, mirror_stop, mirror_ack, mirror_info
Settingkset/bright, a u8, 5 to 100, default 100 (KSET_BRIGHT_DEF, KSET_BRIGHT_MIN). AO_Display is the only writer
TimersDISPLAY_TEST_WAIT_SIG, one-shot, re-armed per field. DISPLAY_MIRROR_TICK_SIG, one-shot of 10 ms, re-armed after every pass and left disarmed while a session is paused

Gotchas

Example code

This sets the brightness to 20 % and stores it. The glass dims once a frame is on it, and config key="bright" on the console answers value=20 stored=yes. Restore it with config key="bright" value=100.

/*
 * display_dim.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Display, the owner of the Kode Dot's panel, for a stored brightness
 * of 20 %, then idles so the console stays up. Type config key="bright" on
 * the console and the answer reads value=20 stored=yes.
 */
#include "kode_ao.h"
 
/* Bring the system up, ask for the new brightness, then idle. */
void app_main(void)
{
    kode_init();
    display_brightness_request(20, true);
 
    while (1) {
        kode_delay_ms(1000);
    }
}

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

In the SDK