LILYGO T-Display-K230 · hardware note · observed
On 2026‑09‑21 the RM69A10 emitted light under Linux, and the kernel console rendered on the glass. The panel had never been deaf: it had been fully initialised by a different panel's init sequence, and never told to leave zero brightness.
the photographs
A single bright photograph of a screen could be a reflection, and a single dark one is exactly what a dead panel looks like. So the framebuffer was driven to three known states from a shell on the board and each was photographed. The change is the evidence, not any one frame.
Writing a pattern at all was necessary, and it is worth saying why: an all‑zero framebuffer on a working panel is indistinguishable from a panel that is off, and for most of this investigation the framebuffer was zeroed. Several days of “the screen is dark” contained an unknown amount of “nothing was ever drawn”.
the cause
The board is driven by the kernel's generic Canaan panel support, which takes its initialisation sequence from the device tree. That sequence was transcribed from LilyGO's RT‑Smart driver for this exact panel on this exact board — and from the wrong function in it.
rm69a10.c what it is what that meant hx8399_v2_init()
What the board sends now is below, quoted from
nix/dts/display-rm69a10-568x1232.dtsi. It follows the live
vendor function with one deliberate difference, which is marked further
down: the vendor's partial-area block is replaced by
ENTER_NORMAL_MODE. Every packet also carries the 3 ms the
vendor's dwc_lpdt_send_pkg() waits after each one and ours
did not.
panel-init-sequence = [
15 03 02 fe fd Raydium page select: FE=FD
15 03 02 80 fc page FC
15 03 02 fe 00 back to the user page 00
39 03 05 2a 00 00 02 37 column window, 0..567
39 03 05 2b 00 00 04 cf row window, 0..1231
15 03 02 13 00 ENTER_NORMAL_MODE
15 03 02 35 00 tearing effect on
15 05 02 51 fe SET_DISPLAY_BRIGHTNESS 0xFE, then 5 ms
05 7b 01 11 SLEEP_OUT, then 123 ms
05 03 01 29 DISPLAY_ON
15 03 02 3a 77 pixel format
]; 51 FE — SET_DISPLAY_BRIGHTNESS. A
DCS‑brightness AMOLED powers up at zero. It can be completely
and correctly initialised, accept every command, report itself
healthy, and emit nothing. This is almost certainly the single line
that was keeping the screen dark. 2A / 2B — the frame memory window,
which the Himax sequence never sets for this geometry. SLEEP_OUT, where the wrong sequence
allowed 1 ms.
The full transcription, command by command, with what each one is for, is
in docs/evidence/rm69a10-init-sequence.md; the comparison against how the vendor firmware drives the same part is
in docs/evidence/rtsmart-panel-bringup.md.
the console
[0.174613] fbcon: Taking over console [3.665931] Console: switching to colour frame buffer device 71x77
71×77 characters is 568/8 by 1232/16: the whole panel at an 8×16 font, which is the arithmetic confirming that fbcon has the real geometry and not a fallback. Two changes were needed, and one of them is a trap worth naming.
the regression
Hours after the photographs above, the panel went dark again and stayed
dark across reboots and a full power cycle. Every status bit the driver
exposes said the display was on. DRM reported the connector
connected, the CRTC enabled, and
dpms On. All of it was true, and none of it was worth
anything.
| hypothesis | test, on the board | result |
|---|---|---|
| the framebuffer is all zeroes | wrote 1.4 MB of /dev/urandom, then all‑0xFF, to /dev/fb0 | still black |
| the pushed touch DTB broke it | decompiled both DTBs and diffed them | only the interrupt type and touchscreen-size-* differ; zero display deltas |
| …still the DTB | pushed the image's original DTB back and rebooted | still black |
| console blanking | black 35 s after a cold boot; the default is 600 s | not blanking |
| panel power or reset GPIO | /sys/kernel/debug/gpio | dsi_reset hi, backlight_gpio hi |
| display power domain off | pm_genpd_summary | disp_domain on, subsystem active |
| display clocks gated | clk_summary | all disp_* enabled |
| the panel latched in a bad state | power-cycled the board by hand | still black |
What broke the tie was the persistent journal.
journalctl --list-boots held all four boots of that card, and
the display lines are byte‑identical in every one of them —
including the first. So that card had never lit the panel, which
dates the regression to the image rather than to anything done at runtime,
and leaves exactly one kernel‑affecting commit between the last lit
image and this one: a patch giving the DSI link 25% burst headroom, built
at 17:12 and never visually confirmed.
There is a postscript in
docs/evidence/dsi-burst-headroom.md
worth reading if you ever multiply a clock: the patch also moved the PHY's
VCO range. phy_clk_freq is in kHz and immediately feeds a
ladder of comparisons that pick the VCO bucket, so 297 000 landed under
< 330000 and 371 250 under < 440000. The
original commit called that out as intended, and it was intended, and the
patch still broke the picture — because nothing moved the VO's pixel
timing to match.
what is not known
Remeasured on the corrected image — headroom patch removed, 49.5 MHz pixel clock, LILYGO 52.19 Hz timings, lit and unsheared — over 30 s of webcam video at a locked manual exposure, because otherwise what gets measured is the camera's gain control. Brightness dips below 90% of the mean: none, against a baseline of 19 dips in 30 s with the worst at 53%. Two dips fall below 95%, which is what “it flickers sometimes” looks like as a number.
Vertical motion remains, about 3 panel rows out of 1232, seen directly as bands moving in unison with no tearing. The measurement note is worth reading for what it refuses to conclude: the apparent displacement grows 9.3× down the frame, which looks exactly like timing error accumulating from a vsync anchor — and the panel is shot at an oblique angle whose perspective magnification is 5.6× across the same strip, in the same direction. Uniform shift and accumulation are not distinguished by that footage, so neither is claimed. Warping the frames into true panel space would remove the perspective term and is the current attempt; the first try produced a neatly rectified photograph of the worktop, and the second had the near and far ends of the panel the wrong way round. Both are written up, which is the useful part.
One divergence is deliberate and settled: the vendor ends its
sequence 31 / 30 / 12 00
— partial columns, partial rows,
ENTER_PARTIAL_MODE over a restricted area — and
copying that exactly lit the panel but banded the image, which is
what a restricted partial area looks like when the VO streams a
full frame into it. ENTER_NORMAL_MODE replaced it, and
photograph 04
is the full-screen result. The divergences that
remain are listed in priority order in the bring‑up note: no
SoC display reset on enable, a stale reset pulse,
vco_cntrl 0x19 against the 0x17 Linux computes, and
CRTC‑before‑encoder ordering that streams video into a
link that is not configured yet.
RDDPM (0x0A) still times out — -110,
CMD_PKT_STATUS 0x50015, GEN_PLD_R_EMPTY
set — on a panel that is visibly working. Earlier in this
investigation that timeout was read as strong evidence the panel was
receiving nothing. That inference is now known to be unsafe, and the
correction belongs on the record next to the conclusion it spoiled.
Reverting restores pclk × 6 at the LILYGO
52.19 Hz timings — 297 MHz on the link, the rate the console
photograph above was taken at — and that image has since been
built, flashed and measured: lit, no shear, and the brightness
numbers in the item above. So the panel currently in the board is
working in the sense that light comes out of it and the console is
legible.
It is not working in the sense a requirement would need. Nothing has been drawn at a known position and photographed against it, the residual motion is unexplained, and the panel's colour, orientation and refresh have never been checked against anything. Lit is a long way from correct, and this note claims only the first.
Evidence: docs/evidence/panel-lit.md,
docs/evidence/dsi-burst-headroom.md,
docs/evidence/panel-dark.md,
docs/evidence/dsi-phy-hang.md,
docs/evidence/rm69a10-init-sequence.md,
docs/evidence/rtsmart-panel-bringup.md,
and the photographs under docs/evidence/panel-photos/.
Vendor source read for this note: rm69a10.c and
gt9895.c in LilyGO's RT‑Smart tree, and
canaan_dsi.c and
drivers/gpu/drm/panel/panel-canaan-universal.c in the pinned
Xuantie kernel. Neither tree is committed here — they are large
vendor clones — so those paths are named but not linked.