How LilyGO’s RT-Smart firmware brings up the RM69A10, end to end
Read against our Linux path. Written 2026-09-21 from the vendor source in
repo/canmv_k230/src/rtsmart/mpp/ plus disassembly of the prebuilt
kernel/lib/libvo.a, compared with the pristine kernel tree at
/nix/store/pn7bm6ck5a3x6pnk1ng30hlf24qn85jx-source/.
Everything below is either quoted source or disassembly I read. Where I am inferring, it says so.
The headline: we are sending the wrong panel’s init sequence
docs/evidence/rm69a10-init-sequence.md records the sequence as coming from
“rm69a10_568x1232_dsi_send()”. There is no function by that name. What was
actually transcribed is rm69a10_OLD_init(), and that function has no
callers.
$ grep -rn "rm69a10_OLD_init\|rm69a10_568x1232_init" repo/canmv_k230/src/rtsmart/mpp
kernel/connector/src/rm69a10.c:39:static void rm69a10_OLD_init(k_u8 test_mode_en)
kernel/connector/src/rm69a10.c:96:static void rm69a10_568x1232_init(k_u8 test_mode_en)
kernel/connector/src/rm69a10.c:100: rt_kprintf("rm69a10_568x1232_init \n");
kernel/connector/src/rm69a10.c:345: rm69a10_568x1232_init(1);
kernel/connector/src/rm69a10.c:347: rm69a10_568x1232_init(0);
One definition, zero call sites. The live dispatcher is rm69a10.c:342-348:
if(info->type == RM69A10_MIPI_2LAN_568X1232_60FPS)
{
if(info->screen_test_mode)
rm69a10_568x1232_init(1);
else
rm69a10_568x1232_init(0);
}
and our panel is exactly that type — mpi_connector.c:43 sets
RM69A10_MIPI_2LAN_568X1232_60FPS for the "rm69a10" entry.
It is not merely unused, it is a different panel’s sequence
rm69a10_OLD_init() (rm69a10.c:41-61) is a byte-for-byte copy of
hx8399_v2_init() (hx8399.c:41-61) — the Himax HX8399 1080x1920 panel.
Same twenty k_u8 param* arrays, same values, same order, same
pag20[50] blue test-pattern array, same 0xB9 0xFF 0x83 0x99 opener
(which is Himax’s SETEXTC “password”). It is dead copy-paste left in the
file when the driver was forked from hx8399.c.
Our nix/dts/display-rm69a10-568x1232.dtsi:54-75 ships that HX8399
sequence. Twenty commands, 297 payload bytes, verified byte-identical — to
the wrong panel.
Confirmed on the vendor’s own boot log
docs/rtsmart-boot-log.txt lines 71-82, captured from this board running
LilyGO’s firmware:
rm69a10_power_on
rm69a10_power_reset
rm69a10 rst_gpio is 22
rm69a10_power_reset
rm69a10 rst_gpio is 22
rm69a10_power_reset
rm69a10 rst_gpio is 22
rm69a10_set_backlight
rm69a10_init
rm69a10_set_phy_freq
rm69a10_dsi_resolution_init
rm69a10_568x1232_init
rm69a10_vo_resolution_init
rm69a10_568x1232_init prints its own name (rm69a10.c:100). It is what
runs on the shipped firmware.
Caveat on provenance: the source in repo/ also has
rt_kprintf("connecter_dsi_send_pkg: %d \n", ret) at rm69a10.c:140 and
two read printfs at :160/:163, and those lines do not appear in the
boot log. So the flashed binary is probably an older build of the same
function, before those debug printfs were added. That does not affect the
conclusion — the function name printed is the dispatcher’s, and the
dispatcher has only ever had one branch — but the body might differ in
detail from the snapshot we hold. I could not check: there is no RT-Smart
image in firmware/, only U-Boot/SPL/OpenSBI blobs.
The sequence that actually runs
rm69a10.c:114-163, in order. Each connecter_dsi_send_pkg() is followed
by an unconditional 3 ms delay inside the transport (see below).
| # | line | bytes | DCS meaning |
|---|---|---|---|
| 1 | 116,139 | fe fd | manufacturer command page select (Raydium) |
| 2 | 117,141 | 80 fc | page-0xFD register write |
| 3 | 118,142 | fe 00 | back to user command set |
| 4 | 119,143 | 2a 00 00 02 37 | set_column_address 0..567 |
| 5 | 120,144 | 2b 00 00 04 cf | set_page_address 0..1231 |
| 6 | 121,145 | 31 00 03 02 34 | set_partial_columns 3..564 |
| 7 | 122,146 | 30 00 00 04 cf | set_partial_rows 0..1231 |
| 8 | 123,147 | 12 00 | enter_partial_mode |
| 9 | 124,148 | 35 00 | set_tear_on |
| 10 | 125,149 | 51 fe | write_display_brightness = 0xFE |
| 150 | connector_delay_us(2000) | ||
| 11 | 126,151 | 11 | exit_sleep_mode |
| 152 | connector_delay_us(120000) — 120 ms | ||
| 12 | 127,153 | 29 | set_display_on |
| 13 | 130,156 | 3a 77 | set_pixel_format = 24 bpp |
| 159,162 | connecter_dsi_read_pkg(0x05), (0x0C) |
Thirteen commands, 36 payload bytes. Not one of them appears in what we send. Three differences stand out as sufficient on their own to keep an AMOLED dark:
0x51 0xFE—write_display_brightness. A DCS-brightness-controlled AMOLED powers up at brightness 0. Nothing else in the sequence turns the emitter on. We never send it.0x2A/0x2B— the frame-memory window. The RM69A10 is a RAM-backed controller; without a column/page window the DPI stream has nowhere to land. We never send them.- 120 ms after
0x11— we allow 1 ms. The MIPI/DCS minimum afterexit_sleep_modebeforeset_display_onis 120 ms on most of this class of part. 1 ms is not a “longer than the vendor” safety margin asrm69a10-init-sequence.mdclaims; that claim was measured against the dead function’s 300 µs.
Also note 0x3A 0x77 (pixel format) is sent after 0x29, and the
manufacturer page dance 0xFE 0xFD / 0x80 0xFC / 0xFE 0x00 opens the
sequence. Neither is expressible as “just append the missing commands” —
the order matters.
1. The ordered bring-up, power-on to lit panel
1.0 Power domain — on device open
connector_dev.c:53, in connector_dev_open():
sysctl_pwr_up(SYSCTL_PD_DISP);
Same thing we now do with pm_runtime_get_sync() pinned at probe. No
divergence.
1.1 kd_mpi_connector_power_set(fd, 1) → rm69a10_power_on()
rm69a10.c:282-307:
if (on) {
// rst vo;
k230_display_rst();
// rst rm69a10
rm69a10_power_reset(1);
rt_thread_mdelay(g_blacklight_delay_ms);
rm69a10_power_reset(0);
rt_thread_mdelay(10);
rm69a10_power_reset(1);
rt_thread_mdelay(20);
g_blacklight_delay_ms = DELAY_MS_BACKLIGHT_DEFAULT;
//enable backlight
rm69a10_set_backlight(1);
}
g_blacklight_delay_ms is DELAY_MS_BACKLIGHT_FIRST = 1 on the first call
(rm69a10.c:33,37) and 200 thereafter. So on a cold boot:
display reset (below)
GPIO22 = 1
1 ms
GPIO22 = 0
10 ms
GPIO22 = 1
20 ms
GPIO25 = 1 (backlight)
rm69a10_power_reset() (rm69a10.c:248-263) does
kd_pin_mode(rst_gpio, GPIO_DM_OUTPUT) then writes
GPIO_PV_HIGH/GPIO_PV_LOW; active-low reset, GPIO22.
rm69a10_set_backlight() (rm69a10.c:265-279) drives GPIO25 high.
Pin numbers from repo/canmv_k230/include/generated/autoconf.h:37,57:
CONFIG_MPP_DSI_LCD_BACKLIGHT_PIN 25, CONFIG_MPP_DSI_LCD_RESET_PIN 22.
1.2 k230_display_rst() — SoC display-subsystem reset
Not in the RT-Smart source tree; it lives in kernel/lib/libvo.a
(kd_vo_reg.o, symbol k230_display_rst). Disassembled:
writel(0x00000000, 0x91101090) ; lui 0x9110 + 0x109, slli 4 -> 0x91101090
rt_thread_mdelay(1)
writel(0xffffffff, 0x91101090)
rt_thread_mdelay(1)
writel(0x0, dsi_base + 0x04) ; PWR_UP = 0 (DSI host held in reset)
kd_vo_software_reset()
and kd_vo_software_reset() (same object):
writel(0xf, vo_base + 0x00) ; VO_SOFT_RST_CTL
writel(0x0, vo_base + 0x08)
writel(0x0, vo_base + 0x04) ; VO_REG_LOAD_CTL
This is the first thing the vendor does, on the power-on path, before the panel reset pulse.
1.3 kd_mpi_connector_init(fd, info) → rm69a10_init()
rm69a10.c:379-393:
if(info->pixclk_div != 0)
connector_set_pixclk(info->pixclk_div);
ret |= rm69a10_set_phy_freq(&info->phy_attr);
ret |= rm69a10_dsi_resolution_init(info);
ret |= rm69a10_vo_resolution_init(&info->resolution, info->bg_color, info->intr_line);
The table entry driving all of this is mpi_connector.c:32-44:
{
"rm69a10",
0, /* screen_test_mode */
0, /* dsi_test_mode */
BACKGROUND_BLACK_COLOR,
9, /* intr_line */
14, /* pixclk_div */
K_DSI_2LAN,
K_BURST_MODE,
K_VO_LP_MODE,
{ 4, 97, 0x19, 0x96 }, /* n, m, voc, hs_freq */
{ 39600, 475200, 788, 568, 40, 140, 40, 1268, 1232, 4, 16, 16 },
RM69A10_MIPI_2LAN_568X1232_60FPS,
},
field order from k_connector_comm.h:80-93; k_connectori_phy_attr is
{n, m, voc, hs_freq} (k_connector_comm.h:71-77).
1.4 Pixel clock — k230_set_pixclk(14)
connector_comm.c:200-203 → k230_set_pixclk() in libvo.a:
reg = readl(clk_base + 0x78) ; clk_base = 0x91100000
reg &= ~0x7F8 ; GENMASK(10,3)
reg |= div << 3 ; div = 14
reg |= 0x80000000
writel(reg, clk_base + 0x78)
Identical to canaan_dsi.c:376-381, which writes (div - 1) << 3 with
div = round(594000/39600) = 15, i.e. also 14. No divergence — except
in when it happens (see §2.7).
1.5 PHY — rm69a10_set_phy_freq() → connector_set_phy_freq() → dwc_mipi_phy_config()
rm69a10.c:310-325:
mipi_phy_attr.m = phy_attr->m;
mipi_phy_attr.n = phy_attr->n;
mipi_phy_attr.hs_freq = phy_attr->hs_freq;
mipi_phy_attr.voc = phy_attr->voc;
mipi_phy_attr.phy_lan_num = K_DSI_2LAN;
connector_set_phy_freq(&mipi_phy_attr);
connector_comm.c:155-158 forwards to dwc_mipi_phy_config(), which is in
libvo.a. Disassembled in full (it is 0x62 bytes):
writel(0x0, phy_base + 0x0c) ; phy_base = dsi_base + 0x400
a0 = attr[0x04] (m)
a1 = attr[0x00] (n)
a2 = attr[0x08] & 0xff (voc)
a3 = attr[0x10] & 0xff (hs_freq)
call k230_dsi_config_4lan_phy(m, n, voc, hs_freq)
return 0
phy_lan_num is at offset 0x0c of k_vo_mipi_phy_attr
(k_vo_comm.h:256-262) and is never loaded. The only reference to offset
0x0c in the function is the writel(0x0, phy_base + 0x0c) at entry, which
is a register write to dsi_base + 0x400 + 0x0c, not a struct read.
See §3 for what this means.
k230_dsi_config_4lan_phy, k230_dsi_phy0_config, k230_dsi_phy1_config,
k230_dsi_phy_pll_config and k230_dsi_write_phy_reg all exist in
libvo.a under exactly those names, and the disassembly matches
canaan_phy.c instruction for instruction in structure. Diffs are in §2.
1.6 DSI host — rm69a10_dsi_resolution_init()
rm69a10.c:328-355:
attr.lan_num = info->lan_num; /* K_DSI_2LAN == 1 */
attr.cmd_mode = info->cmd_mode; /* K_VO_LP_MODE == 0 */
attr.lp_div = 8;
attr.work_mode = info->work_mode; /* K_BURST_MODE == 0 */
memcpy(&attr.resolution, &resolution, ...);
connector_set_dsi_attr(&attr);
... init sequence ...
connector_set_dsi_enable(1);
connector_set_dsi_attr → kd_dsi_dsi_attr() (kd_vo.o):
dwc_dsi_set_lan_num(attr->lan_num) ; attr+0x30
dwc_dsi_set_lpdt_div(attr->lp_div) ; attr+0x3c
dwc_dsi_set_vcid()
dwc_dsi_set_timing(attr)
if (attr->cmd_mode == 0) dwc_dsi_set_lpdt_mode(); else dwc_dsi_set_hs_mode();
if (attr->work_mode != 0) dwc_set_non_brust_mode(attr->work_mode);
For our panel: LP command mode, and burst mode means
dwc_set_non_brust_mode() is not called.
The individual writes, all to dsi_base (DWC MIPI-DSI-Host v1.31 offsets;
the names are my mapping, the offsets and values are read out of the
binary):
| function | register | value |
|---|---|---|
dwc_dsi_set_lan_num | 0xa4 PHY_IF_CFG | 0x2800 + lan_num → 0x2801 |
dwc_dsi_set_lpdt_div | 0x08 CLKMGR_CFG | 0x100 + lp_div → 0x108 |
dwc_dsi_set_vcid | 0x30 GEN_VCID | 0x303 |
dwc_dsi_set_timing | 0x3c VID_PKT_SIZE | hdisplay |
0x40 VID_NUM_CHUNKS | 0 | |
0x44 VID_NULL_SIZE | 0 | |
0x48 VID_HSA_TIME | lbcc(hsync_len) | |
0x4c VID_HBP_TIME | lbcc(hback_porch) | |
0x50 VID_HLINE_TIME | lbcc(htotal) | |
0x54 VID_VSA_LINES | vsync_len | |
0x58 VID_VBP_LINES | vback_porch | |
0x5c VID_VFP_LINES | vfront_porch | |
0x60 VID_VACTIVE_LINES | vdisplay | |
dwc_dsi_set_lpdt_mode | 0x34 MODE_CFG | 0x1 (command mode) |
0x38 VID_MODE_CFG | 0xbf02 | |
0x68 CMD_MODE_CFG | 0x10f7f01 | |
0x2c PCKHDL_CFG | 0x1c | |
0x04 PWR_UP | 0x1 |
lbcc is dw_mipi_dsi_get_hcomponent_lbcc(hcomp, phyclk, pclk) =
hcomp * phyclk / 8 / pclk, rounded up — same formula as
canaan_dsi_get_hcomponent_lbcc() (canaan_dsi.c:201-215).
This block is identical to Linux canaan_dsi_lpdt_init()
(canaan_dsi.c:258-284) plus canaan_dsi_set_lan_num()
(canaan_dsi.c:183-199). No divergence at all.
dwc_dsi_set_hs_mode() exists and differs (PCKHDL_CFG = 0x38, plus
LPCLK_CTRL = 3) but is not taken for this panel, and Linux has no
equivalent. Noted for completeness only.
1.7 The init sequence — transport
connecter_dsi_send_pkg() (connector_comm.c:145-148) → dwc_lpdt_send_pkg()
in libvo.a. Disassembled:
if (cmd_len <= 1):
hdr = 0x05 | (buf[0] << 8) ; DCS short write, vcid 0
writel(hdr, dsi_base + 0x6c) ; GEN_HDR
else:
for each 4-byte chunk of buf:
writel(chunk, dsi_base + 0x70) ; GEN_PLD_DATA
hdr = 0x39 | (cmd_len << 8) ; DCS long write, vcid 0
writel(hdr, dsi_base + 0x6c)
rt_thread_mdelay(3) ; <-- unconditional, every packet
Two things:
- The length split is
1 → 0x05,>= 2 → 0x39. Linux reaches the same wire types:mipi_dsi_dcs_write_buffer()picks0x05/0x15/0x39, andcanaan_dsi_transfer()routes bothDCS_SHORT_WRITE_PARAMandDCS_LONG_WRITEtocanaan_dsi_dcs_write_long(), which emits0x39(canaan_dsi.c:171). Equivalent. - 3 ms after every single packet. Linux applies only the per-command
delaybyte from the DT blob (panel-canaan-universal.c:118-119), which in our dtsi is00for eighteen of twenty commands.
There is no FIFO-full polling in the vendor path at all — the 3 ms is the flow control.
1.8 connector_set_dsi_enable(1) → dwc_dsi_enable()
connector_comm.c:165-168 → dwc_dsi_enable() in libvo.a:
call dwc_dsi_init()
writel(0x1, dsi_base + 0x94) ; LPCLK_CTRL
and dwc_dsi_init():
call dwc_dsi_set_color_coding() ; writel(0x105, dsi_base + 0x10) DPI_COLOR_CODING
writel(0x320068, dsi_base + 0x9c) ; PHY_TMR_CFG
writel(0x2e0080, dsi_base + 0x98) ; PHY_TMR_LPCLK_CFG
writel(0xffffffff, dsi_base + 0xc4); INT_MSK0
writel(0xffffffff, dsi_base + 0xc8); INT_MSK1
writel(0x0, dsi_base + 0x34) ; MODE_CFG -> video mode
writel(0x0, dsi_base + 0x68) ; CMD_MODE_CFG
writel(0x3, dsi_base + 0x94) ; LPCLK_CTRL
Identical, in the same order, to Linux canaan_mipi_dsi_set_dsi_enable()
(canaan_dsi.c:286-297). No divergence.
1.9 VO — rm69a10_vo_resolution_init()
rm69a10.c:358-376:
attr.bg_color = bg_color;
attr.intf_sync = K_VO_OUT_1080P30;
attr.intf_type = K_VO_INTF_MIPI;
attr.sync_info = resolution;
connector_set_vo_init();
connector_set_vtth_intr(1, intr_line);
connector_set_vo_param(&attr);
connector_set_vo_enable();
From libvo.a:
vo_init()calls, in order:kd_vo_wrap_init,kd_vo_set_config_mix,kd_vo_table_init,kd_vo_osd_set_addr_select_mode,kd_vo_osd_set_dma_map,kd_vo_osd_set_dma_request,kd_vo_layer_set_uv_endian_mode,kd_vo_layer_set_y_endian_mode,kd_vo_layer_set_img_blenth,kd_vo_layer_addr_select_mode,layer0_test,kd_vo_set_layer_outstanding,kd_vo_set_vtth_intr.kd_vo_set_vtth_intr(status, vpos):writel((status << 20) | (vpos & 0x1fff), vo_base + 0x3e4)— VO_DISP_IRQ1_CTL.vposis the real line number, 9 for this panel.kd_vo_set_dev_param()callskd_vo_set_timingthenkd_vo_set_background.kd_vo_enable():writel(0x11, vo_base + 0x04)thenkd_vo_timestamp_enable().
Note the vendor brings the VO up last, after the DSI link is already in video mode.
2. Diff list: what RT-Smart does that our Linux path does not
Ordered by how likely each is to be the reason the panel is dark.
2.1 The init sequence is a different panel’s (fatal, near-certain)
- Vendor:
rm69a10_568x1232_init(), 13 commands, 36 bytes, including0x51 0xFEbrightness,0x2A/0x2Bwindow, partial mode, TE on, and 120 ms between0x11and0x29, with0x3A 0x77after0x29.rm69a10.c:114-163. - Ours: the 20-command / 297-byte HX8399 sequence from the dead
rm69a10_OLD_init(), 1 ms between0x11and0x29, no brightness, no window.nix/dts/display-rm69a10-568x1232.dtsi:54-75.
2.2 No SoC display reset on the enable path
- Vendor:
k230_display_rst()runs first thing inrm69a10_power_on()(rm69a10.c:290) —0x91101090 = 0, 1 ms,= 0xffffffff, 1 ms; then DSIPWR_UP = 0; then VO soft reset (VO_SOFT_RST_CTL = 0xf,0x08 = 0,VO_REG_LOAD_CTL = 0). - Ours: the only write to
0x91101090in the kernel tree is incanaan_vo_disable_crtc()(canaan_vo.c:666-676) — the disable path. Nothing resets the display subsystem before bring-up.VO_SOFT_RST_CTL(canaan_vo_regs.h:12) is defined and never written. DSIPWR_UPis only ever set to 1 (canaan_dsi.c:283), never cleared.
2.3 Panel reset is stale and mistimed
- Vendor: high / 1 ms / low / 10 ms / high / 20 ms, immediately before
the init sequence, within the same power-on ioctl
(
rm69a10.c:292-297); backlight asserted after the last delay (rm69a10.c:301). - Ours: the pulse is at probe only — high / 200 ms / low / 200 ms /
high, with no settle delay after the final release
(
panel-canaan-universal.c:323-329) — and the prepare-time pulse is commented out by the vendor (panel-canaan-universal.c:128-143). By the timedrm_panel_prepare()sends the init sequence, the reset happened in a different callback, potentially seconds earlier, and before the DSI host was configured.
2.4 3 ms between every DCS command
- Vendor:
rt_thread_mdelay(3)at the tail ofdwc_lpdt_send_pkg(), unconditionally, after each packet. - Ours:
panel_simple_sleep(cmd->header.delay)only when non-zero (panel-canaan-universal.c:118-119); our dtsi uses00for all but the last two commands.
2.5 vco_cntrl differs
- Vendor:
0x19for this panel (mpi_connector.c:41, the third field of{ 4, 97, 0x19, 0x96 }). - Ours:
canaan_dsi_clk_cfg()computesphy_clk_freq = 39600*3*8/2/2 = 237600, hits the< 330000arm, and setsvoc = 0x17(canaan_dsi.c:338-339). mandnagree exactly: Linux derivesm = 99, n = 5and passes(m-2, n-1) = (97, 4)(canaan_dsi.c:383), matching the vendor table.hs_freqalso agrees (0x96).- So the PLL divider chain is right and only the VCO range code is wrong.
Both values have
>>4 == 1, so the post-divider matches and the output frequency computes to 475.2 MHz either way; whether0x17vs0x19matters is a question about the Synopsys VCO band table, which is not in this tree. Inference, not measured.
2.6 intr_line / vth_line
- Vendor:
intr_line = 9for"rm69a10"(mpi_connector.c:36), written as the literal line number into VO_DISP_IRQ1_CTL[12:0] with the enable bit at 20. - Ours: the dtsi says
vth_line = <10>(display-rm69a10-568x1232.dtsi:24), which is moot — Linux declaresvth_lineincanaan_vo.h:26, never reads it from DT, and instead writes32 - __builtin_clz(vtotal) - 1into the same register (canaan_vo.c:648-649). The earlier edit from 9 to 10 changed nothing, and the vendor’s value for this panel was 9 all along.
2.7 Bring-up order is inverted: VO before DSI vs DSI before VO
- Vendor (
rm69a10.c:385-390): pixclk → PHY → DSI host + init sequence- DSI enable → then VO init / vtth / timing / enable.
- Ours:
drm_atomic_helper_commit_modeset_enables()runs all CRTCatomic_enables first, socanaan_vo_enable_crtc()(canaan_vo.c:652-664) does VO init, timing, background andREG_LOAD_CTL = 0x11beforecanaan_dsi_encoder_enable()(canaan_dsi.c:388) touches the pixel clock, the PHY or the DSI host. The VO is therefore streaming pixels into an unconfigured link, and the pixel-clock divider is reprogrammed underneath a running VO.
2.8 Smaller, probably harmless
| vendor | ours | |
|---|---|---|
| DSI host register block | identical writes, identical order (§1.6, §1.8) | — |
| PHY register block | identical (§3) | — |
PHY_TST_CTRL0 settle in phy1_config | rt_thread_mdelay(1) | msleep(20) (canaan_phy.c:174) |
post-PHY_STATUS settle in config_4lan | rt_thread_mdelay(1) | msleep(20) (canaan_phy.c:252) |
PHY_TST_CTRL1 != 0x580 loop | unbounded | bounded at 1000 (canaan_phy.c:230-231) |
PHY_STATUS != 0x1fbd loops | unbounded | unbounded upstream; bounded in our patched tree |
| VO background | bg_color from the table | hard-coded 0xffffff (canaan_vo.c:660) |
layer0_test() in vo_init | called | no equivalent |
| DCS read busy poll | reads 0xb0 bit 6 — looks like a vendor bug, should be 0x74 | we poll 0x74 correctly |
3. The 2-lane question, answered: it is a non-issue
The leading hypothesis in docs/evidence/dsi-phy-hang.md — that the Linux
driver programs the D-PHY for four lanes because canaan_phy.c exports only
k230_dsi_config_4lan_phy() — is wrong, and the vendor binary proves it.
dwc_mipi_phy_config() is the only consumer of phy_lan_num, and it does
not consume it. Full disassembly (libvo.a, kd_vo_reg.o,
.text.dwc_mipi_phy_config, 0x62 bytes):
8: sd a0, -0x18(s0) ; save attr pointer
10: auipc a5, display_remap ; phy base = remap[1] (dsi_base + 0x400)
1c: addi a5, a5, 0xc
20: li a0, 0x0
22: call __raw_writel ; writel(0, phy_base + 0x0c)
2a: ld a5, -0x18(s0)
2e: lw a4, 0x4(a5) ; attr->m
30: ld a5, -0x18(s0)
34: lw a1, 0x0(a5) ; attr->n
36: ld a5, -0x18(s0)
3a: lw a5, 0x8(a5)
3c: zext.b a2, a5 ; attr->voc
40: ld a5, -0x18(s0)
44: lw a5, 0x10(a5)
46: zext.b a5, a5
4a: mv a3, a5 ; attr->hs_freq
4c: mv a0, a4
4e: call k230_dsi_config_4lan_phy ; (m, n, voc, hs_freq)
56: li a5, 0x0
Struct offsets from k_vo_comm.h:255-262:
typedef struct
{
k_u32 n; /* +0x00 */
k_u32 m; /* +0x04 */
k_u32 voc; /* +0x08 */
k_u32 phy_lan_num; /* +0x0c */
k_u32 hs_freq; /* +0x10 */
} k_vo_mipi_phy_attr;
Offset 0x00, 0x04, 0x08 and 0x10 are loaded. 0x0c is not.
phy_lan_num is dead in the vendor firmware too — every panel, 2-lane and
4-lane alike, gets the identical k230_dsi_config_4lan_phy() call. Setting
mipi_phy_attr.phy_lan_num = K_DSI_2LAN at rm69a10.c:321 has no effect
whatsoever.
Where lane count is consumed
Exactly one register, in both stacks:
- Vendor:
dwc_dsi_set_lan_num(n)→writel(0x2800 + n, dsi_base + 0xa4). WithK_DSI_2LAN == 1(k_vo_comm.h:157-163) that is0x2801. - Ours:
canaan_dsi_set_lan_num()(canaan_dsi.c:183-199),case 2: dsi_write(dsi, PHY_IF_CFG, 0x2801);— the same value, driven frompanel-dsi-lane = <2>viadevice->lanes(canaan_dsi.c:403).
Linux has one additional lane dependency the vendor lacks:
phy_clk_freq = dsi->clk_freq * 3 * 8 / device->lanes / 2
(canaan_dsi.c:325). That is the vendor’s table-driven phyclk = 475200
recomputed, and it comes out right for 2 lanes.
Structural comparison of the PHY code
canaan_phy.c is a faithful transliteration of the libvo.a functions.
I walked all four side by side:
RT-Smart (libvo.a) | Linux | verdict |
|---|---|---|
k230_dsi_write_phy_reg | canaan_phy.c:42-56 | same |
k230_dsi_phy_pll_config | canaan_phy.c:58-113 | same structure |
k230_dsi_phy0_config(m,n,voc,hsfreq) | canaan_phy.c:115-163 | same |
k230_dsi_phy1_config(hsfreq) | canaan_phy.c:165-234 | same except delays and the loop bound |
k230_dsi_config_4lan_phy(m,n,voc,hsfreq) | canaan_phy.c:236-266 | same except mdelay(1) vs msleep(20) |
k230_dsi_config_4lan_phy disassembles to exactly:
k230_dsi_phy0_config(m, n, voc, hs_freq)
k230_dsi_phy1_config(hs_freq)
readl(phy_base + 0x04); writel(0x0, phy_base + 0x04)
writel(0xd, dsi_base + 0xa0); writel(0xf, dsi_base + 0xa0)
while (readl(dsi_base + 0xb0) != 0x1fbd) ;
rt_thread_mdelay(1) /* Linux: msleep(20) */
writel(0x1, dsi_base + 0x94)
readl(phy_base + 0x04); writel(0x400000, phy_base + 0x04)
while (readl(dsi_base + 0xb0) != 0x1fbd) ;
readl(dsi_base + 0xb0)
0x1fbd is the vendor’s own expected value on a 2-lane panel — the same
constant, in the same firmware, that lights this exact board. It was never
a 4-lane encoding.
Conclusion. nix/patches/canaan-dsi-2lane-phy-path.patch, currently
staged, is chasing a defect that does not exist. The vendor programs the
D-PHY identically for 2 and 4 lanes, and our PHY already reaches 0x1fbd
on hardware. That work should be dropped and the effort moved to §2.1.
4. Panel supply rails
There are none to model. The vendor’s entire panel power story is two GPIOs:
- GPIO22, reset,
rm69a10_power_reset()(rm69a10.c:248-263),CONFIG_MPP_DSI_LCD_RESET_PIN=22. - GPIO25, “backlight”,
rm69a10_set_backlight()(rm69a10.c:265-279),CONFIG_MPP_DSI_LCD_BACKLIGHT_PIN=25.
No regulator, no PMIC write, no I²C to a charge pump, no third GPIO. Grep
for VCI / ELVDD / AVDD / regulator across
repo/canmv_k230/src/rtsmart/mpp/kernel/connector/ returns nothing.
So the “missing VCI/ELVDD/AVDD” suspect from dsi-phy-hang.md is
retired: if the vendor needs no such rail, neither do we. On an AMOLED
whose ELVDD comes from an on-FPC charge pump, the equivalent of “turn on
the supply” is 0x51 — the DCS brightness write we are not sending.
Our side already drives both GPIOs (panel-canaan-universal.c:318, 331,
asserted at :324-328 and :339), so there is nothing to add; only the
reset timing differs (§2.3).
What to do next, in order
- Replace the init sequence in
nix/dts/display-rm69a10-568x1232.dtsiwith the thirteen commands fromrm69a10_568x1232_init(), with the 120 ms after0x11, and0x3A 0x77after0x29. This is the one change that is both high-probability and cheap. The DTdelaybyte is u8 milliseconds, so 120 fits; the 2000 µs before0x11becomes 2 ms. - Set the DT
delaybyte to 3 on every command, matching the vendor transport. - Drop
nix/patches/canaan-dsi-2lane-phy-path.patch(§3). - If still dark: move the reset pulse into
canaan_panel_prepare()with the vendor’s 1/10/20 ms timing, and addk230_display_rst()’s0x91101090poke to the enable path (§2.2, §2.3). - Then
voc = 0x19(§2.5) and the VO/DSI ordering (§2.7).
The DCS read (0x0A) that currently times out should start answering as
soon as the panel is actually initialised — the vendor’s own code reads
0x05 and 0x0C right after 0x29 (rm69a10.c:159-163) and expects a
reply, so a successful read is the pass/fail signal for step 1.