panel-canaan-universal.c · hardware note

A panel is a byte string and eleven numbers

The Canaan DRM panel driver has no knowledge of any display. Everything that makes an ST7701 an ST7701 lives in a 97-line .dtsi, so the RM69A10 is a new file rather than a new driver — provided its numbers fit the same slots. Two of them do not.

Driver
panel-canaan-universal.c
395 lines, no panel-specific code
Init sequence
20 commands
297 payload bytes
As a property
357 bytes
297 payload + 20×3 framing
Timing
39.6 MHz over 788×1268
≈ 39.6 Hz, 2 lanes
Status
compiles and round-trips
never lit a panel

the contract

Every property the driver reads

Where it reads each one, and what it becomes. The two rows at the bottom are the interesting ones: the settings the driver does not read, because it hardcodes them.

Device-tree propertyRead atBecomes
compatible = "canaan,universal" canaan_panel_of_match[] The only string this driver matches. Panel identity is not encoded anywhere — every Canaan panel is “universal”.
reg = <0> the MIPI DSI core The DSI virtual channel. Always 0 for a single panel.
panel-dsi-lane parse_dt → ctx->lan_num
probe → dsi->lanes
How many DSI data lanes the host is told to use: 2 for the ST7701, and 2 for the RM69A10. Note the property name — the driver’s internal field is lan_num, but the binding is panel-dsi-lane, and a fragment that writes the field name instead compiles and is silently ignored.
panel-width-mm
panel-height-mm
parse_dt → ctx->width_mm
get_modes → display_info
Physical size in millimetres, which is what DRM and every userspace DPI calculation assume. The reference file puts 480 and 800 in these fields — the pixel counts. Ours puts 44 and 95.
display-timings { timing-0 } of_get_videomode(np, &ctx->vm, 0) The standard videomode binding: clock-frequency, hactive, vactive and the six porch and sync values, converted straight into a drm_display_mode and flagged PREFERRED.
panel-init-sequence = [ … ] of_get_property +
panel_simple_parse_cmd_seq
A raw blob of length-prefixed DCS commands. This is the entire panel personality, and for this panel it is 297 bytes of it.
dsi_reset-gpios devm_gpiod_get(dev, "dsi_reset") Note the consumer string: dsi_reset, not the conventional reset. Supplied by the board file, not the panel fragment. On this board it is GPIO22, active high.
backlight_gpio-gpios devm_gpiod_get(dev, "backlight_gpio") Stored in a field called power_on and driven high once at probe. A panel-enable line, not a brightness control — there is no PWM and no backlight node. GPIO25, active high, also from the board file.
ports/port@0/endpoint DRM of_graph Pairs with &dsi port@1. Both halves must name each other or the bridge chain never forms and nothing probes.
&vo { vth_line = <9>; } canaan,k230-vo — not the panel driver The scanline at which the video output raises its tearing interrupt. It lives in the panel fragment because it is a per-panel number, but it overrides a property on a different node.
— not from the device tree — probe: dsi->mode_flags =
MIPI_DSI_MODE_VIDEO_SYNC_PULSE
Hardcoded, for every panel. The vendor’s own RT-Smart driver runs the RM69A10 in K_BURST_MODE. No device-tree property can change this.
— not from the device tree — probe: dsi->format =
MIPI_DSI_FMT_RGB888
Hardcoded. 24 bits per pixel on the wire, regardless of what the init sequence sets on the panel side.

the blob

Reading the init sequence

The blob is a flat stream of three-byte headers, each followed by its payload. No terminator and no count: the parser walks it twice, once to count commands and once to build them, and rejects the whole property if the lengths do not land exactly on the end.

ONE COMMAND — HEADER, THEN PAYLOAD 39 00 04 b9 ff 83 99 the next header starts here — there is no separator and no total count anywhere. header — 3 bytes payload — exactly payload_length bytes data_type 0x05, 0x15 or 0x39. All three are handled the same way; anything else aborts the probe. delay Milliseconds to sleep AFTER the command. A u8, so 255 ms is the longest wait, and 1 ms the shortest non-zero one. payload_length Includes the DCS command byte itself, so a bare command with no parameters is length 1, not 0. 05 01 01 11 DCS 0x11 — exit_sleep_mode, then wait 1 ms the vendor waits 300 microseconds here, which this field cannot express
Wire-level detail the parser hides: panel_simple_xfer_dsi_cmd_seq calls mipi_dsi_dcs_write_buffer for all three accepted data_type values, and that helper picks the real DSI packet type from the buffer length itself. The type byte is therefore decorative. The reference blob uses 0x39 even for single-byte commands, where 0x05 would be correct, and it works; ours uses the correct type for each length anyway, because a file that is right for the right reason is cheaper to read later.

20 commands, in order, drawn in proportion to payload 0xB4 · 45 B and 0xE0 · 55 B in violet — a third of the blob

