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 in numbers:
| Thing | Value |
|---|---|
| Priority | 15 (KODE_PRIO_DISPLAY) |
| Queue | 16 events |
| Stack | 6144 bytes |
| Claims | KBSP_RES_PANEL; with KODEOS_MIRROR also KBSP_RES_JPEG and KBSP_RES_PPA, the P4's JPEG codec and its pixel scaler |
| Subscribes to | CONFIG_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
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.
- A round opens from AWAKE. If a screenshot or a shot is lent, or the mirror runs, AO_Display answers busy (
glass lent,mirror running). Otherwise it parks. With no renderer ever seen, it rests at once. With one, the screen stays lit and the link up, flushes still reach the glass (the renderer draws its last page), and a one-shot ofKODE_LOWPOWER_GLYPHS_WAIT_MS(1500 ms) starts. DISPLAY_GLYPHS_SIGarrives. This is the renderer's word that it has stopped: a strip of glyphs, or a NULL strip for "stopped, with nothing to give". Flushes are dropped from now on. For ASLEEP, AO_Display sets the brightness to the strip'srest_pct(orKODE_LOWPOWER_DIM_PCT, 20 %), takes the link down, writes the time into the panel's memory at once and answers ready. For OFF, it puts the panel to sleep, takes the link down and answers ready. A rest from ASLEEP to OFF brings the link up for the sleep-in first and takes it down again.- The one-shot ends first. The renderer did not park. AO_Display answers busy
renderer not parkedand the screen stays lit. - A step that fails answers busy with its reason (
display link,display panel), andglass not restoredanswers a round that finds the glass half restored. DISPLAY_MINUTE_REQ_SIG. Each minute while ASLEEP, AO_LowPower asks for the time. AO_Display reads the wall clock, converts it to local time with the zone the face keeps inTZ, composeshh:mmfrom the strip (display_clock.c, with the 12 h dial whenh12is set) and writes the rectangle withkdrv_display_gram_write(). It writes nothing when the clock is not set or no glyphs were handed. A write that fails is tried once more afterKODE_LOWPOWER_MINUTE_RETRY_MS(150 ms), in the same step, because the QP tick is stopped while the board sleeps. A task that stops the flash cache is the usual cause, and it is over by then.- AWAKE.
LOWPOWER_STATE_SIGwith state AWAKE brings the link up, wakes the panel if it was asleep, sets the stored brightness and publishesDISPLAY_REPAINT_SIG. The restore is idempotent, and the repaint goes out on every exit from a rest it began, a busy refusal included, because the renderer parked for the round and waits for it. A restore that fails is tried again everyKODE_DISPLAY_RESTORE_RETRY_MS(200 ms), since a board that stays awake hears no further AWAKE.
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
| Signal | Event | From | What AO_Display does |
|---|---|---|---|
DISPLAY_FLUSH_SIG | FlushEvt: x1, y1, x2, y2, pixels, last | AO_Gui, kscreen | kdrv_display_flush(); on failure kdrv_display_flush_abort(). last marks the final chunk, where the driver flips the framebuffer |
DISPLAY_BRIGHTNESS_SIG | BrightnessEvt: percent, persist | AO_Gui, any app | Drives 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_SIG | KodeConfigReqEvt | AO_Console | Answers the key bright; a set applies and stores like a persisted slider release |
DISPLAY_STATE_REQ_SIG | none | any part that starts late | Publishes DISPLAY_STATE_SIG again. Deferred while powering |
DISPLAY_CAPTURE_REQ_SIG | DisplayCaptureReqEvt: who | AO_Console | Freezes the shadow and posts DISPLAY_CAPTURE_RSP_SIG to who |
DISPLAY_CAPTURE_END_SIG | none | the requester | Releases the shadow |
DISPLAY_SHOT_REQ_SIG | DisplayShotReqEvt: who, quality | AO_Net | Encodes the glass as one JPEG and posts DISPLAY_SHOT_RSP_SIG to who |
DISPLAY_SHOT_END_SIG | none | the requester | Ends the loan |
DISPLAY_MIRROR_START_SIG, _STOP_, _ACK_, _INFO_REQ_ | MirrorStartEvt, MirrorAckEvt | AO_Console | The live mirror (KODEOS_MIRROR only) |
DISPLAY_TEST_REQ_SIG | none | AO_Autotest | Starts the production row |
IOEXP_FIELD_RSP_SIG | IoexpFieldEvt | AO_IoExp | Only in Display_powering: the two answers about P7 |
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIG | LowPowerRoundEvt, LowPowerStateEvt | AO_LowPower | With CONFIG_KODE_LOWPOWER: rest the glass, and put it back on state AWAKE |
DISPLAY_MINUTE_REQ_SIG | none | AO_LowPower | With CONFIG_KODE_LOWPOWER: write the time into the panel's memory |
DISPLAY_GLYPHS_SIG | DisplayGlyphsEvt | The 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_SIG | none (timers) | Itself | The wait for the glyphs, and the retry of a restore that failed |
Signals out
| Signal | Event | Meaning |
|---|---|---|
DISPLAY_STATE_SIG | DisplayStateEvt: up, bright | Published 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_SIG | DisplayCaptureRspEvt: len, w, h, status, painted | Posted to the asker. painted false means nothing was flushed since boot, so the image is black |
DISPLAY_SHOT_RSP_SIG | DisplayShotRspEvt: len, status | KODE_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_SIG | MirrorInfoEvt | The mirror's live state |
DISPLAY_TEST_SIG | DisplayTestEvt: step, total, phase, field | One record of the production row |
TEST_RESULT_SIG | named display | The row's verdict |
DISPLAY_REPAINT_SIG | none | On 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
| Call | What 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).
| Thing | Value |
|---|---|
| Brightness during the row | 100 % (KODE_DISPLAY_TEST_BRIGHT), through the driver, so nothing reaches the store |
| Painting | Bands 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 |
| Phases | DISPLAY_TEST_ASK, _PASS, _FAIL, _TIMEOUT, _DONE, _ABSENT |
| The pad | Display_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 meanwhile | Every DISPLAY_FLUSH_SIG is completed with kdrv_display_flush_abort() and dropped, so the launcher never paints over the field |
| Exit | Every 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
| Thing | Value |
|---|---|
KODEOS_SCREENSHOT | Compiles screenshot |
KODEOS_MIRROR | Depends on KODEOS_SCREENSHOT. Compiles the mirror, the JPEG codec and the scaler, the two extra claims and the shot path |
KODEOS_SHOT_HTTP | Depends on the Kode channel and KODEOS_MIRROR. Lets sdk/kaos/net ask for shots |
| Console | config key="bright" value=N, screenshot, test component=display, mirror_start, mirror_stop, mirror_ack, mirror_info |
| Setting | kset/bright, a u8, 5 to 100, default 100 (KSET_BRIGHT_DEF, KSET_BRIGHT_MIN). AO_Display is the only writer |
| Timers | DISPLAY_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
components/kaos/display:display_ao.c, the state machine, the loans, the mirror and the production row.kaos.h: the public calls and the contract of the flush semaphore.components/kdrv/display: the CO5300 driver the AO calls; seesdk/kdrv/co5300.