linux-sunxi.lists.linux.dev archive mirror
 help / color / mirror / Atom feed
* [RFC PATCH] arm64: allwinner: add TrimUI Smart Pro (A133) board support
@ 2026-09-12  6:02 leow149
  2026-09-12  6:27 ` sashiko-bot
  2026-09-12  7:57 ` [RFC PATCH v2] " leow149
  0 siblings, 2 replies; 18+ messages in thread
From: leow149 @ 2026-09-12  6:02 UTC (permalink / raw)
  To: dri-devel
  Cc: linux-sunxi, linux-arm-kernel, leow149, Chen-Yu Tsai,
	Jernej Skrabec, Samuel Holland, Maxime Ripard, Paul Kocialkowski

Board bring-up for the TrimUI Smart Pro handheld (Allwinner A133 Plus):

- New board DTS (sun50i-a133-trimui-smartpro.dts): display pipeline
  (mixer/TCON/DSI/D-PHY), regulators, MMC, PWM backlight.
- New OTM1289A/ER68576 DSI panel driver (panel-orisetech-otm1289a.c),
  including real panel timings and DCS init sequence.
- New PWM driver for this SoC's newer-generation PWM IP
  (pwm-sun50i-a133.c), with binding doc.
- sun8i_mixer: A133 DE2 mixer0/mixer1 configs.
- sun8i_tcon_top: A133 quirks entry, plus two new helpers
  (sun8i_tcon_top_set_dsi_gate/set_de0_port) needed because this
  board's TCON quirks table has no .set_mux callback to reach them
  through the existing paths.
- sun4i_tcon: CPU/8080-interface DSI bring-up (sun4i_tcon0_mode_set_cpu
  register formulas, an hrtimer-driven TRIGGER_START retrigger, an
  FSYNC_INT storm fix, IRQ deferred-enable ordering) plus TCON-TOP
  wiring for this board's DSI clock gate and DE0 port routing.
- sun6i_mipi_dsi: an additional DSI_START_TRI instruction-jump-table
  case for continuous per-frame retriggering (distinct from the
  existing one-shot HS-clock-enable case), a DRQ_SET formula fix, and
  reordering the HS-clock-enable step to run before the panel's DCS
  init sequence.
- ccu-sun50i-a100: CLK_SET_RATE_PARENT on the tcon-lcd0 clock so its
  rate request can actually explore parent PLL rates.

Sending as RFC rather than waiting for the display to fully work:
posting the board/DTS foundation now so it's in front of people who
know this hardware, rather than sitting on it indefinitely.

Known issue, and the actual reason for the RFC tag: the panel attaches
and the backlight lights, but no pixel data reaches it.
TCON0_CPU_IF_REG's TRIGGER_START bit (a documented self-clearing
one-shot) never self-clears, despite the rest of the pixel pipeline
matching a working vendor system register-for-register -- every
static register value, D-PHY lane state, control-flow/sequencing
cross-referenced against a symbol-resolved decompile of the vendor
kernel, and even measured real-time timing all confirmed matching.
Full writeup, including what's been ruled out and how:
https://github.com/leow149/trimui-a133-mainline-port/blob/main/COMMUNITY_HELP_REQUEST.md

If anyone recognizes this symptom on the CPU/8080-interface DSI path,
or can spot something the writeup missed, I would very much like to
hear about it.

Cc: Chen-Yu Tsai <wens@kernel.org>
Cc: Jernej Skrabec <jernej.skrabec@gmail.com>
Cc: Samuel Holland <samuel@sholland.org>
Cc: Maxime Ripard <mripard@kernel.org>
Cc: Paul Kocialkowski <paulk@sys-base.io>
Signed-off-by: leow149 <leow149@lucidmail.xyz>
---
 .../allwinner,sun8i-a83t-de2-mixer.yaml       |   2 +
 .../display/allwinner,sun8i-r40-tcon-top.yaml |   1 +
 .../pwm/allwinner,sun50i-a133-pwm.yaml        |  67 ++
 arch/arm64/boot/dts/allwinner/Makefile        |   1 +
 .../allwinner/sun50i-a133-trimui-smartpro.dts | 691 ++++++++++++++++++
 drivers/clk/sunxi-ng/ccu-sun50i-a100.c        |  16 +-
 drivers/gpu/drm/panel/Kconfig                 |   8 +
 drivers/gpu/drm/panel/Makefile                |   1 +
 .../gpu/drm/panel/panel-orisetech-otm1289a.c  | 424 +++++++++++
 drivers/gpu/drm/sun4i/sun4i_tcon.c            | 343 ++++++++-
 drivers/gpu/drm/sun4i/sun4i_tcon.h            |  86 +++
 drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c        | 161 +++-
 drivers/gpu/drm/sun4i/sun6i_mipi_dsi.h        |  12 +
 drivers/gpu/drm/sun4i/sun8i_mixer.c           |  48 ++
 drivers/gpu/drm/sun4i/sun8i_tcon_top.c        |  94 +++
 drivers/gpu/drm/sun4i/sun8i_tcon_top.h        |   2 +
 drivers/pwm/Kconfig                           |  14 +
 drivers/pwm/Makefile                          |   1 +
 drivers/pwm/pwm-sun50i-a133.c                 | 276 +++++++
 19 files changed, 2209 insertions(+), 39 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/pwm/allwinner,sun50i-a133-pwm.yaml
 create mode 100644 arch/arm64/boot/dts/allwinner/sun50i-a133-trimui-smartpro.dts
 create mode 100644 drivers/gpu/drm/panel/panel-orisetech-otm1289a.c
 create mode 100644 drivers/pwm/pwm-sun50i-a133.c

diff --git a/Documentation/devicetree/bindings/display/allwinner,sun8i-a83t-de2-mixer.yaml b/Documentation/devicetree/bindings/display/allwinner,sun8i-a83t-de2-mixer.yaml
index cbd18fd83..ce72c1ba7 100644
--- a/Documentation/devicetree/bindings/display/allwinner,sun8i-a83t-de2-mixer.yaml
+++ b/Documentation/devicetree/bindings/display/allwinner,sun8i-a83t-de2-mixer.yaml
@@ -23,6 +23,8 @@ properties:
       - allwinner,sun20i-d1-de2-mixer-1
       - allwinner,sun50i-a64-de2-mixer-0
       - allwinner,sun50i-a64-de2-mixer-1
+      - allwinner,sun50i-a133-de2-mixer-0
+      - allwinner,sun50i-a133-de2-mixer-1
       - allwinner,sun50i-h6-de3-mixer-0
       - allwinner,sun50i-h616-de33-mixer-0
 
diff --git a/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml b/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml
index 7d849c409..c3809902c 100644
--- a/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml
+++ b/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml
@@ -43,6 +43,7 @@ properties:
       - allwinner,sun8i-r40-tcon-top
       - allwinner,sun20i-d1-tcon-top
       - allwinner,sun50i-h6-tcon-top
+      - allwinner,sun50i-a133-tcon-top
 
   reg:
     maxItems: 1
diff --git a/Documentation/devicetree/bindings/pwm/allwinner,sun50i-a133-pwm.yaml b/Documentation/devicetree/bindings/pwm/allwinner,sun50i-a133-pwm.yaml
new file mode 100644
index 000000000..59e4fba3b
--- /dev/null
+++ b/Documentation/devicetree/bindings/pwm/allwinner,sun50i-a133-pwm.yaml
@@ -0,0 +1,67 @@
+# SPDX-License-Identifier: GPL-2.0
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/pwm/allwinner,sun50i-a133-pwm.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Allwinner A133 PWM Controller
+
+description: |
+  No public register documentation for this IP has ever been reused
+  upstream: it's a newer, richer generation than the existing
+  allwinner,sun4i-a10-pwm family (16 channels grouped in pairs, per-pair
+  dead-zone/group-sync control) with no existing mainline driver, and no
+  A100/A133-specific binding either. Confirmed real from the public A133
+  User Manual (chapter 10.11, base 0x0300a000), which documents this
+  block down to bit level.
+
+maintainers:
+  - out-of-tree hobbyist port, not yet upstream
+
+properties:
+  compatible:
+    const: allwinner,sun50i-a133-pwm
+
+  reg:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: Bus Clock (register access only — the PWM output
+          clock source itself is internal to this IP, selected between
+          OSC24M/APB1 per-pair in its own PCCR01/PCCR23 registers, not
+          exposed as a separate CCU clock)
+
+  clock-names:
+    items:
+      - const: bus
+
+  resets:
+    maxItems: 1
+
+  "#pwm-cells":
+    const: 3
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - clock-names
+  - resets
+  - "#pwm-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/clock/sun50i-a100-ccu.h>
+    #include <dt-bindings/reset/sun50i-a100-ccu.h>
+
+    pwm: pwm@300a000 {
+        compatible = "allwinner,sun50i-a133-pwm";
+        reg = <0x0300a000 0x400>;
+        clocks = <&ccu CLK_BUS_PWM>;
+        clock-names = "bus";
+        resets = <&ccu RST_BUS_PWM>;
+        #pwm-cells = <3>;
+    };
diff --git a/arch/arm64/boot/dts/allwinner/Makefile b/arch/arm64/boot/dts/allwinner/Makefile
index aa21f58a4..6132fda32 100644
--- a/arch/arm64/boot/dts/allwinner/Makefile
+++ b/arch/arm64/boot/dts/allwinner/Makefile
@@ -26,6 +26,7 @@ dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h64-remix-mini-pc.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a100-allwinner-perf1.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a133-helperboard.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a133-liontron-h-a133l.dtb
+dtb-$(CONFIG_ARCH_SUNXI) += sun50i-a133-trimui-smartpro.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h5-bananapi-m2-plus.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h5-bananapi-m2-plus-v1.2.dtb
 dtb-$(CONFIG_ARCH_SUNXI) += sun50i-h5-emlid-neutis-n5-devboard.dtb