The vendor also issues two DCS reads after the sequence, of the error register and the pixel format, apparently for debugging. The panel-init-sequence binding is write-only, so there is nowhere to put them. One further command, a solid blue test pattern, is sent only when the vendor’s test_mode_en is set, and is not part of normal initialisation.

lifecycle

When the reset line actually moves

This is the part that will cost someone an afternoon. The reset pulse happens in probe, once. The reset code inside prepare — the callback DRM invokes every time the panel comes back — is present in the source and entirely commented out.

ONCE, AT PROBE canaan_panel_dsi_probe() gpiod_direction_output(rst, 1) sleep 200 ms set rst = 0 sleep 200 ms set rst = 1 power_on = 1 (the enable pin) parse_dt(); mipi_dsi_attach() no delay after the final rising edge EVERY TIME DRM ENABLES IT canaan_panel_prepare() // gpiod_set_value(power_on, 1); // gpiod_set_value(reset, 1); // sleep 200; set 0; sleep 200; // set 1; sleep 200; commented out in the source xfer_dsi_cmd_seq(init_seq) the only thing prepare actually does EVERY TIME DRM DISABLES IT canaan_panel_unprepare() power_on = 0 reset = 0 sleep 500 ms Both lines are now low, and nothing in prepare raises them. The next prepare talks DCS to a panel that is held in reset. suspend / resume, or any modeset cycle FOR COMPARISON — WHAT THE VENDOR'S OWN RM69A10 CODE DOES ON EVERY POWER-ON k230_display_rst(); reset the VO block first rst = 1; delay 1 ms (200 ms on later calls) rst = 0; delay 10 ms rst = 1; delay 20 ms then backlight = 1, then the DCS sequence Low for 10 ms, not 200. Settles for 20 ms after release, where the Linux driver waits none at all before its first DCS write.
Source: drivers/gpu/drm/panel/panel-canaan-universal.c at 7d4e1f4, read against mpp/kernel/connector/src/rm69a10.c in LilyGO’s BSP. If the display works on first boot and dies after a blank, this is why. The fix is in the driver, not the device tree, which means the panel work may need a patch as well as a .dtsi.

comparison

The two panels, number for number

Left column is what the reference .dtsi contains. Right column is what the vendor’s connector table says about the RM69A10 — the same numbers the shipped RT-Smart firmware runs on, which makes them observations of working firmware rather than datasheet claims.

PropertyST7701 — the referenceRM69A10 — oursNote
Active pixels480 × 800568 × 1232portrait either way
Panel typeRGB LCDAMOLEDno backlight to dim
clock-frequency39 600 00039 600 000identical, coincidentally
hsync / hbp / hfp20 / 20 / 8040 / 140 / 40htotal 600 against 788
vsync / vbp / vfp10 / 70 / 2204 / 16 / 16vtotal 1100 against 1268
Refresh, computed60.0 Hz39.6 Hzpclk ÷ (htotal × vtotal)
DSI lanes22—
Per-lane rate475.2 Mbps475.2 Mbpsmatches the vendor phyclk exactly
DSI video modesync-pulse, from the driverK_BURST_MODE, per the vendorthe driver cannot express burst
vth_line109from the connector table
Init commands4420fewer commands
Init payload237 bytes297 bytesand more payload — the AMOLED is not simpler
As a property369 bytes357 bytespayload plus three framing bytes per command
Pixel format sent0x3A 0x50 → 16 bppnot sentthe driver forces RGB888 regardless
panel-width-mm480 — a pixel count44we diverged; see below
Reset GPIOIO22, active highIO22, active highsame pin, same polarity
Backlight GPIOIO25, active highIO25, active highsame pin, same polarity

the real thing

The fragment we wrote

An earlier draft of this note carried a fragment that compiled and was structurally wrong: the panel sat at the root of the tree rather than inside &dsi, the lane count used the driver’s internal field name instead of the binding, the panel node carried its own reset-gpios, and both the endpoint graph and &vo { vth_line } were missing. Compiling in isolation proved the syntax parsed; it said nothing about whether the driver would ever see the node. This is the corrected file.

// nix/dts/display-rm69a10-568x1232.dtsi (abridged: the 20-command blob
// is shown with its four largest payloads elided for width)

&vo {
    vth_line = <9>;            // intr_line from the vendor connector table; ST7701 uses 10
};

