panel-canaan-universal.c · hardware note
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.
the contract
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.
lan_num, but the binding is
panel-dsi-lane, and a fragment that writes the field name
instead compiles and is silently ignored.
videomode binding: clock-frequency, hactive,
vactive and the six porch and sync values, converted straight into a
drm_display_mode and flagged PREFERRED.
dsi_reset, not the conventional
reset. Supplied by the board file, not the panel
fragment. On this board it is GPIO22, active high.
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.
&dsi port@1. Both halves must name each other or
the bridge chain never forms and nothing probes.
K_BURST_MODE. No device-tree property
can change this.
the blob
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.
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
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.
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
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.
| Property | ST7701 — the reference | RM69A10 — ours | Note |
|---|---|---|---|
| Active pixels | 480 × 800 | 568 × 1232 | portrait either way |
| Panel type | RGB LCD | AMOLED | no backlight to dim |
| clock-frequency | 39 600 000 | 39 600 000 | identical, coincidentally |
| hsync / hbp / hfp | 20 / 20 / 80 | 40 / 140 / 40 | htotal 600 against 788 |
| vsync / vbp / vfp | 10 / 70 / 220 | 4 / 16 / 16 | vtotal 1100 against 1268 |
| Refresh, computed | 60.0 Hz | 39.6 Hz | pclk ÷ (htotal × vtotal) |
| DSI lanes | 2 | 2 | — |
| Per-lane rate | 475.2 Mbps | 475.2 Mbps | matches the vendor phyclk exactly |
| DSI video mode | sync-pulse, from the driver | K_BURST_MODE, per the vendor | the driver cannot express burst |
| vth_line | 10 | 9 | from the connector table |
| Init commands | 44 | 20 | fewer commands |
| Init payload | 237 bytes | 297 bytes | and more payload — the AMOLED is not simpler |
| As a property | 369 bytes | 357 bytes | payload plus three framing bytes per command |
| Pixel format sent | 0x3A 0x50 → 16 bpp | not sent | the driver forces RGB888 regardless |
| panel-width-mm | 480 — a pixel count | 44 | we diverged; see below |
| Reset GPIO | IO22, active high | IO22, active high | same pin, same polarity |
| Backlight GPIO | IO25, active high | IO25, active high | same pin, same polarity |
the real thing
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
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.
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.
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.
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.
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.