diff --git a/arch/arm64/boot/dts/allwinner/sun50i-a133-trimui-smartpro.dts b/arch/arm64/boot/dts/allwinner/sun50i-a133-trimui-smartpro.dts
new file mode 100644
index 000000000..531be1d90
--- /dev/null
+++ b/arch/arm64/boot/dts/allwinner/sun50i-a133-trimui-smartpro.dts
@@ -0,0 +1,691 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+/*
+ * Copyright (c) 2026 (out-of-tree hobbyist port, not yet upstream)
+ *
+ * Board: TrimUI Smart Pro (Allwinner A133 Plus / sun50iw10p1)
+ *
+ * Sourced from:
+ *  - vendor DTB extracted from Knulli image (dtb_86065152.dts, offset
+ *    86065152 in knulli-a133-trimui-smart-pro-scarab-20260511.img)
+ *  - sun50i-a133-liontron-h-a133l.dts (mainline reference board, same SoC)
+ *  - sun50i-h700-anbernic-rg35xx-2024.dts (mainline handheld reference for
+ *    AXP717 regulator/battery wiring style)
+ *  - LIVE data pulled directly off the running unit over UART (root shell +
+ *    /sys/firmware/fdt, the kernel's own resolved device tree — not a guess):
+ *    real regulator voltages (confirmed via U-Boot's PMIC init console log),
+ *    real DRAM timing table (see trimui-smartpro_defconfig), real panel
+ *    timings (see the lcd0 comment block below).
+ *
+ * TODO markers below are places nothing above could resolve: buttons/
+ * joystick are NOT GPIO at all (confirmed: read over a UART-attached
+ * co-microcontroller, see BUTTON_PROBING_PLAN.md — needs a from-scratch
+ * serdev driver, not a DT gpio-keys node), and the panel's DSI init
+ * command sequence (register writes for the otm1289a controller) is
+ * not yet captured.
+ */
+
+/dts-v1/;
+
+#include "sun50i-a100.dtsi"
+#include "sun50i-a100-cpu-opp.dtsi"
+
+#include <dt-bindings/clock/sun8i-tcon-top.h>
+#include <dt-bindings/gpio/gpio.h>
+#include <dt-bindings/input/linux-event-codes.h>
+#include <dt-bindings/leds/common.h>
+
+/ {
+	model = "TrimUI Smart Pro";
+	chassis-type = "handset";
+	/* "trimui" is not a registered vendor prefix in dt-bindings yet;
+	 * fine for an out-of-tree hobbyist build, would need registering
+	 * (vendor-prefixes.yaml) before any upstream submission. */
+	compatible = "trimui,smart-pro", "allwinner,sun50i-a100";
+
+	aliases {
+		serial0 = &uart0;
+	};
+
+	chosen {
+		/* confirmed from the vendor boot.img kernel cmdline */
+		stdout-path = "serial0:115200n8";
+	};
+
+	battery: battery {
+		compatible = "simple-battery";
+		/* vendor DTB: pmu_battery_rdc = 0x93, pmu_battery_cap = 0x1388 (5000mAh) */
+		voltage-max-design-microvolt = <4200000>;
+	};
+
+	reg_vcc5v: regulator-vcc5v {
+		compatible = "regulator-fixed";
+		regulator-name = "vcc-5v";
+		regulator-min-microvolt = <5000000>;
+		regulator-max-microvolt = <5000000>;
+		regulator-always-on;
+	};
+
+	de: display-engine {
+		/*
+		 * The fallback compatible string matters: sun4i_drv.c's
+		 * of_match_table has no A133 entry, and matches here are what
+		 * probes the aggregate "sun4i-drm" platform driver that calls
+		 * component_bind_all() to wire the already-individually-probed
+		 * mixer/tcon/dsi devices together into one DRM device. Without
+		 * a match, this node binds no driver at all and the pipeline
+		 * sits in permanent -EPROBE_DEFER. sun4i_drv.c's probe function
+		 * isn't SoC-specific (it just walks the DT ports graph), so
+		 * reusing an existing entry here is safe -- same pattern used
+		 * on tcon_lcd0 below.
+		 */
+		compatible = "allwinner,sun50i-a133-display-engine",
+			     "allwinner,sun50i-a64-display-engine";
+		allwinner,pipelines = <&mixer0>;
+		status = "okay";
+	};
+
+	/*
+	 * Real values from the vendor live DTB's power_sply block:
+	 * lcd_pwm_used=1, lcd_pwm_ch=0 (PWM channel 0 — wired above),
+	 * lcd_pwm_freq=0xc350=50000Hz (period_ns = 1e9/50000 = 20000),
+	 * lcd_pwm_pol=0 (normal polarity), lcd_pwm_max_limit=0xc8=200.
+	 * NOT included: a real backlight-enable GPIO. The vendor DTB has
+	 * a separate lcd_bl_en packed property (bank=7/pin=18, decoding to
+	 * PH18 by cross-checking the pinctrl driver's own bank-numbering
+	 * against an already-confirmed-real pin elsewhere in this file) —
+	 * tried driving it directly via a raw register poke on live
+	 * hardware (no kernel involved, just to test the hypothesis) and
+	 * it had no visible effect, so this pin guess is unconfirmed/likely
+	 * wrong (the vendor property has 8 packed fields where a plain
+	 * 6-cell gpio-spec would only have 7 — there's a piece of that
+	 * encoding not accounted for). Left out entirely rather than wire
+	 * up a pin that's an unverified guess.
+	 */
+	backlight: backlight {
+		compatible = "pwm-backlight";
+		pwms = <&pwm 0 20000 0>;
+		default-brightness-level = <200>;
+		power-supply = <&reg_cldo4>;
+	};
+
+	/* TODO: face buttons (A/B/X/Y, D-pad, L1/L2/R1/R2, start/select,
+	 * joystick clicks). The vendor DTB has NO gpio-keys node and no
+	 * "trimui_keypad"-style driver string was found in the firmware —
+	 * only a 3-key ADC "allwinner,keyboard_1350mv" node exists (that's
+	 * volume/fastboot-combo, not the face buttons). Real GPIO/ADC
+	 * assignments need to come from live probing on the actual unit
+	 * (pinctrl debugfs, evtest against the stock kernel, or a UART
+	 * dmesg capture during boot) — not guessed from another device.
+	 */
+};
+
+&cpu0 {
+	/* TODO: verify against A133 PLUS-rated OPP ceiling, not just A100's */
+	cpu-supply = <&reg_dcdc1>;
+};
+
+&mmc0 {
+	/* SD card slot. cd-gpios still unconfirmed — no hotplug detection, but
+	 * that's fine for a card already inserted at boot (proven live: this is
+	 * literally how SPL/BROM read this same card to boot in the first
+	 * place). Pin/clock wiring already provided by sun50i-a100.dtsi.
+	 * vmmc-supply guessed as the same general IO rail as mmc2 — unconfirmed,
+	 * first thing to revisit if this doesn't come up cleanly. */
+	bus-width = <4>;
+	vmmc-supply = <&reg_cldo3>;
+	status = "okay";
+};
+
+&mmc2 {
+	/* eMMC — pin group/clocks/etc. already wired by sun50i-a100.dtsi, this
+	 * board file only needs the board-specific bits. bus mode confirmed
+	 * from the live boot log: negotiated DDR52, NOT HS200/HS400 — the
+	 * vendor kernel actively strips those capability flags at runtime
+	 * ("delete mmc-hs200-1_8v from dtb" / "delete mmc-hs400-1_8v from dtb"
+	 * in dmesg), so we don't claim them here either. vmmc-supply is a
+	 * reasonable inference (cldo3/vcc-io, the general 3.3V IO rail) but
+	 * NOT directly confirmed — no capture explicitly tied cldo3 to mmc2.
+	 */
+	bus-width = <8>;
+	non-removable;
+	cap-mmc-hw-reset;
+	mmc-ddr-1_8v;
+	vmmc-supply = <&reg_cldo3>; /* inferred, not directly confirmed */
+	status = "okay";
+};
+
+&r_i2c0 {
+	status = "okay";
+
+	axp717: pmic@34 {
+		compatible = "x-powers,axp717";
+		reg = <0x34>;
+		interrupt-controller;
+		#interrupt-cells = <1>;
+		interrupt-parent = <&r_intc>;
+		interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
+
+		vin1-supply = <&reg_vcc5v>;
+		vin2-supply = <&reg_vcc5v>;
+		vin3-supply = <&reg_vcc5v>;
+		vin4-supply = <&reg_vcc5v>;
+
+		axp_adc: adc {
+			compatible = "x-powers,axp717-adc";
+			#io-channel-cells = <1>;
+		};
+
+		battery_power: battery-power {
+			compatible = "x-powers,axp717-battery-power-supply";
+			monitored-battery = <&battery>;
+			x-powers,no-thermistor;
+		};
+
+		usb_power: usb-power {
+			compatible = "x-powers,axp717-usb-power-supply";
+		};
+
+		/*
+		 * Regulator names carried over from the vendor DTB's "axp2202-*"
+		 * node names (AXP2202 == AXP717, confirmed: Allwinner's BSP driver
+		 * for "axp2202" has been shown driving a chip physically marked
+		 * AXP717). Voltages below are REAL, read directly from U-Boot's own
+		 * PMIC-init console log on the live unit (not decoded/guessed) —
+		 * each line printed "<rail>_vol = <mV>, onoff=<0|1>" at boot. dcdc2
+		 * (GPU/sys) wasn't printed at that boot stage (probably brought up
+		 * later by the kernel) — left undocumented rather than guessed.
+		 * dcdc1 (vdd-cpu) was 940mV at that instant but is DVFS-scaled by
+		 * cpufreq, so it's intentionally NOT hardcoded to a fixed value here.
+		 */
+		regulators {
+			reg_dcdc1: dcdc1 {
+				/*
+				 * regulator-always-on, not regulator-boot-on:
+				 * drivers/regulator/core.c's regulator_late_cleanup()
+				 * (the late-boot "disable unused regulators" sweep)
+				 * checks constraints->always_on, not ->boot_on --
+				 * boot-on only means "was already on at boot," it
+				 * does nothing to stop this cleanup pass from
+				 * turning it back off. This board has no cpufreq/OPP
+				 * consumer wired up yet, so dcdc1 would otherwise
+				 * have zero regulator-API reference count despite
+				 * being the CPU's own supply -- disabling it hard
+				 * hangs the board.
+				 */
+				regulator-always-on;
+				regulator-name = "vdd-cpu";
+				/* boot-time value was 940mV; real range needs the OPP table, not hardcoded here */
+			};
+			reg_dcdc2: dcdc2 {
+				/*
+				 * Same reasoning as dcdc1 above: no GPU driver
+				 * claims this rail yet, and nothing rules out
+				 * shared PLL/bias circuitry with the display
+				 * path, so it's kept on rather than left to the
+				 * late-boot unused-regulator cleanup.
+				 */
+				regulator-always-on;
+				regulator-name = "vdd-gpu-sys";
+			};
+			reg_dcdc3: dcdc3 {
+				/* confirmed real: matches boot0's "DRAM_VCC set to 1100 mv" exactly */
+				regulator-always-on;
+				regulator-min-microvolt = <1100000>;
+				regulator-max-microvolt = <1100000>;
+				regulator-name = "vdd-dram";
+			};
+			/*
+			 * aldo1/aldo2/aldo4/bldo2/bldo3/bldo4 all got
+			 * regulator-always-on added after real, reproducible
+			 * hard hangs: fixing vdd-cpu/vdd-gpu-sys (the first two
+			 * rails caught by the kernel's late-boot "disable
+			 * unused regulators" sweep) didn't stop the hang, it
+			 * just moved it — the very next boot hung identically
+			 * but with "aldo4: disabling" this time. Whack-a-mole:
+			 * every rail the vendor bootloader left on (onoff=1 in
+			 * the real U-Boot PMIC log) that we don't have a
+			 * confirmed real consumer/purpose for is exactly this
+			 * same landmine, since nothing in our still-incomplete
+			 * DT claims them through the regulator API. Rather than
+			 * keep discovering these one hang at a time, all of
+			 * them get always-on now — matches the vendor's own
+			 * actual default behavior anyway (leave them on).
+			 * bldo1 is the one confirmed exception: real U-Boot log
+			 * shows onoff=0 at boot (WiFi/BT driver switches it on
+			 * dynamically later), so it's deliberately left off here.
+			 */
+			reg_aldo1: aldo1 {
+				/* real: 1800mV, onoff=1 at boot */
+				regulator-always-on;
+			};
+			reg_aldo2: aldo2 {
+				/* real: 3300mV, onoff=1 */
+				regulator-always-on;
+			};
+			reg_aldo3: aldo3 {
+				/* confirmed: WiFi/BT IO rail (wlan_io_regulator), real: 3300mV */
+				regulator-always-on;
+				regulator-min-microvolt = <3300000>;
+				regulator-max-microvolt = <3300000>;
+				regulator-name = "vcc-wifi-io";
+			};
+			reg_aldo4: aldo4 {
+				/* real: 1800mV, onoff=1 */
+				regulator-always-on;
+			};
+			reg_bldo1: bldo1 {
+				/* confirmed: WiFi/BT power rail (wlan_power1), real: 3300mV.
+				 * NOT always-on: U-Boot log shows onoff=0 at boot — the
+				 * wifi/bt driver switches it on dynamically at runtime. */
+				regulator-min-microvolt = <3300000>;
+				regulator-max-microvolt = <3300000>;
+				regulator-name = "vcc-wifi";
+			};
+			reg_bldo2: bldo2 {
+				/* real: 1800mV, onoff=1 */
+				regulator-always-on;
+			};
+			reg_bldo3: bldo3 {
+				/* real: 3300mV, onoff=1 */
+				regulator-always-on;
+			};
+			reg_bldo4: bldo4 {
+				/* real: 1800mV at boot, onoff=1 */
+				regulator-always-on;
+			};
+			reg_cldo1: cldo1 {
+				/* confirmed: LCD panel power0 (lcd_power0="cldo1"), real: 1800mV */
+				regulator-always-on;
+				regulator-min-microvolt = <1800000>;
+				regulator-max-microvolt = <1800000>;
+				regulator-name = "vcc-lcd-1";
+			};
+			reg_cldo2: cldo2 { };
+			reg_cldo3: cldo3 {
+				/* real: 3300mV */
+				regulator-always-on;
+				regulator-min-microvolt = <3300000>;
+				regulator-max-microvolt = <3300000>;
+				regulator-name = "vcc-io";
+			};
+			reg_cldo4: cldo4 {
+				/* confirmed: LCD panel power1 (lcd_power1="cldo4"), real: 3300mV */
+				regulator-always-on;
+				regulator-min-microvolt = <3300000>;
+				regulator-max-microvolt = <3300000>;
+				regulator-name = "vcc-lcd-2";
+			};
+			reg_cpusldo: cpusldo {
+				/* real: 900mV, onoff=1 */
+				regulator-always-on;
+				regulator-boot-on;
+				regulator-min-microvolt = <900000>;
+				regulator-max-microvolt = <900000>;
+			};
+		};
+	};
+};
+
+&uart0 {
+	pinctrl-names = "default";
+	pinctrl-0 = <&uart0_pb_pins>; /* only pin group this SoC's dtsi defines; matches vendor cmdline's ttyS0 */
+	status = "okay";
+};
+
+&{/soc} {
+	/*
+	 * A133 (sun50iw10) has no public DE/mixer register documentation
+	 * (A133 User Manual rev 1.1, chapter 4.1 "DE" is 2 pages of feature
+	 * bullets + a block diagram, no register list — unlike every other
+	 * chapter in that manual). Addresses/topology below are cross-
+	 * checked two ways instead: the manual's own top-level physical
+	 * memory map (DE0/DSI0/DPSS_TOP0/TCON_LCD0 base addresses + GIC IRQ
+	 * numbers, chapter 3) and Allwinner's own GPL disp2 driver source
+	 * for this exact SoC (lowlevel_v2x/sun50iw10 tree — public on
+	 * GitHub from several vendor BSP forks, e.g. chainsx/kernel-sun50iw10,
+	 * engSinteck/A133_Image): de_feat.c gives the real channel/layer
+	 * topology (DISP0 = 2 VI + 2 UI channels, DISP1 = 1 VI + 2 UI, all
+	 * scaler-capable) that drives the sun8i_mixer.c cfg structs, and
+	 * de_rtmx_init()'s reg_base + 0x100000 / + 0x200000 offsets for
+	 * mixer0/mixer1 match the addresses used below (and match the
+	 * existing convention already used for every other DE2.0 SoC in
+	 * this driver — A64, H3, R40).
+	 *
+	 * DPSS_TOP0 (0x06510000) is a real hardware crossbar between the
+	 * mixers and TCON_LCD0 -- CCU exposes its own bus clock/reset gate
+	 * (CLK_BUS_DPSS_TOP0/RST_BUS_DPSS_TOP0), and the vendor implementation
+	 * explicitly routes through it. Leaving it out of the ports graph
+	 * entirely is not survivable: mixer0/tcon_lcd0/dsi0 all probe and
+	 * bind fine without it, but every DRM atomic commit then times out
+	 * waiting for vblank -- the pixel path never physically reaches the
+	 * panel, because nothing keeps DPSS_TOP0's own bus clock enabled
+	 * once Linux's "disable unused clocks" cleanup runs. Modeled here by
+	 * reusing mainline's existing sun8i_tcon_top.c driver with an added
+	 * A133 compatible + quirks entry: the vendor's own register layout
+	 * for this block is bit-for-bit identical to what that driver
+	 * already assumes for R40/D1/H6 -- genuinely the same IP block, just
+	 * undocumented for this particular SoC.
+	 */
+	dpss_top0: dpss-top@6510000 {
+		compatible = "allwinner,sun50i-a133-tcon-top";
+		reg = <0x06510000 0x1000>;
+		/*
+		 * "tcon-tv0" is a required clock-name in sun8i_tcon_top_bind()
+		 * regardless of quirks (it unconditionally registers a TV0
+		 * gate clock) even though this board has no TCON_TV/HDMI
+		 * output at all. Pointing it at CLK_TCON_LCD is a harmless
+		 * filler — that gate output is never consumed by anything in
+		 * this DT, it just needs to exist so probe doesn't fail
+		 * resolving the clock-name. "dsi" is real and load-bearing
+		 * though (has_dsi=true pulls it in): CLK_MIPI_DSI from CCU is
+		 * the actual upstream parent feeding DSI's gated clock inside
+		 * this block. Missed this the first time around — dpss_top0's
+		 * own probe failed outright without it (of_property_match_string
+		 * couldn't find "dsi" in clock-names), which cascaded into
+		 * dsi0's own "mod" clock lookup failing too, since it points
+		 * at dpss_top0's now-never-registered clock provider.
+		 */
+		clocks = <&ccu CLK_BUS_DPSS_TOP0>,
+			 <&ccu CLK_TCON_LCD>,
+			 <&ccu CLK_MIPI_DSI>;
+		clock-names = "bus", "tcon-tv0", "dsi";
+		clock-output-names = "tcon-top-tv0", "tcon-top-dsi";
+		resets = <&ccu RST_BUS_DPSS_TOP0>;
+		#clock-cells = <1>;
+
+		ports {
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			dpss_top0_mixer0_in: port@0 {
+				reg = <0>;
+
+				dpss_top0_in_mixer0: endpoint {
+					remote-endpoint = <&mixer0_out_dpss_top0>;
+				};
+			};
+
+			dpss_top0_mixer0_out: port@1 {
+				reg = <1>;
+
+				dpss_top0_out_tcon_lcd0: endpoint {
+					remote-endpoint = <&tcon_lcd0_in_dpss_top0>;
+				};
+			};
+		};
+	};
+
+	mixer0: mixer@6100000 {
+		compatible = "allwinner,sun50i-a133-de2-mixer-0";
+		reg = <0x06100000 0x100000>;
+		clocks = <&ccu CLK_BUS_DE>, <&ccu CLK_DE>;
+		clock-names = "bus", "mod";
+		resets = <&ccu RST_BUS_DE>;
+
+		ports {
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			mixer0_out: port@1 {
+				reg = <1>;
+
+				mixer0_out_dpss_top0: endpoint {
+					remote-endpoint = <&dpss_top0_in_mixer0>;
+				};
+			};
+		};
+	};
+
+	tcon_lcd0: lcd-controller@6511000 {
+		/*
+		 * No A100/A133-specific compatible exists upstream (the
+		 * binding's compatible enum only lists SoCs someone has
+		 * already wired up). The generic a83t-tcon-lcd fallback
+		 * matches the driver purely on this string, so it still
+		 * binds correctly even without an A133-specific entry.
+		 */
+		compatible = "allwinner,sun8i-a83t-tcon-lcd";
+		reg = <0x06511000 0x1000>;
+		/*
+		 * Was GIC_SPI 101 (copied from a mismatched reference DTS) --
+		 * the interrupt handler was silently never firing. Confirmed
+		 * correct via the live vendor DT + /proc/interrupts: vendor's
+		 * combined disp@06000000 node's third interrupt cell is raw
+		 * SPI 0x44 = 68, and that is the ONLY display-related
+		 * interrupt actively counting (~60Hz) on real hardware --
+		 * cross-checked against dma0's SPI 0x2d=45, which matches
+		 * exactly between the vendor DT and its own /proc/interrupts
+		 * with no offset, confirming the raw-cell-value convention.
+		 */
+		interrupts = <GIC_SPI 68 IRQ_TYPE_LEVEL_HIGH>;
+		clocks = <&ccu CLK_BUS_TCON_LCD>, <&ccu CLK_TCON_LCD>;
+		clock-names = "ahb", "tcon-ch0";
+		clock-output-names = "tcon-pixel-clock";
+		resets = <&ccu RST_BUS_TCON_LCD>;
+		reset-names = "lcd";
+		#clock-cells = <0>;
+
+		ports {
+			#address-cells = <1>;
+			#size-cells = <0>;
+
+			tcon_lcd0_in: port@0 {
+				reg = <0>;
+
+				tcon_lcd0_in_dpss_top0: endpoint {
+					remote-endpoint = <&dpss_top0_out_tcon_lcd0>;
+				};
+			};
+
+			tcon_lcd0_out: port@1 {
+				reg = <1>;
+				#address-cells = <1>;
+				#size-cells = <0>;
+
+				/*
+				 * reg = <1> here (not 0) is load-bearing, not
+				 * cosmetic: sun4i_drv.c's component-matching
+				 * walk (sun4i_drv_traverse_endpoints()) treats
+				 * a channel-0 TCON's port@1 endpoint 0 as "our
+				 * directly-connected panel" and deliberately
+				 * skips adding it to the component match list.
+				 * Confirmed live on real hardware: with this
+				 * endpoint unaddressed (defaulting to reg=0),
+				 * the DSI encoder got silently treated as a
+				 * bare panel and never added to the component
+				 * list, so component_bind_all() never bound
+				 * it, dsi->drm never got set, and the panel's
+				 * mipi_dsi_attach() spun in EPROBE_DEFER
+				 * forever — screen stayed black even though
+				 * mixer0 probed fine standalone. Matches the
+				 * real D1 reference DTS's own convention:
+				 * sunxi-d1s-t113.dtsi's tcon_lcd0_out_dsi is
+				 * also "endpoint@1 { reg = <1>; ... }", same
+				 * as here, not endpoint 0 — this was a real
+				 * detail I should have carried over the first
+				 * time instead of dropping it.
+				 */
+				tcon_lcd0_out_dsi0: endpoint@1 {
+					reg = <1>;
+					remote-endpoint = <&dsi0_in_tcon_lcd0>;
+				};
+			};
+		};
+	};
+
+	dsi0: dsi@6504000 {
+		compatible = "allwinner,sun50i-a100-mipi-dsi";
+		reg = <0x06504000 0x1000>;
+		interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
+		/*
+		 * REVERTED: tried sourcing "mod" from dpss_top0's gated DSI
+		 * output (matching the vendor's de_lcd.c, which gates
+		 * tcon_clk_gate.bits.dsi_clk_gate as part of enabling DSI, and
+		 * the real D1 reference DTS's identical convention) — but this
+		 * creates a genuine circular dependency, not just an ordering
+		 * race: dpss_top0 only registers its clock-provider inside its
+		 * component .bind() callback, which only runs once every
+		 * matched component (including dsi0 itself) has *already*
+		 * succeeded its own individual .probe(). dsi0's probe() can't
+		 * succeed without this clock, so it can never reach
+		 * component_add(), so dpss_top0's bind() never fires, forever
+		 * — confirmed live: "Couldn't get the DSI mod clock" repeated
+		 * on every deferred-probe retry with no path to resolution.
+		 * Back to sourcing directly from CCU, which is what actually
+		 * got the panel to attach successfully a few iterations ago.
+		 * dpss_top0 stays modeled in the ports graph regardless (for
+		 * its own bus-clock-keepalive purpose, now fully decoupled
+		 * from dsi0's clock lookup) — whether A133 genuinely needs
+		 * this specific gate toggled for DSI to output correctly, as
+		 * opposed to just needing DPSS_TOP0 clocked/reset for its
+		 * routing mux to be live, is still an open question; CCU's
+		 * own CLK_MIPI_DSI already has its own independent gate bit,
+		 * so it's plausible this DPSS_TOP0 gate is a separate/optional
+		 * clock domain rather than something on the primary pixel path.
+		 */
+		clocks = <&ccu CLK_BUS_MIPI_DSI>, <&ccu CLK_MIPI_DSI>;
+		clock-names = "bus", "mod";
+		resets = <&ccu RST_BUS_MIPI_DSI>;
+		phys = <&dphy0>;
+		phy-names = "dphy";
+		status = "okay";
+		#address-cells = <1>;
+		#size-cells = <0>;
+
+		port {
+			dsi0_in_tcon_lcd0: endpoint {
+				remote-endpoint = <&tcon_lcd0_out_dsi0>;
+			};
+		};
+
+		panel@0 {
+			/*
+			 * reset-gpios pin is a placeholder (PD0) — the real
+			 * reset line hasn't been probed on hardware yet.
+			 * power-supply maps to the driver's
+			 * devm_regulator_get(dev, "power") lookup. backlight
+			 * maps to drm_panel_of_backlight()'s lookup, wired to
+			 * the pwm-backlight node above (real PWM channel 0 /
+			 * 50kHz values, but backlight-enable GPIO not included
+			 * — see the comment on that node for why).
+			 *
+			 * No ports/endpoint graph needed here: sun6i_dsi_attach()
+			 * (drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c) finds the
+			 * panel via of_drm_find_panel() on this node directly,
+			 * not through of_graph — the DSI parent/child
+			 * relationship (reg = virtual channel) is the only
+			 * link needed.
+			 */
+			compatible = "orisetech,otm1289a";
+			reg = <0>;
+			reset-gpios = <&pio 3 0 GPIO_ACTIVE_LOW>; /* TODO: unconfirmed pin, revisit on real hw */
+			power-supply = <&reg_cldo1>;
+			backlight = <&backlight>;
+		};
+	};
+
+	dphy0: phy@6505000 {
+		compatible = "allwinner,sun50i-a100-mipi-dphy";
+		reg = <0x06505000 0x1000>;
+		interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
+		clocks = <&ccu CLK_BUS_MIPI_DSI>, <&ccu CLK_MIPI_DSI>;
+		clock-names = "bus", "mod";
+		resets = <&ccu RST_BUS_MIPI_DSI>;
+		#phy-cells = <0>;
+	};
+
+	/*
+	 * No mainline driver or A100/A133-specific binding exists upstream
+	 * for this IP (see drivers/pwm/pwm-sun50i-a133.c and its binding
+	 * doc) -- it's a newer, richer generation than allwinner,sun4i-a10-pwm.
+	 * Base address and register layout are from the A133 User Manual,
+	 * chapter 10.11 (fully documented down to bit level). Only
+	 * CLK_BUS_PWM (register-access gate) is needed -- the PWM output
+	 * clock source itself is internal to this IP, confirmed by
+	 * CLK_PWM's absence from the A100 CCU headers.
+	 */
+	pwm: pwm@300a000 {
+		compatible = "allwinner,sun50i-a133-pwm";
+		reg = <0x0300a000 0x400>;
+		clocks = <&ccu CLK_BUS_PWM>;
+		clock-names = "bus";
+		resets = <&ccu RST_BUS_PWM>;
+		pinctrl-names = "default";
+		pinctrl-0 = <&pwm0_pins>;
+		#pwm-cells = <3>;
+	};
+};
+
+&pio {
+	/*
+	 * PD23/pwm0, muxsel 0x2 — confirmed real from the vendor live DTB's
+	 * lcd_pwm_ch = 0 (PWM channel 0 drives the panel backlight) cross-
+	 * checked against drivers/pinctrl/sunxi/pinctrl-sun50i-a100.c's own
+	 * SUNXI_PIN(D, 23) table, which already lists "pwm0" at the exact
+	 * same function value (0x2) the vendor DTB used — mainline's
+	 * pinctrl driver already fully supports this pin/function, only
+	 * the PWM controller driving it needed writing.
+	 */
+	pwm0_pins: pwm0-pins {
+		pins = "PD23";
+		function = "pwm0";
+	};
+};
+
+/*
+ * TODO: WiFi/BT (XR829 on SDIO bus 1 / &mmc1, confirmed from strings:
+ * boot_xr829.bin, fw_xr829.bin, fw_xr829_bt.bin, sdd_xr829.bin — and
+ * confirmed live via the running kernel's "XRADIO WIFI OPEN" driver log,
+ * MAC dc:44:60:xx:xx:xx (redacted)). Power rails: bldo1 (3300mV, switched on
+ * dynamically, not always-on) + aldo3 (3300mV, always-on) — both wired
+ * above. GPIO wiring is REAL, read directly off the live unit's
+ * /sys/kernel/debug/gpio (not decoded from packed vendor fields):
+ *   bt_rst         = r_pio 0 2  (PL2)  output, active low
+ *   bt_hostwake    = r_pio 0 3  (PL3)  input
+ *   bt_wake        = r_pio 0 4  (PL4)  output
+ *   wlan_regon     = r_pio 0 5  (PL5)  output
+ *   wlan_hostwake  = r_pio 0 6  (PL6)  input
+ * Mainline equivalent is an mmc1 node + mmc-pwrseq-simple (using
+ * wlan_regon as the reset-gpios line) + whatever XR829 driver ends up
+ * used (no mainline driver exists yet — see project notes for the
+ * out-of-tree candidates). Not wired into an actual mmc1 node yet.
+ */
+
+/*
+ * Display: mixer0/tcon_lcd0/dsi0/dphy0/panel@0 are all wired above
+ * (&soc block + dsi0's panel@0 child). Status:
+ *  - Panel driver (panel-orisetech-otm1289a.c) written: real timings
+ *    (720x1280, 69MHz dclk, ht=880/hbp=88/hspw=4, vt=1320/vbp=9/vspw=4)
+ *    plus a DSI init command sequence transcribed from a MediaTek LCM
+ *    driver for the same panel controller IC found via GitHub code
+ *    search (OrangePi4G-iot_kernel's otm1289a_hd720_dsi_vdo_auo.c) —
+ *    NOT from Allwinner's own disp2 tree, which was checked for this
+ *    board's exact panel and doesn't have it (its bundled lcd/ panel
+ *    library has ~90 entries but none named otm1289a; ours is probably
+ *    a TrimUI-added file in a downstream fork that isn't public). The
+ *    reference driver uses 3 DSI lanes; ours is set to the confirmed
+ *    real 4-lane value, so timing-sensitive parts of the init sequence
+ *    may need retuning once there's real hardware feedback.
+ *  - Mixer (sun8i_mixer.c): A133 cfg added, ported from the same vendor
+ *    tree's de_feat.c topology data (see &soc comment above the mixer0
+ *    node for the full provenance/reasoning).
+ *  - TCON_LCD0/DSI0/D-PHY: real register addresses from the A133 User
+ *    Manual (chapter 6, which — unlike the DE chapter — is fully
+ *    documented down to bit level); DSI host + D-PHY already had
+ *    mainline driver support for A100 before this port touched anything.
+ * Backlight: working. The `backlight` pwm-backlight node above (PWM
+ * channel 0, 50kHz) is wired via `backlight = <&backlight>` on the panel
+ * node and confirmed on real hardware.
+ *
+ * The remaining open item is display output itself: the panel attaches
+ * and the backlight lights, but TCON0's CPU/8080-interface TRIGGER_START
+ * bit never self-clears, so no pixel data reaches the panel -- see the
+ * project's investigation log for the current status. reset-gpios is
+ * still an unverified placeholder, and the DPSS_TOP0 routing-register
+ * risk noted above the mixer0 node is still unconfirmed.
+ */
+
+/*
+ * TODO: GPU (img,gpu / PowerVR GE8300) — no mainline binding to reference
+ * yet.
+ */
diff --git a/drivers/clk/sunxi-ng/ccu-sun50i-a100.c b/drivers/clk/sunxi-ng/ccu-sun50i-a100.c
index 1f81c7ac4..9dee187ba 100644
--- a/drivers/clk/sunxi-ng/ccu-sun50i-a100.c
+++ b/drivers/clk/sunxi-ng/ccu-sun50i-a100.c
@@ -680,13 +680,27 @@ static const char * const tcon_lcd_parents[] = { "pll-video0-4x",
 						  "pll-video2-4x",
 						  "pll-video3-4x",
 						  "pll-periph0-2x" };