&dsi {
    ports {
        port@1 {
            reg = <1>;
            dsi_out_rm69a10: endpoint { remote-endpoint = <&rm69a10_in>; };
        };
    };

    lcd: panel@0 {                // a DSI child, and labelled so the board can reach it
        compatible = "canaan,universal";
        reg = <0>;

        // DIVERGENCE: the reference puts PIXEL COUNTS in these. These are real
        // millimetres, computed from the stated 4.1" diagonal — not measured.
        panel-width-mm  = <44>;
        panel-height-mm = <95>;

        panel-dsi-lane  = <2>;

        // framing: data_type, delay_ms, payload_length, payload…
        // delay is a u8 of MILLIseconds. The vendor waits 300 us after 0x11
        // and 100 us after 0x29; neither is expressible, so both become 1 ms,
        // which is longer than the vendor waits.
        panel-init-sequence = [
            39 00 04 b9 ff 83 99
            15 00 02 d2 aa
            39 00 10 b1 02 04 71 91 01 32 33 11 11 ab 4d 56 73 02 02
            39 00 10 b2 00 80 80 ae 05 07 5a 11 00 00 10 1e 70 03 d4
            39 00 2d b4 00 ff 02 c0 02 c0 …          45 bytes
            39 00 22 d3 00 00 00 00 00 00 …          34 bytes
            39 00 21 d5 18 18 19 19 18 18 …          33 bytes
            39 00 21 d6 18 18 19 19 40 40 …          33 bytes
            39 00 11 d8 a2 aa 02 a0 a2 a8 02 a0 b0 00 00 00 b0 00 00 00
            15 00 02 bd 01
            39 00 11 d8 b0 00 00 00 b0 00 00 00 e2 aa 03 f0 e2 aa 03 f0
            15 00 02 bd 02
            39 00 09 d8 e2 aa 03 f0 e2 aa 03 f0
            15 00 02 bd 00
            39 00 03 b6 8d 8d
            15 00 02 cc 04
            39 00 03 c6 ff f9
            39 00 37 e0 00 12 1f 1a 40 4a …          55 bytes — the gamma table
            05 01 01 11                              exit sleep, then 1 ms
            05 01 01 29                              display on, then 1 ms
        ];

        display-timings {
            timing-0 {
                clock-frequency = <39600000>;
                hactive = <568>;   vactive = <1232>;
                hfront-porch = <40>;  hback-porch = <140>;  hsync-len = <40>;
                vfront-porch = <16>;  vback-porch = <16>;   vsync-len = <4>;
            };
        };

        ports {                     // the other half of the endpoint pair
            #address-cells = <1>;  #size-cells = <0>;
            port@0 {
                reg = <0>;
                rm69a10_in: endpoint { remote-endpoint = <&dsi_out_rm69a10>; };
            };
        };
    };
};

// NOT HERE, deliberately: no reset-gpios and no backlight GPIO on the panel
//   node. The board .dts attaches both to the 'lcd' label, which is how the
//   reference splits panel from board.

open

What still has to be settled

1

Sync-pulse video mode against burst

The driver hardcodes MIPI_DSI_MODE_VIDEO_SYNC_PULSE; the vendor runs this panel in burst mode, and the porches in the timing row were chosen for burst. Either the RM69A10 tolerates sync-pulse at these timings, or panel-canaan-universal.c needs a mode_flags property adding. There is no way to find out except to try it.

2

Whether prepare needs fixing before anything works at all

A first boot might well come up, because probe does pulse reset. The question is whether DRM calls unprepare at any point during normal bring-up — a mode negotiation, a console switch — before the first frame. If it does, the panel is in reset by the time prepare runs and nothing will appear. Uncommenting the reset block is a four-line patch; it may be a prerequisite rather than a polish item.

3

What IO25 actually gates

The reference board file settles the easy half: GPIO25, active high, consumed as backlight_gpio. The U-Boot pinmux calls it LCD_EN and the RT-Smart config calls it MPP_DSI_LCD_BACKLIGHT_PIN. An AMOLED has no backlight, so it is presumably a panel power enable — and if it gates a rail, the driver’s habit of dropping it in unprepare and never raising it again matters more, not less. The schematic around that net has not been read.

4

Brightness, and the delays the binding cannot express

An AMOLED’s brightness is a DCS write, which this binding can only do at init, so a real control means either a backlight device in the driver or a userspace path that writes DCS directly. Separately, the vendor’s two post-command delays are 300 and 100 microseconds and the binding’s field is a u8 of milliseconds; both become 1 ms, which is longer than the vendor waits. Worth watching if the panel misbehaves at init.

5

Rotation and mirroring

568 × 1232 is a phone-shaped portrait panel. Nothing in this binding carries a rotation property, and the vendor’s mirror control is a runtime patch of the 0xCC parameter that a static byte string cannot express — ours carries 0x04, the unmirrored default. Whatever orientation the panel comes up in is what DRM will report.

Driver: drivers/gpu/drm/panel/panel-canaan-universal.c. Reference fragment: arch/riscv/boot/dts/canaan/display-st7701-480x800.dtsi. Both at ruyisdk/linux-xuantie-kernel 7d4e1f444f461dbe3833bd99a4640e7b6c2cd529; the byte and command counts for both panels are from parsing the two blobs, not from the files' own comments.

RM69A10 values: mpp/kernel/connector/src/rm69a10.c and connector_info_list[] in mpp/userapps/src/connector/mpi_connector.c, plus MPP_DSI_LCD_RESET_PIN=22 and BACKLIGHT_PIN=25 from the vendor's auto.conf. The per-command transcription with a source line each is committed in this repository.

Refresh rates, lane rates and physical dimensions here are arithmetic on those values, not measurements. Nothing on this page has lit a panel.