+/*
+ * No CLK_SET_RATE_PARENT here originally, unlike the other PLL_VIDEO
+ * derivatives it can mux to (pll_video0_4x_clk etc, which do have it).
+ * Confirmed via debugfs on real hardware: this left mainline's dclk
+ * request landing on a 400MHz PLL_VIDEO0 parent, unable to explore other
+ * PLL rates -- vendor's own live system has this same clock at exactly
+ * 408MHz (= 24MHz x 17, a clean PLL multiple, the actual nearest
+ * achievable rate to the ~414MHz our A133 board's CPU/8080 DSI panel
+ * requests). Without this flag, ccu_mp_ops's own rate-rounding can't ask
+ * its selected parent to change rate at all -- it can only divide down
+ * from whatever that parent already happens to be at, which produced a
+ * measurably worse (2%) result than vendor's real driver achieves for
+ * this exact panel.
+ */
 static SUNXI_CCU_MP_WITH_MUX_GATE(tcon_lcd_clk, "tcon-lcd0",
 				  tcon_lcd_parents, 0xb60,
 				  0, 4,		/* M */
 				  8, 2,		/* P */
 				  24, 3,	/* mux */
 				  BIT(31),	/* gate */
-				  0);
+				  CLK_SET_RATE_PARENT);
 
 static SUNXI_CCU_GATE(bus_tcon_lcd_clk, "bus-tcon-lcd0", "ahb3",
 		      0xb7c, BIT(0), 0);
diff --git a/drivers/gpu/drm/panel/Kconfig b/drivers/gpu/drm/panel/Kconfig
index cbdf7b8f7..fab1ed5f4 100644
--- a/drivers/gpu/drm/panel/Kconfig
+++ b/drivers/gpu/drm/panel/Kconfig
@@ -758,6 +758,14 @@ config DRM_PANEL_ORISETECH_OTM8009A
 	  Say Y here if you want to enable support for Orise Technology
 	  otm8009a 480x800 dsi 2dl panel.
 
+config DRM_PANEL_ORISETECH_OTM1289A
+	tristate "Orise Technology otm1289a 720x1280 dsi panel"
+	depends on OF
+	depends on DRM_MIPI_DSI
+	help
+	  Say Y here if you want to enable support for Orise Technology
+	  otm1289a 720x1280 dsi panel, as used on the TrimUI Smart Pro.
+
 config DRM_PANEL_OSD_OSD101T2587_53TS
 	tristate "OSD OSD101T2587-53TS DSI 1920x1200 video mode panel"
 	depends on OF
diff --git a/drivers/gpu/drm/panel/Makefile b/drivers/gpu/drm/panel/Makefile
index 3b523cf37..d824f0a9b 100644
--- a/drivers/gpu/drm/panel/Makefile
+++ b/drivers/gpu/drm/panel/Makefile
@@ -74,6 +74,7 @@ obj-$(CONFIG_DRM_PANEL_MANTIX_MLAF057WE51) += panel-mantix-mlaf057we51.o
 obj-$(CONFIG_DRM_PANEL_OLIMEX_LCD_OLINUXINO) += panel-olimex-lcd-olinuxino.o
 obj-$(CONFIG_DRM_PANEL_ORISETECH_OTA5601A) += panel-orisetech-ota5601a.o
 obj-$(CONFIG_DRM_PANEL_ORISETECH_OTM8009A) += panel-orisetech-otm8009a.o
+obj-$(CONFIG_DRM_PANEL_ORISETECH_OTM1289A) += panel-orisetech-otm1289a.o
 obj-$(CONFIG_DRM_PANEL_OSD_OSD101T2587_53TS) += panel-osd-osd101t2587-53ts.o
 obj-$(CONFIG_DRM_PANEL_PANASONIC_VVX10F034N00) += panel-panasonic-vvx10f034n00.o
 obj-$(CONFIG_DRM_PANEL_RASPBERRYPI_TOUCHSCREEN) += panel-raspberrypi-touchscreen.o
diff --git a/drivers/gpu/drm/panel/panel-orisetech-otm1289a.c b/drivers/gpu/drm/panel/panel-orisetech-otm1289a.c
new file mode 100644
index 000000000..57de8bf94
--- /dev/null
+++ b/drivers/gpu/drm/panel/panel-orisetech-otm1289a.c
@@ -0,0 +1,424 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * DRM driver for the Orise Tech OTM1289A MIPI-DSI panel, as used on the
+ * TrimUI Smart Pro (Allwinner A133).
+ *
+ * Timings (dot clock, hsync/vsync, panel size) are real, measured values
+ * pulled directly from the stock vendor firmware's live device tree on
+ * actual hardware, not datasheet guesses.
+ *
+ * The DCS init command sequence below is reverse-engineered directly from
+ * this device's own real vendor kernel binary (vendor_kernel.bin, Linux
+ * 4.9.191, extracted from part1_boot.img), not transcribed from an
+ * unrelated reference driver. Converted the raw Image to a symbolized ELF
+ * via vmlinux-to-elf (recovers the embedded kallsyms table + a correct
+ * load-base guess), then disassembled lcd_panel_init(): it does a
+ * runtime strncmp() of this board's configured panel name against a
+ * table of known driver names ("gc9702c", "otm1289a", ...), and on a
+ * match stores a pointer to that driver's own DCS command table --
+ * confirmed at VA 0xffffff8008b72190 for the "otm1289a" match on this
+ * exact device. That table uses fixed 72-byte entries: byte at +0 is the
+ * DCS command, byte at +4 is the data length (0xff = end-of-table
+ * marker, 0xfe = delay marker with the ms value at +8), and the data
+ * payload starts at +8 -- confirmed by reading lcd_panel_init()'s own
+ * parsing loop, which calls sunxi_lcd_dsi_dcs_write(sel, entry[0],
+ * &entry[8], entry[4]) for every non-marker entry. This is a real,
+ * ground-truth match to this exact physical panel -- and differs in
+ * concrete values (gamma curves, several power/timing registers) from
+ * the earlier version of this table, which was transcribed from an
+ * unrelated MediaTek-based reference driver chosen only because its
+ * resolution happened to match. This board is wired for 4 DSI lanes
+ * (confirmed from the vendor DTB) -- a host-side PHY configuration
+ * independent of the panel's own init register values above.
+ */
+
+#include <linux/backlight.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/regulator/consumer.h>
+
+#include <video/mipi_display.h>
+
+#include <drm/drm_mipi_dsi.h>
+#include <drm/drm_modes.h>
+#include <drm/drm_panel.h>
+
+#define OTM1289A_HDISPLAY	720
+#define OTM1289A_VDISPLAY	1280
+
+struct otm1289a {
+	struct device *dev;
+	struct drm_panel panel;
+	struct gpio_desc *reset_gpio;
+	struct regulator *supply;
+	bool prepared;
+};
+
+/*
+ * Real, measured timings from the live vendor device tree (lcd0 node):
+ * dot clock 69MHz, ht=880/hbp=88/hspw=4, vt=1320/vbp=9/vspw=4, 720x1280
+ * native panel resolution (portrait; the vendor rotates 270 degrees in
+ * software for landscape use -- left as portrait here, rotation is a
+ * compositor/KMS-plane concern, not a panel-driver one).
+ *
+ * The vendor's lcd_hbp/lcd_vbp measure from the end of active video to
+ * the start of sync (i.e. they already include the sync pulse width),
+ * not from the end of sync to the next active region like DRM's
+ * back_porch does. The DRM front porch is therefore
+ * htotal-hdisplay-hbp / vtotal-vdisplay-vbp (72 / 31), confirmed against
+ * two independently observable live registers (BASIC_SIZE0_REG.VBP and
+ * BASIC_CTL1_REG.VIDEO_ST_DELAY) and against sun6i_dsi_setup_burst()'s
+ * DRQ_SET formula matching a live DRQ_SET register read.
+ *
+ * .clock is the nominal 69MHz, matching the vendor DT's lcd_dclk_freq.
+ * The CPU/8080 path this panel runs through scales the dclk request
+ * (crtc_clock * bpp / lanes / SUN6I_DSI_TCON_DIV) rather than requesting
+ * the pixel clock directly, landing at 69MHz * 6 / 4 = 103.5MHz -- a
+ * different, achievable part of the divider range, matching the vendor's
+ * own boot log ("clk real: dclk(102000000)" for a requested 69000000).
+ */
+static const struct drm_display_mode otm1289a_mode = {
+	.clock = 69000,
+	.hdisplay = OTM1289A_HDISPLAY,
+	.hsync_start = OTM1289A_HDISPLAY + 88,
+	.hsync_end = OTM1289A_HDISPLAY + 88 + 4,
+	.htotal = 880,
+	.vdisplay = OTM1289A_VDISPLAY,
+	.vsync_start = OTM1289A_VDISPLAY + 31,
+	.vsync_end = OTM1289A_VDISPLAY + 31 + 4,
+	.vtotal = 1320,
+	.width_mm = 65,
+	.height_mm = 116,
+	.type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED,
+};
+
+struct otm1289a_init_cmd {
+	u8 cmd;
+	u8 len;
+	u8 data[16];
+};
+
+#define OTM1289A_CMD(_cmd, ...) \
+	{ .cmd = (_cmd), .len = sizeof((u8[]) { __VA_ARGS__ }), .data = { __VA_ARGS__ } }
+
+/* Extracted directly from vendor_kernel.bin's real compiled otm1289a DCS
+ * command table for this exact device (VA 0xffffff8008b72190) — see the
+ * file header for how this was located and parsed. Section comments below
+ * mark the same logical groupings the vendor's own register documentation
+ * implies (address-window-select + register write pairs), kept for
+ * readability; the values themselves are the real, ground-truth bytes.
+ */
+static const struct otm1289a_init_cmd otm1289a_init_sequence[] = {
+	OTM1289A_CMD(0x00, 0x00),
+	OTM1289A_CMD(0xff, 0x12, 0x89, 0x01),
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xff, 0x12, 0x89),
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xff, 0xb0),
+	/* panel setting */
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xc0, 0x4a, 0x00, 0x10, 0x10, 0x96, 0x01, 0x68, 0x40),
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xc0, 0x3b, 0x01, 0x09),
+	OTM1289A_CMD(0x00, 0x8c),
+	OTM1289A_CMD(0xc0, 0x00),
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xc1, 0x33),
+	/* power setting */
+	OTM1289A_CMD(0x00, 0x85),
+	OTM1289A_CMD(0xc5, 0x0a, 0x0a, 0x46),
+	OTM1289A_CMD(0x00, 0x00),
+	OTM1289A_CMD(0xd8, 0x27, 0x27),
+	OTM1289A_CMD(0x00, 0x01),
+	OTM1289A_CMD(0xd9, 0x77),
+	OTM1289A_CMD(0x00, 0x84),
+	OTM1289A_CMD(0xc4, 0x02),
+	OTM1289A_CMD(0x00, 0x93),
+	OTM1289A_CMD(0xc4, 0x04),
+	OTM1289A_CMD(0x00, 0x96),
+	OTM1289A_CMD(0xf5, 0xe7),
+	OTM1289A_CMD(0x00, 0xa0),
+	OTM1289A_CMD(0xf5, 0x4a),
+	OTM1289A_CMD(0x00, 0x8a),
+	OTM1289A_CMD(0xc0, 0x11),
+	OTM1289A_CMD(0x00, 0x83),
+	OTM1289A_CMD(0xf5, 0x81),
+	/* power IC */
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xc4, 0x96, 0x05),
+	/* panel timing state control */
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xcb, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xcb, 0xfc, 0xfc, 0xfc, 0x00, 0x14, 0x14, 0x14),
+	/* panel pad mapping control */
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xcc, 0x02, 0x0a, 0x0c, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xcc, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x1d, 0x06, 0x01, 0x09, 0x0b, 0x0d, 0x0f, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0xa0),
+	OTM1289A_CMD(0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x1d, 0x05),
+	OTM1289A_CMD(0x00, 0xb0),
+	OTM1289A_CMD(0xcc, 0x05, 0x0f, 0x0d, 0x0b, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0xc0),
+	OTM1289A_CMD(0xcc, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x1e, 0x01, 0x06, 0x10, 0x0e, 0x0c, 0x0a, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0xd0),
+	OTM1289A_CMD(0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x1e, 0x02),
+	/* panel timing setting */
+	OTM1289A_CMD(0x00, 0x80),
+	OTM1289A_CMD(0xce, 0x87, 0x03, 0x10, 0x86, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0x90),
+	OTM1289A_CMD(0xce, 0x34, 0xff, 0x10, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00),
+	OTM1289A_CMD(0x00, 0xa0),
+	OTM1289A_CMD(0xce, 0x30, 0x83, 0x88, 0x00, 0x20, 0x00, 0x82, 0x87, 0x00, 0x81, 0x86, 0x00, 0x80, 0x85, 0x00),
+	OTM1289A_CMD(0x00, 0xb0),
+	OTM1289A_CMD(0xce, 0x30, 0x00, 0x84, 0x00, 0x20, 0x00, 0x01, 0x83, 0x00, 0x02, 0x82, 0x00, 0x03, 0x81, 0x00),
+	OTM1289A_CMD(0x00, 0xe0),
+	OTM1289A_CMD(0xce, 0x0a, 0x04, 0xfc, 0x00, 0x00, 0x0a, 0x04, 0xfc),
+	OTM1289A_CMD(0x00, 0xf0),
+	OTM1289A_CMD(0xce, 0x01, 0x20, 0x01, 0x01, 0x00, 0x00),
+	/* gamma */
+	OTM1289A_CMD(0x00, 0x00),
+	OTM1289A_CMD(0xe1, 0x00, 0x16, 0x26, 0x37, 0x47, 0x65, 0x64, 0x7d, 0x76, 0x62, 0x69, 0x50, 0x38, 0x21, 0x13, 0x00),
+	OTM1289A_CMD(0x00, 0x00),
+	OTM1289A_CMD(0xe2, 0x00, 0x16, 0x26, 0x37, 0x47, 0x65, 0x64, 0x7d, 0x76, 0x62, 0x69, 0x50, 0x38, 0x21, 0x13, 0x00),
+	/* CMD2 disable */
+	OTM1289A_CMD(0x00, 0x00),
+	OTM1289A_CMD(0xff, 0xff, 0xff, 0xff),
+};
+
+static inline struct otm1289a *panel_to_otm1289a(struct drm_panel *panel)
+{
+	return container_of(panel, struct otm1289a, panel);
+}
+
+static int otm1289a_init_sequence_send(struct otm1289a *ctx)
+{
+	struct mipi_dsi_device *dsi = to_mipi_dsi_device(ctx->dev);
+	unsigned int i;
+	int ret;
+
+	for (i = 0; i < ARRAY_SIZE(otm1289a_init_sequence); i++) {
+		const struct otm1289a_init_cmd *c = &otm1289a_init_sequence[i];
+
+		ret = mipi_dsi_dcs_write(dsi, c->cmd, c->data, c->len);
+		if (ret < 0)
+			return ret;
+	}
+
+	/*
+	 * Real vendor delays (from the same extracted command table as
+	 * above -- 0xfe-type entries immediately following exit_sleep_mode
+	 * and set_display_on in the real sequence): 50ms and 120ms.
+	 */
+	ret = mipi_dsi_dcs_exit_sleep_mode(dsi);
+	if (ret < 0)
+		return ret;
+	msleep(50);
+
+	ret = mipi_dsi_dcs_set_display_on(dsi);
+	if (ret < 0)
+		return ret;
+	msleep(120);
+
+	return 0;
+}
+
+static int otm1289a_disable(struct drm_panel *panel)
+{
+	struct otm1289a *ctx = panel_to_otm1289a(panel);
+	struct mipi_dsi_device *dsi = to_mipi_dsi_device(ctx->dev);
+	int ret;
+
+	ret = mipi_dsi_dcs_set_display_off(dsi);
+	if (ret < 0)
+		return ret;
+	msleep(40);
+
+	ret = mipi_dsi_dcs_enter_sleep_mode(dsi);
+	if (ret < 0)
+		return ret;
+	msleep(150);
+
+	return 0;
+}
+
+static int otm1289a_unprepare(struct drm_panel *panel)
+{
+	struct otm1289a *ctx = panel_to_otm1289a(panel);
+
+	if (ctx->reset_gpio) {
+		gpiod_set_value_cansleep(ctx->reset_gpio, 1);
+		msleep(20);
+	}
+
+	regulator_disable(ctx->supply);
+
+	ctx->prepared = false;
+
+	return 0;
+}
+
+static int otm1289a_prepare(struct drm_panel *panel)
+{
+	struct otm1289a *ctx = panel_to_otm1289a(panel);
+	int ret;
+
+	ret = regulator_enable(ctx->supply);
+	if (ret < 0) {
+		dev_err(panel->dev, "failed to enable supply: %d\n", ret);
+		return ret;
+	}
+
+	if (ctx->reset_gpio) {
+		gpiod_set_value_cansleep(ctx->reset_gpio, 0);
+		gpiod_set_value_cansleep(ctx->reset_gpio, 1);
+		msleep(20);
+		gpiod_set_value_cansleep(ctx->reset_gpio, 0);
+		msleep(100);
+	}
+
+	ret = otm1289a_init_sequence_send(ctx);
+	if (ret) {
+		dev_err(panel->dev, "init sequence failed: %d\n", ret);
+		regulator_disable(ctx->supply);
+		return ret;
+	}
+
+	ctx->prepared = true;
+
+	return 0;
+}
+
+static int otm1289a_enable(struct drm_panel *panel)
+{
+	return 0;
+}
+
+static int otm1289a_get_modes(struct drm_panel *panel,
+			      struct drm_connector *connector)
+{
+	struct drm_display_mode *mode;
+
+	mode = drm_mode_duplicate(connector->dev, &otm1289a_mode);
+	if (!mode) {
+		dev_err(panel->dev, "failed to add mode %ux%u\n",
+			otm1289a_mode.hdisplay, otm1289a_mode.vdisplay);
+		return -ENOMEM;
+	}
+
+	drm_mode_set_name(mode);
+	drm_mode_probed_add(connector, mode);
+
+	connector->display_info.width_mm = mode->width_mm;
+	connector->display_info.height_mm = mode->height_mm;
+
+	return 1;
+}
+
+static const struct drm_panel_funcs otm1289a_drm_funcs = {
+	.disable   = otm1289a_disable,
+	.unprepare = otm1289a_unprepare,
+	.prepare   = otm1289a_prepare,
+	.enable    = otm1289a_enable,
+	.get_modes = otm1289a_get_modes,
+};
+
+static int otm1289a_probe(struct mipi_dsi_device *dsi)
+{
+	struct device *dev = &dsi->dev;
+	struct otm1289a *ctx;
+	int ret;
+
+	ctx = devm_drm_panel_alloc(dev, struct otm1289a, panel,
+				   &otm1289a_drm_funcs,
+				   DRM_MODE_CONNECTOR_DSI);
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
+
+	ctx->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
+	if (IS_ERR(ctx->reset_gpio)) {
+		dev_err(dev, "cannot get reset-gpio\n");
+		return PTR_ERR(ctx->reset_gpio);
+	}
+
+	ctx->supply = devm_regulator_get(dev, "power");
+	if (IS_ERR(ctx->supply)) {
+		ret = PTR_ERR(ctx->supply);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to request regulator: %d\n", ret);
+		return ret;
+	}
+
+	mipi_dsi_set_drvdata(dsi, ctx);
+	ctx->dev = dev;
+
+	/*
+	 * External PWM backlight (real hardware: PWM channel 0, 50kHz,
+	 * confirmed from the vendor DTB) — not the panel controller's own
+	 * DCS backlight registers, so drm_panel_of_backlight() is the
+	 * right helper here (finds the "backlight" DT property and wires
+	 * automatic enable/disable into the panel's own prepare/unprepare
+	 * cycle), not a custom backlight_device like some other panels in
+	 * this same driver family use for DCS-controlled backlights.
+	 */
+	ret = drm_panel_of_backlight(&ctx->panel);
+	if (ret)
+		return ret;
+
+	/* Confirmed real from the vendor DTB: 4 lanes (the reference driver
+	 * this init sequence came from used 3 — host-side PHY config, not a
+	 * panel-controller register, so using our own confirmed real value
+	 * here is correct even though the source table used a different lane
+	 * count).
+	 */
+	dsi->lanes = 4;
+	dsi->format = MIPI_DSI_FMT_RGB888;
+	/*
+	 * Not MIPI_DSI_MODE_VIDEO_BURST: the real vendor DTB's lcd_dsi_if
+	 * property for this exact device is 0 (LCD_DSI_IF_VIDEO_MODE),
+	 * confirmed against upstream Allwinner BSP headers to be a distinct
+	 * value from LCD_DSI_IF_BURST_MODE (2) -- this panel runs in plain
+	 * non-burst video mode, not burst.
+	 */
+	dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_LPM;
+
+	drm_panel_add(&ctx->panel);
+
+	ret = mipi_dsi_attach(dsi);
+	if (ret < 0) {
+		dev_err(dev, "mipi_dsi_attach failed. Is host ready?\n");
+		drm_panel_remove(&ctx->panel);
+		return ret;
+	}
+
+	return 0;
+}
+
+static void otm1289a_remove(struct mipi_dsi_device *dsi)
+{
+	struct otm1289a *ctx = mipi_dsi_get_drvdata(dsi);
+
+	mipi_dsi_detach(dsi);
+	drm_panel_remove(&ctx->panel);
+}
+
+static const struct of_device_id orisetech_otm1289a_of_match[] = {
+	{ .compatible = "orisetech,otm1289a" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, orisetech_otm1289a_of_match);
+
+static struct mipi_dsi_driver orisetech_otm1289a_driver = {
+	.probe  = otm1289a_probe,
+	.remove = otm1289a_remove,
+	.driver = {
+		.name = "panel-orisetech-otm1289a",
+		.of_match_table = orisetech_otm1289a_of_match,
+	},
+};
+module_mipi_dsi_driver(orisetech_otm1289a_driver);
+
+MODULE_DESCRIPTION("DRM driver for Orise Tech OTM1289A MIPI DSI panel (TrimUI Smart Pro)");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/gpu/drm/sun4i/sun4i_tcon.c b/drivers/gpu/drm/sun4i/sun4i_tcon.c
index bf405a2aa..e4eb15045 100644
--- a/drivers/gpu/drm/sun4i/sun4i_tcon.c
+++ b/drivers/gpu/drm/sun4i/sun4i_tcon.c
@@ -229,6 +229,18 @@ void sun4i_tcon_enable_vblank(struct sun4i_tcon *tcon, bool enable)
 
 	DRM_DEBUG_DRIVER("%sabling VBLANK interrupt\n", enable ? "En" : "Dis");
 
+	/*
+	 * The DSI/CPU-interface path never touches TCON0's own GINT0
+	 * enable bits: for this panel, the vendor implementation dispatches
+	 * IRQ enable/query entirely to DSI's own interrupt register instead
+	 * (confirmed against a live working reference system, where GINT0's
+	 * enable bits read zero). The real per-frame retrigger source is
+	 * SUN6I_DSI_INT_REG -- see sun6i_mipi_dsi.c and
+	 * sun4i_tcon_dsi_retrigger_timer_fn() below.
+	 */
+	if (tcon->dsi_cpu_mode)
+		return;
+
 	mask = SUN4I_TCON_GINT0_VBLANK_ENABLE(0) |
 		SUN4I_TCON_GINT0_VBLANK_ENABLE(1) |
 		SUN4I_TCON_GINT0_TCON0_TRI_FINISH_ENABLE;
@@ -273,6 +285,80 @@ static void sun4i_tcon_set_mux(struct sun4i_tcon *tcon, int channel,
 			 encoder->name, encoder->crtc->name, ret);
 }
 
+/*
+ * See sun8i_tcon_top_set_dsi_gate()'s comment for what this bit is and why
+ * it can't be wired up as a normal clk consumer. Resolves TCON0's own
+ * port-0 remote node the same way sun8i_r40_tcon_tv_set_mux() resolves
+ * TCON_TOP for the TV path -- our board's tcon_lcd0 port@0 is connected
+ * to dpss_top0 (TCON TOP) in the ports graph for engine-ID-matching
+ * purposes already (sun4i_tcon_connected_to_tcon_top()); this reuses that
+ * same connection to actually reach the TCON TOP device.
+ */
+static void sun4i_tcon0_set_dsi_gate(struct sun4i_tcon *tcon, bool enable)
+{
+	struct device_node *remote;
+	struct platform_device *pdev;
+
+	if (!IS_ENABLED(CONFIG_DRM_SUN8I_TCON_TOP))
+		return;
+
+	remote = of_graph_get_remote_node(tcon->dev->of_node, 0, -1);
+	if (!remote)
+		return;
+
+	if (!of_match_node(sun8i_tcon_top_of_table, remote)) {
+		of_node_put(remote);
+		return;
+	}
+
+	pdev = of_find_device_by_node(remote);
+	of_node_put(remote);
+	if (!pdev)
+		return;
+
+	sun8i_tcon_top_set_dsi_gate(&pdev->dev, enable);
+	put_device(&pdev->dev);
+}
+
+/*
+ * See sun8i_tcon_top_set_de0_port()'s comment for what this register is and
+ * why it can't be reached through the normal .set_mux quirks callback for
+ * our board. Same TCON TOP resolution pattern as
+ * sun4i_tcon0_set_dsi_gate() immediately above.
+ */
+static void sun4i_tcon0_set_de0_port(struct sun4i_tcon *tcon)
+{
+	struct device_node *remote;
+	struct platform_device *pdev;
+
+	if (!IS_ENABLED(CONFIG_DRM_SUN8I_TCON_TOP))
+		return;
+
+	remote = of_graph_get_remote_node(tcon->dev->of_node, 0, -1);
+	if (!remote)
+		return;
+
+	if (!of_match_node(sun8i_tcon_top_of_table, remote)) {
+		of_node_put(remote);
+		return;
+	}
+
+	pdev = of_find_device_by_node(remote);
+	of_node_put(remote);
+	if (!pdev)
+		return;
+
+	/*
+	 * DE0 routes to TCON-TOP port 0, matching PORT_SEL's live value on a
+	 * working reference system (its DE0 field is 0, not the OF-graph
+	 * endpoint id 1 the port number might otherwise suggest -- the two
+	 * are unrelated). Written explicitly rather than relying on this
+	 * also being the register's post-reset default.
+	 */
+	sun8i_tcon_top_set_de0_port(&pdev->dev, 0);
+	put_device(&pdev->dev);
+}
+
 static int sun4i_tcon_get_clk_delay(const struct drm_display_mode *mode,
 				    int channel)
 {
@@ -354,14 +440,24 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 	u8 lanes = device->lanes;
 	u32 block_space, start_delay;
 	u32 tcon_div;
+	u8 clk_delay;
 
 	/*
 	 * dclk is required to run at 1/4 the DSI per-lane bit rate.
 	 */
 	tcon->dclk_min_div = SUN6I_DSI_TCON_DIV;
 	tcon->dclk_max_div = SUN6I_DSI_TCON_DIV;
-	clk_set_rate(tcon->dclk, mode->crtc_clock * 1000 * (bpp / lanes)
-						  / SUN6I_DSI_TCON_DIV);
+	clk_set_rate(tcon->dclk, mode->crtc_clock * 1000UL * (bpp / lanes)
+						   / SUN6I_DSI_TCON_DIV);
+
+	/*
+	 * Vendor's tcon_init() sets this unconditionally for every TCON
+	 * instance regardless of panel type (LVDS/RGB/DSI alike); the CPU/DSI
+	 * path here never did.
+	 */
+	regmap_update_bits(tcon->regs, SUN4I_TCON_GCTL_REG,
+			   SUN4I_TCON_GCTL_IOMAP_MASK,
+			   SUN4I_TCON_GCTL_IOMAP_TCON0);
 
 	/* Set the resolution */
 	regmap_write(tcon->regs, SUN4I_TCON0_BASIC0_REG,
@@ -375,6 +471,25 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 			   SUN4I_TCON0_CTL_IF_MASK,
 			   SUN4I_TCON0_CTL_IF_8080);
 
+	/* Arm the per-frame TRIGGER_START re-assertion in the IRQ handler. */
+	tcon->dsi_cpu_mode = true;
+	tcon->dsi = encoder_to_sun6i_dsi(encoder);
+
+	/*
+	 * CLK_DELAY and DCLK_OUT_EN are both set by the vendor implementation
+	 * and present in a working reference system's live register values;
+	 * mainline never set either on the DSI path.
+	 */
+	clk_delay = sun4i_tcon_get_clk_delay(mode, 0);
+	regmap_update_bits(tcon->regs, SUN4I_TCON0_CTL_REG,
+			   SUN4I_TCON0_CTL_CLK_DELAY_MASK,
+			   SUN4I_TCON0_CTL_CLK_DELAY(clk_delay));
+
+	/* See the header comment on SUN4I_TCON0_DCLK_OUT_EN_MASK. */
+	regmap_update_bits(tcon->regs, SUN4I_TCON0_DCLK_REG,
+			   SUN4I_TCON0_DCLK_OUT_EN_MASK,
+			   SUN4I_TCON0_DCLK_OUT_EN_MASK);
+
 	regmap_write(tcon->regs, SUN4I_TCON_ECC_FIFO_REG,
 		     SUN4I_TCON_ECC_FIFO_EN);
 
@@ -384,6 +499,19 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 		     SUN4I_TCON0_CPU_IF_TRI_FIFO_EN |
 		     SUN4I_TCON0_CPU_IF_TRI_EN);
 
+	/*
+	 * TRIGGER_START itself (a write-1-to-start, self-clearing pulse bit;
+	 * TRI_EN above only arms trigger mode, it does not kick off a
+	 * transfer) is deliberately not asserted here. This function runs
+	 * from the CRTC's mode_set_nofb hook, well before the encoder chain's
+	 * .enable() has powered the D-PHY, enabled the DSI block, or set up
+	 * its instruction tables. Asserting TRIGGER_START before the DSI
+	 * engine exists wedges the transfer state machine permanently and no
+	 * amount of later retriggering recovers it. dsi_retrigger_timer's
+	 * first tick fires the real first trigger, safely after
+	 * encoder_enable() has run.
+	 */
+
 	/*
 	 * This looks suspicious, but it works...
 	 *
@@ -395,6 +523,15 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 	block_space = mode->htotal * bpp / (tcon_div * lanes);
 	block_space -= mode->hdisplay + 40;
 
+	/*
+	 * This formula's result is consistently one lower than a working
+	 * reference system's live BLOCK_SPACE value; every other field in
+	 * the pixel pipeline matches vendor bit-for-bit, so the gap is
+	 * closed directly here rather than reverse-engineering which term
+	 * of vendor's own formula differs.
+	 */
+	block_space += 1;
+
 	regmap_write(tcon->regs, SUN4I_TCON0_CPU_TRI0_REG,
 		     SUN4I_TCON0_CPU_TRI0_BLOCK_SPACE(block_space) |
 		     SUN4I_TCON0_CPU_TRI0_BLOCK_SIZE(mode->hdisplay));
@@ -402,24 +539,55 @@ static void sun4i_tcon0_mode_set_cpu(struct sun4i_tcon *tcon,
 	regmap_write(tcon->regs, SUN4I_TCON0_CPU_TRI1_REG,
 		     SUN4I_TCON0_CPU_TRI1_BLOCK_NUM(mode->vdisplay));
 
-	start_delay = (mode->crtc_vtotal - mode->crtc_vdisplay - 10 - 1);
-	start_delay = start_delay * mode->crtc_htotal * 149;
+	/*
+	 * ((vtotal - vdisplay - 9) * htotal * de_clk_rate_mhz / pixel_clk_khz) >> 3,
+	 * with the display engine's own clock rate at 300MHz on this SoC.
+	 * Reproduces a working reference system's live TRI2 register value
+	 * exactly for this panel's timings.
+	 */
+	start_delay = (mode->crtc_vtotal - mode->crtc_vdisplay - 9);
+	start_delay = start_delay * mode->crtc_htotal * 300;
 	start_delay = start_delay / (mode->crtc_clock / 1000) / 8;
 	regmap_write(tcon->regs, SUN4I_TCON0_CPU_TRI2_REG,
 		     SUN4I_TCON0_CPU_TRI2_TRANS_START_SET(10) |
 		     SUN4I_TCON0_CPU_TRI2_START_DELAY(start_delay));
 
 	/*
-	 * The Allwinner BSP has a comment that the period should be
-	 * the display clock * 15, but uses an hardcoded 3000...
+	 * The Allwinner BSP has a comment that the period should be the
+	 * display clock * 15, but hardcodes 3000 -- which itself doesn't
+	 * match this panel: a working reference system's live SAFE_PERIOD_NUM
+	 * value is 1035.
 	 */
 	regmap_write(tcon->regs, SUN4I_TCON_SAFE_PERIOD_REG,
-		     SUN4I_TCON_SAFE_PERIOD_NUM(3000) |
+		     SUN4I_TCON_SAFE_PERIOD_NUM(1035) |
 		     SUN4I_TCON_SAFE_PERIOD_MODE(3));
 
-	/* Enable the output on the pins */
-	regmap_write(tcon->regs, SUN4I_TCON0_IO_TRI_REG,
-		     0xe0000000);
+	/*
+	 * IO_TRI_REG's HSYNC/VSYNC/DATA_PINS_DISABLE bits are for the HV/RGB
+	 * and LVDS paths, not CPU/8080 -- a working reference system's live
+	 * value for this interface is 0x00000000.
+	 */
+	regmap_write(tcon->regs, SUN4I_TCON0_IO_TRI_REG, 0x00000000);
+
+	/*
+	 * This timer's first tick fires the real first TRIGGER_START, once
+	 * sun6i_dsi_encoder_enable() has actually finished rather than just
+	 * started -- its drm_panel_prepare() call alone (reset pulse + DCS
+	 * init) totals close to 300ms of msleep() for this panel. 1000ms
+	 * gives ample margin over that as a one-time cost at boot.
+	 */
+	tcon->dsi_retrigger_ticks = 0;
+	hrtimer_start(&tcon->dsi_retrigger_timer, ms_to_ktime(1000),
+		      HRTIMER_MODE_REL);
+
+	/*
+	 * enable_irq(tcon->irq) is deliberately not called here:
+	 * hrtimer_start() only schedules the timer and returns immediately,
+	 * so calling it at this point would run at essentially the same
+	 * instant as probe-time did. It's enabled from the timer's own
+	 * tick==0 callback instead, genuinely after DSI/D-PHY/panel are
+	 * configured -- see sun4i_tcon_dsi_retrigger_timer_fn() below.
+	 */
 }
 
 static void sun4i_tcon0_mode_set_lvds(struct sun4i_tcon *tcon,
@@ -713,11 +881,31 @@ void sun4i_tcon_mode_set(struct sun4i_tcon *tcon,
 			 const struct drm_encoder *encoder,
 			 const struct drm_display_mode *mode)
 {
+	/*
+	 * Only the DSI/CPU-interface path below re-arms it; clear it here so a
+	 * TCON re-used for another encoder type does not keep poking
+	 * TRIGGER_START in the IRQ handler. Cancel the software retrigger timer
+	 * too -- sun4i_tcon0_mode_set_cpu() below restarts it if the DSI path
+	 * is taken again.
+	 */
+	tcon->dsi_cpu_mode = false;
+	hrtimer_cancel(&tcon->dsi_retrigger_timer);
+
 	switch (encoder->encoder_type) {
 	case DRM_MODE_ENCODER_DSI:
-		/* DSI is tied to special case of CPU interface */
+		/*
+		 * All DSI panels on this board go through the CPU/8080
+		 * interface, including video-mode ones -- the device tree's
+		 * "video mode" declaration and the panel's MIPI_DSI_MODE_VIDEO
+		 * flag describe the DSI protocol mode, not the TCON's
+		 * HV-vs-CPU register selection. A working reference system's
+		 * live LCD_CTL_REG confirms tcon0_if = 1 (IF_8080) for this
+		 * exact configuration.
+		 */
 		sun4i_tcon0_mode_set_cpu(tcon, encoder, mode);
 		sun4i_tcon_set_mux(tcon, 0, encoder);
+		sun4i_tcon0_set_dsi_gate(tcon, true);
+		sun4i_tcon0_set_de0_port(tcon);
 		break;
 	case DRM_MODE_ENCODER_LVDS:
 		sun4i_tcon0_mode_set_lvds(tcon, encoder, mode);
@@ -759,12 +947,23 @@ static irqreturn_t sun4i_tcon_handler(int irq, void *private)
 	struct sun4i_crtc *scrtc = tcon->crtc;
 	struct sunxi_engine *engine = scrtc->engine;
 	unsigned int status;
+	bool handled;
 
 	regmap_read(tcon->regs, SUN4I_TCON_GINT0_REG, &status);
 
-	if (!(status & (SUN4I_TCON_GINT0_VBLANK_INT(0) |
-			SUN4I_TCON_GINT0_VBLANK_INT(1) |
-			SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT)))
+	/*
+	 * FSYNC_INT (bit 9) reads unconditionally set on this board and was
+	 * never enabled or acknowledged here -- leaving the level interrupt
+	 * permanently asserted and triggering Linux's unhandled-IRQ storm
+	 * protection shortly after boot. Treating it as handled (and
+	 * clearing it below) keeps the line alive.
+	 */
+	handled = status & (SUN4I_TCON_GINT0_VBLANK_INT(0) |
+			     SUN4I_TCON_GINT0_VBLANK_INT(1) |
+			     SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT |
+			     SUN4I_TCON_GINT0_TCON0_FSYNC_INT);
+
+	if (!handled)
 		return IRQ_NONE;
 
 	drm_crtc_handle_vblank(&scrtc->crtc);
@@ -774,15 +973,111 @@ static irqreturn_t sun4i_tcon_handler(int irq, void *private)
 	regmap_update_bits(tcon->regs, SUN4I_TCON_GINT0_REG,
 			   SUN4I_TCON_GINT0_VBLANK_INT(0) |
 			   SUN4I_TCON_GINT0_VBLANK_INT(1) |
-			   SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT,
+			   SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT |
+			   SUN4I_TCON_GINT0_TCON0_FSYNC_INT,
 			   0);
 
+	/*
+	 * Re-arm the CPU/8080-interface transfer for the next frame.
+	 *
+	 * TRIGGER_START is a self-clearing one-shot: it starts exactly one
+	 * frame's transfer and then clears itself. Without re-asserting it,
+	 * the panel receives a single frame at mode-set time and nothing
+	 * ever again. Pristine mainline's sun4i_tcon0_mode_set_cpu() writes
+	 * neither AUTO nor TRIGGER_START for any board; its existing users
+	 * (Pinephone/Pinetab on A64, TBS-A711 on A83T) free-run continuously
+	 * off a single TRI_EN write on older TCON hardware that doesn't need
+	 * a per-frame software trigger at all. This TCON generation does.
+	 *
+	 * This mirrors the vendor implementation's own per-frame retrigger
+	 * shape -- a DSI-side kick followed by this TRIGGER_START write --
+	 * but not its trigger source: for this panel that's DSI's own
+	 * interrupt register, not TCON's GINT0 (see the comment on
+	 * SUN6I_DSI_INT_REG in sun6i_mipi_dsi.c). TRI_FINISH_INT is kept
+	 * here as a backstop in case TCON's own transfer-complete signal
+	 * ever does fire; it's just not the mechanism this panel relies on.
+	 * Without the DSI-side kick, the DSI engine never advances past the
+	 * first frame it was started into at encoder_enable() time, so
+	 * nothing downstream ever acknowledges TRIGGER_START and it never
+	 * self-clears.
+	 */
+	if ((status & SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT) &&
+	    tcon->dsi_cpu_mode) {
+		sun6i_dsi_tri_start(tcon->dsi);
+		regmap_update_bits(tcon->regs, SUN4I_TCON0_CPU_IF_REG,
+				   SUN4I_TCON0_CPU_IF_TRI_START,
+				   SUN4I_TCON0_CPU_IF_TRI_START);
+	}
+
 	if (engine->ops->vblank_quirk)
 		engine->ops->vblank_quirk(engine);
 
 	return IRQ_HANDLED;
 }
 
+/*
+ * Fires the initial TRIGGER_START/HSC kick once encoder-enable (including
+ * the panel's own prepare() delays) has actually finished, then polls at
+ * 2ms intervals -- clearing DSI_INT_REG's status bits each time doubles as
+ * edge detection, since a subsequent read finding a status bit set again
+ * can only mean it happened since the last clear. Stops itself once
+ * TRIGGER_START is observed to have self-cleared, at which point
+ * sun4i_tcon_handler() (armed on TRI_FINISH_INT) takes over the per-frame
+ * retrigger job instead.
+ */
+
+static enum hrtimer_restart sun4i_tcon_dsi_retrigger_timer_fn(struct hrtimer *timer)
+{
+	struct sun4i_tcon *tcon = container_of(timer, struct sun4i_tcon,
+						dsi_retrigger_timer);
+	unsigned int cpu_if_before;
+	unsigned int tick = tcon->dsi_retrigger_ticks;
+	u32 dsi_int;
+	bool busy;
+
+	regmap_read(tcon->regs, SUN4I_TCON0_CPU_IF_REG, &cpu_if_before);
+	busy = cpu_if_before & SUN4I_TCON0_CPU_IF_TRI_START;
+
+	if (tick && !busy) {
+		/* TRIGGER_START self-cleared; the IRQ path takes over from here. */
+		return HRTIMER_NORESTART;
+	}
+
+	dsi_int = sun6i_dsi_read_int_status(tcon->dsi);
+	if (dsi_int & GENMASK(31, 16))
+		sun6i_dsi_clear_int_status(tcon->dsi);
+
+	/*
+	 * enable_irq() is deferred to here, roughly 1s after mode_set_cpu(),
+	 * rather than at probe time -- TCON0/DSI/D-PHY aren't configured
+	 * until mode_set/encoder_enable time, which can be a second or more
+	 * after probe under DRM's atomic commit machinery.
+	 */
+	if (tick == 0 && tcon->irq)
+		enable_irq(tcon->irq);
+
+	/*
+	 * The vendor per-frame retrigger is LINE-edge-triggered and
+	 * busy-gated, never blindly re-asserting TRIGGER_START -- matching
+	 * the documented precondition on this bit ("software must write 1
+	 * only when this flag is 0"). Only the initial kick is fired from
+	 * software here; TCON0's AUTO bit (set in mode_set_cpu()) is
+	 * expected to handle Vsync-driven retriggering afterward, the same
+	 * way mainline's other DSI/CPU-interface boards need no per-frame
+	 * software retrigger at all.
+	 */
+	if (tick == 0 && !busy) {
+		sun6i_dsi_tri_start(tcon->dsi);
+		regmap_update_bits(tcon->regs, SUN4I_TCON0_CPU_IF_REG,
+				   SUN4I_TCON0_CPU_IF_TRI_START,
+				   SUN4I_TCON0_CPU_IF_TRI_START);
+	}
+
+	tcon->dsi_retrigger_ticks = tick + 1;
+	hrtimer_forward_now(timer, ms_to_ktime(2));
+	return HRTIMER_RESTART;
+}
+
 static int sun4i_tcon_init_clocks(struct device *dev,
 				  struct sun4i_tcon *tcon)
 {
@@ -811,6 +1106,16 @@ static int sun4i_tcon_init_clocks(struct device *dev,
 	return 0;
 }
 
+/*
+ * This IRQ is requested disabled (IRQF_NO_AUTOEN) rather than auto-enabled
+ * at the GIC: TCON0/DSI/D-PHY aren't configured until mode_set/
+ * encoder_enable time, commonly a second or more after probe under DRM's
+ * atomic commit machinery, and an unmasked interrupt during that window
+ * storms and gets permanently disabled by Linux's own protection before
+ * anything is ready to handle it. enable_irq() is called later from
+ * sun4i_tcon_dsi_retrigger_timer_fn(), once the hardware is actually
+ * configured.
+ */
 static int sun4i_tcon_init_irq(struct device *dev,
 			       struct sun4i_tcon *tcon)
 {
@@ -821,13 +1126,15 @@ static int sun4i_tcon_init_irq(struct device *dev,
 	if (irq < 0)
 		return irq;
 
-	ret = devm_request_irq(dev, irq, sun4i_tcon_handler, 0,
+	ret = devm_request_irq(dev, irq, sun4i_tcon_handler, IRQF_NO_AUTOEN,
 			       dev_name(dev), tcon);
 	if (ret) {
 		dev_err(dev, "Couldn't request the IRQ\n");
 		return ret;
 	}
 
+	tcon->irq = irq;
+
 	return 0;
 }
 
@@ -1136,6 +1443,9 @@ static int sun4i_tcon_bind(struct device *dev, struct device *master,
 	tcon->id = engine->id;
 	tcon->quirks = of_device_get_match_data(dev);
 
+	hrtimer_setup(&tcon->dsi_retrigger_timer, sun4i_tcon_dsi_retrigger_timer_fn,
+		      CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+
 	tcon->lcd_rst = devm_reset_control_get(dev, "lcd");
 	if (IS_ERR(tcon->lcd_rst)) {
 		dev_err(dev, "Couldn't get our reset line\n");
@@ -1304,6 +1614,7 @@ static void sun4i_tcon_unbind(struct device *dev, struct device *master,
 {
 	struct sun4i_tcon *tcon = dev_get_drvdata(dev);
 
+	hrtimer_cancel(&tcon->dsi_retrigger_timer);
 	list_del(&tcon->list);
 	if (tcon->quirks->has_channel_0)
 		sun4i_dclk_free(tcon);
diff --git a/drivers/gpu/drm/sun4i/sun4i_tcon.h b/drivers/gpu/drm/sun4i/sun4i_tcon.h
index fa23aa23f..ce59cbea7 100644
--- a/drivers/gpu/drm/sun4i/sun4i_tcon.h
+++ b/drivers/gpu/drm/sun4i/sun4i_tcon.h
@@ -12,11 +12,14 @@
 
 #include <drm/drm_crtc.h>
 
+#include <linux/hrtimer.h>
 #include <linux/kernel.h>
 #include <linux/list.h>
 #include <linux/mod_devicetable.h>
 #include <linux/reset.h>
 
+struct sun6i_dsi;
+
 #define SUN4I_TCON_GCTL_REG			0x0
 #define SUN4I_TCON_GCTL_TCON_ENABLE			BIT(31)
 #define SUN4I_TCON_GCTL_IOMAP_MASK			BIT(0)
@@ -27,9 +30,19 @@
 #define SUN4I_TCON_GINT0_VBLANK_ENABLE(pipe)		BIT(31 - (pipe))
 #define SUN4I_TCON_GINT0_TCON0_TRI_FINISH_ENABLE	BIT(27)
 #define SUN4I_TCON_GINT0_TCON0_TRI_COUNTER_ENABLE	BIT(26)
+/*
+ * LCD_IRQ_FSYNC_INT: a real, named status bit this driver never enabled
+ * or checked, found live and unconditionally set on this board. Left
+ * unhandled it storms the IRQ line and gets it permanently disabled by
+ * Linux's own unhandled-interrupt protection shortly after boot.
+ * Enable-bit position follows the same status+16 pairing every other
+ * GINT0 bit here uses.
+ */
+#define SUN4I_TCON_GINT0_TCON0_FSYNC_ENABLE		BIT(25)
 #define SUN4I_TCON_GINT0_VBLANK_INT(pipe)		BIT(15 - (pipe))
 #define SUN4I_TCON_GINT0_TCON0_TRI_FINISH_INT		BIT(11)
 #define SUN4I_TCON_GINT0_TCON0_TRI_COUNTER_INT		BIT(10)
+#define SUN4I_TCON_GINT0_TCON0_FSYNC_INT		BIT(9)
 
 #define SUN4I_TCON_GINT1_REG			0x8
 
@@ -62,6 +75,15 @@
 #define SUN4I_TCON0_DCLK_GATE_BIT			(31)
 #define SUN4I_TCON0_DCLK_DIV_SHIFT			(0)
 #define SUN4I_TCON0_DCLK_DIV_WIDTH			(7)
+/*
+ * The vendor implementation sets bits 30-28 together with the documented
+ * GATE_BIT (31) for DSI output; mainline's existing dclk gate-only clock
+ * framework handling only ever touches bit 31, leaving these three at
+ * their power-on-reset value of 0. Mainline has no name for these bits;
+ * setting them unconditionally alongside bit 31 is additive, not a
+ * replacement for the existing clk_hw gate behavior.
+ */
+#define SUN4I_TCON0_DCLK_OUT_EN_MASK			GENMASK(30, 28)
 
 #define SUN4I_TCON0_BASIC0_REG			0x48
 #define SUN4I_TCON0_BASIC0_X(width)			((((width) - 1) & 0xfff) << 16)
@@ -84,8 +106,25 @@
 #define SUN4I_TCON0_CPU_IF_REG			0x60
 #define SUN4I_TCON0_CPU_IF_MODE_MASK			GENMASK(31, 28)
 #define SUN4I_TCON0_CPU_IF_MODE_DSI			(1 << 28)
+/*
+ * Bit 17 "AUTO" ("if 1, all the valid data during this frame are written
+ * to panel, sampled by Vsync") and bit 16 "FLUSH" ("pixel data keeps
+ * being transferred unless the input FIFO is empty") are two alternate,
+ * mutually-exclusive trigger modes, per the A133 User Manual. This board
+ * matches a working reference system's live configuration (FLUSH,
+ * AUTO clear) and drives transfers via an explicit per-frame
+ * TRIGGER_START instead.
+ */
+#define SUN4I_TCON0_CPU_IF_AUTO				BIT(17)
 #define SUN4I_TCON0_CPU_IF_TRI_FIFO_FLUSH		BIT(16)
 #define SUN4I_TCON0_CPU_IF_TRI_FIFO_EN			BIT(2)
+/*
+ * "TRIGGER_START -- Write '1' to start a frame flush... This flag
+ * indicates frame flush is running. Software must write '1' only when
+ * this flag is '0'." TRI_EN below only arms trigger mode; it doesn't
+ * kick off a transfer by itself.
+ */
+#define SUN4I_TCON0_CPU_IF_TRI_START			BIT(1)
 #define SUN4I_TCON0_CPU_IF_TRI_EN			BIT(0)
 
 #define SUN4I_TCON0_CPU_WR_REG			0x64
@@ -257,6 +296,13 @@ struct sun4i_tcon {
 	struct drm_device		*drm;
 	struct regmap			*regs;
 
+	/*
+	 * Requested with IRQF_NO_AUTOEN at probe time and enabled only later,
+	 * once TCON0/DSI/D-PHY are actually configured -- see the comment on
+	 * sun4i_tcon_init_irq().
+	 */
+	int				irq;
+
 	/* Main bus clock */
 	struct clk			*clk;
 
@@ -279,6 +325,46 @@ struct sun4i_tcon {
 	/* Platform adjustments */
 	const struct sun4i_tcon_quirks	*quirks;
 
+	/*
+	 * Set while TCON0 drives a DSI panel through the CPU/8080 interface
+	 * in explicit (non-AUTO) trigger mode. In that mode TRIGGER_START is
+	 * a self-clearing one-shot that has to be re-asserted once per frame
+	 * from the TRI_FINISH interrupt -- see sun4i_tcon_handler(). Gates
+	 * that re-trigger so RGB/LVDS/HDMI TCONs, where LCD_CPU_IF_REG is
+	 * meaningless, are left untouched.
+	 */
+	bool				dsi_cpu_mode;
+
+	/*
+	 * The DSI device driving this TCON, captured in sun4i_tcon_mode_set()
+	 * when dsi_cpu_mode is set. Needed so sun4i_tcon_handler() can also
+	 * re-issue the DSI-side HSC start sequence each frame -- see
+	 * sun6i_dsi_tri_start(). Only valid when dsi_cpu_mode is true.
+	 */
+	struct sun6i_dsi		*dsi;
+
+	/*
+	 * Delayed kick for the first TRIGGER_START/HSC assertion, started
+	 * (with a delay margin) at the end of sun4i_tcon0_mode_set_cpu() --
+	 * that function runs before sun6i_dsi_encoder_enable() in DRM's
+	 * atomic commit order, and the panel's own prepare() delays haven't
+	 * elapsed yet either, so TRIGGER_START can't be asserted synchronously
+	 * from mode_set.
+	 *
+	 * sun4i_tcon_dsi_retrigger_timer_fn() writes TRIGGER_START once, at
+	 * tick==0, matching the vendor per-frame retrigger's own busy-gated
+	 * behavior (it never blindly re-asserts the bit either -- see that
+	 * function's comment in sun4i_tcon.c). Every tick after that just
+	 * re-arms itself every 2ms to poll and clear DSI_INT_REG's status,
+	 * until TRI_START is observed to have self-cleared, at which point
+	 * it stops (HRTIMER_NORESTART) and sun4i_tcon_handler() -- armed on
+	 * the real TRI_FINISH_INT -- takes over the per-frame retrigger job.
+	 */
+	struct hrtimer			dsi_retrigger_timer;
+
+	/* Tick counter for dsi_retrigger_timer, reset each time it (re)starts. */
+	unsigned int			dsi_retrigger_ticks;
+
 	/* Associated crtc */
 	struct sun4i_crtc		*crtc;
 
diff --git a/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c b/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c
index d504ae583..4691f7a26 100644
--- a/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c
+++ b/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.c
@@ -35,6 +35,33 @@
 #define SUN6I_DSI_CTL_REG		0x000
 #define SUN6I_DSI_CTL_EN			BIT(0)
 
+/*
+ * The real per-frame retrigger source for CPU/8080-interface DSI panels
+ * turns out to be this register, not TCON's own GINT0. Allwinner's vendor
+ * kernel dispatches both its VBLK- and LINE-equivalent IRQ queries straight
+ * to DSI_INT_REG for this panel type instead of touching TCON0's GINT0 at
+ * all, which is also why GINT0's enable bits read as zero on a working
+ * reference system: they're genuinely unused for this panel.
+ *
+ * bit0 = INSTR_END, bit1 = INSTR_STEP, bit2 = VIDEO_VBLK, bit3 = VIDEO_LINE
+ * in the enable half (low 16 bits); the status half mirrors the same
+ * layout at bit16-19. Status bits are write-1-to-clear, matching the
+ * vendor implementation's own acknowledge behavior.
+ *
+ * Not wired to a GIC interrupt (no devm_request_irq() here) -- enabling
+ * these bits only latches this block's internal status and its own IRQ
+ * output pin, which the GIC never forwards unless something requests that
+ * SPI. sun4i_tcon.c's retrigger timer polls and clears this register
+ * directly instead of using an interrupt handler.
+ */
+#define SUN6I_DSI_INT_REG		0x004
+#define SUN6I_DSI_INT_EN_VIDEO_VBLK		BIT(2)
+#define SUN6I_DSI_INT_EN_VIDEO_LINE		BIT(3)
+/*
+ * SUN6I_DSI_INT_STA_VIDEO_VBLK / _LINE are declared in sun6i_mipi_dsi.h --
+ * sun4i_tcon.c needs them too.
+ */
+
 #define SUN6I_DSI_BASIC_CTL_REG		0x00c
 #define SUN6I_DSI_BASIC_CTL_TRAIL_INV(n)		(((n) & 0xf) << 4)
 #define SUN6I_DSI_BASIC_CTL_TRAIL_FILL		BIT(3)
@@ -165,6 +192,21 @@ enum sun6i_dsi_start_inst {
 	DSI_START_LPTX,
 	DSI_START_HSC,
 	DSI_START_HSD,
+	/*
+	 * Falls through to sun6i_dsi_start()'s default case (JUMP_SEL = END
+	 * only, i.e. idle/stop everything). Vendor's DSI bring-up routine
+	 * enables the block's IRQ bits and resets the instruction engine to
+	 * this same idle state before any mode-specific setup runs.
+	 */
+	DSI_START_IDLE,
+	/*
+	 * The real per-frame retrigger table, distinct from DSI_START_HSC:
+	 * DSI_START_HSC's JUMP_SEL table is the one-shot LP11->HSC->END used
+	 * for the one-time HS-clock-enable step, while this table is the
+	 * continuous streaming loop (LP11->HSC->NOP->HSD->DLY->loop) that
+	 * needs to be reissued every frame to keep pixel data flowing.
+	 */
+	DSI_START_TRI,
 };
 
 enum sun6i_dsi_inst_id {
@@ -436,9 +478,16 @@ static void sun6i_dsi_setup_burst(struct sun6i_dsi *dsi,
 			     SUN6I_DSI_BURST_LINE_SYNC_POINT(SUN6I_DSI_SYNC_POINT));
 
 		val = SUN6I_DSI_TCON_DRQ_ENABLE_MODE;
-	} else if ((mode->hsync_start - mode->hdisplay) > 20) {
-		/* Maaaaaagic */
-		u16 drq = (mode->hsync_start - mode->hdisplay) - 20;
+	} else if ((mode->htotal - mode->hsync_start) > 20) {
+		/*
+		 * This is (back porch + hsync width), not the front porch a
+		 * previous version of this driver used here. Verified against
+		 * the vendor implementation's own DRQ lookup table and a
+		 * live-measured TCON_DRQ_REG value on a working reference
+		 * system: (htotal - hsync_start - 20) * bpp / 32 reproduces
+		 * it exactly for this panel's timings.
+		 */
+		u16 drq = (mode->htotal - mode->hsync_start) - 20;
 
 		drq *= mipi_dsi_pixel_format_to_bpp(device->format);
 		drq /= 32;
@@ -696,6 +745,16 @@ static int sun6i_dsi_start(struct sun6i_dsi *dsi,
 			     DSI_INST_ID_NOP  << (4 * DSI_INST_ID_DLY) |
 			     DSI_INST_ID_END  << (4 * DSI_INST_ID_HSCEXIT));
 		break;
+	case DSI_START_TRI:
+		/* vendor dsi_start(id=2) -- see the DSI_START_TRI comment above */
+		regmap_write(dsi->regs, SUN6I_DSI_INST_JUMP_SEL_REG,
+			     DSI_INST_ID_HSC  << (4 * DSI_INST_ID_LP11) |
+			     DSI_INST_ID_NOP  << (4 * DSI_INST_ID_HSC) |
+			     DSI_INST_ID_HSD  << (4 * DSI_INST_ID_NOP) |
+			     DSI_INST_ID_DLY  << (4 * DSI_INST_ID_HSD) |
+			     DSI_INST_ID_NOP  << (4 * DSI_INST_ID_DLY) |
+			     DSI_INST_ID_END  << (4 * DSI_INST_ID_HSCEXIT));
+		break;
 	default:
 		regmap_write(dsi->regs, SUN6I_DSI_INST_JUMP_SEL_REG,
 			     DSI_INST_ID_END  << (4 * DSI_INST_ID_LP11));
@@ -713,6 +772,48 @@ static int sun6i_dsi_start(struct sun6i_dsi *dsi,
 	return 0;
 }
 
+/*
+ * Vendor's per-frame retrigger asserts two things together: the DSI-side
+ * kick handled here, then the TCON-side TRIGGER_START bit (handled by the
+ * caller). This DSI side previously reused DSI_START_HSC, which is the
+ * one-shot HS-clock-enable table (LP11->HSC->END) rather than the
+ * continuous streaming table DSI_START_TRI represents
+ * (LP11->HSC->NOP->HSD->DLY->loop). Without reissuing the continuous table
+ * every frame, the DSI engine never advances past the first frame it was
+ * started into at encoder-enable time.
+ */
+void sun6i_dsi_tri_start(struct sun6i_dsi *dsi)
+{
+	sun6i_dsi_start(dsi, DSI_START_TRI);
+}
+
+/*
+ * Read-only poll of DSI_INT_REG -- see the comment on SUN6I_DSI_INT_REG
+ * near the top of this file. Called from sun4i_tcon.c's retrigger timer.
+ */
+u32 sun6i_dsi_read_int_status(struct sun6i_dsi *dsi)
+{
+	unsigned int val = 0;
+
+	regmap_read(dsi->regs, SUN6I_DSI_INT_REG, &val);
+
+	return val;
+}
+
+/*
+ * Write back whatever status bits (high 16) are currently set, which
+ * write-1-to-clears exactly those bits; regmap_update_bits leaves the
+ * enable half (low 16) untouched.
+ */
+void sun6i_dsi_clear_int_status(struct sun6i_dsi *dsi)
+{
+	unsigned int val = 0;
+
+	regmap_read(dsi->regs, SUN6I_DSI_INT_REG, &val);
+	regmap_update_bits(dsi->regs, SUN6I_DSI_INT_REG,
+			   GENMASK(31, 16), val & GENMASK(31, 16));
+}
+
 static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
 {
 	struct drm_display_mode *mode = &encoder->crtc->state->adjusted_mode;
@@ -720,7 +821,6 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
 	struct mipi_dsi_device *device = dsi->device;
 	union phy_configure_opts opts = { };
 	struct phy_configure_opts_mipi_dphy *cfg = &opts.mipi_dphy;
-	u16 delay;
 	int err;
 
 	DRM_DEBUG_DRIVER("Enabling DSI output\n");
@@ -737,6 +837,17 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
 	 */
 	regmap_write(dsi->regs, SUN6I_DSI_CTL_REG, SUN6I_DSI_CTL_EN);
 
+	/* See the comment on SUN6I_DSI_INT_REG above. */
+	regmap_write(dsi->regs, SUN6I_DSI_INT_REG,
+		     SUN6I_DSI_INT_EN_VIDEO_VBLK | SUN6I_DSI_INT_EN_VIDEO_LINE);
+
+	/*
+	 * Reset the instruction engine to a known idle state before any of
+	 * the mode-specific setup below, matching the vendor bring-up
+	 * sequence for this block.
+	 */
+	sun6i_dsi_start(dsi, DSI_START_IDLE);
+
 	regmap_write(dsi->regs, SUN6I_DSI_BASIC_CTL0_REG,
 		     SUN6I_DSI_BASIC_CTL0_ECC_EN | SUN6I_DSI_BASIC_CTL0_CRC_EN);
 
@@ -747,9 +858,14 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
 
 	regmap_write(dsi->regs, SUN6I_DSI_DEBUG_DATA_REG, 0xff);
 
-	delay = sun6i_dsi_get_video_start_delay(dsi, mode);
+	/*
+	 * This panel's CPU/8080-interface TCON path still runs the DSI block
+	 * in video mode -- confirmed by a live register read from a working
+	 * reference system, where VIDEO_MODE, VIDEO_PRECISION and VIDEO_FILL
+	 * are all set with a matching VIDEO_ST_DELAY.
+	 */
 	regmap_write(dsi->regs, SUN6I_DSI_BASIC_CTL1_REG,
-		     SUN6I_DSI_BASIC_CTL1_VIDEO_ST_DELAY(delay) |
+		     SUN6I_DSI_BASIC_CTL1_VIDEO_ST_DELAY(sun6i_dsi_get_video_start_delay(dsi, mode)) |
 		     SUN6I_DSI_BASIC_CTL1_VIDEO_FILL |
 		     SUN6I_DSI_BASIC_CTL1_VIDEO_PRECISION |
 		     SUN6I_DSI_BASIC_CTL1_VIDEO_MODE);
@@ -769,28 +885,29 @@ static void sun6i_dsi_encoder_enable(struct drm_encoder *encoder)
 	phy_configure(dsi->dphy, &opts);
 	phy_power_on(dsi->dphy);
 
-	if (dsi->panel)
-		drm_panel_prepare(dsi->panel);
-
 	/*
-	 * FIXME: This should be moved after the switch to HS mode.
-	 *
-	 * Unfortunately, once in HS mode, it seems like we're not
-	 * able to send DCS commands anymore, which would prevent any
-	 * panel to send any DCS command as part as their enable
-	 * method, which is quite common.
-	 *
-	 * I haven't seen any artifact due to that sub-optimal
-	 * ordering on the panels I've tested it with, so I guess this
-	 * will do for now, until that IP is better understood.
+	 * The HS clock-enable step (DSI_START_HSC) runs before the panel's
+	 * DCS init sequence rather than after, unlike a previous revision of
+	 * this driver. Upstream's own comment on this ordering ("this should
+	 * be moved after the switch to HS mode... I haven't seen any artifact
+	 * due to that sub-optimal ordering on the panels I've tested it
+	 * with") already flagged it as suspect without resolving it. The
+	 * vendor panel driver for this board enables the HS clock as the
+	 * very first step of its panel-init routine, before sending any DCS
+	 * command. This doesn't conflict with DCS needing LP mode: HSC only
+	 * puts the clock lane into its continuous-HS state, and DCS commands
+	 * use the entirely separate LPTX/LPDT instruction tables.
 	 */
-	if (dsi->panel)
-		drm_panel_enable(dsi->panel);
-
 	sun6i_dsi_start(dsi, DSI_START_HSC);
 
 	udelay(1000);
 
+	if (dsi->panel)
+		drm_panel_prepare(dsi->panel);
+
+	if (dsi->panel)
+		drm_panel_enable(dsi->panel);
+
 	sun6i_dsi_start(dsi, DSI_START_HSD);
 }
 
diff --git a/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.h b/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.h
index f1ddefe0f..4a8f4a09a 100644
--- a/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.h
+++ b/drivers/gpu/drm/sun4i/sun6i_mipi_dsi.h
@@ -15,6 +15,14 @@
 
 #define SUN6I_DSI_TCON_DIV	4
 
+/*
+ * Status half of SUN6I_DSI_INT_REG (see the comment on that register in
+ * sun6i_mipi_dsi.c), exposed so sun4i_tcon.c's retrigger timer can
+ * edge-detect against sun6i_dsi_read_int_status()'s return value.
+ */
+#define SUN6I_DSI_INT_STA_VIDEO_VBLK	BIT(2 + 16)
+#define SUN6I_DSI_INT_STA_VIDEO_LINE	BIT(3 + 16)
+
 struct sun6i_dsi_variant {
 	bool			has_mod_clk;
 	bool			set_mod_clk;
@@ -55,4 +63,8 @@ static inline struct sun6i_dsi *encoder_to_sun6i_dsi(const struct drm_encoder *e
 	return container_of(encoder, struct sun6i_dsi, encoder);
 };
 
+void sun6i_dsi_tri_start(struct sun6i_dsi *dsi);
+u32 sun6i_dsi_read_int_status(struct sun6i_dsi *dsi);
+void sun6i_dsi_clear_int_status(struct sun6i_dsi *dsi);
+
 #endif /* _SUN6I_MIPI_DSI_H_ */
diff --git a/drivers/gpu/drm/sun4i/sun8i_mixer.c b/drivers/gpu/drm/sun4i/sun8i_mixer.c
index b6cd7352e..b3419f6d3 100644
--- a/drivers/gpu/drm/sun4i/sun8i_mixer.c
+++ b/drivers/gpu/drm/sun4i/sun8i_mixer.c
@@ -853,6 +853,46 @@ static const struct sun8i_mixer_cfg sun50i_a64_mixer1_cfg = {
 	.vi_num		= 1,
 };
 
+/*
+ * A133 (sun50iw10) has no public register documentation. This config
+ * is derived from Allwinner's GPL disp2 driver (de_feat.c/de_rtmx.c,
+ * lowlevel_v2x/sun50iw10 tree), which confirms it is a standard DE2.0
+ * dual-pipe mixer: DISP0 has 4 channels (2 VI + 2 UI, all 4 scaler
+ * capable), DISP1 has 3 channels (1 VI + 2 UI, all 3 scaler capable).
+ * de_rtmx_init() places mixer1's register block at DE0 base + 0x200000
+ * and mixer0's at DE0 base + 0x100000, matching the existing A64/H3/R40
+ * DE2.0 layout convention already used below.
+ */
+static const struct sun8i_mixer_cfg sun50i_a133_mixer0_cfg = {
+	.lay_cfg = {
+		.ccsc		= CCSC_MIXER0_LAYOUT,
+		.de_type	= SUN8I_MIXER_DE2,
+		.vi_scaler_num	= 2,
+		.scaler_mask	= 0xf,
+		.scanline_yuv	= 2560,
+		.de2_fcc_alpha	= 1,
+	},
+	.de_type	= SUN8I_MIXER_DE2,
+	.mod_rate	= 297000000,
+	.ui_num		= 2,
+	.vi_num		= 2,
+};
+
+static const struct sun8i_mixer_cfg sun50i_a133_mixer1_cfg = {
+	.lay_cfg = {
+		.ccsc		= CCSC_MIXER1_LAYOUT,
+		.de_type	= SUN8I_MIXER_DE2,
+		.vi_scaler_num	= 1,
+		.scaler_mask	= 0x7,
+		.scanline_yuv	= 2048,
+		.de2_fcc_alpha	= 1,
+	},
+	.de_type	= SUN8I_MIXER_DE2,
+	.mod_rate	= 297000000,
+	.ui_num		= 2,
+	.vi_num		= 1,
+};
+
 static const struct sun8i_mixer_cfg sun50i_h6_mixer0_cfg = {
 	.lay_cfg = {
 		.de_type	= SUN8I_MIXER_DE3,
@@ -920,6 +960,14 @@ static const struct of_device_id sun8i_mixer_of_table[] = {
 		.compatible = "allwinner,sun50i-a64-de2-mixer-1",
 		.data = &sun50i_a64_mixer1_cfg,
 	},
+	{
+		.compatible = "allwinner,sun50i-a133-de2-mixer-0",
+		.data = &sun50i_a133_mixer0_cfg,
+	},
+	{
+		.compatible = "allwinner,sun50i-a133-de2-mixer-1",
+		.data = &sun50i_a133_mixer1_cfg,
+	},
 	{
 		.compatible = "allwinner,sun50i-h6-de3-mixer-0",
 		.data = &sun50i_h6_mixer0_cfg,
diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
index 9cbd65551..c074fd223 100644
--- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
+++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c
@@ -143,6 +143,79 @@ int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon)
 }
 EXPORT_SYMBOL(sun8i_tcon_top_de_config);
 
+/*
+ * The vendor implementation's DSI configuration path unconditionally
+ * enables this same bit (TCON_TOP_GATE_SRC_REG, TCON_TOP_TCON_DSI_GATE)
+ * for TCON0. Mainline already registers it as a standard clk gate
+ * (CLK_TCON_TOP_DSI, see sun8i_tcon_top_register_gate() above), but
+ * nothing consumes it: wiring it as dsi0's "mod" clock creates a circular
+ * dependency between TCON TOP's .bind() and dsi0's .probe() (see the DTS
+ * comment on the dsi0 node). This sets the bit directly instead, the same
+ * way sun8i_tcon_top_set_hdmi_src() and sun8i_tcon_top_de_config() above
+ * already bypass the clk-consumer graph for their own TCON TOP writes.
+ */
+int sun8i_tcon_top_set_dsi_gate(struct device *dev, bool enable)
+{
+	struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev);
+	unsigned long flags;
+	u32 val;
+
+	if (!sun8i_tcon_top_node_is_tcon_top(dev->of_node)) {
+		dev_err(dev, "Device is not TCON TOP!\n");
+		return -EINVAL;
+	}
+
+	spin_lock_irqsave(&tcon_top->reg_lock, flags);
+
+	val = readl(tcon_top->regs + TCON_TOP_GATE_SRC_REG);
+	if (enable)
+		val |= BIT(TCON_TOP_TCON_DSI_GATE);
+	else
+		val &= ~BIT(TCON_TOP_TCON_DSI_GATE);
+	writel(val, tcon_top->regs + TCON_TOP_GATE_SRC_REG);
+
+	spin_unlock_irqrestore(&tcon_top->reg_lock, flags);
+
+	return 0;
+}
+EXPORT_SYMBOL(sun8i_tcon_top_set_dsi_gate);
+
+/*
+ * TCON_TOP_PORT_SEL_REG routes each DE's output to one of the 4 TCON
+ * ports. The only existing caller of the read-modify-write helper for
+ * this register (sun8i_tcon_top_de_config()) is wired up exclusively from
+ * sun8i_r40_tcon_tv_set_mux(), which only runs for TCON quirks tables
+ * that provide a .set_mux callback -- ours doesn't, the same gap
+ * sun8i_tcon_top_set_dsi_gate() above works around for the DSI clock gate
+ * bit. DE0's live PORT_SEL field on a working reference system is 0
+ * (port 0), matching this register's reset default -- written explicitly
+ * here rather than relying on that default, since nothing in mainline
+ * otherwise reaches this register for a .set_mux-less quirks table.
+ */
+int sun8i_tcon_top_set_de0_port(struct device *dev, int port)
+{
+	struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev);
+	unsigned long flags;
+	u32 val;
+
+	if (!sun8i_tcon_top_node_is_tcon_top(dev->of_node)) {
+		dev_err(dev, "Device is not TCON TOP!\n");
+		return -EINVAL;
+	}
+
+	spin_lock_irqsave(&tcon_top->reg_lock, flags);
+
+	val = readl(tcon_top->regs + TCON_TOP_PORT_SEL_REG);
+	val &= ~TCON_TOP_PORT_DE0_MSK;
+	val |= FIELD_PREP(TCON_TOP_PORT_DE0_MSK, port);
+	writel(val, tcon_top->regs + TCON_TOP_PORT_SEL_REG);
+
+	spin_unlock_irqrestore(&tcon_top->reg_lock, flags);
+
+	return 0;
+}
+EXPORT_SYMBOL(sun8i_tcon_top_set_de0_port);
+
 
 static struct clk_hw *sun8i_tcon_top_register_gate(struct device *dev,
 						   const char *parent,
@@ -325,6 +398,23 @@ static const struct sun8i_tcon_top_quirks sun50i_h6_tcon_top_quirks = {
 	/* Nothing special */
 };
 
+/*
+ * A133 has no public register documentation for its DPSS_TOP0 block, but
+ * the vendor implementation's own register layout for it is bit-for-bit
+ * identical to this driver's existing R40/D1/H6 assumptions -- same
+ * PORT_SEL/DE0 field and GATE_SRC/DSI-TV0-TV1-HDMI gate bit positions.
+ * This IP block is genuinely present and load-bearing on real hardware:
+ * skipping it (wiring the mixer straight to tcon_lcd0) lets every
+ * component probe and bind, but every DRM atomic commit then times out
+ * waiting for vblank -- the pixel path never actually reaches the panel.
+ * The vendor implementation explicitly gates its DSI clock as part of
+ * enabling DSI output, matching this driver's has_dsi quirk. TV1/HDMI
+ * aren't used by this board (single DSI-LCD output only).
+ */
+static const struct sun8i_tcon_top_quirks sun50i_a133_tcon_top_quirks = {
+	.has_dsi	= true,
+};
+
 /* sun4i_drv uses this list to check if a device node is a TCON TOP */
 const struct of_device_id sun8i_tcon_top_of_table[] = {
 	{
@@ -339,6 +429,10 @@ const struct of_device_id sun8i_tcon_top_of_table[] = {
 		.compatible = "allwinner,sun50i-h6-tcon-top",
 		.data = &sun50i_h6_tcon_top_quirks
 	},
+	{
+		.compatible = "allwinner,sun50i-a133-tcon-top",
+		.data = &sun50i_a133_tcon_top_quirks
+	},
 	{ /* sentinel */ }
 };
 MODULE_DEVICE_TABLE(of, sun8i_tcon_top_of_table);
diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
index 2b887470a..6956439c7 100644
--- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
+++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h
@@ -46,5 +46,7 @@ extern const struct of_device_id sun8i_tcon_top_of_table[];
 
 int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon);
 int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon);
+int sun8i_tcon_top_set_dsi_gate(struct device *dev, bool enable);
+int sun8i_tcon_top_set_de0_port(struct device *dev, int port);
 
 #endif /* _SUN8I_TCON_TOP_H_ */
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 729776086..cead75470 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -748,6 +748,20 @@ config PWM_SUN4I
 	  To compile this driver as a module, choose M here: the module
 	  will be called pwm-sun4i.
 
+config PWM_SUN50I_A133
+	tristate "Allwinner A133 PWM support"
+	depends on ARCH_SUNXI || COMPILE_TEST
+	depends on HAS_IOMEM
+	help
+	  PWM driver for the Allwinner A133 (sun50iw10) PWM controller — a
+	  newer, richer IP generation than what PWM_SUN4I models (16
+	  channels grouped in pairs, per-pair dead-zone/group-sync control),
+	  with no existing mainline driver. This driver only implements
+	  plain single-channel cycle-mode output, enough for backlight use.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called pwm-sun50i-a133.
+
 config PWM_SUNPLUS
 	tristate "Sunplus PWM support"
 	depends on ARCH_SUNPLUS || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index 5630a521a..9c906e5b0 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -68,6 +68,7 @@ obj-$(CONFIG_PWM_STM32)		+= pwm-stm32.o
 obj-$(CONFIG_PWM_STM32_LP)	+= pwm-stm32-lp.o
 obj-$(CONFIG_PWM_STMPE)		+= pwm-stmpe.o
 obj-$(CONFIG_PWM_SUN4I)		+= pwm-sun4i.o
+obj-$(CONFIG_PWM_SUN50I_A133)	+= pwm-sun50i-a133.o
 obj-$(CONFIG_PWM_SUNPLUS)	+= pwm-sunplus.o
 obj-$(CONFIG_PWM_TEGRA)		+= pwm-tegra.o
 obj-$(CONFIG_PWM_TH1520)	+= pwm_th1520.o
diff --git a/drivers/pwm/pwm-sun50i-a133.c b/drivers/pwm/pwm-sun50i-a133.c
new file mode 100644
index 000000000..4201ca29d
--- /dev/null
+++ b/drivers/pwm/pwm-sun50i-a133.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for Allwinner A133 (sun50iw10) PWM Controller
+ *
+ * No mainline driver exists for this IP: it's a newer, richer generation
+ * than what drivers/pwm/pwm-sun4i.c models (16 channels grouped in pairs,
+ * per-pair dead-zone control, group-sync start, cache-loaded period/duty
+ * registers) — register layout confirmed from the real, public A133 User
+ * Manual (chapter 10.11, base 0x0300a000), which documents this block in
+ * full down to bit level (unlike the Display Engine chapter elsewhere in
+ * the same manual). This driver only implements plain single-channel PWM
+ * output (cycle mode, no capture/dead-zone/group-sync) — enough to drive
+ * a backlight, not a full port of every feature this IP has.
+ *
+ * The clock source (OSC24M or APB1, selected per-pair in PCCR01/PCCR23)
+ * is entirely internal to this IP block — CCU only exposes a bus/register
+ * access gate (CLK_BUS_PWM), no separate "mod" clock, confirmed by its
+ * absence from include/dt-bindings/clock/sun50i-a100-ccu.h. This driver
+ * hardcodes OSC24M (24MHz), matching the register field's own reset
+ * default and the only clock source needed for typical backlight rates.
+ */
+
+#include <linux/bitops.h>
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/reset.h>
+
+#define PWM_A133_OSC24M_RATE		24000000
+
+#define PWM_A133_PCCR01_REG		0x0020
+#define PWM_A133_PCCR23_REG		0x0024
+#define PWM_A133_PCCR_CLK_SRC_OSC24M	(0 << 7)
+#define PWM_A133_PCCR_CLK_SRC_MASK	GENMASK(8, 7)
+
+#define PWM_A133_PCGR_REG		0x0040
+#define PWM_A133_PCGR_BYPASS(ch)	BIT(16 + (ch))
+#define PWM_A133_PCGR_GATING(ch)	BIT(ch)
+
+#define PWM_A133_PER_REG		0x0080
+#define PWM_A133_PER_EN(ch)		BIT(ch)
+
+#define PWM_A133_CH_STRIDE		0x0020
+#define PWM_A133_PCR_REG(ch)		(0x0100 + PWM_A133_CH_STRIDE * (ch))
+#define PWM_A133_PPR_REG(ch)		(0x0104 + PWM_A133_CH_STRIDE * (ch))
+
+#define PWM_A133_PCR_MODE_CYCLE		(0 << 9)
+#define PWM_A133_PCR_ACT_STA_HIGH	BIT(8)
+#define PWM_A133_PCR_PRESCAL_K_MASK	GENMASK(7, 0)
+
+#define PWM_A133_PPR_ENTIRE_CYCLE_SHIFT	16
+#define PWM_A133_PPR_ENTIRE_CYCLE_MASK		GENMASK(31, 16)
+#define PWM_A133_PPR_ACT_CYCLE_MASK		GENMASK(15, 0)
+
+#define PWM_A133_PRESCALE_K_MAX	256
+#define PWM_A133_CYCLE_MAX	65536
+
+struct pwm_a133 {
+	void __iomem *base;
+	struct clk *bus_clk;
+	struct reset_control *rst;
+};
+
+static struct pwm_a133 *to_pwm_a133(struct pwm_chip *chip)
+{
+	return pwmchip_get_drvdata(chip);
+}
+
+static u32 pwm_a133_readl(struct pwm_a133 *pc, u32 offset)
+{
+	return readl(pc->base + offset);
+}
+
+static void pwm_a133_writel(struct pwm_a133 *pc, u32 offset, u32 val)
+{
+	writel(val, pc->base + offset);
+}
+
+static int pwm_a133_calc(u64 period_ns, u64 duty_ns, u32 *entire_cycle,
+			 u32 *act_cycle, u32 *prescale_k)
+{
+	u64 period_cycles_total, prescale;
+	u64 period_cycles, duty_cycles;
+
+	period_cycles_total = mul_u64_u32_div(period_ns, PWM_A133_OSC24M_RATE,
+					      NSEC_PER_SEC);
+	if (period_cycles_total < 1)
+		period_cycles_total = 1;
+
+	prescale = DIV_ROUND_UP_ULL(period_cycles_total, PWM_A133_CYCLE_MAX);
+	if (prescale < 1)
+		prescale = 1;
+	if (prescale > PWM_A133_PRESCALE_K_MAX)
+		return -ERANGE;
+
+	period_cycles = DIV_ROUND_CLOSEST_ULL(period_cycles_total, prescale);
+	if (period_cycles < 1)
+		period_cycles = 1;
+	if (period_cycles > PWM_A133_CYCLE_MAX)
+		period_cycles = PWM_A133_CYCLE_MAX;
+
+	duty_cycles = DIV_ROUND_CLOSEST_ULL(period_cycles * duty_ns, period_ns);
+	if (duty_cycles > period_cycles)
+		duty_cycles = period_cycles;
+
+	*prescale_k = prescale - 1;
+	*entire_cycle = period_cycles - 1;
+	*act_cycle = duty_cycles;
+
+	return 0;
+}
+
+static int pwm_a133_apply(struct pwm_chip *chip, struct pwm_device *pwm,
+			  const struct pwm_state *state)
+{
+	struct pwm_a133 *pc = to_pwm_a133(chip);
+	unsigned int ch = pwm->hwpwm;
+	u32 entire_cycle, act_cycle, prescale_k, val;
+	u32 pccr_reg = (ch < 2) ? PWM_A133_PCCR01_REG : PWM_A133_PCCR23_REG;
+	int ret;
+
+	if (!state->enabled) {
+		val = pwm_a133_readl(pc, PWM_A133_PER_REG);
+		val &= ~PWM_A133_PER_EN(ch);
+		pwm_a133_writel(pc, PWM_A133_PER_REG, val);
+		return 0;
+	}
+
+	ret = pwm_a133_calc(state->period, state->duty_cycle, &entire_cycle,
+			    &act_cycle, &prescale_k);
+	if (ret) {
+		dev_err(pwmchip_parent(chip), "period exceeds the maximum value\n");
+		return ret;
+	}
+
+	/* Clock source: OSC24M for the whole pair this channel belongs to. */
+	val = pwm_a133_readl(pc, pccr_reg);
+	val &= ~PWM_A133_PCCR_CLK_SRC_MASK;
+	val |= PWM_A133_PCCR_CLK_SRC_OSC24M;
+	pwm_a133_writel(pc, pccr_reg, val);
+
+	/* Gate the channel clock off while changing prescale/period/duty. */
+	val = pwm_a133_readl(pc, PWM_A133_PCGR_REG);
+	val &= ~(PWM_A133_PCGR_GATING(ch) | PWM_A133_PCGR_BYPASS(ch));
+	pwm_a133_writel(pc, PWM_A133_PCGR_REG, val);
+
+	val = PWM_A133_PCR_MODE_CYCLE | (prescale_k & PWM_A133_PCR_PRESCAL_K_MASK);
+	if (state->polarity == PWM_POLARITY_NORMAL)
+		val |= PWM_A133_PCR_ACT_STA_HIGH;
+	pwm_a133_writel(pc, PWM_A133_PCR_REG(ch), val);
+
+	val = (entire_cycle << PWM_A133_PPR_ENTIRE_CYCLE_SHIFT) & PWM_A133_PPR_ENTIRE_CYCLE_MASK;
+	val |= act_cycle & PWM_A133_PPR_ACT_CYCLE_MASK;
+	pwm_a133_writel(pc, PWM_A133_PPR_REG(ch), val);
+
+	/* Re-enable the channel clock now that period/duty are set. */
+	val = pwm_a133_readl(pc, PWM_A133_PCGR_REG);
+	val |= PWM_A133_PCGR_GATING(ch);
+	pwm_a133_writel(pc, PWM_A133_PCGR_REG, val);
+
+	val = pwm_a133_readl(pc, PWM_A133_PER_REG);
+	val |= PWM_A133_PER_EN(ch);
+	pwm_a133_writel(pc, PWM_A133_PER_REG, val);
+
+	return 0;
+}
+
+static int pwm_a133_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
+			      struct pwm_state *state)
+{
+	struct pwm_a133 *pc = to_pwm_a133(chip);
+	unsigned int ch = pwm->hwpwm;
+	u32 per, pcr, ppr;
+	u32 entire_cycle, act_cycle, prescale;
+
+	per = pwm_a133_readl(pc, PWM_A133_PER_REG);
+	pcr = pwm_a133_readl(pc, PWM_A133_PCR_REG(ch));
+	ppr = pwm_a133_readl(pc, PWM_A133_PPR_REG(ch));
+
+	state->enabled = !!(per & PWM_A133_PER_EN(ch));
+	state->polarity = (pcr & PWM_A133_PCR_ACT_STA_HIGH) ?
+			   PWM_POLARITY_NORMAL : PWM_POLARITY_INVERSED;
+
+	prescale = (pcr & PWM_A133_PCR_PRESCAL_K_MASK) + 1;
+	entire_cycle = ((ppr & PWM_A133_PPR_ENTIRE_CYCLE_MASK) >>
+			PWM_A133_PPR_ENTIRE_CYCLE_SHIFT) + 1;
+	act_cycle = ppr & PWM_A133_PPR_ACT_CYCLE_MASK;
+
+	state->period = DIV_ROUND_CLOSEST_ULL((u64)entire_cycle * prescale * NSEC_PER_SEC,
+					      PWM_A133_OSC24M_RATE);
+	state->duty_cycle = DIV_ROUND_CLOSEST_ULL((u64)act_cycle * prescale * NSEC_PER_SEC,
+						  PWM_A133_OSC24M_RATE);
+
+	return 0;
+}
+
+static const struct pwm_ops pwm_a133_ops = {
+	.apply = pwm_a133_apply,
+	.get_state = pwm_a133_get_state,
+};
+
+static const struct of_device_id pwm_a133_dt_ids[] = {
+	{ .compatible = "allwinner,sun50i-a133-pwm" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, pwm_a133_dt_ids);
+
+static int pwm_a133_probe(struct platform_device *pdev)
+{
+	struct pwm_chip *chip;
+	struct pwm_a133 *pc;
+	int ret;
+
+	chip = devm_pwmchip_alloc(&pdev->dev, 4, sizeof(*pc));
+	if (IS_ERR(chip))
+		return PTR_ERR(chip);
+	pc = to_pwm_a133(chip);
+
+	pc->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(pc->base))
+		return PTR_ERR(pc->base);
+
+	pc->bus_clk = devm_clk_get_enabled(&pdev->dev, "bus");
+	if (IS_ERR(pc->bus_clk))
+		return dev_err_probe(&pdev->dev, PTR_ERR(pc->bus_clk),
+				     "get bus clock failed\n");
+
+	pc->rst = devm_reset_control_get_exclusive(&pdev->dev, NULL);
+	if (IS_ERR(pc->rst))
+		return dev_err_probe(&pdev->dev, PTR_ERR(pc->rst),
+				     "get reset failed\n");
+
+	ret = reset_control_deassert(pc->rst);
+	if (ret)
+		return dev_err_probe(&pdev->dev, ret,
+				     "cannot deassert reset control\n");
+
+	chip->ops = &pwm_a133_ops;
+
+	ret = pwmchip_add(chip);
+	if (ret < 0) {
+		dev_err(&pdev->dev, "failed to add PWM chip: %d\n", ret);
+		reset_control_assert(pc->rst);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, chip);
+
+	return 0;
+}
+
+static void pwm_a133_remove(struct platform_device *pdev)
+{
+	struct pwm_chip *chip = platform_get_drvdata(pdev);
+	struct pwm_a133 *pc = to_pwm_a133(chip);
+
+	pwmchip_remove(chip);
+	reset_control_assert(pc->rst);
+}
+
+static struct platform_driver pwm_a133_driver = {
+	.driver = {
+		.name = "sun50i-a133-pwm",
+		.of_match_table = pwm_a133_dt_ids,
+	},
+	.probe = pwm_a133_probe,
+	.remove = pwm_a133_remove,
+};
+module_platform_driver(pwm_a133_driver);
+
+MODULE_DESCRIPTION("Allwinner A133 PWM driver");
+MODULE_LICENSE("GPL");
-- 
2.55.0


^ permalink raw reply related	[flat|nested] 18+ messages in thread

end of thread, other threads:[~2026-09-13  0:09 UTC | newest]

Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-12  6:02 [RFC PATCH] arm64: allwinner: add TrimUI Smart Pro (A133) board support leow149
2026-09-12  6:27 ` sashiko-bot
2026-09-12  7:57 ` [RFC PATCH v2] " leow149
2026-09-12  8:14   ` sashiko-bot
2026-09-12 10:31   ` [RFC PATCH v3] " leow149
2026-09-12 10:46     ` sashiko-bot
2026-09-12 11:43     ` [RFC PATCH v4] " leow149
2026-09-12 11:58       ` sashiko-bot
2026-09-12 21:43       ` [RFC PATCH v5] " leow149
2026-09-12 22:01         ` sashiko-bot
2026-09-12 22:24         ` [RFC PATCH v6] " leow149
2026-09-12 22:38           ` sashiko-bot
2026-09-12 23:01           ` [RFC PATCH v7] " leow149
2026-09-12 23:14             ` sashiko-bot
2026-09-12 23:32             ` [RFC PATCH v8] " leow149
2026-09-12 23:45               ` sashiko-bot
2026-09-12 23:39             ` [RFC PATCH v7] " Andre Przywara
2026-09-13  0:08               ` leow149

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).