* [PATCH 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang
` (16 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Document the Saturn display processing unit found on the SpacemiT K3 SoC.
The K3 instantiates two of them, one per display path. They share a
compatible: an instance differs only in which register window, interrupt,
clock and reset lines it takes.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
.../display/spacemit/spacemit,k3-saturn-dpu.yaml | 132 +++++++++++++++++++++
1 file changed, 132 insertions(+)
diff --git a/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-saturn-dpu.yaml b/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-saturn-dpu.yaml
new file mode 100644
index 000000000000..97ea9da4b68e
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-saturn-dpu.yaml
@@ -0,0 +1,132 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/spacemit/spacemit,k3-saturn-dpu.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SpacemiT K3 Saturn Display Processing Unit
+
+maintainers:
+ - Cody Kang <codykang.hk@gmail.com>
+
+description:
+ The Saturn DPU is the display processing unit of the SpacemiT K3 SoC. It
+ composes the display planes and drives the timing for one display path. Its
+ output goes to a DisplayPort or embedded DisplayPort controller. The K3 has
+ two instances.
+
+properties:
+ compatible:
+ const: spacemit,k3-saturn-dpu
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ items:
+ - description: Online (live display) compose path.
+ - description: Offline (writeback) compose path.
+
+ interrupt-names:
+ items:
+ - const: online
+ - const: offline
+
+ clocks:
+ items:
+ - description: Pixel clock.
+ - description: Memory clock for the compose pipeline.
+ - description: AXI bus clock.
+ - description: DSI escape clock.
+ - description: DSC clock.
+
+ clock-names:
+ items:
+ - const: pxclk
+ - const: mclk
+ - const: aclk
+ - const: escclk
+ - const: dscclk
+
+ resets:
+ items:
+ - description: Memory clock domain.
+ - description: LCD domain, shared with the downstream controller.
+ - description: DSI escape clock domain.
+ - description: AXI clock domain.
+ - description: DSC domain.
+
+ reset-names:
+ items:
+ - const: mclk
+ - const: lcd
+ - const: esc
+ - const: aclk
+ - const: dsc
+
+ ports:
+ $ref: /schemas/graph.yaml#/properties/ports
+
+ properties:
+ port@0:
+ $ref: /schemas/graph.yaml#/properties/port
+ description: Video output to the DisplayPort or eDP controller.
+
+ required:
+ - port@0
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - interrupt-names
+ - clocks
+ - clock-names
+ - resets
+ - reset-names
+ - ports
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/spacemit,k3-clocks.h>
+ #include <dt-bindings/reset/spacemit,k3-resets.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ dpu@c0340000 {
+ compatible = "spacemit,k3-saturn-dpu";
+ reg = <0x0 0xc0340000 0x0 0x54000>;
+ interrupts = <90 IRQ_TYPE_LEVEL_HIGH>,
+ <89 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "online", "offline";
+ clocks = <&syscon_apmu CLK_APMU_LCD_PXCLK>,
+ <&syscon_apmu CLK_APMU_LCD_MCLK>,
+ <&syscon_apmu CLK_APMU_DPU_ACLK>,
+ <&syscon_apmu CLK_APMU_DSI_ESC>,
+ <&syscon_apmu CLK_APMU_LCD_DSC>;
+ clock-names = "pxclk", "mclk", "aclk", "escclk", "dscclk";
+ resets = <&syscon_apmu RESET_APMU_LCD_MCLK>,
+ <&syscon_apmu RESET_APMU_LCD>,
+ <&syscon_apmu RESET_APMU_DSI_ESC>,
+ <&syscon_apmu RESET_APMU_DPU_ACLK>,
+ <&syscon_apmu RESET_APMU_LCD_DSCCLK>;
+ reset-names = "mclk", "lcd", "esc", "aclk", "dsc";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ endpoint {
+ remote-endpoint = <&edp0_in>;
+ };
+ };
+ };
+ };
+ };
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
2026-07-25 3:16 ` [PATCH 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang
` (15 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
The SpacemiT K3 DisplayPort controller drives its four transmit lanes
through an integrated Innosilicon DP PHY, one PHY per controller. Add a
binding so the controller can reference its transmit PHY and the pixel
clock the PHY's PLL sources.
The PHY's registers are interleaved with its parent controller's inside
one MMIO window, so it owns neither a reg nor a reset and is modelled as
a child of the controller.
It exposes its PLL-generated pixel clock through #clock-cells, so the
APMU pixel-clock mux can select it as a parent through the clock
framework.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
.../bindings/phy/spacemit,k3-inno-dp-phy.yaml | 48 ++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/Documentation/devicetree/bindings/phy/spacemit,k3-inno-dp-phy.yaml b/Documentation/devicetree/bindings/phy/spacemit,k3-inno-dp-phy.yaml
new file mode 100644
index 000000000000..f8f1ef372951
--- /dev/null
+++ b/Documentation/devicetree/bindings/phy/spacemit,k3-inno-dp-phy.yaml
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/phy/spacemit,k3-inno-dp-phy.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SpacemiT K3 Innosilicon DisplayPort PHY
+
+maintainers:
+ - Cody Kang <codykang.hk@gmail.com>
+
+description:
+ Innosilicon four-lane DisplayPort transmit PHY integrated in the SpacemiT
+ K3 SoC, one per DisplayPort controller. Its registers are interleaved with
+ the controller's own inside a single register window, so it is described as
+ a child of the controller and owns neither a reg nor a reset.
+
+ The PHY provides the pixel clock its PLL generates, which the APMU
+ pixel-clock mux takes as one of its inputs.
+
+properties:
+ compatible:
+ const: spacemit,k3-inno-dp-phy
+
+ "#phy-cells":
+ const: 0
+
+ "#clock-cells":
+ const: 0
+
+ clock-output-names:
+ maxItems: 1
+
+required:
+ - compatible
+ - "#phy-cells"
+ - "#clock-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ phy {
+ compatible = "spacemit,k3-inno-dp-phy";
+ #phy-cells = <0>;
+ #clock-cells = <0>;
+ clock-output-names = "edp0_pll_pxclk";
+ };
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
2026-07-25 3:16 ` [PATCH 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang
2026-07-25 3:16 ` [PATCH 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang
` (14 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Both K3 instances are the same IP and share one compatible; which one
behaves as eDP is decided by what its downstream port is wired to, not by
which register window it sits in, so the binding does not distinguish them.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
.../display/spacemit/spacemit,k3-inno-dp.yaml | 140 +++++++++++++++++++++
1 file changed, 140 insertions(+)
diff --git a/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-inno-dp.yaml b/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-inno-dp.yaml
new file mode 100644
index 000000000000..865ea310daaf
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/spacemit/spacemit,k3-inno-dp.yaml
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/spacemit/spacemit,k3-inno-dp.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SpacemiT K3 Innosilicon DisplayPort / embedded DisplayPort Controller
+
+maintainers:
+ - Cody Kang <codykang.hk@gmail.com>
+
+description:
+ Innosilicon DisplayPort transmit controller integrated in the SpacemiT K3
+ SoC. The K3 has two instances of the same block sharing one compatible;
+ each acts as embedded or external DisplayPort depending on whether its
+ output port drives a panel or a connector.
+
+properties:
+ compatible:
+ const: spacemit,k3-inno-dp
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+ description: Link and hot-plug events.
+
+ clocks:
+ items:
+ - description: Pixel clock for the controller.
+ - description:
+ Pixel clock generated by the DP PHY PLL; the pixel clock above is
+ parented onto it in use.
+
+ clock-names:
+ items:
+ - const: pxclk
+ - const: pll
+
+ resets:
+ maxItems: 1
+ description: Controller-wide reset signal.
+
+ phys:
+ maxItems: 1
+ description: Phandle to the K3 Innosilicon DP PHY.
+
+ phy-names:
+ items:
+ - const: dp
+
+ ports:
+ $ref: /schemas/graph.yaml#/properties/ports
+
+ properties:
+ port@0:
+ $ref: /schemas/graph.yaml#/properties/port
+ description: Video input from the DPU.
+
+ port@1:
+ $ref: /schemas/graph.yaml#/properties/port
+ description: Output to the panel or to the DisplayPort connector.
+
+ required:
+ - port@0
+
+ aux-bus:
+ $ref: /schemas/display/dp-aux-bus.yaml#
+ description:
+ The DisplayPort AUX bus. Present when the output drives an embedded
+ panel, which is described as a child of this node and identifies itself
+ over AUX. Absent when the output is a connector.
+
+ phy:
+ $ref: /schemas/phy/spacemit,k3-inno-dp-phy.yaml#
+ description:
+ The DisplayPort PHY. Its registers live inside this controller's
+ register window, so it is described here as a child node.
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+ - phys
+ - phy-names
+ - ports
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/spacemit,k3-clocks.h>
+ #include <dt-bindings/reset/spacemit,k3-resets.h>
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ soc {
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ edp@cac84000 {
+ compatible = "spacemit,k3-inno-dp";
+ reg = <0x0 0xcac84000 0x0 0x4000>;
+ interrupts = <132 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&syscon_apmu CLK_APMU_EDP0_PXCLK>, <&edp0_phy>;
+ clock-names = "pxclk", "pll";
+ resets = <&syscon_apmu RESET_APMU_EDP0>;
+ phys = <&edp0_phy>;
+ phy-names = "dp";
+
+ edp0_phy: phy {
+ compatible = "spacemit,k3-inno-dp-phy";
+ #phy-cells = <0>;
+ #clock-cells = <0>;
+ clock-output-names = "edp0_pll_pxclk";
+ };
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ endpoint {
+ remote-endpoint = <&dpu0_out>;
+ };
+ };
+
+ port@1 {
+ reg = <1>;
+ endpoint {
+ remote-endpoint = <&panel_in>;
+ };
+ };
+ };
+ };
+ };
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (2 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:16 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang
` (13 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
The APMU's eDP pixel-clock muxes select the DP PHY PLLs as parents, so the
K3 syscon takes two clock inputs the other variants do not have.
Gate them behind the K3 compatible instead of widening the shared clock
list, so a non-K3 syscon cannot silently accept clocks it has no mux for.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
.../bindings/soc/spacemit/spacemit,k1-syscon.yaml | 30 +++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml b/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
index d3a7c93c3c54..b8fc6632a392 100644
--- a/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
+++ b/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
@@ -31,14 +31,18 @@ properties:
maxItems: 1
clocks:
- maxItems: 4
+ minItems: 4
+ maxItems: 6
clock-names:
+ minItems: 4
items:
- const: osc
- const: vctcxo_1m
- const: vctcxo_3m
- const: vctcxo_24m
+ - const: edp0_pll_pxclk
+ - const: edp1_pll_pxclk
"#clock-cells":
const: 1
@@ -95,6 +99,30 @@ allOf:
- clocks
- clock-names
- "#clock-cells"
+ # Only the K3 APMU exposes the two eDP/DP PHY PLL pixel clocks as extra
+ # inputs (parents of its eDP pixel-clock muxes); every other variant has
+ # the four base oscillator inputs only.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: spacemit,k3-syscon-apmu
+ then:
+ properties:
+ clocks:
+ minItems: 4
+ maxItems: 6
+ clock-names:
+ minItems: 4
+ maxItems: 6
+ else:
+ properties:
+ clocks:
+ minItems: 4
+ maxItems: 4
+ clock-names:
+ minItems: 4
+ maxItems: 4
additionalProperties: false
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
2026-07-25 3:16 ` [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang
@ 2026-07-25 5:16 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:16 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, vkoul, neil.armstrong, robh, devicetree, olteanv,
conor+dt, linux-phy
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The newly added clock names in the device tree binding do not match the `fw_name` expected by the clock driver, breaking parent clock resolution.
--
--- Patch [4]: [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU ---
commit 4027b4adb6d5e7a26089ca7f70a25150faf1098a
Author: Cody Kang <codykang.hk@gmail.com>
dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
The APMU's eDP pixel-clock muxes select the DP PHY PLLs as parents, so the
K3 syscon takes two clock inputs the other variants do not have.
> diff --git a/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml b/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
> index d3a7c93c3c54f..b8fc6632a3928 100644
> --- a/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
> +++ b/Documentation/devicetree/bindings/soc/spacemit/spacemit,k1-syscon.yaml
[ ... ]
> @@ -31,14 +31,18 @@ properties:
> maxItems: 1
>
> clocks:
> - maxItems: 4
> + minItems: 4
> + maxItems: 6
>
> clock-names:
> + minItems: 4
> items:
> - const: osc
> - const: vctcxo_1m
> - const: vctcxo_3m
> - const: vctcxo_24m
> + - const: edp0_pll_pxclk
> + - const: edp1_pll_pxclk
[Severity: High]
Will this mismatch with the clock driver's expected firmware names?
Looking at edp0_pclk_parents in drivers/clk/spacemit/ccu-k3.c, the driver
hardcodes the expected device tree string as "external_clk":
drivers/clk/spacemit/ccu-k3.c:edp0_pclk_parents[] = {
...
CCU_PARENT_NAME(external_clk),
...
};
Since this binding strictly mandates "edp0_pll_pxclk" and "edp1_pll_pxclk"
instead of "external_clk", the Common Clock Framework will search the
clock-names array for "external_clk" and fail to resolve the parent clocks
for the eDP pixel clock muxes.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=4
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (3 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:16 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang
` (12 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
The PHY's registers are interleaved into its parent DP controller's MMIO
window rather than a window of its own, so it probes as a child of the
controller, carries no reg or reset, and reaches its registers through
the controller's regmap.
It registers its pixel PLL as a clock provider. That lets the display
controller ask for a mode's pixel rate through clk_set_rate(), and it
lets the APMU pixel-clock mux parent to the PHY through the clock
framework. Without it the controller and PHY would have to know each
other's registers.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/phy/spacemit/Kconfig | 13 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k3-inno-dp.c | 948 ++++++++++++++++++++++++++++++++++
3 files changed, 962 insertions(+)
diff --git a/drivers/phy/spacemit/Kconfig b/drivers/phy/spacemit/Kconfig
index 50b0005acf66..098f33998f73 100644
--- a/drivers/phy/spacemit/Kconfig
+++ b/drivers/phy/spacemit/Kconfig
@@ -23,3 +23,16 @@ config PHY_SPACEMIT_K1_USB2
help
Enable this to support K1 USB 2.0 PHY driver. This driver takes care of
enabling and clock setup and will be used by K1 udc/ehci/otg/xhci driver.
+
+config PHY_SPACEMIT_K3_INNO_DP
+ tristate "SpacemiT K3 Innosilicon DisplayPort PHY driver"
+ depends on (ARCH_SPACEMIT || COMPILE_TEST) && OF
+ depends on COMMON_CLK
+ select GENERIC_PHY
+ select REGMAP
+ help
+ Enable support for the Innosilicon DisplayPort transmit PHY
+ integrated in the SpacemiT K3 SoC. The PHY shares its MMIO window
+ with its parent DP/eDP controller (drivers/gpu/drm/spacemit/) and
+ exposes lane / rate / vswing / pre-emphasis control through the
+ generic PHY framework.
diff --git a/drivers/phy/spacemit/Makefile b/drivers/phy/spacemit/Makefile
index a821a21d6142..960c5827f5e5 100644
--- a/drivers/phy/spacemit/Makefile
+++ b/drivers/phy/spacemit/Makefile
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_PHY_SPACEMIT_K1_PCIE) += phy-k1-pcie.o
obj-$(CONFIG_PHY_SPACEMIT_K1_USB2) += phy-k1-usb2.o
+obj-$(CONFIG_PHY_SPACEMIT_K3_INNO_DP) += phy-k3-inno-dp.o
diff --git a/drivers/phy/spacemit/phy-k3-inno-dp.c b/drivers/phy/spacemit/phy-k3-inno-dp.c
new file mode 100644
index 000000000000..9dd62edb7a5a
--- /dev/null
+++ b/drivers/phy/spacemit/phy-k3-inno-dp.c
@@ -0,0 +1,948 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SpacemiT K3 Innosilicon DisplayPort PHY driver.
+ *
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/clk-provider.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+/* Offsets are into the parent controller's MMIO window */
+#define DPTX_PHY_CTRL 0x0100
+#define DPTX_PHY_CTRL_XMIT_EN GENMASK(20, 17)
+#define DPTX_PHY_CTRL_NUM_LANES GENMASK(6, 5)
+#define DPTX_PHY_CTRL_RATE GENMASK(1, 0)
+
+/* One 6-bit slot per lane; only the low 4 bits of each slot are used. */
+#define DPTX_PHY_LANE_TRIM 0x0104
+#define DPTX_PHY_LANE_BITS_PER 6
+#define DPTX_PHY_LANE_FIELD_MASK 0xf /* preemp[1:0] | vswing[1:0] */
+#define DPTX_PHY_LANE_VSWING_SHIFT 2 /* within the lane field */
+
+/* MPLL is the link PLL (lane rate) */
+#define DPTX_ANA_MPLL 0x0180
+#define DPTX_ANA_MPLL_FBDIV_LBIT GENMASK(31, 24)
+#define DPTX_ANA_MPLL_FBDIV_HBIT GENMASK(19, 16)
+#define DPTX_ANA_MPLL_PREDIV GENMASK(13, 8)
+#define DPTX_ANA_MPLL_LOCKED BIT(7)
+#define DPTX_ANA_MPLL_DACPD BIT(5)
+#define DPTX_ANA_MPLL_DSMPD BIT(4)
+#define DPTX_ANA_MPLL_PD BIT(0)
+
+#define DPTX_ANA_MPLL_FRAC 0x0184
+#define DPTX_ANA_MPLL_FRAC_LBIT GENMASK(23, 16)
+#define DPTX_ANA_MPLL_FRAC_MBIT GENMASK(15, 8)
+#define DPTX_ANA_MPLL_FRAC_HBIT GENMASK(7, 0)
+
+#define DPTX_ANA_MPLL_DIV 0x0188
+#define DPTX_ANA_MPLL_VCOCLK_DIV8_EN BIT(16)
+#define DPTX_ANA_MPLL_POSTDIVEN BIT(11)
+#define DPTX_ANA_MPLL_POSTDIV GENMASK(10, 8)
+
+/* PREPLL is the pixel PLL exposed as a clock. */
+#define DPTX_ANA_PREPLL 0x0190
+#define DPTX_ANA_PREPLL_FBDIV2_LBIT GENMASK(31, 24)
+#define DPTX_ANA_PREPLL_FBDIV2_HBIT GENMASK(19, 16)
+#define DPTX_ANA_PREPLL_PREDIV GENMASK(13, 8)
+#define DPTX_ANA_PREPLL_LOCKED BIT(7)
+#define DPTX_ANA_PREPLL_DACPD BIT(5)
+#define DPTX_ANA_PREPLL_DSMPD BIT(4)
+#define DPTX_ANA_PREPLL_PCLK_NORMAL BIT(1)
+#define DPTX_ANA_PREPLL_PD BIT(0)
+
+#define DPTX_ANA_PREPLL_DIV 0x0194
+#define DPTX_ANA_PREPLL_PRECLK_DIVM GENMASK(17, 16)
+#define DPTX_ANA_PREPLL_PRECLK_DIVAUX GENMASK(12, 8)
+
+#define DPTX_ANA_PREPLL_CTRL 0x0198
+#define DPTX_ANA_PREPLL_PCLKDIV5_EN BIT(31)
+#define DPTX_ANA_PREPLL_PCLK_DIVAUX GENMASK(28, 24)
+#define DPTX_ANA_PREPLL_LOCK_BYPEN BIT(17)
+#define DPTX_ANA_PREPLL_HDMI_EN BIT(9)
+#define DPTX_ANA_PREPLL_DP_EN BIT(8)
+
+#define DPTX_ANA_PREPLL_FRAC 0x019c
+#define DPTX_ANA_PREPLL_FRAC2_LBIT GENMASK(23, 16)
+#define DPTX_ANA_PREPLL_FRAC2_MBIT GENMASK(15, 8)
+#define DPTX_ANA_PREPLL_FRAC2_HBIT GENMASK(7, 0)
+
+#define DPTX_ANA_TX_CTRL 0x01a0
+#define DPTX_ANA_MPLL_CLKDIV_16M GENMASK(13, 8)
+
+/* PRECLK_DIVM register value -> divide factor */
+static const u8 k3_inno_dp_divm_factors[] = { 1, 2, 3, 5 };
+
+#define DPTX_VCO_MIN_KHZ 1000000
+#define DPTX_VCO_MAX_KHZ 3000000
+#define DPTX_PLL_FRAC_MOD (1 << 24)
+#define DPTX_PLL_ERR_TOLERANCE 10
+
+struct k3_inno_dp_phy {
+ struct device *dev;
+ struct regmap *regmap;
+ struct phy *phy;
+ u8 lanes; /* tracks last set_lanes */
+
+ struct clk_hw pxclk_hw; /* the PREPLL's pixel clock */
+ unsigned long pxclk_rate; /* last programmed PREPLL rate */
+ u32 ref_clk_khz; /* PLL reference */
+};
+
+static int k3_inno_dp_phy_power_on(struct phy *phy)
+{
+ struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+ u32 lane_en;
+ u32 val;
+ int ret;
+
+ switch (p->lanes) {
+ case 1:
+ lane_en = 0x1;
+ break;
+ case 2:
+ lane_en = 0x3;
+ break;
+ case 4:
+ default:
+ lane_en = 0xf;
+ break;
+ }
+
+ ret = regmap_write_bits(p->regmap, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD, 0);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+ DPTX_ANA_PREPLL_PD, 0);
+ if (ret)
+ return ret;
+ usleep_range(2000, 4000);
+
+ ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+ DPTX_PHY_CTRL_XMIT_EN,
+ FIELD_PREP(DPTX_PHY_CTRL_XMIT_EN, lane_en));
+ if (ret)
+ return ret;
+ usleep_range(2000, 4000);
+
+ ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_MPLL, val,
+ val & DPTX_ANA_MPLL_LOCKED,
+ 2000, 10 * 1000);
+ if (ret) {
+ dev_err(p->dev, "DP PHY core PLL lock timed out\n");
+ return ret;
+ }
+
+ ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_PREPLL, val,
+ val & DPTX_ANA_PREPLL_LOCKED,
+ 2000, 10 * 1000);
+ if (ret)
+ dev_err(p->dev, "DP PHY pixel PLL lock timed out\n");
+
+ return ret;
+}
+
+static int k3_inno_dp_phy_power_off(struct phy *phy)
+{
+ struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+
+ regmap_write_bits(p->regmap, DPTX_PHY_CTRL, DPTX_PHY_CTRL_XMIT_EN, 0);
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+ DPTX_ANA_PREPLL_PD);
+ usleep_range(2000, 4000);
+
+ return 0;
+}
+
+static int k3_inno_dp_phy_set_lanes(struct k3_inno_dp_phy *p, u8 lanes)
+{
+ u32 val;
+ int ret;
+
+ switch (lanes) {
+ case 1:
+ val = 0;
+ break;
+ case 2:
+ val = 1;
+ break;
+ case 4:
+ val = 2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+ DPTX_PHY_CTRL_NUM_LANES,
+ FIELD_PREP(DPTX_PHY_CTRL_NUM_LANES, val));
+ if (ret)
+ return ret;
+
+ p->lanes = lanes;
+ return 0;
+}
+
+struct k3_inno_dp_mpll_cfg {
+ unsigned int link_rate; /* Mbps per lane */
+ u8 prediv;
+ u16 fbdiv;
+ u32 frac;
+ u8 postdiv;
+ u8 postdiv_en;
+ u8 frac_pd;
+ u8 vcoclk_div8_en;
+ u8 clk_16mdiv;
+};
+
+static const struct k3_inno_dp_mpll_cfg k3_inno_dp_mpll_cfgs[] = {
+ {
+ .link_rate = 1620,
+ .prediv = 0x02,
+ .fbdiv = 0x87,
+ .frac = 0,
+ .postdiv = 0,
+ .postdiv_en = 1,
+ .frac_pd = 0x3,
+ .vcoclk_div8_en = 1,
+ .clk_16mdiv = 12,
+ }, {
+ .link_rate = 2700,
+ .prediv = 0x02,
+ .fbdiv = 0xe1,
+ .frac = 0,
+ .postdiv = 0,
+ .postdiv_en = 1,
+ .frac_pd = 0x3,
+ .vcoclk_div8_en = 1,
+ .clk_16mdiv = 21,
+ }, {
+ .link_rate = 5400,
+ .prediv = 0x02,
+ .fbdiv = 0xe1,
+ .frac = 0,
+ .postdiv = 0,
+ .postdiv_en = 0,
+ .frac_pd = 0x3,
+ .vcoclk_div8_en = 0,
+ .clk_16mdiv = 42,
+ },
+};
+
+static void k3_inno_dp_phy_program_mpll(struct k3_inno_dp_phy *p,
+ const struct k3_inno_dp_mpll_cfg *cfg)
+{
+ struct regmap *rm = p->regmap;
+
+ /*
+ * Shared with an unwired HDMI path: select DP first, otherwise the
+ * PLLs still lock but nothing reaches the lanes and AUX times out.
+ */
+ regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_DP_EN,
+ DPTX_ANA_PREPLL_DP_EN);
+ regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_HDMI_EN, 0);
+
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD,
+ FIELD_PREP(DPTX_ANA_MPLL_PD, 1));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PREDIV,
+ FIELD_PREP(DPTX_ANA_MPLL_PREDIV, cfg->prediv));
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_FBDIV_LBIT,
+ FIELD_PREP(DPTX_ANA_MPLL_FBDIV_LBIT, cfg->fbdiv & 0xff));
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_FBDIV_HBIT,
+ FIELD_PREP(DPTX_ANA_MPLL_FBDIV_HBIT, (cfg->fbdiv >> 8) & 0xf));
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_DACPD,
+ FIELD_PREP(DPTX_ANA_MPLL_DACPD, (cfg->frac_pd >> 1) & 0x1));
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_DSMPD,
+ FIELD_PREP(DPTX_ANA_MPLL_DSMPD, cfg->frac_pd & 0x1));
+
+ regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_LBIT,
+ FIELD_PREP(DPTX_ANA_MPLL_FRAC_LBIT, cfg->frac & 0xff));
+ regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_MBIT,
+ FIELD_PREP(DPTX_ANA_MPLL_FRAC_MBIT, (cfg->frac >> 8) & 0xff));
+ regmap_write_bits(rm, DPTX_ANA_MPLL_FRAC, DPTX_ANA_MPLL_FRAC_HBIT,
+ FIELD_PREP(DPTX_ANA_MPLL_FRAC_HBIT, (cfg->frac >> 16) & 0xff));
+
+ regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_POSTDIV,
+ FIELD_PREP(DPTX_ANA_MPLL_POSTDIV, cfg->postdiv));
+ regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_POSTDIVEN,
+ FIELD_PREP(DPTX_ANA_MPLL_POSTDIVEN, cfg->postdiv_en));
+ regmap_write_bits(rm, DPTX_ANA_MPLL_DIV, DPTX_ANA_MPLL_VCOCLK_DIV8_EN,
+ FIELD_PREP(DPTX_ANA_MPLL_VCOCLK_DIV8_EN, cfg->vcoclk_div8_en));
+
+ regmap_write_bits(rm, DPTX_ANA_TX_CTRL, DPTX_ANA_MPLL_CLKDIV_16M,
+ FIELD_PREP(DPTX_ANA_MPLL_CLKDIV_16M, cfg->clk_16mdiv));
+
+ regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_LOCK_BYPEN,
+ FIELD_PREP(DPTX_ANA_PREPLL_LOCK_BYPEN, 1));
+
+ regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD,
+ FIELD_PREP(DPTX_ANA_MPLL_PD, 0));
+ usleep_range(2000, 4000);
+}
+
+static int k3_inno_dp_phy_set_rate(struct k3_inno_dp_phy *p,
+ unsigned int link_rate)
+{
+ const struct k3_inno_dp_mpll_cfg *cfg = NULL;
+ unsigned int i;
+ u32 sel;
+
+ switch (link_rate) {
+ case 1620:
+ sel = 0;
+ break;
+ case 2700:
+ sel = 1;
+ break;
+ case 5400:
+ sel = 2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(k3_inno_dp_mpll_cfgs); i++) {
+ if (k3_inno_dp_mpll_cfgs[i].link_rate == link_rate) {
+ cfg = &k3_inno_dp_mpll_cfgs[i];
+ break;
+ }
+ }
+ if (!cfg)
+ return -EINVAL;
+
+ k3_inno_dp_phy_program_mpll(p, cfg);
+
+ return regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
+ DPTX_PHY_CTRL_RATE,
+ FIELD_PREP(DPTX_PHY_CTRL_RATE, sel));
+}
+
+static int k3_inno_dp_phy_set_voltages(struct k3_inno_dp_phy *p,
+ struct phy_configure_opts_dp *opts)
+{
+ u8 lane;
+ int ret;
+
+ for (lane = 0; lane < opts->lanes; lane++) {
+ unsigned int shift = lane * DPTX_PHY_LANE_BITS_PER;
+ u32 mask = DPTX_PHY_LANE_FIELD_MASK << shift;
+ u32 val = (((opts->voltage[lane] & 0x3) << DPTX_PHY_LANE_VSWING_SHIFT) |
+ (opts->pre[lane] & 0x3)) << shift;
+
+ ret = regmap_write_bits(p->regmap, DPTX_PHY_LANE_TRIM, mask,
+ val);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int k3_inno_dp_phy_configure(struct phy *phy,
+ union phy_configure_opts *opts)
+{
+ struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
+ int ret;
+
+ if (opts->dp.set_lanes) {
+ ret = k3_inno_dp_phy_set_lanes(p, opts->dp.lanes);
+ if (ret)
+ return ret;
+ }
+
+ if (opts->dp.set_rate) {
+ ret = k3_inno_dp_phy_set_rate(p, opts->dp.link_rate);
+ if (ret)
+ return ret;
+ }
+
+ if (opts->dp.set_voltages) {
+ ret = k3_inno_dp_phy_set_voltages(p, &opts->dp);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static u32 k3_inno_dp_div64(u64 *n, u32 base)
+{
+ return do_div(*n, base);
+}
+
+static int k3_inno_dp_better_cfg(bool new_valid, bool new_is_int, u8 new_pre, u32 new_vco,
+ bool best_valid, bool best_is_int, u8 best_pre, u32 best_vco)
+{
+ if (!new_valid)
+ return 0;
+ if (!best_valid)
+ return 1;
+
+ if (new_is_int && !best_is_int)
+ return 1;
+ if (!new_is_int && best_is_int)
+ return 0;
+
+ if (new_pre < best_pre)
+ return 1;
+ if (new_pre > best_pre)
+ return 0;
+
+ if (new_vco > best_vco)
+ return 1;
+
+ return 0;
+}
+
+struct k3_inno_dp_prepll_cfg {
+ u32 target_pclk_khz;
+ u32 vco_freq_khz;
+ u8 prediv;
+ u16 fbdiv;
+ u32 frac_pd;
+ u32 frac;
+ u8 div5_en;
+ u8 divm;
+ u8 divaux;
+ u8 divp;
+ u32 actual_pclk_khz;
+ bool valid;
+};
+
+static const struct k3_inno_dp_prepll_cfg k3_inno_dp_prepll_cfgs[] = {
+ { 614400, 2460000, 0x05, 512, 0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 614400, true },
+ { 594000, 2376000, 0x01, 99, 0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 594000, true },
+ { 551040, 2760000, 0x05, 574, 0x3, 0x0, 0x1, 0x0, 0x00, 0x0, 551040, true },
+ { 533250, 2130000, 0x08, 711, 0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 533250, true },
+ { 443250, 1770000, 0x08, 591, 0x3, 0x0, 0x0, 0x1, 0x01, 0x1, 443250, true },
+ { 375000, 3000000, 0x01, 125, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 375000, true },
+ { 372000, 2980000, 0x01, 124, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 372000, true },
+ { 348500, 2790000, 0x06, 697, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 348500, true },
+ { 307200, 1540000, 0x01, 64, 0x3, 0x0, 0x1, 0x0, 0x00, 0x0, 307200, true },
+ { 297000, 2376000, 0x01, 99, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 297000, true },
+ { 280000, 1680000, 0x01, 70, 0x3, 0x0, 0x0, 0x2, 0x01, 0x1, 280000, true },
+ { 277440, 2770000, 0x05, 578, 0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 277440, true },
+ { 245760, 2457600, 0x05, 512, 0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 245760, true },
+ { 241500, 1932000, 0x02, 161, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 241500, true },
+ { 236000, 2830000, 0x01, 118, 0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 236000, true },
+ { 204800, 2048000, 0x03, 256, 0x3, 0x0, 0x0, 0x3, 0x01, 0x1, 204800, true },
+ { 193250, 2320000, 0x08, 773, 0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 193250, true },
+ { 189000, 1510000, 0x01, 63, 0x3, 0x0, 0x0, 0x0, 0x04, 0x1, 189000, true },
+ { 187500, 3000000, 0x01, 125, 0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 187500, true },
+ { 162000, 2592000, 0x01, 108, 0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 162000, true },
+ { 156000, 2810000, 0x01, 117, 0x3, 0x0, 0x0, 0x0, 0x09, 0x1, 156000, true },
+ { 150000, 3000000, 0x01, 125, 0x3, 0x0, 0x0, 0x0, 0x0a, 0x1, 150000, true },
+ { 148500, 2376000, 0x01, 99, 0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 148500, true },
+ { 146000, 1750000, 0x01, 73, 0x3, 0x0, 0x0, 0x0, 0x06, 0x1, 146000, true },
+ { 142860, 2860000, 0x14, 2381, 0x3, 0x0, 0x0, 0x0, 0x0a, 0x1, 142860, true },
+ { 140000, 2520000, 0x01, 105, 0x3, 0x0, 0x0, 0x0, 0x09, 0x1, 140000, true },
+ { 138500, 2220000, 0x03, 277, 0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 138500, true },
+ { 122000, 2930000, 0x01, 122, 0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 122000, true },
+ { 121750, 2920000, 0x04, 487, 0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 121750, true },
+ { 108000, 2810000, 0x01, 117, 0x3, 0x0, 0x0, 0x0, 0x0d, 0x1, 108000, true },
+ { 106500, 1700000, 0x01, 71, 0x3, 0x0, 0x0, 0x0, 0x08, 0x1, 106500, true },
+ { 83500, 2000000, 0x02, 167, 0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 83500, true },
+ { 79500, 2540000, 0x01, 106, 0x3, 0x0, 0x0, 0x0, 0x10, 0x1, 79500, true },
+ { 75000, 3000000, 0x01, 125, 0x3, 0x0, 0x0, 0x0, 0x14, 0x1, 75000, true },
+ { 74250, 2376000, 0x01, 99, 0x3, 0x0, 0x0, 0x0, 0x10, 0x1, 74250, true },
+ { 65000, 1560000, 0x01, 65, 0x3, 0x0, 0x0, 0x0, 0x0c, 0x1, 65000, true },
+ { 40000, 2880000, 0x01, 120, 0x3, 0x0, 0x0, 0x0, 0x12, 0x2, 40000, true },
+ { 27000, 2810000, 0x01, 117, 0x3, 0x0, 0x0, 0x0, 0x1a, 0x2, 27000, true },
+ { 25600, 2300000, 0x01, 96, 0x3, 0x0, 0x0, 0x0, 0x0f, 0x3, 25600, true },
+ { 25200, 2520000, 0x01, 105, 0x3, 0x0, 0x0, 0x0, 0x19, 0x2, 25200, true },
+};
+
+static u64 k3_inno_dp_abs_diff(u64 a, u64 b)
+{
+ return (a > b) ? (a - b) : (b - a);
+}
+
+static u32 k3_inno_dp_pll_rate_khz(u8 pre, u16 fb, u32 frac,
+ u32 ref_clk_khz, u32 total_div)
+{
+ u64 vco_hz;
+ u64 ref_hz = (u64)ref_clk_khz * 1000;
+ u64 int_part;
+ u64 frac_part;
+
+ /* Integer part: (Ref * FB) / Pre */
+ int_part = ref_hz * fb;
+ int_part += (pre / 2);
+ k3_inno_dp_div64(&int_part, pre);
+
+ /* Fractional part: (Ref * Frac) / (Pre * 2^24) */
+ frac_part = ref_hz * frac;
+
+ frac_part += (pre / 2);
+ k3_inno_dp_div64(&frac_part, pre);
+
+ frac_part += (DPTX_PLL_FRAC_MOD / 2);
+ k3_inno_dp_div64(&frac_part, DPTX_PLL_FRAC_MOD);
+
+ vco_hz = int_part + frac_part;
+
+ /* Final Rate = VCO / total_div */
+ vco_hz += (total_div / 2);
+ k3_inno_dp_div64(&vco_hz, total_div);
+
+ vco_hz += 500;
+ k3_inno_dp_div64(&vco_hz, 1000);
+
+ return (u32)vco_hz;
+}
+
+static int k3_inno_dp_prepll_div_total(const struct k3_inno_dp_prepll_cfg *cfg,
+ u32 *div_total)
+{
+ if (!cfg || !div_total || !cfg->valid)
+ return -EINVAL;
+
+ /* div5_en fixes div_total at 5; divp is unused on this path */
+ if (cfg->div5_en) {
+ *div_total = 5;
+ return 0;
+ }
+
+ if (!cfg->divp)
+ return -EINVAL;
+
+ if (cfg->divaux == 1) {
+ if (cfg->divm >= ARRAY_SIZE(k3_inno_dp_divm_factors))
+ return -EINVAL;
+
+ *div_total = 2 * k3_inno_dp_divm_factors[cfg->divm] * cfg->divp;
+ return 0;
+ }
+
+ if (cfg->divaux < 2)
+ return -EINVAL;
+
+ *div_total = 2 * cfg->divaux * cfg->divp;
+ return 0;
+}
+
+static bool k3_inno_dp_prepll_cfg_matches(const struct k3_inno_dp_prepll_cfg *cfg,
+ u32 target_pclk_khz,
+ u32 ref_clk_khz)
+{
+ u32 div_total;
+ u32 actual_pclk_khz;
+
+ if (k3_inno_dp_prepll_div_total(cfg, &div_total))
+ return false;
+
+ actual_pclk_khz = k3_inno_dp_pll_rate_khz(cfg->prediv, cfg->fbdiv,
+ cfg->frac, ref_clk_khz,
+ div_total);
+
+ if (cfg->actual_pclk_khz &&
+ k3_inno_dp_abs_diff(actual_pclk_khz,
+ cfg->actual_pclk_khz) > DPTX_PLL_ERR_TOLERANCE)
+ return false;
+
+ return k3_inno_dp_abs_diff(actual_pclk_khz,
+ target_pclk_khz) <= DPTX_PLL_ERR_TOLERANCE;
+}
+
+static const struct k3_inno_dp_prepll_cfg *k3_inno_dp_find_prepll_cfg(u32 pclk_khz,
+ u32 ref_clk_khz)
+{
+ const struct k3_inno_dp_prepll_cfg *best_match = NULL;
+ u32 min_diff = 0xFFFFFFFF;
+ int num_configs = ARRAY_SIZE(k3_inno_dp_prepll_cfgs);
+
+ for (int i = 0; i < num_configs; i++) {
+ u32 current_target = k3_inno_dp_prepll_cfgs[i].target_pclk_khz;
+ u32 diff = k3_inno_dp_abs_diff(pclk_khz, current_target);
+
+ if (diff == 0 && k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+ pclk_khz, ref_clk_khz)) {
+ return &k3_inno_dp_prepll_cfgs[i];
+ }
+
+ if ((pclk_khz / 100) == (current_target / 100) &&
+ k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+ pclk_khz, ref_clk_khz)) {
+ return &k3_inno_dp_prepll_cfgs[i];
+ }
+
+ if (diff < min_diff && diff < 500 &&
+ k3_inno_dp_prepll_cfg_matches(&k3_inno_dp_prepll_cfgs[i],
+ pclk_khz, ref_clk_khz)) {
+ min_diff = diff;
+ best_match = &k3_inno_dp_prepll_cfgs[i];
+ }
+ }
+
+ return best_match;
+}
+
+static int k3_inno_dp_solve_frac(u32 target_vco_khz, u32 ref_clk_khz,
+ u8 *best_pre, u16 *best_fb, u32 *best_frac)
+{
+ u64 min_err = ~0ULL;
+ int found = 0;
+ bool best_is_int = false;
+ int pre;
+
+ for (pre = 1; pre <= 63; pre++) {
+ u64 ref_clk_hz = (u64)ref_clk_khz * 1000;
+ u64 target_vco_hz = (u64)target_vco_khz * 1000;
+
+ /*
+ * Calculate required multiplier: Mult = (TargetVCO * Pre) / Ref
+ */
+ u64 num = target_vco_hz * pre;
+ u64 den = ref_clk_hz;
+ u64 remainder;
+ u64 fb_val;
+ u64 frac_val;
+ u64 actual_vco;
+ u64 diff;
+ bool current_is_int;
+
+ fb_val = num;
+ remainder = k3_inno_dp_div64(&fb_val, (u32)den);
+
+ if (fb_val > 4095)
+ continue;
+
+ /* Frac = (Remainder * 2^24 + Ref/2) / Ref */
+ frac_val = remainder * DPTX_PLL_FRAC_MOD;
+ frac_val += (den / 2);
+ k3_inno_dp_div64(&frac_val, (u32)den);
+
+ if (frac_val > 0xFFFFFF)
+ frac_val = 0xFFFFFF;
+
+ /* VCO = (Ref * FB / Pre) + (Ref * Frac / (Pre * 2^24)) */
+ {
+ u64 vco_int, vco_frac;
+
+ vco_int = ref_clk_hz * fb_val;
+ k3_inno_dp_div64(&vco_int, pre);
+ vco_frac = ref_clk_hz * frac_val;
+ k3_inno_dp_div64(&vco_frac, pre);
+ k3_inno_dp_div64(&vco_frac, DPTX_PLL_FRAC_MOD);
+
+ actual_vco = vco_int + vco_frac;
+ }
+
+ diff = k3_inno_dp_abs_diff(actual_vco, target_vco_hz);
+ current_is_int = (frac_val == 0);
+
+ if (diff < min_err) {
+ min_err = diff;
+ *best_pre = pre;
+ *best_fb = (u16)fb_val;
+ *best_frac = (u32)frac_val;
+ best_is_int = current_is_int;
+ found = 1;
+ } else if (diff == min_err) {
+ if (current_is_int && !best_is_int) {
+ *best_pre = pre;
+ *best_fb = (u16)fb_val;
+ *best_frac = (u32)frac_val;
+ best_is_int = true;
+ found = 1;
+ }
+ }
+ }
+
+ return found ? 0 : -EINVAL;
+}
+
+/* divaux must be 1 for the DivM path and > 1 for the DivAux path. */
+static void k3_inno_dp_try_prepll(u32 target_pclk_khz, u32 ref_clk_khz,
+ u32 div_total, u8 div5_en, u8 divaux,
+ u8 divm, u8 divp,
+ struct k3_inno_dp_prepll_cfg *best)
+{
+ struct k3_inno_dp_prepll_cfg curr = {0};
+ u32 target_vco = target_pclk_khz * div_total;
+ u32 actual_pclk;
+ u32 frac;
+ u16 fb;
+ u8 pre;
+
+ if (target_vco < DPTX_VCO_MIN_KHZ || target_vco > DPTX_VCO_MAX_KHZ)
+ return;
+
+ if (k3_inno_dp_solve_frac(target_vco, ref_clk_khz, &pre, &fb, &frac))
+ return;
+
+ actual_pclk = k3_inno_dp_pll_rate_khz(pre, fb, frac, ref_clk_khz,
+ div_total);
+ if (k3_inno_dp_abs_diff(actual_pclk, target_pclk_khz) > DPTX_PLL_ERR_TOLERANCE)
+ return;
+
+ curr.valid = true;
+ curr.vco_freq_khz = target_vco;
+ curr.actual_pclk_khz = actual_pclk;
+ curr.prediv = pre;
+ curr.fbdiv = fb;
+ curr.frac = frac;
+ curr.frac_pd = (frac == 0) ? 3 : 0;
+ curr.div5_en = div5_en;
+ curr.divaux = divaux;
+ curr.divm = divm;
+ curr.divp = divp;
+
+ if (k3_inno_dp_better_cfg(curr.valid, curr.frac == 0, curr.prediv,
+ curr.vco_freq_khz, best->valid,
+ best->frac == 0, best->prediv,
+ best->vco_freq_khz))
+ *best = curr;
+}
+
+static int k3_inno_dp_solve_prepll(u32 target_pclk_khz, u32 ref_clk_khz,
+ struct k3_inno_dp_prepll_cfg *cfg)
+{
+ struct k3_inno_dp_prepll_cfg best = {0};
+ int pclk_div;
+ int i;
+
+ /* Strategy 1: Div5 path (VCO = PCLK * 5) */
+ k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz, 5, 1, 0, 0, 0,
+ &best);
+
+ for (pclk_div = 1; pclk_div <= 31; pclk_div++) {
+ /* Strategy 2: DivM path */
+ for (i = 0; i < ARRAY_SIZE(k3_inno_dp_divm_factors); i++)
+ k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz,
+ 2 * k3_inno_dp_divm_factors[i] * pclk_div, 0, 1,
+ i, pclk_div, &best);
+
+ /* Strategy 3: DivAux path */
+ for (i = 2; i <= 31; i++)
+ k3_inno_dp_try_prepll(target_pclk_khz, ref_clk_khz,
+ 2 * i * pclk_div,
+ 0, i, 0, pclk_div, &best);
+ }
+
+ if (!best.valid)
+ return -EINVAL;
+
+ *cfg = best;
+ return 0;
+}
+
+static void k3_inno_dp_program_prepll(struct k3_inno_dp_phy *p,
+ const struct k3_inno_dp_prepll_cfg *cfg)
+{
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+ FIELD_PREP(DPTX_ANA_PREPLL_PD, 1));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PREDIV,
+ FIELD_PREP(DPTX_ANA_PREPLL_PREDIV, cfg->prediv));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+ DPTX_ANA_PREPLL_FBDIV2_LBIT,
+ FIELD_PREP(DPTX_ANA_PREPLL_FBDIV2_LBIT, cfg->fbdiv & 0xFF));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+ DPTX_ANA_PREPLL_FBDIV2_HBIT,
+ FIELD_PREP(DPTX_ANA_PREPLL_FBDIV2_HBIT, (cfg->fbdiv >> 8) & 0xF));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_DACPD,
+ FIELD_PREP(DPTX_ANA_PREPLL_DACPD, (cfg->frac_pd >> 1) & 0x1));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_DSMPD,
+ FIELD_PREP(DPTX_ANA_PREPLL_DSMPD, cfg->frac_pd & 0x1));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+ DPTX_ANA_PREPLL_FRAC2_LBIT,
+ FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_LBIT, cfg->frac & 0xFF));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+ DPTX_ANA_PREPLL_FRAC2_MBIT,
+ FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_MBIT, (cfg->frac >> 8) & 0xFF));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_FRAC,
+ DPTX_ANA_PREPLL_FRAC2_HBIT,
+ FIELD_PREP(DPTX_ANA_PREPLL_FRAC2_HBIT, (cfg->frac >> 16) & 0xFF));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_DIV,
+ DPTX_ANA_PREPLL_PRECLK_DIVM,
+ FIELD_PREP(DPTX_ANA_PREPLL_PRECLK_DIVM, cfg->divm));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_DIV,
+ DPTX_ANA_PREPLL_PRECLK_DIVAUX,
+ FIELD_PREP(DPTX_ANA_PREPLL_PRECLK_DIVAUX, cfg->divaux));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_CTRL,
+ DPTX_ANA_PREPLL_PCLKDIV5_EN,
+ FIELD_PREP(DPTX_ANA_PREPLL_PCLKDIV5_EN, cfg->div5_en));
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL_CTRL,
+ DPTX_ANA_PREPLL_PCLK_DIVAUX,
+ FIELD_PREP(DPTX_ANA_PREPLL_PCLK_DIVAUX, cfg->divp));
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
+ DPTX_ANA_PREPLL_PCLK_NORMAL,
+ FIELD_PREP(DPTX_ANA_PREPLL_PCLK_NORMAL, 1));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
+ FIELD_PREP(DPTX_ANA_PREPLL_PD, 0));
+ usleep_range(2000, 4000);
+}
+
+#define to_k3_inno_dp_phy_clk(_hw) \
+ container_of(_hw, struct k3_inno_dp_phy, pxclk_hw)
+
+static unsigned long k3_inno_dp_pxclk_recalc_rate(struct clk_hw *hw,
+ unsigned long parent_rate)
+{
+ struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+
+ return p->pxclk_rate;
+}
+
+static int k3_inno_dp_pxclk_determine_rate(struct clk_hw *hw,
+ struct clk_rate_request *req)
+{
+ struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+ const struct k3_inno_dp_prepll_cfg *cfg;
+ struct k3_inno_dp_prepll_cfg solved;
+ u32 khz = div_u64(req->rate, 1000);
+ int ret;
+
+ cfg = k3_inno_dp_find_prepll_cfg(khz, p->ref_clk_khz);
+ if (cfg) {
+ req->rate = (unsigned long)cfg->actual_pclk_khz * 1000;
+ return 0;
+ }
+
+ ret = k3_inno_dp_solve_prepll(khz, p->ref_clk_khz, &solved);
+ if (ret)
+ return ret;
+
+ req->rate = (unsigned long)solved.actual_pclk_khz * 1000;
+
+ return 0;
+}
+
+static int k3_inno_dp_pxclk_set_rate(struct clk_hw *hw, unsigned long rate,
+ unsigned long parent_rate)
+{
+ struct k3_inno_dp_phy *p = to_k3_inno_dp_phy_clk(hw);
+ const struct k3_inno_dp_prepll_cfg *cfg;
+ struct k3_inno_dp_prepll_cfg solved;
+ u32 khz = div_u64(rate, 1000);
+ int ret;
+
+ cfg = k3_inno_dp_find_prepll_cfg(khz, p->ref_clk_khz);
+ if (!cfg) {
+ ret = k3_inno_dp_solve_prepll(khz, p->ref_clk_khz, &solved);
+ if (ret) {
+ dev_err(p->dev, "no PLL setting for %lu Hz\n", rate);
+ return ret;
+ }
+ cfg = &solved;
+ }
+
+ k3_inno_dp_program_prepll(p, cfg);
+ p->pxclk_rate = (unsigned long)cfg->actual_pclk_khz * 1000;
+
+ return 0;
+}
+
+static const struct clk_ops k3_inno_dp_pxclk_ops = {
+ .recalc_rate = k3_inno_dp_pxclk_recalc_rate,
+ .determine_rate = k3_inno_dp_pxclk_determine_rate,
+ .set_rate = k3_inno_dp_pxclk_set_rate,
+};
+
+static const struct phy_ops k3_inno_dp_phy_ops = {
+ .power_on = k3_inno_dp_phy_power_on,
+ .power_off = k3_inno_dp_phy_power_off,
+ .configure = k3_inno_dp_phy_configure,
+ .owner = THIS_MODULE,
+};
+
+static int k3_inno_dp_phy_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct k3_inno_dp_phy *p;
+ struct phy_provider *provider;
+ struct clk_init_data init = {};
+ int ret;
+
+ p = devm_kzalloc(dev, sizeof(*p), GFP_KERNEL);
+ if (!p)
+ return -ENOMEM;
+
+ p->dev = dev;
+
+ p->regmap = dev_get_regmap(dev->parent, "dp");
+ if (!p->regmap)
+ return dev_err_probe(dev, -ENODEV,
+ "parent DP regmap not available\n");
+
+ p->phy = devm_phy_create(dev, dev->of_node, &k3_inno_dp_phy_ops);
+ if (IS_ERR(p->phy))
+ return dev_err_probe(dev, PTR_ERR(p->phy),
+ "failed to create DP PHY\n");
+
+ phy_set_drvdata(p->phy, p);
+
+ provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate);
+ if (IS_ERR(provider))
+ return dev_err_probe(dev, PTR_ERR(provider),
+ "failed to register PHY provider\n");
+
+ /* PHY takes a 24 MHz reference clock */
+ p->ref_clk_khz = 24000;
+
+ /* The APMU pixel-clock mux references this clock by name. */
+ of_property_read_string(dev->of_node, "clock-output-names", &init.name);
+ if (!init.name)
+ init.name = dev_name(dev);
+ init.ops = &k3_inno_dp_pxclk_ops;
+ init.parent_names = NULL;
+ init.num_parents = 0;
+ init.flags = 0;
+ p->pxclk_hw.init = &init;
+
+ ret = devm_clk_hw_register(dev, &p->pxclk_hw);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to register pixel clock\n");
+
+ ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get,
+ &p->pxclk_hw);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to add pixel clock provider\n");
+
+ return 0;
+}
+
+static const struct of_device_id k3_inno_dp_phy_of_match[] = {
+ { .compatible = "spacemit,k3-inno-dp-phy" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, k3_inno_dp_phy_of_match);
+
+static struct platform_driver k3_inno_dp_phy_driver = {
+ .probe = k3_inno_dp_phy_probe,
+ .driver = {
+ .name = "spacemit-k3-inno-dp-phy",
+ .of_match_table = k3_inno_dp_phy_of_match,
+ },
+};
+module_platform_driver(k3_inno_dp_phy_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DisplayPort PHY driver");
+MODULE_AUTHOR("Cody Kang <codykang.hk@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
2026-07-25 3:16 ` [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang
@ 2026-07-25 5:16 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:16 UTC (permalink / raw)
To: Cody Kang
Cc: devicetree, conor+dt, olteanv, linux-phy, vkoul, neil.armstrong,
dri-devel, robh
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] Hardware state (PREPLL) is accessed concurrently without protection across PHY and CCF frameworks.
- [Medium] The driver fails to power off the core DP link PLL (MPLL) when the PHY is disabled, causing a power leak.
- [Medium] Missing cleanup in the `k3_inno_dp_phy_power_on` error paths leaves PHY components actively consuming power.
- [Medium] Dynamically reducing the lane count via `phy_configure()` does not disable the unused transmit lane drivers.
- [Medium] Unconditional PLL power-up during rate configuration overrides the PHY power state.
- [Medium] Missing bounds check on `opts->lanes` causes out-of-bounds array reads and undefined shift behavior in `k3_inno_dp_phy_set_voltages`.
- [Medium] The pixel clock (`pxclk`) is registered with the CCF without `.prepare`/`.unprepare` or `.enable`/`.disable` hooks, yet its power state is manipulated implicitly.
--
--- Patch [5]: [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver ---
commit 46b764d91a977bdc796e404d7eaabb0362e753b0
Author: Cody Kang <codykang.hk@gmail.com>
Subject: phy: spacemit: add Innosilicon DP TX PHY driver
This commit adds support for the Innosilicon DisplayPort transmit PHY
integrated in the SpacemiT K3 SoC. The PHY shares its MMIO window with its
parent DP/eDP controller and exposes lane, rate, and voltage control. It
also registers its pixel PLL as a clock provider.
> diff --git a/drivers/phy/spacemit/phy-k3-inno-dp.c b/drivers/phy/spacemit/phy-k3-inno-dp.c
> new file mode 100644
> index 0000000000000..9dd62edb7a5af
> --- /dev/null
> +++ b/drivers/phy/spacemit/phy-k3-inno-dp.c
[ ... ]
> @@ -103,45 +103,45 @@ static int k3_inno_dp_phy_power_on(struct phy *phy)
> + struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
> + u32 lane_en;
> + u32 val;
> + int ret;
> +
> + switch (p->lanes) {
> + case 1:
> + lane_en = 0x1;
> + break;
> + case 2:
> + lane_en = 0x3;
> + break;
> + case 4:
> + default:
> + lane_en = 0xf;
> + break;
> + }
> +
> + ret = regmap_write_bits(p->regmap, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD, 0);
> + if (ret)
> + return ret;
> + ret = regmap_write_bits(p->regmap, DPTX_ANA_PREPLL,
> + DPTX_ANA_PREPLL_PD, 0);
> + if (ret)
> + return ret;
[Severity: High]
Are there potential concurrency issues between the CCF and PHY frameworks when
accessing the PREPLL?
The CCF calls k3_inno_dp_pxclk_set_rate() holding the prepare_lock, which
powers down the PREPLL, sleeps, and programs registers. Meanwhile, the PHY
framework calls k3_inno_dp_phy_power_on() holding phy->mutex.
Could a concurrent phy_power_on() clear the PREPLL_PD bit during the sleep
window, causing the clock framework to write configuration values to a
running PLL?
> + usleep_range(2000, 4000);
> +
> + ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
> + DPTX_PHY_CTRL_XMIT_EN,
> + FIELD_PREP(DPTX_PHY_CTRL_XMIT_EN, lane_en));
> + if (ret)
> + return ret;
> + usleep_range(2000, 4000);
> +
> + ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_MPLL, val,
> + val & DPTX_ANA_MPLL_LOCKED,
> + 2000, 10 * 1000);
> + if (ret) {
> + dev_err(p->dev, "DP PHY core PLL lock timed out\n");
> + return ret;
> + }
[Severity: Medium]
If regmap_read_poll_timeout() times out here or on the PREPLL lock, we return
an error without reverting the powered-up hardware state.
Since the PHY core framework doesn't invoke phy_power_off() if phy_power_on()
fails, does this leave the MPLL, PREPLL, and transmit lanes indefinitely
powered on in an error state?
> +
> + ret = regmap_read_poll_timeout(p->regmap, DPTX_ANA_PREPLL, val,
> + val & DPTX_ANA_PREPLL_LOCKED,
> + 2000, 10 * 1000);
> + if (ret)
> + dev_err(p->dev, "DP PHY pixel PLL lock timed out\n");
> +
> + return ret;
> +}
> +
> +static int k3_inno_dp_phy_power_off(struct phy *phy)
> +{
> + struct k3_inno_dp_phy *p = phy_get_drvdata(phy);
> +
> + regmap_write_bits(p->regmap, DPTX_PHY_CTRL, DPTX_PHY_CTRL_XMIT_EN, 0);
> + usleep_range(2000, 4000);
> +
> + regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
> + DPTX_ANA_PREPLL_PD);
> + usleep_range(2000, 4000);
> +
> + return 0;
> +}
[Severity: Medium]
This powers down the pixel PLL and disables the transmit lanes, but it
appears to skip powering down the core DP link PLL.
Does this leave the MPLL running indefinitely and leaking power when the PHY
is meant to be disabled? Should we also be setting DPTX_ANA_MPLL_PD here?
[ ... ]
> +static int k3_inno_dp_phy_set_lanes(struct k3_inno_dp_phy *p, u8 lanes)
> +{
> + u32 val;
> + int ret;
[ ... ]
> + ret = regmap_write_bits(p->regmap, DPTX_PHY_CTRL,
> + DPTX_PHY_CTRL_NUM_LANES,
> + FIELD_PREP(DPTX_PHY_CTRL_NUM_LANES, val));
> + if (ret)
> + return ret;
> +
> + p->lanes = lanes;
> + return 0;
> +}
[Severity: Medium]
If a caller dynamically reduces the lane count via phy_configure() (for
instance, falling back from 4 lanes to 2 during DP link training), this
updates the logical lane count but doesn't synchronize the
DPTX_PHY_CTRL_XMIT_EN register bits.
Will this leave the unused physical transmit lane drivers actively driven?
[ ... ]
> +static void k3_inno_dp_phy_program_mpll(struct k3_inno_dp_phy *p,
> + const struct k3_inno_dp_mpll_cfg *cfg)
> +{
> + struct regmap *rm = p->regmap;
[ ... ]
> + regmap_write_bits(rm, DPTX_ANA_PREPLL_CTRL, DPTX_ANA_PREPLL_LOCK_BYPEN,
> + FIELD_PREP(DPTX_ANA_PREPLL_LOCK_BYPEN, 1));
> +
> + regmap_write_bits(rm, DPTX_ANA_MPLL, DPTX_ANA_MPLL_PD,
> + FIELD_PREP(DPTX_ANA_MPLL_PD, 0));
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
When configuring the PLL rate, this clears the power down bit
(DPTX_ANA_MPLL_PD) at the end of the programming sequence.
If a rate configuration occurs while the PHY is intentionally suspended, will
this force the PLLs on and override the PHY's intended power state?
[ ... ]
> +static int k3_inno_dp_phy_set_voltages(struct k3_inno_dp_phy *p,
> + struct phy_configure_opts_dp *opts)
> +{
> + u8 lane;
> + int ret;
> +
> + for (lane = 0; lane < opts->lanes; lane++) {
> + unsigned int shift = lane * DPTX_PHY_LANE_BITS_PER;
> + u32 mask = DPTX_PHY_LANE_FIELD_MASK << shift;
> + u32 val = (((opts->voltage[lane] & 0x3) << DPTX_PHY_LANE_VSWING_SHIFT) |
> + (opts->pre[lane] & 0x3)) << shift;
[Severity: Medium]
Could an unvalidated or uninitialized opts->lanes cause an out-of-bounds
array read on opts->voltage and opts->pre, as well as undefined bit shifts
here?
In k3_inno_dp_phy_configure(), k3_inno_dp_phy_set_lanes() is only called if
opts->dp.set_lanes is true. If a caller passes opts->dp.set_lanes = 0 but
opts->dp.set_voltages = 1, opts->dp.lanes skips validation.
[ ... ]
> +static void k3_inno_dp_program_prepll(struct k3_inno_dp_phy *p,
> + const struct k3_inno_dp_prepll_cfg *cfg)
> +{
> + regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
> + FIELD_PREP(DPTX_ANA_PREPLL_PD, 1));
> + usleep_range(2000, 4000);
> +
[ ... ]
> + regmap_write_bits(p->regmap, DPTX_ANA_PREPLL, DPTX_ANA_PREPLL_PD,
> + FIELD_PREP(DPTX_ANA_PREPLL_PD, 0));
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
Similar to the MPLL configuration, this unconditionally clears the PREPLL
power down bit at the end of programming, potentially overriding the intended
power state of the PHY.
[ ... ]
> +static const struct clk_ops k3_inno_dp_pxclk_ops = {
> + .recalc_rate = k3_inno_dp_pxclk_recalc_rate,
> + .determine_rate = k3_inno_dp_pxclk_determine_rate,
> + .set_rate = k3_inno_dp_pxclk_set_rate,
> +};
[Severity: Medium]
The pixel clock is registered with the Common Clock Framework without
.prepare/.unprepare or .enable/.disable hooks.
Since the CCF tracks the clock state purely in software while phy_power_off()
physically powers down the PREPLL, can this cause a mismatch where downstream
consumers lose their clock unexpectedly while the CCF thinks it is still
running?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=5
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (4 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 07/17] drm/spacemit: add Saturn DPU register model Cody Kang
` (11 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
The APMU eDP/DP pixel-clock muxes were parented to a placeholder, because
their real parents (the DP PHY PLLs) did not exist as clocks yet. They do
now, so point each mux at its PLL and let the pixel clock reach the
controller through the clock framework.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/clk/spacemit/ccu-k3.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/clk/spacemit/ccu-k3.c b/drivers/clk/spacemit/ccu-k3.c
index cb0c4277f72a..afbbbd797826 100644
--- a/drivers/clk/spacemit/ccu-k3.c
+++ b/drivers/clk/spacemit/ccu-k3.c
@@ -937,13 +937,13 @@ CCU_MUX_DIV_GATE_FC_DEFINE(ufs_aclk, ufs_aclk_parents, APMU_UFS_CLK_RES_CTRL, 5,
static const struct clk_parent_data edp0_pclk_parents[] = {
CCU_PARENT_HW(lcd_pxclk),
- CCU_PARENT_NAME(external_clk),
+ CCU_PARENT_NAME(edp0_pll_pxclk),
};
CCU_MUX_GATE_DEFINE(edp0_pxclk, edp0_pclk_parents, APMU_LCD_EDP_CTRL, 2, 1, BIT(1), 0);
static const struct clk_parent_data edp1_pclk_parents[] = {
CCU_PARENT_HW(dsi4ln2_lcd_pxclk),
- CCU_PARENT_NAME(external_clk),
+ CCU_PARENT_NAME(edp1_pll_pxclk),
};
CCU_MUX_GATE_DEFINE(edp1_pxclk, edp1_pclk_parents, APMU_LCD_EDP_CTRL, 18, 1, BIT(17), 0);
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 07/17] drm/spacemit: add Saturn DPU register model
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (5 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang
` (10 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
The Saturn display processing unit is programmed through a set of
register blocks (composer, RDMA, timing generator, cmdlist engine,
display MMU) laid out identically in both SoC instances. Add the block
address map, the per-field word offset and mask tables consumed by the
field accessors, the interrupt bit layout and the compositor and
output-format ID encodings. The address map describes the whole
IP block on purpose, including blocks the driver does not touch; the
field tables carry the fields the driver programs, with each block's
full word count kept as its NWORDS extent.
Consumers arrive with the following patches; the driver only becomes
buildable once the final patch adds the Kconfig and Makefile glue.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/spacemit/dpu/saturn_regs/dpu_int.h | 25 ++
.../gpu/drm/spacemit/dpu/saturn_regs/reg_map_hee.h | 64 +++++
.../drm/spacemit/dpu/saturn_regs/saturn_fields.h | 298 +++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_dpu_ids.h | 22 ++
4 files changed, 409 insertions(+)
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_regs/dpu_int.h b/drivers/gpu/drm/spacemit/dpu/saturn_regs/dpu_int.h
new file mode 100644
index 000000000000..2c7fa00ba31e
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_regs/dpu_int.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef DPU_INT_REG_H
+#define DPU_INT_REG_H
+
+#include <linux/bits.h>
+
+#define DPU_INT_CMDLIST_CH_FRM_CFG_DONE_MASK (0xff)
+#define DPU_INT_CMDLIST_CH_FRM_CFG_DONE (DPU_INT_CMDLIST_CH_FRM_CFG_DONE_MASK << 11)
+#define DPU_INT_FRM_TIMING_UNFLOW BIT(8)
+#define DPU_INT_CFG_RDY_CLR BIT(5)
+#define DPU_INT_FRM_TIMING_CFG_LINE BIT(4)
+#define DPU_INT_FRM_TIMING_CFG_EOF (BIT(2) | BIT(3))
+#define DPU_INT_FRM_TIMING_EOF BIT(1)
+#define DPU_INT_FRM_TIMING_VSYNC BIT(0)
+#define DPU_REST_INT_BITS (DPU_INT_FRM_TIMING_CFG_EOF | \
+ DPU_INT_FRM_TIMING_CFG_LINE | \
+ DPU_INT_FRM_TIMING_EOF | \
+ DPU_INT_CMDLIST_CH_FRM_CFG_DONE)
+
+#endif
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_regs/reg_map_hee.h b/drivers/gpu/drm/spacemit/dpu/saturn_regs/reg_map_hee.h
new file mode 100644
index 000000000000..9490021cdb91
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_regs/reg_map_hee.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SATURN_REG_MAP_hee_H_
+#define _SATURN_REG_MAP_hee_H_
+
+#include "dpu_int.h"
+
+/* Base address, then bit definitions */
+
+#define DPU_TOP_BASE_ADDR 0x0
+#define DPU_CTRL_BASE_ADDR 0x3c0 /* dpu_ctl_top start address */
+#define CMDLIST_BASE_ADDR 0x500
+#define DPU_INT_BASE_ADDR 0x700
+
+#define DPU_ONLINE_IRQ_MSK (3 * 4)
+#define DPU_ONLINE_IRQ_STS (12 * 4)
+
+#define DMA_TOP_BASE_ADDR (RDMA0_BASE_ADDR + 0x280)
+
+#define RDMA0_BASE_ADDR (0x00001000)
+#define RDMA1_BASE_ADDR (RDMA0_BASE_ADDR + 0x1000)
+#define RDMA2_BASE_ADDR (RDMA0_BASE_ADDR + 0xa000)
+#define RDMA3_BASE_ADDR (RDMA0_BASE_ADDR + 0xb000)
+
+#define MMU_BASE_ADDR (0x1100)
+#define MMU_TOP_BASE_ADDR (MMU_BASE_ADDR + 0x100)
+
+#define TBU0_ADDR 0x1100
+#define TBU1_ADDR 0x1180
+#define TBU2_ADDR 0x2100
+#define TBU3_ADDR 0x2180
+#define TBU4_ADDR 0xb100
+#define TBU5_ADDR 0xb180
+#define TBU6_ADDR 0xc100
+#define TBU7_ADDR 0xc180
+
+#define CMP0_BASE_ADDR 0x00018000
+#define CMP1_BASE_ADDR (CMP0_BASE_ADDR + 0x18000)
+#define CMP2_BASE_ADDR (CMP0_BASE_ADDR + 0x30000)
+
+#define PREPIPE_SCAL0_BASE_ADDR 0x14000
+#define PREPIPE_SCAL1_BASE_ADDR 0x14400
+#define PREPIPE_SCAL2_BASE_ADDR 0x14800
+#define PREPIPE_SCAL3_BASE_ADDR 0x14c00
+
+#define POST_PIPE_ADDR (CMP1_BASE_ADDR + 0x300)
+
+#define EE_ADDR (CMP1_BASE_ADDR + 0xd00)
+
+#define DPU_SCENE_CTL_BASE_ADDR (0x300)
+#define DPU_SCENE_CTL_SIZE 0x40
+#define DPU_SCENE_CTL_ADDR(i) (DPU_SCENE_CTL_BASE_ADDR + (i) * DPU_SCENE_CTL_SIZE)
+
+#define TMG0_BASE_ADDR (0x00051000 + 0x200)
+
+#define CMDLIST_CH_Y 0x188
+#define CMDLIST_CH_START_CMPS_Y 0x90
+#define CMDLIST_CFG_READY 0x34
+
+#endif
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_regs/saturn_fields.h b/drivers/gpu/drm/spacemit/dpu/saturn_regs/saturn_fields.h
new file mode 100644
index 000000000000..f745dfdaa83d
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_regs/saturn_fields.h
@@ -0,0 +1,298 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ * Saturn DPU register field map: word offset and mask per field, consumed by
+ * the dpu_write()/dpu_read() accessors.
+ */
+
+#ifndef SATURN_FIELDS_H
+#define SATURN_FIELDS_H
+
+#include <linux/bits.h>
+
+/* CMDLIST_REG: 128 x 32-bit registers */
+#define CMDLIST_REG__NWORDS 128
+#define CMDLIST_REG__cmdlist_reg_0__cmdlist_ch_start_addrl_WORD(i) (0 + (i))
+#define CMDLIST_REG__cmdlist_reg_0__cmdlist_ch_start_addrl_MASK GENMASK(31, 4)
+#define CMDLIST_REG__cmdlist_reg_16__cmdlist_ch_start_addrh_WORD(i) (16 + (i))
+#define CMDLIST_REG__cmdlist_reg_16__cmdlist_ch_start_addrh_MASK GENMASK(5, 0)
+#define CMDLIST_REG__cmdlist_reg_48__cmdlist_ch_y_first_WORD(i) (48 + (i))
+#define CMDLIST_REG__cmdlist_reg_48__cmdlist_ch_y_first_MASK GENMASK(24, 12)
+
+/* CMPS_X_REG: 146 x 32-bit registers */
+#define CMPS_X_REG__NWORDS 146
+#define CMPS_X_REG__module_enable_WORD 0
+#define CMPS_X_REG__module_enable_MASK GENMASK(0, 0)
+#define CMPS_X_REG__dst_w_WORD 1
+#define CMPS_X_REG__dst_w_MASK GENMASK(15, 0)
+#define CMPS_X_REG__dst_h_WORD 1
+#define CMPS_X_REG__dst_h_MASK GENMASK(31, 16)
+#define CMPS_X_REG__bg_color_r_WORD 2
+#define CMPS_X_REG__bg_color_r_MASK GENMASK(9, 0)
+#define CMPS_X_REG__bg_color_g_WORD 3
+#define CMPS_X_REG__bg_color_g_MASK GENMASK(9, 0)
+#define CMPS_X_REG__bg_color_b_WORD 4
+#define CMPS_X_REG__bg_color_b_MASK GENMASK(9, 0)
+#define CMPS_X_REG__bg_color_a_WORD 5
+#define CMPS_X_REG__bg_color_a_MASK GENMASK(7, 0)
+#define CMPS_X_REG__layer00_area_left_WORD 8
+#define CMPS_X_REG__layer00_area_left_MASK GENMASK(15, 0)
+#define CMPS_X_REG__layer00_area_right_WORD 8
+#define CMPS_X_REG__layer00_area_right_MASK GENMASK(31, 16)
+#define CMPS_X_REG__layer00_area_top_WORD 9
+#define CMPS_X_REG__layer00_area_top_MASK GENMASK(15, 0)
+#define CMPS_X_REG__layer00_area_bottom_WORD 9
+#define CMPS_X_REG__layer00_area_bottom_MASK GENMASK(31, 16)
+#define CMPS_X_REG__layer00_solid_color_r_WORD 10
+#define CMPS_X_REG__layer00_solid_color_r_MASK GENMASK(9, 0)
+#define CMPS_X_REG__layer00_solid_color_g_WORD 11
+#define CMPS_X_REG__layer00_solid_color_g_MASK GENMASK(9, 0)
+#define CMPS_X_REG__layer00_solid_color_b_WORD 12
+#define CMPS_X_REG__layer00_solid_color_b_MASK GENMASK(9, 0)
+#define CMPS_X_REG__layer00_solid_color_a_WORD 13
+#define CMPS_X_REG__layer00_solid_color_a_MASK GENMASK(7, 0)
+#define CMPS_X_REG__layer00_blend_mode_WORD 13
+#define CMPS_X_REG__layer00_blend_mode_MASK GENMASK(15, 14)
+#define CMPS_X_REG__layer00_colorkey_en_WORD 13
+#define CMPS_X_REG__layer00_colorkey_en_MASK GENMASK(18, 18)
+#define CMPS_X_REG__layer00_en_WORD 14
+#define CMPS_X_REG__layer00_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer00_blend_sel_WORD 14
+#define CMPS_X_REG__layer00_blend_sel_MASK GENMASK(2, 1)
+#define CMPS_X_REG__layer00_solid_en_WORD 14
+#define CMPS_X_REG__layer00_solid_en_MASK GENMASK(3, 3)
+#define CMPS_X_REG__layer00_dma_id_WORD 14
+#define CMPS_X_REG__layer00_dma_id_MASK GENMASK(7, 4)
+#define CMPS_X_REG__layer00_layer_alpha_WORD 14
+#define CMPS_X_REG__layer00_layer_alpha_MASK GENMASK(15, 8)
+#define CMPS_X_REG__layer00_alpha_ratio_WORD 14
+#define CMPS_X_REG__layer00_alpha_ratio_MASK GENMASK(23, 16)
+#define CMPS_X_REG__layer01_en_WORD 21
+#define CMPS_X_REG__layer01_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer02_en_WORD 28
+#define CMPS_X_REG__layer02_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer03_en_WORD 35
+#define CMPS_X_REG__layer03_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer04_en_WORD 42
+#define CMPS_X_REG__layer04_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer05_en_WORD 49
+#define CMPS_X_REG__layer05_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer06_en_WORD 56
+#define CMPS_X_REG__layer06_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer07_en_WORD 63
+#define CMPS_X_REG__layer07_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer08_en_WORD 70
+#define CMPS_X_REG__layer08_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer09_en_WORD 77
+#define CMPS_X_REG__layer09_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer10_en_WORD 84
+#define CMPS_X_REG__layer10_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer11_en_WORD 91
+#define CMPS_X_REG__layer11_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer12_en_WORD 98
+#define CMPS_X_REG__layer12_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer13_en_WORD 105
+#define CMPS_X_REG__layer13_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer14_en_WORD 112
+#define CMPS_X_REG__layer14_en_MASK GENMASK(0, 0)
+#define CMPS_X_REG__layer15_en_WORD 119
+#define CMPS_X_REG__layer15_en_MASK GENMASK(0, 0)
+
+/* DMA_TOP_REG: 25 x 32-bit registers */
+#define DMA_TOP_REG__NWORDS 25
+#define DMA_TOP_REG__image_rr_ratio_WORD 1
+#define DMA_TOP_REG__image_rr_ratio_MASK GENMASK(7, 0)
+#define DMA_TOP_REG__pixel_num_th_WORD 1
+#define DMA_TOP_REG__pixel_num_th_MASK GENMASK(14, 9)
+#define DMA_TOP_REG__rdma_timeout_limit_WORD 2
+#define DMA_TOP_REG__rdma_timeout_limit_MASK GENMASK(15, 0)
+#define DMA_TOP_REG__wdma_timeout_limit_WORD 2
+#define DMA_TOP_REG__wdma_timeout_limit_MASK GENMASK(31, 16)
+
+/* DPU_CTL_REG: 15 x 32-bit registers */
+#define DPU_CTL_REG__NWORDS 15
+#define DPU_CTL_REG__nml_rch_en_WORD 0
+#define DPU_CTL_REG__nml_rch_en_MASK GENMASK(9, 0)
+#define DPU_CTL_REG__nml_scl_en_WORD 0
+#define DPU_CTL_REG__nml_scl_en_MASK GENMASK(13, 10)
+#define DPU_CTL_REG__nml_outctl_en_WORD 0
+#define DPU_CTL_REG__nml_outctl_en_MASK GENMASK(18, 18)
+#define DPU_CTL_REG__nml_frm_timing_en_WORD 0
+#define DPU_CTL_REG__nml_frm_timing_en_MASK GENMASK(25, 23)
+#define DPU_CTL_REG__both_cfg_rdy_WORD 2
+#define DPU_CTL_REG__both_cfg_rdy_MASK GENMASK(0, 0)
+#define DPU_CTL_REG__video_mod_WORD 3
+#define DPU_CTL_REG__video_mod_MASK GENMASK(0, 0)
+#define DPU_CTL_REG__dbg_mod_WORD 3
+#define DPU_CTL_REG__dbg_mod_MASK GENMASK(1, 1)
+#define DPU_CTL_REG__timing_inter0_WORD 3
+#define DPU_CTL_REG__timing_inter0_MASK GENMASK(7, 2)
+#define DPU_CTL_REG__timing_inter1_WORD 3
+#define DPU_CTL_REG__timing_inter1_MASK GENMASK(13, 8)
+#define DPU_CTL_REG__sw_start_WORD 4
+#define DPU_CTL_REG__sw_start_MASK GENMASK(0, 0)
+#define DPU_CTL_REG__nml_rch_vrt_reuse_WORD 7
+#define DPU_CTL_REG__nml_rch_vrt_reuse_MASK GENMASK(9, 0)
+
+/* DPU_CTL_TOP_REG: 53 x 32-bit registers */
+#define DPU_CTL_TOP_REG__NWORDS 53
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_0__cmdlist_rch_en_WORD(i) (0 + (i))
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_0__cmdlist_rch_en_MASK GENMASK(0, 0)
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_10__cmdlist_cmps_other_en_WORD(i) (10 + (i))
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_10__cmdlist_cmps_other_en_MASK GENMASK(0, 0)
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_10__cmdlist_cmps_top_en_WORD(i) (10 + (i))
+#define DPU_CTL_TOP_REG__dpu_ctl_top_reg_10__cmdlist_cmps_top_en_MASK GENMASK(1, 1)
+
+/* DPU_INTP_REG: 50 x 32-bit registers */
+#define DPU_INTP_REG__NWORDS 50
+
+/* EE_REG: 51 x 32-bit registers */
+#define EE_REG__NWORDS 51
+#define EE_REG__m_benable_WORD 0
+#define EE_REG__m_benable_MASK GENMASK(0, 0)
+
+/* MMU_TBU_REG: 21 x 32-bit registers */
+#define MMU_TBU_REG__NWORDS 21
+#define MMU_TBU_REG__tbu_base_addr0_low_WORD 1
+#define MMU_TBU_REG__tbu_base_addr0_low_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_base_addr0_high_WORD 2
+#define MMU_TBU_REG__tbu_base_addr0_high_MASK GENMASK(11, 0)
+#define MMU_TBU_REG__tbu_base_addr1_low_WORD 3
+#define MMU_TBU_REG__tbu_base_addr1_low_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_base_addr1_high_WORD 4
+#define MMU_TBU_REG__tbu_base_addr1_high_MASK GENMASK(11, 0)
+#define MMU_TBU_REG__tbu_base_addr2_low_WORD 5
+#define MMU_TBU_REG__tbu_base_addr2_low_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_base_addr2_high_WORD 6
+#define MMU_TBU_REG__tbu_base_addr2_high_MASK GENMASK(11, 0)
+#define MMU_TBU_REG__tbu_va0_WORD 7
+#define MMU_TBU_REG__tbu_va0_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_va1_WORD 8
+#define MMU_TBU_REG__tbu_va1_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_va2_WORD 9
+#define MMU_TBU_REG__tbu_va2_MASK GENMASK(31, 0)
+#define MMU_TBU_REG__tbu_size0_WORD 10
+#define MMU_TBU_REG__tbu_size0_MASK GENMASK(15, 0)
+#define MMU_TBU_REG__tbu_size1_WORD 11
+#define MMU_TBU_REG__tbu_size1_MASK GENMASK(15, 0)
+#define MMU_TBU_REG__tbu_size2_WORD 12
+#define MMU_TBU_REG__tbu_size2_MASK GENMASK(15, 0)
+#define MMU_TBU_REG__vsync_update_en_WORD 13
+#define MMU_TBU_REG__vsync_update_en_MASK GENMASK(1, 1)
+
+/* MMU_TOP_REG: 23 x 32-bit registers */
+#define MMU_TOP_REG__NWORDS 23
+#define MMU_TOP_REG__rdma_timelimit_WORD 0
+#define MMU_TOP_REG__rdma_timelimit_MASK GENMASK(31, 16)
+#define MMU_TOP_REG__sram0_tlb_axi_port_sel_WORD 1
+#define MMU_TOP_REG__sram0_tlb_axi_port_sel_MASK GENMASK(1, 0)
+#define MMU_TOP_REG__sram1_tlb_axi_port_sel_WORD 1
+#define MMU_TOP_REG__sram1_tlb_axi_port_sel_MASK GENMASK(3, 2)
+#define MMU_TOP_REG__dmac0_rd_outs_num_WORD 2
+#define MMU_TOP_REG__dmac0_rd_outs_num_MASK GENMASK(7, 0)
+#define MMU_TOP_REG__dmac1_rd_outs_num_WORD 3
+#define MMU_TOP_REG__dmac1_rd_outs_num_MASK GENMASK(7, 0)
+
+/* POSTPIPE_REG: 50 x 32-bit registers */
+#define POSTPIPE_REG__NWORDS 50
+#define POSTPIPE_REG__m_inwidth_WORD 13
+#define POSTPIPE_REG__m_inwidth_MASK GENMASK(15, 0)
+#define POSTPIPE_REG__m_inheight_WORD 13
+#define POSTPIPE_REG__m_inheight_MASK GENMASK(31, 16)
+
+/* RDMA_PATH_X_REG: 60 x 32-bit registers */
+#define RDMA_PATH_X_REG__NWORDS 60
+#define RDMA_PATH_X_REG__layer_mode_WORD 0
+#define RDMA_PATH_X_REG__layer_mode_MASK GENMASK(1, 0)
+#define RDMA_PATH_X_REG__layer_cmpsr_id_WORD 0
+#define RDMA_PATH_X_REG__layer_cmpsr_id_MASK GENMASK(15, 13)
+#define RDMA_PATH_X_REG__is_two_layers_WORD 1
+#define RDMA_PATH_X_REG__is_two_layers_MASK GENMASK(0, 0)
+#define RDMA_PATH_X_REG__is_offline_WORD 1
+#define RDMA_PATH_X_REG__is_offline_MASK GENMASK(1, 1)
+#define RDMA_PATH_X_REG__base_addr0_low_ly0_WORD 9
+#define RDMA_PATH_X_REG__base_addr0_low_ly0_MASK GENMASK(31, 0)
+#define RDMA_PATH_X_REG__base_addr0_high_ly0_WORD 10
+#define RDMA_PATH_X_REG__base_addr0_high_ly0_MASK GENMASK(1, 0)
+#define RDMA_PATH_X_REG__base_addr1_low_ly0_WORD 11
+#define RDMA_PATH_X_REG__base_addr1_low_ly0_MASK GENMASK(31, 0)
+#define RDMA_PATH_X_REG__base_addr1_high_ly0_WORD 12
+#define RDMA_PATH_X_REG__base_addr1_high_ly0_MASK GENMASK(1, 0)
+#define RDMA_PATH_X_REG__base_addr2_low_ly0_WORD 13
+#define RDMA_PATH_X_REG__base_addr2_low_ly0_MASK GENMASK(31, 0)
+#define RDMA_PATH_X_REG__base_addr2_high_ly0_WORD 14
+#define RDMA_PATH_X_REG__base_addr2_high_ly0_MASK GENMASK(1, 0)
+#define RDMA_PATH_X_REG__rdma_stride0_layer0_WORD 15
+#define RDMA_PATH_X_REG__rdma_stride0_layer0_MASK GENMASK(15, 0)
+#define RDMA_PATH_X_REG__rdma_stride1_layer0_WORD 15
+#define RDMA_PATH_X_REG__rdma_stride1_layer0_MASK GENMASK(31, 16)
+#define RDMA_PATH_X_REG__img_width_ly0_WORD 16
+#define RDMA_PATH_X_REG__img_width_ly0_MASK GENMASK(15, 0)
+#define RDMA_PATH_X_REG__img_height_ly0_WORD 16
+#define RDMA_PATH_X_REG__img_height_ly0_MASK GENMASK(31, 16)
+#define RDMA_PATH_X_REG__bbox_start_x_ly0_WORD 17
+#define RDMA_PATH_X_REG__bbox_start_x_ly0_MASK GENMASK(15, 0)
+#define RDMA_PATH_X_REG__bbox_start_y_ly0_WORD 17
+#define RDMA_PATH_X_REG__bbox_start_y_ly0_MASK GENMASK(31, 16)
+#define RDMA_PATH_X_REG__bbox_end_x_ly0_WORD 18
+#define RDMA_PATH_X_REG__bbox_end_x_ly0_MASK GENMASK(15, 0)
+#define RDMA_PATH_X_REG__bbox_end_y_ly0_WORD 18
+#define RDMA_PATH_X_REG__bbox_end_y_ly0_MASK GENMASK(31, 16)
+#define RDMA_PATH_X_REG__pixel_format_WORD 29
+#define RDMA_PATH_X_REG__pixel_format_MASK GENMASK(5, 0)
+#define RDMA_PATH_X_REG__uv_swap_WORD 29
+#define RDMA_PATH_X_REG__uv_swap_MASK GENMASK(6, 6)
+#define RDMA_PATH_X_REG__rot_mode_ly0_WORD 29
+#define RDMA_PATH_X_REG__rot_mode_ly0_MASK GENMASK(10, 8)
+
+/* TMG_REG: 30 x 32-bit registers */
+#define TMG_REG__NWORDS 30
+#define TMG_REG__split_en_WORD 0
+#define TMG_REG__split_en_MASK GENMASK(0, 0)
+#define TMG_REG__cmd_screen_WORD 0
+#define TMG_REG__cmd_screen_MASK GENMASK(1, 1)
+#define TMG_REG__fm_timing_en_WORD 0
+#define TMG_REG__fm_timing_en_MASK GENMASK(2, 2)
+#define TMG_REG__cmd_wait_en_WORD 0
+#define TMG_REG__cmd_wait_en_MASK GENMASK(3, 3)
+#define TMG_REG__cmd_wait_te_WORD 0
+#define TMG_REG__cmd_wait_te_MASK GENMASK(4, 4)
+#define TMG_REG__disp_ready_man_en_WORD 0
+#define TMG_REG__disp_ready_man_en_MASK GENMASK(5, 5)
+#define TMG_REG__hsp_WORD 0
+#define TMG_REG__hsp_MASK GENMASK(7, 7)
+#define TMG_REG__vsp_WORD 0
+#define TMG_REG__vsp_MASK GENMASK(8, 8)
+#define TMG_REG__user_WORD 2
+#define TMG_REG__user_MASK GENMASK(15, 12)
+#define TMG_REG__hfp_WORD 3
+#define TMG_REG__hfp_MASK GENMASK(27, 16)
+#define TMG_REG__hsync_width_WORD 4
+#define TMG_REG__hsync_width_MASK GENMASK(9, 0)
+#define TMG_REG__hbp_WORD 4
+#define TMG_REG__hbp_MASK GENMASK(27, 16)
+#define TMG_REG__vfp_WORD 5
+#define TMG_REG__vfp_MASK GENMASK(15, 0)
+#define TMG_REG__vsync_width_WORD 5
+#define TMG_REG__vsync_width_MASK GENMASK(25, 16)
+#define TMG_REG__vbp_WORD 6
+#define TMG_REG__vbp_MASK GENMASK(11, 0)
+#define TMG_REG__h_active_WORD 6
+#define TMG_REG__h_active_MASK GENMASK(29, 16)
+#define TMG_REG__v_active_WORD 7
+#define TMG_REG__v_active_MASK GENMASK(13, 0)
+#define TMG_REG__background_r_WORD 9
+#define TMG_REG__background_r_MASK GENMASK(11, 0)
+#define TMG_REG__background_g_WORD 9
+#define TMG_REG__background_g_MASK GENMASK(27, 16)
+#define TMG_REG__background_b_WORD 10
+#define TMG_REG__background_b_MASK GENMASK(11, 0)
+#define TMG_REG__eof_1st_ln_dly_num_WORD 11
+#define TMG_REG__eof_1st_ln_dly_num_MASK GENMASK(15, 0)
+#define TMG_REG__eof_2nd_ln_dly_num_WORD 11
+#define TMG_REG__eof_2nd_ln_dly_num_MASK GENMASK(31, 16)
+#define TMG_REG__sof_pre_ln_num_WORD 14
+#define TMG_REG__sof_pre_ln_num_MASK GENMASK(16, 0)
+
+#endif /* SATURN_FIELDS_H */
diff --git a/drivers/gpu/drm/spacemit/spacemit_dpu_ids.h b/drivers/gpu/drm/spacemit/spacemit_dpu_ids.h
new file mode 100644
index 000000000000..41014dda927b
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_dpu_ids.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ */
+
+#ifndef _SPACEMIT_DPU_IDS_H_
+#define _SPACEMIT_DPU_IDS_H_
+
+/* Saturn online compositor instance ids (hardware IRQ-table indices). */
+#define COMPOSER0 0
+#define COMPOSER1 1
+#define COMPOSER2 2
+#define COMPOSER3 3
+
+/* DPU output-format field encoding (dpuctrl out-format register). */
+#define OUTFMT_RGB121212 0
+#define OUTFMT_RGB101010 1
+#define OUTFMT_RGB888 2
+#define OUTFMT_RGB666 12
+#define OUTFMT_RGB565 13
+
+#endif /* _SPACEMIT_DPU_IDS_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (6 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 07/17] drm/spacemit: add Saturn DPU register model Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:16 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang
` (9 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Register programming of the Saturn DPU is batched: the driver builds
per-module register lists in memory and the hardware command-list
engine replays them at frame boundaries, while scanout buffers are
mapped through the DPU's private address translation unit (DMMU)
rather than scanned out by physical address.
Add the shared driver types, the MMIO field accessors, the trace
events, the command-list builder and the DMMU table code. Some DPU
configuration registers are write-only, so a read-modify-write would
corrupt neighbouring fields; byte- and halfword-aligned fields are
therefore written at their native width instead.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/spacemit/dpu/dpu_saturn.h | 35 +++
drivers/gpu/drm/spacemit/dpu/dpu_trace.h | 350 ++++++++++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_cmdlist.c | 304 ++++++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_cmdlist.h | 146 ++++++++++++
drivers/gpu/drm/spacemit/spacemit_crtc.h | 259 ++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_dmmu.c | 103 ++++++++
drivers/gpu/drm/spacemit/spacemit_dmmu.h | 40 ++++
drivers/gpu/drm/spacemit/spacemit_dpu_reg.h | 108 +++++++++
drivers/gpu/drm/spacemit/spacemit_drm.h | 68 ++++++
9 files changed, 1413 insertions(+)
diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.h b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.h
new file mode 100644
index 000000000000..22d77d56eca1
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _DPU_SATURN_H_
+#define _DPU_SATURN_H_
+
+#include "../spacemit_crtc.h"
+
+struct dpu_format_id {
+ u32 format; /* DRM fourcc */
+ u8 id;
+ u8 bpp; /* bits per pixel */
+};
+
+#define SPACEMIT_DPU_INVALID_FORMAT_ID 0xff
+
+enum format_features {
+ FORMAT_RGB = BIT(0),
+};
+
+struct spacemit_hw_rdma {
+ u16 formats;
+};
+
+int dpu_parse_clocks(struct device *dev, struct dpu_clk_context *clk_ctx);
+u8 spacemit_plane_hw_get_format_id(u32 format);
+u32 saturn_conf_dpuctrl_rdma(struct spacemit_crtc *a_crtc);
+extern const struct spacemit_hw_device k3_saturn_dpu;
+void saturn_enable_irq_mask(struct spacemit_crtc *a_crtc, bool enable,
+ u32 offset, u32 mask);
+void spacemit_cfg_rdy_timer_handler(struct timer_list *t);
+#endif
diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_trace.h b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
new file mode 100644
index 000000000000..abfe34a67a4b
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
@@ -0,0 +1,350 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM spacemit_dpu
+
+#if !defined(_DPU_TRACE_H_) || defined(TRACE_HEADER_MULTI_READ)
+#define _DPU_TRACE_H_
+
+#include <linux/stringify.h>
+#include <linux/types.h>
+#include <linux/tracepoint.h>
+
+#include "dpu_saturn.h"
+
+TRACE_EVENT(drm_display_mode_info,
+ TP_PROTO(struct drm_display_mode *mode),
+ TP_ARGS(mode),
+ TP_STRUCT__entry(
+ __field(u16, hdisplay)
+ __field(u16, hbp)
+ __field(u16, hfp)
+ __field(u16, hsync)
+ __field(u16, vdisplay)
+ __field(u16, vbp)
+ __field(u16, vfp)
+ __field(u16, vsync)
+ ),
+ TP_fast_assign(
+ __entry->hdisplay = mode->hdisplay;
+ __entry->hbp = mode->htotal - mode->hsync_end;
+ __entry->hfp = mode->hsync_start - mode->hdisplay;
+ __entry->hsync = mode->hsync_end - mode->hsync_start;
+ __entry->vdisplay = mode->vdisplay;
+ __entry->vbp = mode->vtotal - mode->vsync_end;
+ __entry->vfp = mode->vsync_start - mode->vdisplay;
+ __entry->vsync = mode->vsync_end - mode->vsync_start;
+ ),
+ TP_printk("drm_display_mode: hdisplay=%d vdisplay=%d hsync=%d vsync=%d hbp=%d hfp=%d vbp=%d vfp=%d",
+ __entry->hdisplay, __entry->vdisplay, __entry->hsync, __entry->vsync,
+ __entry->hbp, __entry->hfp, __entry->vbp, __entry->vfp
+ )
+);
+
+TRACE_EVENT(dpu_plane_info,
+ TP_PROTO(struct drm_plane_state *state, struct drm_framebuffer *fb,
+ u32 rdma_id, u32 alpha, u32 rotation),
+ TP_ARGS(state, fb, rdma_id, alpha, rotation),
+ TP_STRUCT__entry(
+ __field(u32, rdma_id)
+ __field(u32, src_w)
+ __field(u32, src_h)
+ __field(u32, src_x)
+ __field(u32, src_y)
+ __field(u32, crtc_w)
+ __field(u32, crtc_h)
+ __field(u32, crtc_x)
+ __field(u32, crtc_y)
+ __field(u32, width)
+ __field(u32, height)
+ __field(u32, format)
+ __field(u32, blend_mode)
+ __field(u32, alpha)
+ __field(u32, zpos)
+ __field(u32, rotation)
+ ),
+ TP_fast_assign(
+ __entry->rdma_id = rdma_id;
+ __entry->src_w = state->src_w >> 16;
+ __entry->src_h = state->src_h >> 16;
+ __entry->src_x = state->src_x >> 16;
+ __entry->src_y = state->src_y >> 16;
+ __entry->crtc_w = state->crtc_w;
+ __entry->crtc_h = state->crtc_h;
+ __entry->crtc_x = state->crtc_x;
+ __entry->crtc_y = state->crtc_y;
+ __entry->width = fb->width;
+ __entry->height = fb->height;
+ __entry->format = fb->format->format;
+ __entry->blend_mode = state->pixel_blend_mode;
+ __entry->alpha = alpha;
+ __entry->zpos = state->zpos;
+ __entry->rotation = rotation;
+ ),
+ TP_printk("rdma_id=%d src: w=%d h=%d x=%d y=%d crtc: w=%d h=%d x=%d y=%d width=%d height=%d fmt=0x%x blend=%d alpha=%d zpos=%d rot=%d",
+ __entry->rdma_id, __entry->src_w,
+ __entry->src_h, __entry->src_x,
+ __entry->src_y, __entry->crtc_w,
+ __entry->crtc_h, __entry->crtc_x,
+ __entry->crtc_y, __entry->width,
+ __entry->height, __entry->format,
+ __entry->blend_mode, __entry->alpha,
+ __entry->zpos, __entry->rotation)
+);
+
+DECLARE_EVENT_CLASS(dpu_status_template,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status),
+ TP_STRUCT__entry(
+ __string(name_str, name)
+ __field(int, status)
+ ),
+ TP_fast_assign(
+ __assign_str(name_str);
+ __entry->status = status;
+ ),
+ TP_printk("%s: 0x%x",
+ __get_str(name_str), __entry->status)
+);
+
+DEFINE_EVENT(dpu_status_template, dpuctrl,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status)
+);
+DEFINE_EVENT(dpu_status_template, spacemit_plane_update_hw_channel,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status)
+);
+DEFINE_EVENT(dpu_status_template, saturn_irq_enable,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status)
+);
+DEFINE_EVENT(dpu_status_template, saturn_enable_vsync,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status)
+);
+DEFINE_EVENT(dpu_status_template, dpu_isr_status,
+ TP_PROTO(const char *name, int status),
+ TP_ARGS(name, status)
+);
+
+DECLARE_EVENT_CLASS(dpu_uint64_t_data_template,
+ TP_PROTO(const char *name, u64 data),
+ TP_ARGS(name, data),
+ TP_STRUCT__entry(
+ __string(name_str, name)
+ __field(u64, data)
+ ),
+ TP_fast_assign(
+ __assign_str(name_str);
+ __entry->data = data;
+ ),
+ TP_printk("%s: %lld",
+ __get_str(name_str), __entry->data)
+);
+
+DEFINE_EVENT(dpu_uint64_t_data_template, u64_data,
+ TP_PROTO(const char *name, u64 data),
+ TP_ARGS(name, data)
+);
+
+TRACE_EVENT(spacemit_dpu_reg_write,
+ TP_PROTO(u32 data, u32 base, const char *name),
+ TP_ARGS(data, base, name),
+ TP_STRUCT__entry(
+ __field(u32, data)
+ __field(u32, base)
+ __string(name_str, name)
+ ),
+ TP_fast_assign(
+ __entry->data = data;
+ __entry->base = base;
+ __assign_str(name_str);
+ ),
+ TP_printk("dpu write 0x%x to addr 0x%x field %s",
+ __entry->data, __entry->base, __get_str(name_str))
+);
+
+TRACE_EVENT(spacemit_plane_disable_hw_channel,
+ TP_PROTO(u32 layer_id, u32 rdma_id),
+ TP_ARGS(layer_id, rdma_id),
+ TP_STRUCT__entry(
+ __field(u32, layer_id)
+ __field(u32, rdma_id)
+ ),
+ TP_fast_assign(
+ __entry->layer_id = layer_id;
+ __entry->rdma_id = rdma_id;
+ ),
+ TP_printk("layer_id:%d rdma_id:%d",
+ __entry->layer_id, __entry->rdma_id)
+);
+
+DECLARE_EVENT_CLASS(dpu_func_template,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id),
+ TP_STRUCT__entry(
+ __field(u32, dev_id)
+ ),
+ TP_fast_assign(
+ __entry->dev_id = dev_id;
+ ),
+ TP_printk("dev_id=%d", __entry->dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_plane_atomic_check,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_plane_atomic_update,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_plane_atomic_duplicate_state,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_plane_atomic_destroy_state,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_plane_init,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, dpu_enable_clocks,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, dpu_disable_clocks,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, dpu_uninit,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, dpu_isr,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, dpu_run,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_atomic_enable,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_atomic_disable,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_atomic_check,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_atomic_begin,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_atomic_flush,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_enable_vblank,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_disable_vblank,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_run,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_init,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, spacemit_crtc_uninit,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DEFINE_EVENT(dpu_func_template, saturn_ctrl_cfg_ready_timer,
+ TP_PROTO(u32 dev_id),
+ TP_ARGS(dev_id)
+);
+DECLARE_EVENT_CLASS(dpu_ctrl_template,
+ TP_PROTO(int id, int enable),
+ TP_ARGS(id, enable),
+ TP_STRUCT__entry(
+ __field(u32, id)
+ __field(u32, enable)
+ ),
+ TP_fast_assign(
+ __entry->id = id;
+ __entry->enable = enable;
+ ),
+ TP_printk("id=%d enable:%d",
+ __entry->id, __entry->enable)
+);
+DEFINE_EVENT(dpu_ctrl_template, saturn_ctrl_cfg_ready,
+ TP_PROTO(int id, int enable),
+ TP_ARGS(id, enable)
+);
+
+DEFINE_EVENT(dpu_ctrl_template, saturn_ctrl_sw_start,
+ TP_PROTO(int id, int enable),
+ TP_ARGS(id, enable)
+);
+
+TRACE_EVENT(plat_cmdlist_dump_node,
+ TP_PROTO(char *dump),
+ TP_ARGS(dump),
+ TP_STRUCT__entry(
+ __string(dump_str, dump)
+ ),
+ TP_fast_assign(
+ __assign_str(dump_str);
+ ),
+ TP_printk("%s", __get_str(dump_str))
+);
+
+TRACE_EVENT(cmdlist_dump_node,
+ TP_PROTO(char *type, int rdma_id, u8 dev_id, int zpos, u16 nod_len),
+ TP_ARGS(type, rdma_id, dev_id, zpos, nod_len),
+ TP_STRUCT__entry(
+ __string(type_str, type)
+ __field(int, rdma_id)
+ __field(u8, dev_id)
+ __field(int, zpos)
+ __field(u16, nod_len)
+ ),
+ TP_fast_assign(
+ __assign_str(type_str);
+ __entry->rdma_id = rdma_id;
+ __entry->dev_id = dev_id;
+ __entry->zpos = zpos;
+ __entry->nod_len = nod_len;
+ ),
+ TP_printk("cmdlist[%s]: rdma_id:%d crtc:%d zpos:%d rows:%d",
+ __get_str(type_str),
+ __entry->rdma_id,
+ __entry->dev_id,
+ __entry->zpos,
+ __entry->nod_len)
+);
+#endif /* _DPU_TRACE_H_ */
+
+/* This part must be outside protection */
+#undef TRACE_INCLUDE_FILE
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH ../../drivers/gpu/drm/spacemit/dpu/
+
+#define TRACE_INCLUDE_FILE dpu_trace
+#include <trace/define_trace.h>
diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.c b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
new file mode 100644
index 000000000000..787d38e9e135
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
@@ -0,0 +1,304 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/dma-mapping.h>
+#include <linux/sort.h>
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_plane_helper.h>
+#include "spacemit_cmdlist.h"
+#include "spacemit_crtc.h"
+#include "spacemit_dpu_reg.h"
+#include "spacemit_drm.h"
+#include "dpu/dpu_saturn.h"
+#include "dpu/dpu_trace.h"
+
+struct spacemit_plane_state *cl_to_spacemit_pstate(const struct cmdlist *cl)
+{
+ return container_of(cl, struct spacemit_plane_state, cl);
+}
+
+struct spacemit_crtc_state *cl_to_spacemit_cstate(const struct cmdlist *cl)
+{
+ return container_of(cl, struct spacemit_crtc_state, cl);
+}
+
+struct cmdlist *plane_to_cl(struct drm_plane *plane)
+{
+ struct spacemit_plane_state *spacemit_pstate =
+ to_spacemit_plane_state(plane->state);
+
+ return &spacemit_pstate->cl;
+}
+
+struct cmdlist *crtc_to_cl(struct drm_crtc *crtc)
+{
+ struct spacemit_crtc_state *spacemit_cstate =
+ to_spacemit_crtc_state(crtc->state);
+
+ return &spacemit_cstate->cl;
+}
+
+static void cmdlist_dump_node(struct cmdlist *cl)
+{
+ struct spacemit_drm_private *priv;
+ struct spacemit_hw_device *hwdev;
+ struct spacemit_crtc *a_crtc;
+ char *type;
+ int zpos = -1, rdma_id = -1;
+
+ if (!cl)
+ return;
+
+ if (cl->type == CMDLIST_PLANE) {
+ struct spacemit_plane_state *spacemit_pstate =
+ cl_to_spacemit_pstate(cl);
+
+ a_crtc = to_spacemit_crtc(spacemit_pstate->state.crtc);
+ rdma_id = spacemit_pstate->rdma_id;
+ zpos = (int)(spacemit_pstate->state.zpos);
+ type = "RDMA";
+ } else if (cl->type == CMDLIST_CRTC) {
+ struct spacemit_crtc_state *spacemit_cstate =
+ cl_to_spacemit_cstate(cl);
+
+ a_crtc = to_spacemit_crtc(spacemit_cstate->base.crtc);
+ type = "CRTC";
+ } else {
+ return;
+ }
+
+ trace_cmdlist_dump_node(type, rdma_id, a_crtc->dev_id, zpos,
+ cl->nod_len);
+
+ priv = a_crtc->crtc.dev->dev_private;
+ hwdev = priv->hwdev;
+ hwdev->cmdlist_dump_node(cl);
+}
+
+void cmdlist_regs_packing(struct cmdlist *cl, enum cmdlist_mode_type mod,
+ struct cmdlist_regs *cl_regs)
+{
+ struct spacemit_crtc *a_crtc;
+ struct spacemit_drm_private *priv;
+ struct spacemit_hw_device *hwdev;
+ u32 *reg_base;
+ int i;
+ u32 index;
+
+ if (!cl_regs)
+ return;
+
+ if (cl->type == CMDLIST_PLANE) {
+ struct spacemit_plane_state *spacemit_pstate =
+ cl_to_spacemit_pstate(cl);
+
+ a_crtc = to_spacemit_crtc(spacemit_pstate->state.crtc);
+
+ drm_dbg(a_crtc->crtc.dev, "rch_id = %d, ch_y = %u\n",
+ spacemit_pstate->rdma_id,
+ spacemit_pstate->state.crtc_y);
+ } else if (cl->type == CMDLIST_CRTC) {
+ struct spacemit_crtc_state *spacemit_cstate =
+ cl_to_spacemit_cstate(cl);
+
+ a_crtc = to_spacemit_crtc(spacemit_cstate->base.crtc);
+ } else {
+ WARN_ONCE(1, "unsupported cmdlist type %d\n", cl->type);
+ return;
+ }
+
+ priv = a_crtc->crtc.dev->dev_private;
+ hwdev = priv->hwdev;
+ reg_base = cl_regs->module;
+
+ cl->mode_mask |= mod;
+
+ /*
+ * Without the atomic_check() buffer the staged register writes vanish.
+ */
+ if (drm_WARN_ON(a_crtc->crtc.dev, !cl->va))
+ return;
+
+ for (i = 0; i < cl_regs->size;) {
+ u32 value[CMDLIST_ROW_REGS] = {0x0};
+ u32 offset, strobe = 0;
+ u8 regs_in_row = 0;
+
+ if (cl_regs->flags[i]) {
+ offset = cl_regs->base + i * sizeof(u32);
+ for (u8 j = 0; j < CMDLIST_ROW_REGS; j++) {
+ index = i + j;
+ if (likely(index < cl_regs->size)) {
+ if (cl_regs->flags[index]) {
+ value[j] = reg_base[index];
+ regs_in_row++;
+ strobe |= CMDLIST_REG_STROBE(j);
+ }
+ } else {
+ break;
+ }
+ }
+ hwdev->cmdlist_fill_data_row(cl, strobe, offset, value);
+ /*
+ * the row covers the whole window; a shorter stride
+ * would revisit covered words as duplicate rows
+ */
+ i += CMDLIST_ROW_REGS;
+ cl->nod_len++;
+ } else {
+ i++;
+ }
+ }
+
+ drm_dbg(a_crtc->crtc.dev, "row_num = %d\n", cl->nod_len);
+}
+
+void cmdlist_sort_by_group(struct drm_crtc *crtc)
+{
+ struct cmdlist *first_cl;
+ struct cmdlist *last_cl;
+ struct cmdlist *p;
+ struct cmdlist *prev;
+ struct drm_plane *plane;
+ struct spacemit_crtc_rdma *rdmas =
+ to_spacemit_crtc_state(crtc->state)->rdmas;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+
+ drm_atomic_crtc_for_each_plane(plane, crtc) {
+ struct spacemit_plane_state *spacemit_pstate =
+ to_spacemit_plane_state(plane->state);
+ u32 rdma_id = spacemit_pstate->rdma_id;
+
+ first_cl = &spacemit_pstate->cl;
+ if (!first_cl->va)
+ continue;
+
+ last_cl = first_cl;
+ last_cl->next = NULL;
+
+ rdmas[rdma_id].in_use = true;
+ if (priv->cmdlist_groups[rdma_id]) {
+ p = priv->cmdlist_groups[rdma_id];
+ prev = NULL;
+ while (p) {
+ if (cl_to_spacemit_pstate(p)->state.crtc_y <
+ spacemit_pstate->state.crtc_y) {
+ prev = p;
+ p = p->next;
+ } else {
+ break;
+ }
+ }
+ if (!prev) {
+ priv->cmdlist_groups[rdma_id] = first_cl;
+ last_cl->next = p;
+ } else {
+ prev->next = first_cl;
+ last_cl->next = p;
+ }
+ } else {
+ priv->cmdlist_groups[rdma_id] = first_cl;
+ }
+ }
+}
+
+void cmdlist_atomic_commit(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state)
+{
+ int i;
+ struct cmdlist *cur_cl, *first_cl;
+ struct spacemit_crtc *a_crtc = NULL;
+ u32 chy, addrl, addrh;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct spacemit_crtc_rdma *cur_rdmas =
+ to_spacemit_crtc_state(crtc->state)->rdmas;
+ struct spacemit_crtc_rdma *old_rdmas =
+ to_spacemit_crtc_state(old_state)->rdmas;
+
+ a_crtc = to_spacemit_crtc(crtc);
+
+ for (i = 0; i < hwdev->rdma_nums; i++) {
+ if (old_rdmas[i].in_use)
+ hwdev->enable_cmdlist(a_crtc, hwdev, i, false);
+
+ if (cur_rdmas[i].in_use) {
+ drm_dbg(crtc->dev, "cmdlist group = %d\n", i);
+ cur_cl = priv->cmdlist_groups[i];
+ first_cl = cur_cl;
+ while (cur_cl) {
+ hwdev->cmdlist_fill_conf_row(cur_cl, hwdev,
+ CMDLIST_CMP_INVALID);
+ cmdlist_dump_node(cur_cl);
+ cur_cl = cur_cl->next;
+ }
+ chy = cl_to_spacemit_pstate(first_cl)->state.crtc_y;
+ addrl = (lower_32_bits(priv->cmdlist_groups[i]->pa) &
+ CMDLIST_ADDRL_ALIGN_MASK) >> CMDLIST_ADDRL_ALIGN_BITS;
+
+ addrh = upper_32_bits(priv->cmdlist_groups[i]->pa);
+ hwdev->cfg_cmdlist(hwdev, i, chy, addrl, addrh);
+ hwdev->enable_cmdlist(a_crtc, hwdev, i, true);
+ priv->cmdlist_groups[i] = NULL;
+ }
+ }
+}
+
+void crtc_cmdlist_sort_by_group(struct spacemit_crtc *a_crtc)
+{
+ struct cmdlist *first_cl;
+ struct cmdlist *last_cl;
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct drm_crtc_state *crtc_state = a_crtc->crtc.state;
+ struct spacemit_crtc_state *spacemit_crtc_state =
+ to_spacemit_crtc_state(crtc_state);
+ u8 crtc_id = drm_crtc_index(crtc);
+
+ first_cl = &spacemit_crtc_state->cl;
+ last_cl = first_cl;
+ last_cl->next = NULL;
+ priv->cmdlist_groups[hwdev->rdma_nums + crtc_id] = first_cl;
+}
+
+void crtc_cmdlist_atomic_commit(struct spacemit_crtc *a_crtc)
+{
+ struct cmdlist *cur_cl;
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ u8 crtc_id = drm_crtc_index(crtc);
+ u8 dev_id = a_crtc->dev_id;
+
+ drm_dbg(crtc->dev, "cmdlist group = %d\n",
+ hwdev->rdma_nums + crtc_id);
+ cur_cl = priv->cmdlist_groups[hwdev->rdma_nums + crtc_id];
+ while (cur_cl) {
+ hwdev->cmdlist_fill_conf_row(cur_cl, hwdev, dev_id);
+ cmdlist_dump_node(cur_cl);
+ cur_cl = cur_cl->next;
+ }
+ hwdev->crtc_cmdlist(cur_cl, hwdev, priv, crtc_id, dev_id);
+}
+
+/* Called from atomic_check(), where -ENOMEM is still an answer. */
+int spacemit_cmdlist_alloc(struct device *dev, struct cmdlist *cl)
+{
+ if (cl->va)
+ return 0;
+
+ cl->size = PER_CMDLIST_SIZE;
+ cl->va = dma_alloc_coherent(dev, cl->size, &cl->pa,
+ GFP_KERNEL | __GFP_ZERO);
+ if (!cl->va)
+ return -ENOMEM;
+
+ return 0;
+}
diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.h b/drivers/gpu/drm/spacemit/spacemit_cmdlist.h
new file mode 100644
index 000000000000..d5acdbeb3b0e
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_cmdlist.h
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SATURN_FW_CMDLIST_H_
+#define _SATURN_FW_CMDLIST_H_
+
+#include <linux/stddef.h>
+#include <linux/types.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_plane.h>
+
+struct spacemit_crtc;
+struct spacemit_plane_state;
+
+#define PER_CMDLIST_SIZE 4096
+#define CMDLIST_ROW_REGS (3)
+#define CMDLIST_CMP_INVALID (0xff)
+/* poison for the per-cmdlist compose sentinels, never written to hw */
+#define CMDLIST_SENTINEL_POISON 0xDEADBEEF
+
+/**
+ * enum cmdlist_type - what surface owns a cmdlist instance
+ * @CMDLIST_PLANE: Per-plane RDMA register batches.
+ * @CMDLIST_CRTC: Compose-side batches submitted on every flush.
+ */
+enum cmdlist_type {
+ CMDLIST_PLANE = 1,
+ CMDLIST_CRTC,
+};
+
+/**
+ * struct cmdlist - A DMA-backed batch of register writes replayed at CFG_RDY
+ * @nod_len: Number of register-write rows currently filled.
+ * @size: Capacity of the DMA buffer in bytes.
+ * @mode_mask: cmdlist_mode_type sources; gates which rows are pulled.
+ * @va: Kernel virtual address of the DMA buffer.
+ * @pa: DMA address programmed into the cmdlist base register.
+ * @next: Cmdlists in submission order; the CRTC-level one comes last.
+ * @type: Originating surface; see &enum cmdlist_type.
+ * @rch_start_cmps_y: Compositor-Y start sentinel.
+ * @cmdlist_ch_y_other: Companion sentinel to @rch_start_cmps_y.
+ */
+struct cmdlist {
+ u16 nod_len;
+ u32 size;
+ u32 mode_mask;
+ void *va;
+ dma_addr_t pa;
+ struct cmdlist *next;
+ u8 type;
+ u32 rch_start_cmps_y;
+ u32 cmdlist_ch_y_other;
+};
+
+enum cmdlist_mode_type {
+ CMDLIST_MOD_RDMA = BIT(0),
+ CMDLIST_MOD_COMP = BIT(3),
+ CMDLIST_MOD_DMMU = BIT(6),
+};
+
+#define CMDLIST_REG_STROBE(index) (0xf << ((index) * 4))
+
+#define CMDLIST_ADDRL_ALIGN_BITS (4) /* From cmdlist_reg_0[] in CMDLIST_REG */
+#define CMDLIST_ADDRL_ALIGN_MASK \
+ ((u32)(~(BIT(CMDLIST_ADDRL_ALIGN_BITS) - 1)))
+/* NULL on failure is safe: every consumer falls back to direct MMIO. */
+#define alloc_cmdlist_regs(module_name) \
+({ \
+ struct cmdlist_regs *cl = kzalloc(sizeof(*cl), GFP_KERNEL); \
+ if (cl) { \
+ cl->size = module_name##__NWORDS; \
+ cl->module = kzalloc(4 * module_name##__NWORDS, GFP_KERNEL); \
+ cl->flags = kzalloc(module_name##__NWORDS, GFP_KERNEL); \
+ if (!cl->module || !cl->flags) { \
+ kfree(cl->module); \
+ kfree(cl->flags); \
+ kfree(cl); \
+ cl = NULL; \
+ } \
+ } \
+ cl; \
+})
+
+#define free_cmdlist_regs(cl_p) \
+do { \
+ struct cmdlist_regs *cl = (cl_p); \
+ if (cl) { \
+ kfree(cl->module); \
+ kfree(cl->flags); \
+ kfree(cl); \
+ } \
+} while (0)
+
+/*
+ * First touch seeds the word from a live MMIO read: only register blocks that
+ * read back what was written may be routed through cmdlist.
+ */
+#define cmdlist_write_reg(hwdev, module_name, module_base, field, data, cl_regs) \
+do { \
+ struct cmdlist_regs *cl_p = (cl_regs); \
+ u32 *__words = cl_p->module; \
+ const unsigned int __w = dpu_field_word(module_name, field); \
+ if (cl_p->flags[__w] == 0) { \
+ __words[__w] = readl((hwdev)->base + (module_base) + 4 * __w); \
+ cl_p->base = (module_base); \
+ cl_p->flags[__w] = 1; \
+ } \
+ __words[__w] &= ~dpu_field_mask(module_name, field); \
+ __words[__w] |= FIELD_PREP(dpu_field_mask(module_name, field), \
+ (u32)(data)); \
+} while (0)
+
+/* Raw whole-word variant (the value32[] call sites); no seeding needed. */
+#define cmdlist_write_word(module_base, word, data, cl_regs) \
+do { \
+ struct cmdlist_regs *cl_p = (cl_regs); \
+ u32 *__words = cl_p->module; \
+ cl_p->base = (module_base); \
+ cl_p->flags[(word)] = 1; \
+ __words[(word)] = (u32)(data); \
+} while (0)
+
+struct cmdlist_regs {
+ u32 base;
+ u16 size;
+ u32 *module;
+ u8 *flags;
+};
+
+void cmdlist_regs_packing(struct cmdlist *cl, enum cmdlist_mode_type mod,
+ struct cmdlist_regs *cl_regs);
+void cmdlist_sort_by_group(struct drm_crtc *crtc);
+void cmdlist_atomic_commit(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state);
+struct spacemit_plane_state *cl_to_spacemit_pstate(const struct cmdlist *cl);
+void crtc_cmdlist_sort_by_group(struct spacemit_crtc *a_crtc);
+void crtc_cmdlist_atomic_commit(struct spacemit_crtc *a_crtc);
+struct spacemit_crtc_state *cl_to_spacemit_cstate(const struct cmdlist *cl);
+int spacemit_cmdlist_alloc(struct device *dev, struct cmdlist *cl);
+struct cmdlist *plane_to_cl(struct drm_plane *plane);
+struct cmdlist *crtc_to_cl(struct drm_crtc *crtc);
+
+#endif /* _SATURN_FW_CMDLIST_H_ */
diff --git a/drivers/gpu/drm/spacemit/spacemit_crtc.h b/drivers/gpu/drm/spacemit/spacemit_crtc.h
new file mode 100644
index 000000000000..27754866bc9a
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_crtc.h
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SPACEMIT_CRTC_H_
+#define _SPACEMIT_CRTC_H_
+
+#include <linux/delay.h>
+#include <linux/string.h>
+#include <linux/platform_device.h>
+#include <linux/device.h>
+#include <linux/kernel.h>
+#include <linux/bug.h>
+#include <linux/regmap.h>
+#include <video/videomode.h>
+#include <linux/workqueue.h>
+
+#include <drm/drm_atomic_uapi.h>
+#include <drm/drm_print.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_vblank.h>
+#include "spacemit_dpu_ids.h"
+#include "spacemit_cmdlist.h"
+#include "spacemit_drm.h"
+#include <linux/spinlock.h>
+
+#define RDMA_INVALID_ID (~0)
+
+#define DPU_MCLK_DEFAULT 307200000
+#define DPU_AXICLK_DEFAULT 409000000
+
+enum spacemit_dpu_irq {
+ INT_UNDERRUN,
+ INT_CFG_RDY,
+ INT_VSYNC,
+ INT_EOF,
+ INT_REST,
+ INT_VSYNC_UPDATE,
+};
+
+struct spacemit_crtc_fbcmem {
+ u32 start;
+ u32 size;
+ bool map;
+};
+
+struct spacemit_crtc_rdma {
+ struct spacemit_crtc_fbcmem fbcmem;
+ bool in_use;
+ u32 use_cnt;
+};
+
+struct dpu_mmu_tbl {
+ u32 size;
+ void *va;
+ dma_addr_t pa;
+};
+
+
+/**
+ * struct spacemit_crtc - Per-instance state for a Saturn DPU CRTC
+ * @dev: Underlying platform device for this DPU instance.
+ * @crtc: Embedded DRM CRTC; reachable via to_spacemit_crtc().
+ * @core: Vtable of DPU pipeline operations, populated at probe.
+ * @mmu_tbl: DPU-local page table mapping planes' buffers into the TBUs.
+ * @dev_id: Vendor-numbered DPU instance ID (0 or 1 on K3).
+ * @irq_online: Online-compose IRQ, kept for synchronize_irq().
+ * @cfg_rdy_timer: Watchdog for a CFG_RDY that never arrives.
+ * @is_1st_f: True until the first frame of a new modeset is transferred.
+ * @first_modeset: True until the first .atomic_enable after probe; that first
+ * pass resets the DPU to stop the U-Boot pipeline.
+ * @clk_ctx: Cached clock handles, enabled by dpu_enable_clocks().
+ * @aclk: Requested DPU AXI clock rate (Hz).
+ * @out_format: Output pixel format register cache.
+ * @mclk_reset: DPU MCLK reset line.
+ * @lcd_reset: LCD-side reset line shared with the bridge.
+ * @esc_reset: Escape-clock reset line.
+ * @aclk_reset: AXI clock reset line.
+ * @dsc_reset: DSC reset line.
+ * @cl_rdma: Cmdlist register slot for RDMA programming.
+ * @flip_done: Set once the DPU has committed the most recent page flip.
+ * @clocks_on: True while clocks run; MMIO on a gated DPU stalls the bus.
+ * @ur_reported: True once an underrun has been logged since the last CFG_RDY.
+ * @ur_mask_armed: True while clr_int_sts() may still mask the underrun IRQ.
+ */
+struct spacemit_crtc {
+ struct device *dev;
+ struct drm_crtc crtc;
+ const struct dpu_core_ops *core;
+ struct dpu_mmu_tbl mmu_tbl;
+ int dev_id;
+ int irq_online;
+ struct timer_list cfg_rdy_timer;
+
+ bool is_1st_f;
+ bool first_modeset;
+ struct dpu_clk_context clk_ctx;
+ u32 aclk;
+ u32 out_format;
+
+ struct reset_control *mclk_reset;
+ struct reset_control *lcd_reset;
+ struct reset_control *esc_reset;
+ struct reset_control *aclk_reset;
+ struct reset_control *dsc_reset;
+
+ struct cmdlist_regs *cl_rdma;
+
+ bool flip_done;
+
+ bool clocks_on;
+
+ bool ur_reported;
+ bool ur_mask_armed;
+};
+
+static inline struct spacemit_crtc *to_spacemit_crtc(struct drm_crtc *crtc)
+{
+ return crtc ? container_of(crtc, struct spacemit_crtc, crtc) : NULL;
+}
+
+struct spacemit_hw_device;
+struct dpu_core_ops {
+ int (*init)(struct spacemit_crtc *a_crtc);
+ void (*uninit)(struct spacemit_crtc *a_crtc);
+ void (*run)(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state);
+ void (*disable_vsync)(struct spacemit_crtc *a_crtc);
+ void (*enable_vsync)(struct spacemit_crtc *a_crtc);
+ u32 (*online_isr)(struct spacemit_crtc *a_crtc);
+ int (*enable_clk)(struct spacemit_crtc *a_crtc);
+ int (*disable_clk)(struct spacemit_crtc *a_crtc);
+ int (*cal_layer_fbcmem_size)(struct drm_plane *plane,
+ struct drm_plane_state *state);
+ int (*adjust_rdma_fbcmem)(struct spacemit_hw_device *hwdev,
+ struct spacemit_crtc_rdma *rdmas);
+};
+
+extern const struct dpu_core_ops dpu_saturn_ops;
+
+int spacemit_crtc_run(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state);
+
+struct spacemit_plane {
+ struct drm_plane plane;
+ struct spacemit_hw_device *hwdev;
+};
+
+/**
+ * struct spacemit_plane_state - Subclassed drm_plane_state for the Saturn DPU
+ * @state: Embedded drm_plane_state; reachable via to_spacemit_plane_state().
+ * @rdma_id: RDMA channel feeding this plane, derived from its zpos.
+ * @format: DPU-internal pixel-format token translated from the fourcc.
+ * @fbcmem_size: Bytes of FBC line buffer reserved for this plane.
+ * @mmu_tbl: Per-plane page table populated by spacemit_dmmu_map().
+ * @cl: Plane-level cmdlist filled during atomic_update.
+ */
+struct spacemit_plane_state {
+ struct drm_plane_state state;
+ u32 rdma_id;
+ u8 format;
+ u32 fbcmem_size;
+ struct dpu_mmu_tbl mmu_tbl;
+ struct cmdlist cl;
+};
+
+struct spacemit_plane *to_spacemit_plane(struct drm_plane *plane);
+
+static inline struct
+spacemit_plane_state *
+to_spacemit_plane_state(const struct drm_plane_state *state)
+{
+ return container_of(state, struct spacemit_plane_state, state);
+}
+
+/**
+ * struct spacemit_crtc_state - Subclassed drm_crtc_state for the Saturn DPU
+ * @base: Embedded drm_crtc_state; reachable via to_spacemit_crtc_state().
+ * @rdmas: Per-RDMA-channel descriptors, hwdev->rdma_nums entries.
+ * @cl: CRTC-level cmdlist used during atomic_flush.
+ */
+struct spacemit_crtc_state {
+ struct drm_crtc_state base;
+ struct spacemit_crtc_rdma *rdmas;
+ struct cmdlist cl;
+};
+
+#define to_spacemit_crtc_state(x) \
+ container_of(x, struct spacemit_crtc_state, base)
+
+struct drm_plane *spacemit_plane_init(struct drm_device *drm,
+ struct spacemit_crtc *a_crtc);
+
+struct spacemit_plane_state;
+struct spacemit_crtc;
+struct tbu_instance;
+struct dpu_mmu_tbl;
+enum spacemit_dpu_irq;
+
+/*
+ * Per-revision Saturn DPU description and operation vtable. One static instance
+ * per DT compatible, selected at probe; reached through spacemit_crtc::core.
+ */
+struct spacemit_hw_device {
+ void __iomem *base;
+ struct regmap *regmap;
+ u8 plane_nums;
+ u8 rdma_nums;
+ u8 crtc_nums;
+ const struct spacemit_hw_rdma *rdmas;
+ u8 n_formats;
+ const struct dpu_format_id *formats;
+ u8 n_fbcmems;
+ const u32 *fbcmem_sizes;
+ void (*enable_vsync)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, bool enable);
+ void (*enable_cfg_irq)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, bool enable);
+ void (*cfg_ready)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+ void (*sw_start)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+ void (*dpu_init)(struct spacemit_crtc *a_crtc);
+ void (*irq_enable)(struct spacemit_crtc *a_crtc, bool enable);
+ void (*plane_update_hw_channel)(struct drm_plane *plane);
+ void (*plane_disable_hw_channel)(struct drm_plane *plane,
+ struct drm_plane_state *old_state);
+ void (*conf_dpuctrl)(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state);
+ u32 (*get_cfg_rdy)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+ u32 (*get_irq_bit)(enum spacemit_dpu_irq irq_id, int dev_id);
+ u32 (*get_int_sts)(struct spacemit_hw_device *hwdev, int dev_id);
+ void (*clr_int_sts)(struct spacemit_crtc *a_crtc, u32 data, int dev_id);
+ void (*enable_cmdlist)(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, int id,
+ bool enable);
+ void (*cfg_cmdlist)(struct spacemit_hw_device *hwdev, int id, u32 chy,
+ u32 addrl, u32 addrh);
+ void (*rdma_dmmu)(struct spacemit_hw_device *hwdev, u8 tbu_id,
+ struct tbu_instance *tbu, struct drm_framebuffer *fb,
+ u32 val, struct cmdlist_regs *cl_rdma,
+ struct drm_plane *plane);
+ int (*get_cl_rdma_buf)(struct spacemit_crtc *a_crtc);
+ void (*cmdlist_fill_data_row)(struct cmdlist *cl, u32 strobe,
+ u32 offset, u32 value[]);
+ void (*cmdlist_fill_conf_row)(struct cmdlist *cl,
+ struct spacemit_hw_device *hwdev,
+ u8 dev_id);
+ void (*crtc_cmdlist)(struct cmdlist *cl,
+ struct spacemit_hw_device *hwdev,
+ struct spacemit_drm_private *priv, u8 crtc_id,
+ u8 dev_id);
+ void (*cmdlist_dump_node)(struct cmdlist *cl);
+};
+#endif
diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.c b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
new file mode 100644
index 000000000000..2ae30dd1a4f2
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/stddef.h>
+#include <linux/gfp.h>
+#include <linux/dma-mapping.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_shmem_helper.h>
+
+#include "spacemit_cmdlist.h"
+#include "spacemit_dmmu.h"
+#include "spacemit_drm.h"
+#include "spacemit_crtc.h"
+#include "spacemit_dpu_reg.h"
+
+static inline void spacemit_dmmu_fill_pgtable(struct dpu_mmu_tbl *tbl,
+ struct sg_table *sgt)
+{
+ struct sg_dma_page_iter dma_iter;
+ u32 max_entries = tbl->size / 4;
+ u32 *ttbr = tbl->va;
+ u32 i = 0, n = 0, temp = 0;
+
+ for_each_sgtable_dma_page(sgt, &dma_iter, 0) {
+ /*
+ * atomic_check sized the table off this same fb; catch a drift
+ */
+ if (WARN_ON_ONCE(n + HW_ALIGN_TTB_NUM >= max_entries))
+ break;
+ ttbr[n++] = (sg_page_iter_dma_address(&dma_iter) >> PAGE_SHIFT) & 0x3FFFFFF;
+ }
+
+ if (WARN_ON_ONCE(!n))
+ return;
+
+ /*
+ * The silicon over-fetches past the last entry; pad it to avoid a DDR
+ * fault.
+ */
+ temp = ttbr[n - 1];
+ for (i = 0; i < HW_ALIGN_TTB_NUM; i++)
+ ttbr[n++] = temp;
+}
+
+int spacemit_dmmu_map(struct drm_framebuffer *fb, struct dpu_mmu_tbl *mmu_tbl,
+ u8 tbu_id, struct cmdlist_regs *cl_rdma,
+ struct drm_plane *plane)
+{
+ struct spacemit_drm_private *priv = fb->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ const struct drm_format_info *format = NULL;
+ struct sg_table *sgt = NULL;
+ u32 total_size, offset1, offset2;
+ struct tbu_instance tbu = { };
+ u8 plane_num;
+ u32 val;
+
+ format = fb->format;
+ sgt = drm_gem_shmem_get_pages_sgt(to_drm_gem_shmem_obj(fb->obj[0]));
+ if (IS_ERR(sgt))
+ return PTR_ERR(sgt);
+
+ plane_num = format->num_planes;
+
+ total_size = roundup(fb->obj[0]->size, PAGE_SIZE);
+
+ offset1 = plane_num > 1 ? fb->offsets[1] : total_size;
+ offset2 = plane_num > 2 ? fb->offsets[2] : total_size;
+
+ switch (plane_num) {
+ case 3:
+ tbu.ttb_pa[2] = mmu_tbl->pa + (offset2 >> PAGE_SHIFT) * 4;
+ tbu.tbu_va[2] = TBU_BASE_VA(tbu_id) + offset2;
+ tbu.ttb_size[2] = PAGE_ALIGN(total_size - rounddown(offset2, PAGE_SIZE))
+ >> PAGE_SHIFT;
+ fallthrough;
+ case 2:
+ tbu.ttb_pa[1] = mmu_tbl->pa + (offset1 >> PAGE_SHIFT) * 4;
+ tbu.tbu_va[1] = TBU_BASE_VA(tbu_id) + offset1;
+ tbu.ttb_size[1] = PAGE_ALIGN(offset2 - rounddown(offset1, PAGE_SIZE)) >> PAGE_SHIFT;
+ fallthrough;
+ case 1:
+ tbu.ttb_pa[0] = mmu_tbl->pa;
+ tbu.tbu_va[0] = TBU_BASE_VA(tbu_id);
+ tbu.ttb_size[0] = PAGE_ALIGN(offset1) >> PAGE_SHIFT;
+ fallthrough;
+ default:
+ break;
+ }
+ spacemit_dmmu_fill_pgtable(mmu_tbl, sgt);
+
+ /* enable = 1, fbc_mode = 0 (linear only), plane count in bits [3:2] */
+ val = 0x1 | ((plane_num - 1) << 2);
+ /* tbu_burst_limit_en = 1, tlb_fetch_active_en = 1 */
+ val |= BIT(4) | BIT(5);
+ val = val | (DPU_QOS_URGENT << 8);
+ hwdev->rdma_dmmu(hwdev, tbu_id, &tbu, fb, val, cl_rdma, plane);
+
+ return 0;
+}
diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.h b/drivers/gpu/drm/spacemit/spacemit_dmmu.h
new file mode 100644
index 000000000000..5aaf1ad77f9f
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_dmmu.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SPACEMIT_DMMU_H_
+#define _SPACEMIT_DMMU_H_
+
+#include <linux/types.h>
+
+#include <drm/drm_file.h>
+#include "spacemit_cmdlist.h"
+
+#define DPU_QOS_URGENT 4
+#define DPU_QOS_NORMAL 3
+#define DPU_QOS_LOW 2
+/* Worst-case TLB alignment over-fetch at the maximum 4096 src_x. */
+#define HW_ALIGN_TTB_NUM 60
+
+#define RD_OUTS_NUM 16
+#define RDMA_TIMELIMIT 0xFFFF
+
+#define BASE_VA 0x10000000ULL
+#define VA_STEP_PER_TBU 0x40000000ULL
+
+#define TBU_BASE_VA(tbu_id) ((u64)BASE_VA + (u64)VA_STEP_PER_TBU * (tbu_id))
+
+struct tbu_instance {
+ u64 ttb_pa[3];
+ u64 tbu_va[3];
+ u32 ttb_size[3];
+};
+
+struct dpu_mmu_tbl;
+int spacemit_dmmu_map(struct drm_framebuffer *fb, struct dpu_mmu_tbl *mmu_tbl,
+ u8 tbu_id, struct cmdlist_regs *cl_rdma,
+ struct drm_plane *plane);
+
+#endif /* _SPACEMIT_DMMU_H_ */
diff --git a/drivers/gpu/drm/spacemit/spacemit_dpu_reg.h b/drivers/gpu/drm/spacemit/spacemit_dpu_reg.h
new file mode 100644
index 000000000000..a57ed34d38e2
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_dpu_reg.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ * Saturn DPU register accessors.
+ */
+
+#ifndef _SPACEMIT_DPU_REG_H_
+#define _SPACEMIT_DPU_REG_H_
+
+#include <linux/bitfield.h>
+#include <linux/io.h>
+
+#include "dpu/dpu_trace.h"
+#include "dpu/saturn_regs/saturn_fields.h"
+
+#define dpu_field_word(module_name, field) (module_name##__##field##_WORD)
+#define dpu_field_mask(module_name, field) (module_name##__##field##_MASK)
+
+/* MMIO field read: returns the field value shifted down to bit 0. */
+#define dpu_read_reg(hwdev, module_name, module_base, field) \
+ FIELD_GET(dpu_field_mask(module_name, field), \
+ readl((hwdev)->base + (module_base) + \
+ 4 * dpu_field_word(module_name, field)))
+
+/* MMIO raw 32-bit register read/write by word index within the block. */
+#define dpu_read_word(hwdev, module_base, word) \
+ readl((hwdev)->base + (module_base) + 4 * (word))
+
+#define dpu_write_reg_word(hwdev, module_base, word, data) \
+do { \
+ trace_spacemit_dpu_reg_write((u32)(data), (module_base), NULL); \
+ writel((u32)(data), \
+ (hwdev)->base + (module_base) + 4 * (word)); \
+} while (0)
+
+/*
+ * Not every DPU config word reads back what was written, so a byte- or
+ * halfword-aligned field is stored at its own width rather than read-modified.
+ */
+#define __dpu_mmio_field_write(__addr, __mask, data) \
+do { \
+ if ((__mask) == 0xffU) \
+ writeb((u8)((data) & 0xffU), __addr); \
+ else if ((__mask) == 0xff00U) \
+ writeb((u8)((data) & 0xffU), (__addr) + 1); \
+ else if ((__mask) == 0xff0000U) \
+ writeb((u8)((data) & 0xffU), (__addr) + 2); \
+ else if ((__mask) == 0xff000000U) \
+ writeb((u8)((data) & 0xffU), (__addr) + 3); \
+ else if ((__mask) == 0xffffU) \
+ writew((u16)((data) & 0xffffU), __addr); \
+ else if ((__mask) == 0xffff0000U) \
+ writew((u16)((data) & 0xffffU), (__addr) + 2); \
+ else if ((__mask) == 0xffffffffU) \
+ writel((u32)(data), __addr); \
+ else \
+ writel((readl(__addr) & ~(__mask)) | \
+ FIELD_PREP(__mask, (u32)(data)), __addr); \
+} while (0)
+
+#define dpu_write_reg(hwdev, module_name, module_base, field, data) \
+do { \
+ void __iomem *__waddr = (hwdev)->base + (module_base) + \
+ 4 * dpu_field_word(module_name, field); \
+ \
+ trace_spacemit_dpu_reg_write((u32)(data), (module_base), #field); \
+ __dpu_mmio_field_write(__waddr, \
+ dpu_field_mask(module_name, field), data); \
+} while (0)
+
+/* Field write into a register array element; the index is a runtime value. */
+#define dpu_write_reg_arr(hwdev, module_name, module_base, arr, i, field, data) \
+do { \
+ void __iomem *__waddr = (hwdev)->base + (module_base) + \
+ 4 * module_name##__##arr##__##field##_WORD(i); \
+ \
+ trace_spacemit_dpu_reg_write((u32)(data), (module_base), #field); \
+ __dpu_mmio_field_write(__waddr, \
+ module_name##__##arr##__##field##_MASK, \
+ data); \
+} while (0)
+
+/*
+ * Unified CPU/cmdlist field write; a NULL cl_regs selects the direct MMIO path.
+ */
+#define _dpu_write_reg(hwdev, module_name, module_base, field, data, cl_regs, ...) \
+do { \
+ if ((cl_regs) != NULL) \
+ cmdlist_write_reg(hwdev, module_name, module_base, \
+ field, data, cl_regs); \
+ else \
+ dpu_write_reg(hwdev, module_name, module_base, field, \
+ data); \
+} while (0)
+
+#define dpu_write(...) _dpu_write_reg(__VA_ARGS__, NULL)
+
+/* Unified CPU/cmdlist raw-word write (the value32[] call sites). */
+#define dpu_write_word(hwdev, module_base, word, data, cl_regs) \
+do { \
+ if ((cl_regs) != NULL) \
+ cmdlist_write_word(module_base, word, data, cl_regs); \
+ else \
+ dpu_write_reg_word(hwdev, module_base, word, data); \
+} while (0)
+
+#endif
diff --git a/drivers/gpu/drm/spacemit/spacemit_drm.h b/drivers/gpu/drm/spacemit/spacemit_drm.h
new file mode 100644
index 000000000000..3f3308920bae
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_drm.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SPACEMIT_DRM_H_
+#define _SPACEMIT_DRM_H_
+
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_vblank.h>
+#include <drm/drm_probe_helper.h>
+#include <linux/dma-mapping.h>
+#include <linux/clk.h>
+#include <linux/reset.h>
+#include "spacemit_dmmu.h"
+
+/* The DPU's clock inputs, as named by the binding. */
+struct dpu_clk_context {
+ struct clk *pxclk;
+ struct clk *mclk;
+ struct clk *escclk;
+ struct clk *aclk;
+ struct clk *dscclk;
+};
+
+/**
+ * struct spacemit_drm_private - Top-level driver state for the SpacemiT DRM
+ * card
+ * @ddev: DRM device, created in the component master bind callback; bind-scope,
+ * see spacemit_kms in spacemit_crtc.c.
+ * @dev: The DPU platform device; the DPU is the drm subsystem root.
+ * @a_crtc: Per-DPU CRTC state.
+ * @hwdev: Per-revision Saturn DPU description and vtable.
+ * @cmdlist_groups: Pool of per-pipeline cmdlist groups.
+ * @clk_ctx: The DPU's clocks, acquired once at probe.
+ * @mclk_reset: DPU MCLK reset line.
+ * @lcd_reset: LCD-side reset line shared with the bridge.
+ * @esc_reset: Escape-clock reset line.
+ * @aclk_reset: AXI clock reset line.
+ * @dsc_reset: DSC reset line.
+ * @irq_online: Online-compose IRQ; requested disabled, enabled at bind.
+ */
+struct spacemit_drm_private {
+ struct drm_device *ddev;
+ struct device *dev;
+ struct spacemit_crtc *a_crtc;
+ struct spacemit_hw_device *hwdev;
+ struct cmdlist **cmdlist_groups;
+
+ struct dpu_clk_context clk_ctx;
+ struct reset_control *mclk_reset;
+ struct reset_control *lcd_reset;
+ struct reset_control *esc_reset;
+ struct reset_control *aclk_reset;
+ struct reset_control *dsc_reset;
+ int irq_online;
+};
+
+extern struct drm_driver spacemit_drm_drv;
+extern const struct component_master_ops spacemit_drm_master_ops;
+
+int spacemit_drm_mode_config_init(struct drm_device *drm);
+int spacemit_drm_of_component_probe(struct device *dev);
+
+#endif /* _SPACEMIT_DRM_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
2026-07-25 3:16 ` [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang
@ 2026-07-25 5:16 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:16 UTC (permalink / raw)
To: Cody Kang
Cc: devicetree, linux-phy, dri-devel, neil.armstrong, conor+dt, vkoul,
robh, olteanv
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Passing exhausted NULL pointer to hardware programming function.
- [High] NULL pointer dereference when accessing state for disabled planes due to stale `in_use` flag.
- [High] DMMU page tables map multi-planar framebuffers out-of-bounds by exclusively using the first GEM object.
- [High] Hardware DMA state buffers are reused across atomic commits without double-buffering.
- [High] Missing bounds check in `cmdlist_regs_packing` causes DMA buffer overflow.
- [Medium] Unconditional dereference of `fb` in `dpu_plane_info` tracepoint.
--
--- Patch [8]: [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU ---
commit 82f0596ca65ff96e2081646dccab37f7bb9d1c0c
Author: Cody Kang <codykang.hk@gmail.com>
Subject: drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
This commit introduces the Saturn DPU driver core types, register accessors,
and MMU table code. It implements a batched register programming model using
in-memory command lists mapped through the DPU's private address translation
unit.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_trace.h b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
> new file mode 100644
> index 0000000000000..abfe34a67a4bd
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
[ ... ]
> +TRACE_EVENT(dpu_plane_info,
> + TP_PROTO(struct drm_plane_state *state, struct drm_framebuffer *fb,
> + u32 rdma_id, u32 alpha, u32 rotation),
> + TP_ARGS(state, fb, rdma_id, alpha, rotation),
> + TP_STRUCT__entry(
> + __field(u32, rdma_id)
> + __field(u32, src_w)
> + __field(u32, src_h)
> + __field(u32, src_x)
> + __field(u32, src_y)
> + __field(u32, crtc_w)
> + __field(u32, crtc_h)
> + __field(u32, crtc_x)
> + __field(u32, crtc_y)
> + __field(u32, width)
> + __field(u32, height)
> + __field(u32, format)
> + __field(u32, blend_mode)
> + __field(u32, alpha)
> + __field(u32, zpos)
> + __field(u32, rotation)
> + ),
> + TP_fast_assign(
> + __entry->rdma_id = rdma_id;
> + __entry->src_w = state->src_w >> 16;
> + __entry->src_h = state->src_h >> 16;
> + __entry->src_x = state->src_x >> 16;
> + __entry->src_y = state->src_y >> 16;
> + __entry->crtc_w = state->crtc_w;
> + __entry->crtc_h = state->crtc_h;
> + __entry->crtc_x = state->crtc_x;
> + __entry->crtc_y = state->crtc_y;
> + __entry->width = fb->width;
> + __entry->height = fb->height;
> + __entry->format = fb->format->format;
[Severity: Medium]
If this tracepoint fires during a plane disable operation, will the fb
parameter be NULL? Since state->fb is typically NULL during a disable, it
looks like evaluating fb->format->format here could cause a kernel panic
when tracing is enabled.
> + __entry->blend_mode = state->pixel_blend_mode;
> + __entry->alpha = alpha;
> + __entry->zpos = state->zpos;
> + __entry->rotation = rotation;
> + ),
> + TP_printk("rdma_id=%d src: w=%d h=%d x=%d y=%d crtc: w=%d h=%d x=%d y=%d width=%d height=%d fmt=0x%x blend=%d alpha=%d zpos=%d rot=%d",
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.c b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
> new file mode 100644
> index 0000000000000..787d38e9e135f
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
[ ... ]
> +void cmdlist_regs_packing(struct cmdlist *cl, enum cmdlist_mode_type mod,
> + struct cmdlist_regs *cl_regs)
> +{
[ ... ]
> + if (cl_regs->flags[i]) {
> + offset = cl_regs->base + i * sizeof(u32);
> + for (u8 j = 0; j < CMDLIST_ROW_REGS; j++) {
> + index = i + j;
> + if (likely(index < cl_regs->size)) {
> + if (cl_regs->flags[index]) {
> + value[j] = reg_base[index];
> + regs_in_row++;
> + strobe |= CMDLIST_REG_STROBE(j);
> + }
> + } else {
> + break;
> + }
> + }
> + hwdev->cmdlist_fill_data_row(cl, strobe, offset, value);
> + /*
> + * the row covers the whole window; a shorter stride
> + * would revisit covered words as duplicate rows
> + */
> + i += CMDLIST_ROW_REGS;
> + cl->nod_len++;
[Severity: High]
Is there a bounds check missing here to prevent cl->nod_len from exceeding
cl->size? If cl is shallow-copied and reused across commits, nod_len might
accumulate and eventually overflow the 4096-byte DMA buffer, overwriting
adjacent physical memory.
> + } else {
> + i++;
> + }
> + }
> +
> + drm_dbg(a_crtc->crtc.dev, "row_num = %d\n", cl->nod_len);
> +}
[ ... ]
> +void cmdlist_atomic_commit(struct drm_crtc *crtc,
> + struct drm_crtc_state *old_state)
> +{
> + int i;
> + struct cmdlist *cur_cl, *first_cl;
> + struct spacemit_crtc *a_crtc = NULL;
> + u32 chy, addrl, addrh;
> + struct spacemit_drm_private *priv = crtc->dev->dev_private;
> + struct spacemit_hw_device *hwdev = priv->hwdev;
> + struct spacemit_crtc_rdma *cur_rdmas =
> + to_spacemit_crtc_state(crtc->state)->rdmas;
> + struct spacemit_crtc_rdma *old_rdmas =
> + to_spacemit_crtc_state(old_state)->rdmas;
> +
> + a_crtc = to_spacemit_crtc(crtc);
> +
> + for (i = 0; i < hwdev->rdma_nums; i++) {
> + if (old_rdmas[i].in_use)
> + hwdev->enable_cmdlist(a_crtc, hwdev, i, false);
> +
> + if (cur_rdmas[i].in_use) {
> + drm_dbg(crtc->dev, "cmdlist group = %d\n", i);
> + cur_cl = priv->cmdlist_groups[i];
> + first_cl = cur_cl;
> + while (cur_cl) {
> + hwdev->cmdlist_fill_conf_row(cur_cl, hwdev,
> + CMDLIST_CMP_INVALID);
> + cmdlist_dump_node(cur_cl);
> + cur_cl = cur_cl->next;
> + }
> + chy = cl_to_spacemit_pstate(first_cl)->state.crtc_y;
[Severity: High]
Could first_cl be NULL here for a disabled plane? During atomic state
duplication, it seems the in_use flag is inherited. If a plane is disabled,
it is removed from the plane_mask and cmdlist_sort_by_group skips clearing
the flag. This would cause cur_cl and first_cl to be NULL, leading
cl_to_spacemit_pstate to apply container_of to a NULL pointer and trigger a
negative offset dereference.
> + addrl = (lower_32_bits(priv->cmdlist_groups[i]->pa) &
> + CMDLIST_ADDRL_ALIGN_MASK) >> CMDLIST_ADDRL_ALIGN_BITS;
> +
> + addrh = upper_32_bits(priv->cmdlist_groups[i]->pa);
> + hwdev->cfg_cmdlist(hwdev, i, chy, addrl, addrh);
> + hwdev->enable_cmdlist(a_crtc, hwdev, i, true);
> + priv->cmdlist_groups[i] = NULL;
> + }
> + }
> +}
[ ... ]
> +void crtc_cmdlist_atomic_commit(struct spacemit_crtc *a_crtc)
> +{
> + struct cmdlist *cur_cl;
> + struct drm_crtc *crtc = &a_crtc->crtc;
> + struct spacemit_drm_private *priv = crtc->dev->dev_private;
> + struct spacemit_hw_device *hwdev = priv->hwdev;
> + u8 crtc_id = drm_crtc_index(crtc);
> + u8 dev_id = a_crtc->dev_id;
> +
> + drm_dbg(crtc->dev, "cmdlist group = %d\n",
> + hwdev->rdma_nums + crtc_id);
> + cur_cl = priv->cmdlist_groups[hwdev->rdma_nums + crtc_id];
> + while (cur_cl) {
> + hwdev->cmdlist_fill_conf_row(cur_cl, hwdev, dev_id);
> + cmdlist_dump_node(cur_cl);
> + cur_cl = cur_cl->next;
> + }
> + hwdev->crtc_cmdlist(cur_cl, hwdev, priv, crtc_id, dev_id);
[Severity: High]
Does this loop exhaust cur_cl to NULL before passing it to
hwdev->crtc_cmdlist? Once the while loop finishes, cur_cl is guaranteed to be
NULL, which seems like it would pass an invalid pointer to the hardware
programming function instead of the intended command list head.
> +}
> +
> +/* Called from atomic_check(), where -ENOMEM is still an answer. */
> +int spacemit_cmdlist_alloc(struct device *dev, struct cmdlist *cl)
> +{
> + if (cl->va)
> + return 0;
> +
> + cl->size = PER_CMDLIST_SIZE;
> + cl->va = dma_alloc_coherent(dev, cl->size, &cl->pa,
> + GFP_KERNEL | __GFP_ZERO);
[Severity: High]
Does bypassing reallocation when cl->va is present cause the active hardware
state to be overwritten during the prepare phase of a non-blocking commit?
It looks like writing new register sequences directly into this reused buffer
could corrupt the in-flight display state being scanned out by the hardware.
> + if (!cl->va)
> + return -ENOMEM;
> +
> + return 0;
> +}
> diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.c b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
> new file mode 100644
> index 0000000000000..2ae30dd1a4f23
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
[ ... ]
> +int spacemit_dmmu_map(struct drm_framebuffer *fb, struct dpu_mmu_tbl *mmu_tbl,
> + u8 tbu_id, struct cmdlist_regs *cl_rdma,
> + struct drm_plane *plane)
> +{
> + struct spacemit_drm_private *priv = fb->dev->dev_private;
> + struct spacemit_hw_device *hwdev = priv->hwdev;
> + const struct drm_format_info *format = NULL;
> + struct sg_table *sgt = NULL;
> + u32 total_size, offset1, offset2;
> + struct tbu_instance tbu = { };
> + u8 plane_num;
> + u32 val;
> +
> + format = fb->format;
> + sgt = drm_gem_shmem_get_pages_sgt(to_drm_gem_shmem_obj(fb->obj[0]));
> + if (IS_ERR(sgt))
> + return PTR_ERR(sgt);
> +
> + plane_num = format->num_planes;
> +
> + total_size = roundup(fb->obj[0]->size, PAGE_SIZE);
> +
> + offset1 = plane_num > 1 ? fb->offsets[1] : total_size;
> + offset2 = plane_num > 2 ? fb->offsets[2] : total_size;
> +
> + switch (plane_num) {
> + case 3:
> + tbu.ttb_pa[2] = mmu_tbl->pa + (offset2 >> PAGE_SHIFT) * 4;
> + tbu.tbu_va[2] = TBU_BASE_VA(tbu_id) + offset2;
> + tbu.ttb_size[2] = PAGE_ALIGN(total_size - rounddown(offset2, PAGE_SIZE))
> + >> PAGE_SHIFT;
> + fallthrough;
> + case 2:
> + tbu.ttb_pa[1] = mmu_tbl->pa + (offset1 >> PAGE_SHIFT) * 4;
> + tbu.tbu_va[1] = TBU_BASE_VA(tbu_id) + offset1;
> + tbu.ttb_size[1] = PAGE_ALIGN(offset2 - rounddown(offset1, PAGE_SIZE)) >> PAGE_SHIFT;
[Severity: High]
For multi-planar formats with discrete GEM objects, does this map the
secondary planes out of bounds? The scatter/gather table is fetched strictly
from fb->obj[0]. Applying secondary offsets to the physical pages of the
first GEM object might break hardware isolation.
Could the size calculation offset2 - rounddown(offset1, PAGE_SIZE) also
underflow if these offsets belong to separate, un-ordered buffers?
> + fallthrough;
> + case 1:
> + tbu.ttb_pa[0] = mmu_tbl->pa;
> + tbu.tbu_va[0] = TBU_BASE_VA(tbu_id);
> + tbu.ttb_size[0] = PAGE_ALIGN(offset1) >> PAGE_SHIFT;
> + fallthrough;
> + default:
> + break;
> + }
> + spacemit_dmmu_fill_pgtable(mmu_tbl, sgt);
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=8
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (7 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:22 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang
` (8 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Add the register-level programming of one Saturn instance: clock and
reset bring-up, scene control and timing generator setup, RDMA channel
and composer layer programming, FBC line-buffer budgeting, interrupt
dispatch and the cfg-ready/vsync handshake that paces commits. The
backend is exposed as an ops table and hardware descriptor to be
aggregated by the KMS layer in an upcoming patch.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/spacemit/dpu/dpu_saturn.c | 483 +++++++++++
drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c | 896 +++++++++++++++++++++
drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c | 152 ++++
drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h | 21 +
drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h | 52 ++
5 files changed, 1604 insertions(+)
diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
new file mode 100644
index 000000000000..8d5f13bc5373
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
@@ -0,0 +1,483 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/pm_qos.h>
+#include <linux/regmap.h>
+#include <linux/of.h>
+#include <linux/of_graph.h>
+#include <linux/types.h>
+#include <linux/math64.h>
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_gem.h>
+#include "dpu_saturn.h"
+#include "saturn_fbcmem.h"
+#include "../spacemit_cmdlist.h"
+#include "../spacemit_dmmu.h"
+#include "../spacemit_dpu_reg.h"
+#include "../spacemit_drm.h"
+#include <video/display_timing.h>
+#include "../spacemit_dpu_ids.h"
+#include "saturn_regs/ops_hee.h"
+
+#define CREATE_TRACE_POINTS
+#include "dpu_trace.h"
+
+#define TOTAL_RDMA_MEMSIZE (68 * 1024)
+static const struct dpu_format_id primary_fmts[] = {
+ { DRM_FORMAT_XRGB8888, 8, 32 }, /* RDMA_FMT_XRGB_8888 */
+ { DRM_FORMAT_ARGB8888, 4, 32 }, /* RDMA_FMT_ARGB_8888 */
+ { DRM_FORMAT_XBGR8888, 9, 32 }, /* RDMA_FMT_XBGR_8888 */
+ { DRM_FORMAT_ABGR8888, 5, 32 }, /* RDMA_FMT_ABGR_8888 */
+ { DRM_FORMAT_RGBX8888, 10, 32 }, /* RDMA_FMT_RGBX_8888 */
+ { DRM_FORMAT_RGBA8888, 6, 32 }, /* RDMA_FMT_RGBA_8888 */
+ { DRM_FORMAT_BGRX8888, 11, 32 }, /* RDMA_FMT_BGRX_8888 */
+ { DRM_FORMAT_BGRA8888, 7, 32 }, /* RDMA_FMT_BGRA_8888 */
+ { DRM_FORMAT_ABGR2101010, 1, 32 }, /* RDMA_FMT_ABGR_2101010 */
+ { DRM_FORMAT_RGB888, 12, 24 }, /* RDMA_FMT_RGB_888 */
+ { DRM_FORMAT_BGR888, 13, 24 }, /* RDMA_FMT_BGR_888 */
+ { DRM_FORMAT_RGB565, 22, 16 }, /* RDMA_FMT_RGB_565 */
+ { DRM_FORMAT_BGR565, 23, 16 }, /* RDMA_FMT_BGR_565 */
+};
+
+static const struct spacemit_hw_rdma saturn_hee_rdmas[] = {
+ {FORMAT_RGB},
+ {FORMAT_RGB},
+ {FORMAT_RGB},
+ {FORMAT_RGB},
+};
+
+static const u32 saturn_hee_fbcmem_sizes[] = {
+ 89600, /* 87.5k */
+ 15360, /* 16k */
+};
+
+const struct spacemit_hw_device k3_saturn_dpu = {
+ .plane_nums = 1,
+ .crtc_nums = 2,
+ .rdma_nums = ARRAY_SIZE(saturn_hee_rdmas),
+ .rdmas = saturn_hee_rdmas,
+ .n_formats = ARRAY_SIZE(primary_fmts),
+ .formats = primary_fmts,
+ .n_fbcmems = ARRAY_SIZE(saturn_hee_fbcmem_sizes),
+ .fbcmem_sizes = saturn_hee_fbcmem_sizes,
+ .enable_vsync = saturn_hee_enable_vsync,
+ .enable_cfg_irq = saturn_hee_enable_cfg_irq,
+ .cfg_ready = saturn_hee_cfg_ready,
+ .sw_start = saturn_hee_sw_start,
+ .irq_enable = saturn_hee_irq_enable,
+ .dpu_init = saturn_hee_dpu_init,
+ .plane_update_hw_channel = saturn_hee_plane_update_hw_channel,
+ .plane_disable_hw_channel = saturn_hee_plane_disable_hw_channel,
+ .conf_dpuctrl = saturn_hee_conf_dpuctrl,
+ .get_cfg_rdy = saturn_hee_get_cfg_rdy,
+ .get_int_sts = saturn_hee_get_int_sts,
+ .get_irq_bit = saturn_hee_get_irq_bit,
+ .clr_int_sts = saturn_hee_clr_int_sts,
+ .enable_cmdlist = saturn_hee_enable_cmdlist,
+ .cfg_cmdlist = saturn_hee_cfg_cmdlist,
+ .rdma_dmmu = saturn_hee_rdma_dmmu,
+ .get_cl_rdma_buf = saturn_hee_get_cl_rdma_buf,
+ .cmdlist_fill_data_row = saturn_hee_cmdlist_fill_data_row,
+ .cmdlist_fill_conf_row = saturn_hee_cmdlist_fill_conf_row,
+ .crtc_cmdlist = saturn_hee_crtc_cmdlist,
+ .cmdlist_dump_node = saturn_hee_cmdlist_dump_node,
+};
+
+void saturn_enable_irq_mask(struct spacemit_crtc *a_crtc, bool enable,
+ u32 offset, u32 mask)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ if (!hwdev->base) {
+ drm_err(a_crtc->crtc.dev, "hwdev->base is NULL\n");
+ return;
+ }
+
+ if (!a_crtc->clocks_on)
+ return;
+
+ regmap_update_bits(hwdev->regmap, offset, mask, enable ? mask : 0);
+}
+
+int dpu_parse_clocks(struct device *dev, struct dpu_clk_context *clk_ctx)
+{
+ clk_ctx->pxclk = devm_clk_get(dev, "pxclk");
+ if (IS_ERR(clk_ctx->pxclk))
+ return dev_err_probe(dev, PTR_ERR(clk_ctx->pxclk),
+ "failed to get pxclk\n");
+
+ clk_ctx->mclk = devm_clk_get(dev, "mclk");
+ if (IS_ERR(clk_ctx->mclk))
+ return dev_err_probe(dev, PTR_ERR(clk_ctx->mclk),
+ "failed to get mclk\n");
+
+ clk_ctx->escclk = devm_clk_get(dev, "escclk");
+ if (IS_ERR(clk_ctx->escclk))
+ return dev_err_probe(dev, PTR_ERR(clk_ctx->escclk),
+ "failed to get escclk\n");
+
+ clk_ctx->aclk = devm_clk_get(dev, "aclk");
+ if (IS_ERR(clk_ctx->aclk))
+ return dev_err_probe(dev, PTR_ERR(clk_ctx->aclk),
+ "failed to get aclk\n");
+
+ clk_ctx->dscclk = devm_clk_get(dev, "dscclk");
+ if (IS_ERR(clk_ctx->dscclk))
+ return dev_err_probe(dev, PTR_ERR(clk_ctx->dscclk),
+ "failed to get dscclk\n");
+
+ return 0;
+}
+
+static int dpu_enable_clocks(struct spacemit_crtc *a_crtc)
+{
+ struct dpu_clk_context *clk_ctx = &a_crtc->clk_ctx;
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_display_mode *mode = &crtc->mode;
+ u64 clk_val;
+ u64 set_clk_val;
+ int ret;
+
+ ret = clk_prepare_enable(clk_ctx->pxclk);
+ if (ret)
+ goto err;
+ set_clk_val = mode->clock * 1000;
+ if (set_clk_val) {
+ set_clk_val = clk_round_rate(clk_ctx->pxclk, set_clk_val);
+ clk_val = clk_get_rate(clk_ctx->pxclk);
+ if (clk_val != set_clk_val) {
+ clk_set_rate(clk_ctx->pxclk, set_clk_val);
+ drm_dbg(crtc->dev, "pxclk=%lld\n", clk_val);
+ }
+ }
+
+ ret = clk_prepare_enable(clk_ctx->mclk);
+ if (ret)
+ goto err_pxclk;
+ clk_val = clk_get_rate(clk_ctx->mclk);
+ if (clk_val != DPU_MCLK_DEFAULT) {
+ clk_val = clk_round_rate(clk_ctx->mclk, DPU_MCLK_DEFAULT);
+ clk_set_rate(clk_ctx->mclk, clk_val);
+ drm_dbg(crtc->dev, "mclk=%lld\n", clk_val);
+ }
+
+ /* The escape clock only has to run for the "esc" reset to propagate. */
+ ret = clk_prepare_enable(clk_ctx->escclk);
+ if (ret)
+ goto err_mclk;
+
+ ret = clk_prepare_enable(clk_ctx->aclk);
+ if (ret)
+ goto err_escclk;
+ clk_val = clk_get_rate(clk_ctx->aclk);
+ if (clk_val != a_crtc->aclk) {
+ set_clk_val = clk_round_rate(clk_ctx->aclk, a_crtc->aclk);
+ if (set_clk_val != clk_val) {
+ clk_set_rate(clk_ctx->aclk, set_clk_val);
+ drm_dbg(crtc->dev, "aclk %llu -> %llu\n", clk_val,
+ set_clk_val);
+ }
+ }
+
+ ret = clk_prepare_enable(clk_ctx->dscclk);
+ if (ret)
+ goto err_aclk;
+
+ trace_dpu_enable_clocks(a_crtc->dev_id);
+ return 0;
+
+err_aclk:
+ clk_disable_unprepare(clk_ctx->aclk);
+err_escclk:
+ clk_disable_unprepare(clk_ctx->escclk);
+err_mclk:
+ clk_disable_unprepare(clk_ctx->mclk);
+err_pxclk:
+ clk_disable_unprepare(clk_ctx->pxclk);
+err:
+ drm_err(crtc->dev, "failed to enable DPU clocks: %d\n", ret);
+ return ret;
+}
+
+static int dpu_disable_clocks(struct spacemit_crtc *a_crtc)
+{
+ struct dpu_clk_context *clk_ctx = &a_crtc->clk_ctx;
+
+ trace_dpu_disable_clocks(a_crtc->dev_id);
+
+ clk_disable_unprepare(clk_ctx->pxclk);
+ clk_disable_unprepare(clk_ctx->mclk);
+ clk_disable_unprepare(clk_ctx->escclk);
+
+ clk_disable_unprepare(clk_ctx->aclk);
+ clk_disable_unprepare(clk_ctx->dscclk);
+
+ return 0;
+}
+
+u8 spacemit_plane_hw_get_format_id(u32 format)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(primary_fmts); i++) {
+ if (primary_fmts[i].format == format)
+ return primary_fmts[i].id;
+ }
+
+ return SPACEMIT_DPU_INVALID_FORMAT_ID;
+}
+
+static void saturn_enable_vsync(struct spacemit_crtc *a_crtc, bool enable)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ trace_saturn_enable_vsync("vsync", enable);
+ hwdev->enable_vsync(a_crtc, hwdev, enable);
+}
+
+void spacemit_cfg_rdy_timer_handler(struct timer_list *t)
+{
+ struct spacemit_crtc *a_crtc = timer_container_of(a_crtc, t,
+ cfg_rdy_timer);
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_device *drm = crtc->dev;
+ struct drm_pending_vblank_event *event;
+ unsigned long flags;
+
+ drm_err_ratelimited(drm, "CFG_RDY handshake timeout\n");
+ trace_saturn_ctrl_cfg_ready_timer(a_crtc->dev_id);
+
+ a_crtc->flip_done = true;
+
+ spin_lock_irqsave(&drm->event_lock, flags);
+ event = crtc->state->event;
+ if (event) {
+ crtc->state->event = NULL;
+ drm_crtc_send_vblank_event(crtc, event);
+ }
+ spin_unlock_irqrestore(&drm->event_lock, flags);
+
+ drm_crtc_vblank_put(crtc);
+}
+
+static void saturn_ctrl_cfg_ready(struct spacemit_crtc *a_crtc, bool enable)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ trace_saturn_ctrl_cfg_ready(a_crtc->dev_id, enable);
+ mod_timer(&a_crtc->cfg_rdy_timer, jiffies + msecs_to_jiffies(3000));
+ hwdev->cfg_ready(a_crtc, hwdev);
+}
+
+static void saturn_ctrl_sw_start(struct spacemit_crtc *a_crtc, bool enable)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ trace_saturn_ctrl_sw_start(a_crtc->dev_id, enable);
+
+ hwdev->sw_start(a_crtc, hwdev);
+}
+
+static int dpu_init(struct spacemit_crtc *a_crtc)
+{
+ unsigned int timeout = 1000;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ if (!a_crtc->clocks_on)
+ return 0;
+
+ while (timeout) {
+ if (hwdev->get_cfg_rdy(a_crtc, hwdev) == 0)
+ break;
+ usleep_range(100, 200);
+ timeout--;
+ }
+ if (timeout == 0)
+ drm_err(a_crtc->crtc.dev, "wait for cfg ready timed out\n");
+
+ hwdev->dpu_init(a_crtc);
+
+ return 0;
+}
+
+static void dpu_uninit(struct spacemit_crtc *a_crtc)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ if (!a_crtc->clocks_on)
+ return;
+
+ trace_dpu_uninit(a_crtc->dev_id);
+ hwdev->irq_enable(a_crtc, false);
+}
+
+static inline void dpu_isr_vblank(struct spacemit_crtc *a_crtc, bool *flip)
+{
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_device *drm = crtc->dev;
+ struct drm_pending_vblank_event *event;
+
+ drm_crtc_handle_vblank(crtc);
+
+ if (*flip)
+ return;
+ *flip = true;
+
+ /*
+ * event_lock serialises this against the commit arming
+ * crtc->state->event.
+ */
+ spin_lock(&drm->event_lock);
+ event = crtc->state->event;
+ if (event) {
+ /*
+ * Consume it exactly once, even against a concurrent
+ * commit_hw_done().
+ */
+ crtc->state->event = NULL;
+ drm_crtc_send_vblank_event(crtc, event);
+ }
+ spin_unlock(&drm->event_lock);
+
+ drm_crtc_vblank_put(crtc);
+}
+
+u32 saturn_conf_dpuctrl_rdma(struct spacemit_crtc *a_crtc)
+{
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_plane *plane;
+ u32 rdma_en = 0;
+
+ drm_atomic_crtc_for_each_plane(plane, crtc) {
+ u32 rdma_id = to_spacemit_plane_state(plane->state)->rdma_id;
+
+ if (rdma_id != RDMA_INVALID_ID)
+ rdma_en |= (1 << rdma_id);
+ }
+
+ trace_dpuctrl("rdma_en", rdma_en);
+
+ return rdma_en;
+}
+
+static u32 dpu_online_isr(struct spacemit_crtc *a_crtc)
+{
+ u32 irq_raw, irq_bit, irq_ur_bit;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ int dev_id = a_crtc->dev_id;
+
+ trace_dpu_isr(a_crtc->dev_id);
+
+ irq_raw = hwdev->get_int_sts(hwdev, dev_id);
+ trace_dpu_isr_status("ONLINE", irq_raw);
+ irq_bit = hwdev->get_irq_bit(INT_UNDERRUN, dev_id);
+ irq_ur_bit = irq_raw & irq_bit;
+ if (irq_ur_bit && !a_crtc->ur_reported) {
+ hwdev->clr_int_sts(a_crtc, irq_ur_bit, dev_id);
+ trace_dpu_isr_status("underrun", irq_ur_bit);
+ drm_err_ratelimited(a_crtc->crtc.dev, "underrun\n");
+ a_crtc->ur_reported = true;
+ }
+ irq_bit = hwdev->get_irq_bit(INT_CFG_RDY, dev_id);
+ if (irq_raw & irq_bit) {
+ hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+ trace_dpu_isr_status("cfg_rdy_clr", irq_raw & irq_bit);
+ /*
+ * A fired watchdog already sent the event and put this flip's
+ * vblank reference; re-arming the vsync path would put twice.
+ */
+ if (timer_delete(&a_crtc->cfg_rdy_timer))
+ a_crtc->flip_done = false;
+ a_crtc->ur_reported = false;
+ if (hwdev->enable_cfg_irq)
+ hwdev->enable_cfg_irq(a_crtc, hwdev, false);
+ }
+ irq_bit = hwdev->get_irq_bit(INT_VSYNC, dev_id);
+ if (irq_raw & irq_bit) {
+ hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+ trace_dpu_isr_status("vsync", irq_raw & irq_bit);
+ dpu_isr_vblank(a_crtc, &a_crtc->flip_done);
+ }
+ irq_bit = hwdev->get_irq_bit(INT_REST, dev_id) | irq_ur_bit;
+ if (irq_raw & irq_bit)
+ hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+
+ irq_bit = hwdev->get_irq_bit(INT_VSYNC_UPDATE, dev_id);
+ if (irq_raw & irq_bit)
+ hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
+
+ return 0;
+}
+
+static void dpu_run(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ trace_dpu_run(a_crtc->dev_id);
+
+ if (hwdev->conf_dpuctrl)
+ hwdev->conf_dpuctrl(crtc, old_state);
+
+ /* flush all config register writes before CFG_RDY is raised */
+ mb();
+
+ drm_crtc_vblank_get(crtc);
+ if (hwdev->enable_cfg_irq)
+ hwdev->enable_cfg_irq(a_crtc, hwdev, true);
+
+ saturn_ctrl_cfg_ready(a_crtc, true);
+
+ if (unlikely(a_crtc->is_1st_f)) {
+ drm_dbg(a_crtc->crtc.dev, "DPU start\n");
+ a_crtc->is_1st_f = false;
+ saturn_ctrl_sw_start(a_crtc, true);
+ }
+ dma_rmb();
+}
+
+static void dpu_enable_vsync(struct spacemit_crtc *a_crtc)
+{
+ saturn_enable_vsync(a_crtc, true);
+}
+
+static void dpu_disable_vsync(struct spacemit_crtc *a_crtc)
+{
+ saturn_enable_vsync(a_crtc, false);
+}
+
+const struct dpu_core_ops dpu_saturn_ops = {
+ .init = dpu_init,
+ .uninit = dpu_uninit,
+ .run = dpu_run,
+ .online_isr = dpu_online_isr,
+ .enable_clk = dpu_enable_clocks,
+ .disable_clk = dpu_disable_clocks,
+ .enable_vsync = dpu_enable_vsync,
+ .disable_vsync = dpu_disable_vsync,
+ .cal_layer_fbcmem_size = saturn_cal_layer_fbcmem_size,
+ .adjust_rdma_fbcmem = saturn_adjust_rdma_fbcmem,
+};
diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
new file mode 100644
index 000000000000..c23e4ca36d26
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
@@ -0,0 +1,896 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/module.h>
+#include <linux/mfd/syscon.h>
+#include <linux/pm_qos.h>
+#include <linux/regmap.h>
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_blend.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem.h>
+#include "dpu_saturn.h"
+#include "saturn_fbcmem.h"
+#include "../spacemit_cmdlist.h"
+#include "../spacemit_dmmu.h"
+#include "../spacemit_dpu_reg.h"
+#include <video/display_timing.h>
+#include "../spacemit_dpu_ids.h"
+#include "saturn_regs/ops_hee.h"
+#include "saturn_regs/reg_map_hee.h"
+
+#include "dpu_trace.h"
+
+#define SATURN_WRITE_TBU_REGS(hwdev, base, reg_id, cl_tbu) \
+do {\
+ dpu_write(hwdev, MMU_TBU_REG, base, tbu_base_addr##reg_id##_low, \
+ tbu->ttb_pa[reg_id] & 0xFFFFFFFF, cl_tbu); \
+ dpu_write(hwdev, MMU_TBU_REG, base, tbu_base_addr##reg_id##_high, \
+ (tbu->ttb_pa[reg_id] >> 32) & 0xFFF, cl_tbu); \
+ dpu_write(hwdev, MMU_TBU_REG, base, tbu_va##reg_id, \
+ (tbu->tbu_va[reg_id] >> 12), cl_tbu); \
+ dpu_write(hwdev, MMU_TBU_REG, base, \
+ tbu_size##reg_id, tbu->ttb_size[reg_id], cl_tbu); \
+ dpu_write(hwdev, MMU_TBU_REG, base, \
+ vsync_update_en, 1, cl_tbu); \
+} while (0)
+
+#define SATURN_WRITE_RDMA_ADDR_REG(hwdev, reg_id, rdma_id, addr, cl_rdma) \
+do { \
+ if (hwdev) { \
+ dpu_write(hwdev, RDMA_PATH_X_REG, RDMA_BASE_ADDR[rdma_id], \
+ base_addr##reg_id##_low_ly0, (addr) & 0xFFFFFFFF, \
+ cl_rdma); \
+ dpu_write(hwdev, RDMA_PATH_X_REG, RDMA_BASE_ADDR[rdma_id], \
+ base_addr##reg_id##_high_ly0, ((addr) >> 32) & 0xFFF, \
+ cl_rdma); \
+ } \
+} while (0)
+
+static const u32 RDMA_BASE_ADDR[] = {
+ RDMA0_BASE_ADDR,
+ RDMA1_BASE_ADDR,
+ RDMA2_BASE_ADDR,
+ RDMA3_BASE_ADDR,
+};
+
+static const u32 CMP_BASE_ADDR[] = {
+ CMP0_BASE_ADDR,
+ CMP1_BASE_ADDR,
+ CMP2_BASE_ADDR,
+};
+
+static const u32 TMG_BASE_ADDR[] = {
+ 0,
+ TMG0_BASE_ADDR,
+ 0,
+};
+
+static const u32 MMU_TBU_BASE_ADDR_ARRAY[] = {
+ TBU0_ADDR, TBU1_ADDR, TBU2_ADDR, TBU3_ADDR,
+ TBU4_ADDR, TBU5_ADDR, TBU6_ADDR, TBU7_ADDR,
+};
+
+void saturn_hee_enable_vsync(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+ bool enable)
+{
+ u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+ u32 mask = saturn_hee_get_irq_bit(INT_VSYNC, a_crtc->dev_id);
+
+ saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+}
+
+void saturn_hee_enable_cfg_irq(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+ bool enable)
+{
+ u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+ u32 mask = saturn_hee_get_irq_bit(INT_CFG_RDY, a_crtc->dev_id);
+
+ saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+}
+
+void saturn_hee_cfg_ready(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev)
+{
+ u32 base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+ saturn_enable_irq_mask(a_crtc, true,
+ DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK,
+ saturn_hee_get_irq_bit(INT_UNDERRUN, a_crtc->dev_id));
+ a_crtc->ur_mask_armed = true;
+ dpu_write(hwdev, DPU_CTL_REG, base, both_cfg_rdy, 1);
+}
+
+void saturn_hee_sw_start(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev)
+{
+ u32 base;
+
+ base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+ dpu_write_reg(hwdev, DPU_CTL_REG, base, sw_start, 1);
+}
+
+static void saturn_init_tmg(struct spacemit_crtc *a_crtc)
+{
+ u32 base = 0;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_display_mode *mode = &crtc->mode;
+ u16 vfp, vbp, vsync, hfp, hbp, hsync;
+ void __iomem *tmg_addr;
+ u32 value;
+
+ /* dev_id names the composer type, not the DPU instance: use TMG0. */
+ tmg_addr = hwdev->base + TMG0_BASE_ADDR;
+
+ hsync = mode->hsync_end - mode->hsync_start;
+ hbp = mode->htotal - mode->hsync_end;
+ hfp = mode->hsync_start - mode->hdisplay;
+ vsync = mode->vsync_end - mode->vsync_start;
+ vbp = mode->vtotal - mode->vsync_end;
+ vfp = mode->vsync_start - mode->vdisplay;
+ trace_drm_display_mode_info(mode);
+
+ base = TMG_BASE_ADDR[a_crtc->dev_id];
+ if (base) {
+ dpu_write(hwdev, TMG_REG, base, disp_ready_man_en, 0);
+ dpu_write(hwdev, TMG_REG, base, background_r, 0);
+ dpu_write(hwdev, TMG_REG, base, background_g, 0x0);
+ dpu_write(hwdev, TMG_REG, base, background_b, 0xff);
+ dpu_write(hwdev, TMG_REG, base, eof_1st_ln_dly_num, vfp - 7);
+ dpu_write(hwdev, TMG_REG, base, eof_2nd_ln_dly_num, vfp - 6);
+ dpu_write(hwdev, TMG_REG, base, split_en, 0);
+ dpu_write(hwdev, TMG_REG, base, cmd_screen, 0);
+ dpu_write(hwdev, TMG_REG, base, cmd_wait_en, 0);
+ dpu_write(hwdev, TMG_REG, base, cmd_wait_te, 0);
+ dpu_write(hwdev, TMG_REG, base, sof_pre_ln_num, 0);
+ dpu_write(hwdev, TMG_REG, base, hfp, hfp);
+ dpu_write(hwdev, TMG_REG, base, hbp, hbp);
+ dpu_write(hwdev, TMG_REG, base, vfp, vfp);
+ dpu_write(hwdev, TMG_REG, base, vbp, vbp);
+ dpu_write(hwdev, TMG_REG, base, hsync_width, hsync);
+ dpu_write(hwdev, TMG_REG, base, vsync_width, vsync);
+ dpu_write(hwdev, TMG_REG, base, hsp, 1);
+ dpu_write(hwdev, TMG_REG, base, vsp, 1);
+ dpu_write(hwdev, TMG_REG, base, h_active, mode->hdisplay);
+ dpu_write(hwdev, TMG_REG, base, v_active, mode->vdisplay);
+ dpu_write(hwdev, TMG_REG, base, fm_timing_en, 1);
+ dpu_write(hwdev, TMG_REG, base, user, a_crtc->out_format);
+ }
+
+ value = readl(tmg_addr + 0x14);
+ value &= 0xFFFF0000;
+ value |= vfp;
+ writel(value, tmg_addr + 0x14);
+
+ /* align vsync with hsync */
+ value = readl(tmg_addr + 0x3c);
+ value |= 0x01;
+ writel(value, tmg_addr + 0x3c);
+}
+
+static void saturn_init_regs(struct spacemit_crtc *a_crtc)
+{
+ u32 base = 0;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ base = CMP_BASE_ADDR[a_crtc->dev_id];
+ dpu_write(hwdev, CMPS_X_REG, base, bg_color_b, 0x0);
+ dpu_write(hwdev, CMPS_X_REG, base, bg_color_r, 0x0);
+ dpu_write(hwdev, CMPS_X_REG, base, bg_color_g, 0x0);
+ dpu_write(hwdev, CMPS_X_REG, base, bg_color_a, 0xFF);
+ dpu_write(hwdev, CMPS_X_REG, base, module_enable, 1);
+ dpu_write(hwdev, CMPS_X_REG, base, layer00_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer01_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer02_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer03_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer04_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer05_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer06_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer07_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer08_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer09_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer10_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer11_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer12_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer13_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer14_en, 0);
+ dpu_write(hwdev, CMPS_X_REG, base, layer15_en, 0);
+
+ base = DPU_TOP_BASE_ADDR;
+
+ base = EE_ADDR;
+ dpu_write(hwdev, EE_REG, base, m_benable, 0);
+
+ saturn_init_tmg(a_crtc);
+}
+
+static void saturn_setup_dma_top(struct spacemit_crtc *a_crtc)
+{
+ u32 base = DMA_TOP_BASE_ADDR;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ dpu_write(hwdev, DMA_TOP_REG, base, image_rr_ratio, 0x10);
+ dpu_write(hwdev, DMA_TOP_REG, base, pixel_num_th, 4);
+
+ dpu_write(hwdev, DMA_TOP_REG, base, rdma_timeout_limit, 0xFFFE);
+ dpu_write(hwdev, DMA_TOP_REG, base, wdma_timeout_limit, 0xFFFF);
+}
+
+static void saturn_setup_mmu_top(struct spacemit_crtc *a_crtc)
+{
+ unsigned int rd_outs_num = 0;
+ u32 base = MMU_TOP_BASE_ADDR;
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ rd_outs_num = RD_OUTS_NUM / 2;
+
+ dpu_write(hwdev, MMU_TOP_REG, base, rdma_timelimit, RDMA_TIMELIMIT);
+
+ dpu_write(hwdev, MMU_TOP_REG, base, sram0_tlb_axi_port_sel, 0);
+ dpu_write(hwdev, MMU_TOP_REG, base, sram1_tlb_axi_port_sel, 0);
+ dpu_write(hwdev, MMU_TOP_REG, base, dmac0_rd_outs_num, rd_outs_num);
+ dpu_write(hwdev, MMU_TOP_REG, base, dmac1_rd_outs_num, rd_outs_num);
+}
+
+void saturn_hee_irq_enable(struct spacemit_crtc *a_crtc, bool enable)
+{
+ u32 offset = DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK;
+ u32 mask;
+
+ trace_saturn_irq_enable("irq online", enable);
+ mask = saturn_hee_get_irq_bit(INT_UNDERRUN, a_crtc->dev_id) |
+ saturn_hee_get_irq_bit(INT_CFG_RDY, a_crtc->dev_id);
+ if (enable) {
+ struct spacemit_drm_private *priv =
+ a_crtc->crtc.dev->dev_private;
+
+ /*
+ * Ack first: a surviving status bit would fire as the mask
+ * opens.
+ */
+ regmap_write(priv->hwdev->regmap,
+ DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_STS, mask);
+ }
+ saturn_enable_irq_mask(a_crtc, enable, offset, mask);
+ a_crtc->ur_mask_armed = true;
+}
+
+void saturn_hee_dpu_init(struct spacemit_crtc *a_crtc)
+{
+ saturn_init_regs(a_crtc);
+ saturn_setup_dma_top(a_crtc);
+ saturn_setup_mmu_top(a_crtc);
+ saturn_hee_irq_enable(a_crtc, true);
+}
+
+#define SATURN_WRITE_COMPOSER_LAYER(id_name) \
+do {\
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _colorkey_en, \
+ 0, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _blend_mode, \
+ blend_mode, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _blend_sel, \
+ alpha_sel, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _layer_alpha, \
+ alpha, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _alpha_ratio, \
+ 0, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_left, \
+ crtc_x, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_top, \
+ crtc_y, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_right, \
+ crtc_x + crtc_w - 1, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _area_bottom, \
+ crtc_y + crtc_h - 1, cl_cmp); \
+ if (unlikely(solid_en)) { \
+ dpu_write(hwdev, CMPS_X_REG, base, \
+ layer ## id_name ## _solid_color_r, \
+ solid_r, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, \
+ layer ## id_name ## _solid_color_g, \
+ solid_g, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, \
+ layer ## id_name ## _solid_color_b, \
+ solid_b, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, \
+ layer ## id_name ## _solid_color_a, \
+ solid_a, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _solid_en, \
+ 1, cl_cmp); \
+ } else { \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _solid_en, \
+ 0, cl_cmp); \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _dma_id, \
+ rdma_id, cl_cmp); \
+ } \
+ dpu_write(hwdev, CMPS_X_REG, base, layer ## id_name ## _en, 1, cl_cmp); \
+} while (0)
+
+static void saturn_write_fbcmem_regs(struct spacemit_hw_device *hwdev,
+ struct drm_plane_state *state, u32 rdma_id,
+ u32 base, struct cmdlist_regs *cl_rdma)
+{
+ struct drm_crtc_state *crtc_state = state->crtc->state;
+ const struct spacemit_crtc_rdma *rdmas =
+ to_spacemit_crtc_state(crtc_state)->rdmas;
+ u32 size = rdmas[rdma_id].fbcmem.size;
+ u32 start = rdmas[rdma_id].fbcmem.start;
+ bool map = rdmas[rdma_id].fbcmem.map;
+
+ dpu_write_word(hwdev, base, 31, map << 28 | start << 16 | size,
+ cl_rdma);
+}
+
+
+void saturn_hee_plane_update_hw_channel(struct drm_plane *plane)
+{
+ struct drm_plane_state *state = plane->state;
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(state->crtc);
+ struct drm_framebuffer *fb = plane->state->fb;
+ struct spacemit_plane_state *spacemit_plane_state =
+ to_spacemit_plane_state(state);
+ struct cmdlist_regs *cl_cmp = NULL;
+ u32 rdma_id = spacemit_plane_state->rdma_id;
+ u8 alpha = state->alpha >> 8;
+ u16 pixel_alpha = state->pixel_blend_mode;
+ u32 src_w, src_h, src_x, src_y;
+ u32 crtc_w, crtc_h, crtc_x, crtc_y;
+ u32 alpha_sel, blend_mode = 0;
+ struct drm_display_mode *mode = &state->crtc->mode;
+
+ u32 base;
+ bool solid_en = false;
+ u32 solid_a, solid_r, solid_g, solid_b;
+ struct spacemit_drm_private *priv = plane->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct cmdlist_regs *cl_rdma = a_crtc->cl_rdma;
+
+ trace_spacemit_plane_update_hw_channel("rdma_id", rdma_id);
+
+ src_w = state->src_w >> 16;
+ src_h = state->src_h >> 16;
+ src_x = state->src_x >> 16;
+ src_y = state->src_y >> 16;
+
+ crtc_w = state->crtc_w;
+ crtc_h = state->crtc_h;
+ crtc_x = state->crtc_x;
+ crtc_y = state->crtc_y;
+
+ drm_dbg(plane->dev, "crtc_x %u crtc_y %u\n", crtc_x, crtc_y);
+
+ if (rdma_id == RDMA_INVALID_ID)
+ solid_en = true;
+
+ trace_dpu_plane_info(state, fb, rdma_id, alpha, state->rotation);
+
+ /* For solid color both src_w and src_h are 0 */
+ if (!solid_en) {
+ base = RDMA_BASE_ADDR[rdma_id];
+ /* linear scanout only: the FBC decode path is not exposed */
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_mode, 0, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_cmpsr_id,
+ a_crtc->dev_id, cl_rdma);
+ /*
+ * force online-mode routing whatever the bootloader left behind
+ */
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, is_offline, 0, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, is_two_layers, 0,
+ cl_rdma);
+
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, img_width_ly0,
+ fb->width, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, img_height_ly0,
+ fb->height, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_start_x_ly0, src_x,
+ cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_start_y_ly0, src_y,
+ cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_end_x_ly0,
+ src_x + src_w - 1, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, bbox_end_y_ly0,
+ src_y + src_h - 1, cl_rdma);
+
+ saturn_write_fbcmem_regs(hwdev, state, rdma_id, base, cl_rdma);
+
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, rot_mode_ly0, 0,
+ cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, uv_swap, 0, cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, pixel_format,
+ spacemit_plane_state->format, cl_rdma);
+ } else {
+ /* Constant-fill layer: zero-size source, transparent black. */
+ solid_r = 0;
+ solid_g = 0;
+ solid_b = 0;
+ solid_a = 0;
+ }
+
+ switch (pixel_alpha) {
+ case DRM_MODE_BLEND_COVERAGE:
+ blend_mode = 0x0;
+ break;
+ case DRM_MODE_BLEND_PREMULTI:
+ blend_mode = 0x1;
+ break;
+ case DRM_MODE_BLEND_PIXEL_NONE:
+ blend_mode = 0x0;
+ break;
+ default:
+ drm_err(plane->dev, "unsupported blend mode for pixel alpha\n");
+ }
+
+ if (state->fb->format && state->fb->format->has_alpha &&
+ pixel_alpha != DRM_MODE_BLEND_PIXEL_NONE) {
+ if (alpha != 0xff)
+ alpha_sel = 0x2;
+ else
+ alpha_sel = 0x1;
+ } else {
+ blend_mode = 0x0;
+ alpha_sel = 0x0;
+ }
+
+ cl_cmp = alloc_cmdlist_regs(CMPS_X_REG);
+
+ base = CMP_BASE_ADDR[a_crtc->dev_id];
+ dpu_write(hwdev, CMPS_X_REG, base, dst_w, mode->hdisplay, cl_cmp);
+ dpu_write(hwdev, CMPS_X_REG, base, dst_h, mode->vdisplay, cl_cmp);
+
+ SATURN_WRITE_COMPOSER_LAYER(00);
+ cmdlist_regs_packing(crtc_to_cl(plane->state->crtc), CMDLIST_MOD_COMP,
+ cl_cmp);
+ free_cmdlist_regs(cl_cmp);
+}
+
+void saturn_hee_plane_disable_hw_channel(struct drm_plane *plane,
+ struct drm_plane_state *old_state)
+{
+ u8 dev_id = to_spacemit_crtc(old_state->crtc)->dev_id;
+ u32 base = CMP_BASE_ADDR[dev_id];
+ u32 rdma_id = to_spacemit_plane_state(old_state)->rdma_id;
+ struct spacemit_drm_private *priv = plane->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct cmdlist_regs *cl_cmp = NULL;
+
+ trace_spacemit_plane_disable_hw_channel(0, rdma_id);
+ cl_cmp = alloc_cmdlist_regs(CMPS_X_REG);
+ dpu_write(hwdev, CMPS_X_REG, base, layer00_en, 0, cl_cmp);
+
+ cmdlist_regs_packing(crtc_to_cl(old_state->crtc), CMDLIST_MOD_COMP,
+ cl_cmp);
+ free_cmdlist_regs(cl_cmp);
+}
+
+void saturn_hee_conf_dpuctrl(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state)
+{
+ u32 rdma_en = 0;
+ u32 base;
+ u32 pp_base = POST_PIPE_ADDR;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ struct drm_display_mode *mode = &crtc->mode;
+ struct drm_crtc_state *crtc_state = a_crtc->crtc.state;
+ struct spacemit_crtc_state *spacemit_crtc_state =
+ to_spacemit_crtc_state(crtc_state);
+ struct cmdlist *cl = NULL;
+ struct cmdlist_regs *pp_cl = NULL;
+
+ base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+ rdma_en = saturn_conf_dpuctrl_rdma(a_crtc);
+
+ pp_cl = alloc_cmdlist_regs(POSTPIPE_REG);
+ /*
+ * postpipe should be configured no matter whether pq function is on or
+ * off
+ */
+ dpu_write_word(hwdev, pp_base, 0, 0, pp_cl);
+ dpu_write(hwdev, POSTPIPE_REG, pp_base, m_inwidth, mode->hdisplay,
+ pp_cl);
+ dpu_write(hwdev, POSTPIPE_REG, pp_base, m_inheight, mode->vdisplay,
+ pp_cl);
+
+ cmdlist_regs_packing(crtc_to_cl(crtc), CMDLIST_MOD_COMP, pp_cl);
+ free_cmdlist_regs(pp_cl);
+
+ cmdlist_sort_by_group(crtc);
+ cmdlist_atomic_commit(crtc, old_state);
+
+ dpu_write(hwdev, DPU_CTL_REG, base, nml_scl_en, 0);
+ dpu_write(hwdev, DPU_CTL_REG, base, nml_rch_en, rdma_en);
+ dpu_write(hwdev, DPU_CTL_REG, base, timing_inter0, 8);
+ dpu_write(hwdev, DPU_CTL_REG, base, timing_inter1, 8);
+ dpu_write(hwdev, DPU_CTL_REG, base, video_mod, 1);
+ dpu_write(hwdev, DPU_CTL_REG, base, dbg_mod, 0);
+
+ dpu_write(hwdev, DPU_CTL_REG, base, nml_outctl_en, 0x1);
+ dpu_write(hwdev, DPU_CTL_REG, base, nml_frm_timing_en, 2);
+ cl = &spacemit_crtc_state->cl;
+ if (cl->va) {
+ crtc_cmdlist_sort_by_group(a_crtc);
+ crtc_cmdlist_atomic_commit(a_crtc);
+ }
+}
+
+u32 saturn_hee_get_cfg_rdy(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev)
+{
+ u32 base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+ return dpu_read_word(hwdev, base, 2);
+}
+
+u32 saturn_hee_get_int_sts(struct spacemit_hw_device *hwdev, int dev_id)
+{
+ /* Both K3 DPU instances are wired as COMPOSER1 (see master_bind). */
+ if (dev_id != COMPOSER1)
+ return 0;
+
+ return dpu_read_word(hwdev, DPU_INT_BASE_ADDR, 12);
+}
+
+static const struct spacemit_crtc_irq_bitfield {
+ enum spacemit_dpu_irq irq_id;
+ u32 irq_bit;
+} irq_list[] = {
+ [INT_UNDERRUN] = {INT_UNDERRUN, DPU_INT_FRM_TIMING_UNFLOW},
+ [INT_CFG_RDY] = {INT_CFG_RDY, DPU_INT_CFG_RDY_CLR},
+ [INT_VSYNC] = {INT_VSYNC, DPU_INT_FRM_TIMING_VSYNC},
+ [INT_EOF] = {INT_EOF, DPU_INT_FRM_TIMING_EOF},
+ [INT_REST] = {INT_REST, DPU_REST_INT_BITS},
+};
+
+u32 saturn_hee_get_irq_bit(enum spacemit_dpu_irq irq_id, int dev_id)
+{
+ if (irq_id >= ARRAY_SIZE(irq_list))
+ return 0;
+
+ if (dev_id == COMPOSER1 || dev_id == COMPOSER2)
+ return irq_list[irq_id].irq_bit;
+ else
+ return 0;
+}
+
+void saturn_hee_clr_int_sts(struct spacemit_crtc *a_crtc, u32 data, int dev_id)
+{
+ struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+
+ /* Both K3 DPU instances are wired as COMPOSER1 (see master_bind). */
+ if (dev_id != COMPOSER1)
+ return;
+
+ regmap_write(hwdev->regmap, DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_STS,
+ data);
+ if ((saturn_hee_get_irq_bit(INT_UNDERRUN, dev_id) & data) &&
+ a_crtc->ur_mask_armed) {
+ saturn_enable_irq_mask(a_crtc, false,
+ DPU_INT_BASE_ADDR + DPU_ONLINE_IRQ_MSK,
+ saturn_hee_get_irq_bit(INT_UNDERRUN, dev_id));
+ a_crtc->ur_mask_armed = false;
+ }
+}
+
+void saturn_hee_enable_cmdlist(struct spacemit_crtc *a_crtc, struct spacemit_hw_device *hwdev,
+ int id, bool enable)
+{
+ u32 val = 0;
+ u32 base = 0;
+ struct drm_crtc *crtc = &a_crtc->crtc;
+ struct drm_crtc_state *crtc_state = crtc->state;
+ struct spacemit_crtc_state *spacemit_state =
+ to_spacemit_crtc_state(crtc_state);
+ struct spacemit_crtc_rdma *rdmas = spacemit_state->rdmas;
+
+ base = DPU_SCENE_CTL_ADDR(a_crtc->dev_id);
+
+ dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+ dpu_ctl_top_reg_0, id, cmdlist_rch_en,
+ enable ? 1 : 0);
+ val = dpu_read_reg(hwdev, DPU_CTL_REG, base, nml_rch_vrt_reuse);
+ if (enable && rdmas[id].use_cnt > 1)
+ val |= (1 << id);
+ else
+ val &= ~(1 << id);
+
+ dpu_write(hwdev, DPU_CTL_REG, base, nml_rch_vrt_reuse, val);
+}
+
+void saturn_hee_cfg_cmdlist(struct spacemit_hw_device *hwdev, int id, u32 chy,
+ u32 addrl, u32 addrh)
+{
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+ cmdlist_reg_48, id, cmdlist_ch_y_first, chy);
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+ cmdlist_reg_0, id, cmdlist_ch_start_addrl, addrl);
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR,
+ cmdlist_reg_16, id, cmdlist_ch_start_addrh, addrh);
+}
+
+void saturn_hee_rdma_dmmu(struct spacemit_hw_device *hwdev, u8 tbu_id, struct tbu_instance *tbu,
+ struct drm_framebuffer *fb, u32 val, struct cmdlist_regs *cl_rdma,
+ struct drm_plane *plane)
+{
+ u32 base;
+ u8 rdma_id = tbu_id / 2;
+ struct cmdlist_regs *cl_tbu = NULL;
+
+ SATURN_WRITE_RDMA_ADDR_REG(hwdev, 0, rdma_id, tbu->tbu_va[0], cl_rdma);
+ SATURN_WRITE_RDMA_ADDR_REG(hwdev, 1, rdma_id, tbu->tbu_va[1], cl_rdma);
+ SATURN_WRITE_RDMA_ADDR_REG(hwdev, 2, rdma_id, tbu->tbu_va[2], cl_rdma);
+
+ base = RDMA_BASE_ADDR[rdma_id];
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, rdma_stride0_layer0,
+ fb->pitches[0], cl_rdma);
+ dpu_write(hwdev, RDMA_PATH_X_REG, base, rdma_stride1_layer0,
+ fb->pitches[1], cl_rdma);
+
+ cl_tbu = alloc_cmdlist_regs(MMU_TBU_REG);
+ base = MMU_TBU_BASE_ADDR_ARRAY[tbu_id];
+ SATURN_WRITE_TBU_REGS(hwdev, base, 0, cl_tbu);
+ SATURN_WRITE_TBU_REGS(hwdev, base, 1, cl_tbu);
+ SATURN_WRITE_TBU_REGS(hwdev, base, 2, cl_tbu);
+
+ dpu_write_word(hwdev, base, 0, val, cl_tbu);
+ cmdlist_regs_packing(plane_to_cl(plane), CMDLIST_MOD_DMMU, cl_tbu);
+ free_cmdlist_regs(cl_tbu);
+}
+
+static u32 saturn_get_cl_chy_addr(void)
+{
+ return CMDLIST_BASE_ADDR + CMDLIST_CH_Y;
+}
+
+static u32 saturn_get_cl_start_cmps_y_addr(void)
+{
+ return DPU_CTRL_BASE_ADDR + CMDLIST_CH_START_CMPS_Y;
+}
+
+static u32 saturn_get_cl_cfg_rdy_addr(void)
+{
+ return DPU_CTRL_BASE_ADDR + CMDLIST_CFG_READY;
+}
+
+int saturn_hee_get_cl_rdma_buf(struct spacemit_crtc *a_crtc)
+{
+ a_crtc->cl_rdma = alloc_cmdlist_regs(RDMA_PATH_X_REG);
+ if (!a_crtc->cl_rdma)
+ return -ENOMEM;
+ else
+ return 0;
+}
+
+struct cmdlist_header {
+ u64 next_list_addr : 39;
+ /* reserved */
+ u32: 1;
+ u32 nod_len : 16;
+ /* 1: the last cmdlist node, 2: pending node, otherwise 0 */
+ u32 nod_type : 2;
+ u32 next_nod_secu : 1;
+ u64 wait_event_low : 5;
+ u64 wait_event : 59;
+ /* reserved */
+ u32: 5;
+};
+
+struct cmdlist_row {
+ u32 module_cfg_addr : 19;
+ u32 module_cfg_strobe : 12;
+ /* the last row tag = 1, else tag = 0. */
+ u32 row_eof_tag : 1;
+ u32 module_regs[3];
+};
+
+/* DMA stream layout consumed by the cmdlist engine; sizes are load-bearing. */
+static_assert(sizeof(struct cmdlist_header) == 16);
+static_assert(sizeof(struct cmdlist_row) == 16);
+
+#define CL_HEADER_SZ sizeof(struct cmdlist_header)
+#define CL_ROW_SZ sizeof(struct cmdlist_row)
+
+#define CMDLIST_RDMA_CFG_RDY(ch) BIT(ch)
+#define CMDLIST_CMPS_CFG_RDY(ch) BIT((ch) + 20)
+
+#define CMDLIST_WAIT_EVENT_BIT_RDMA_RELOAD BIT(4)
+#define CMDLIST_WAIT_EVENT_BIT_CMPS_RELOAD BIT(6)
+
+void saturn_hee_cmdlist_fill_data_row(struct cmdlist *cl, u32 strobe,
+ u32 offset, u32 value[])
+{
+ struct cmdlist_row *row;
+ u8 i;
+
+ /*
+ * packing counts rows blindly; drop the row rather than the buffer end
+ */
+ if (WARN_ONCE(CL_HEADER_SZ + (cl->nod_len + 1) * CL_ROW_SZ > cl->size,
+ "cmdlist buffer full (rows %u)\n", cl->nod_len))
+ return;
+
+ row = (struct cmdlist_row *)((char *)cl->va + CL_HEADER_SZ) + cl->nod_len;
+ row->module_cfg_addr = offset >> 2;
+ row->module_cfg_strobe = strobe;
+
+ for (i = 0; i < CMDLIST_ROW_REGS; i++)
+ if (strobe & CMDLIST_REG_STROBE(i))
+ row->module_regs[i] = value[i];
+}
+
+void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
+ u8 dev_id)
+{
+ struct cmdlist_header *header;
+ struct cmdlist_row *row;
+ struct spacemit_plane_state *spacemit_pstate = NULL;
+ u8 rch_id = 0;
+ u32 wait_event_bits = 0, cfg_rdy_bits = 0;
+ int cur_crtc_y = -1, cur_crtc_h = -1, next_crtc_y = -1;
+
+ /*
+ * CRTC-level cmdlists live inside a crtc_state; only planes have one.
+ */
+ if (cl->type == CMDLIST_PLANE) {
+ spacemit_pstate = cl_to_spacemit_pstate(cl);
+ rch_id = spacemit_pstate->rdma_id;
+ }
+
+ /* At most three rows are appended below; bail before overrunning. */
+ if (WARN_ONCE(CL_HEADER_SZ + (cl->nod_len + 3) * CL_ROW_SZ > cl->size,
+ "cmdlist buffer full (type %u, rows %u)\n",
+ cl->type, cl->nod_len))
+ return;
+
+ header = (struct cmdlist_header *)(cl->va);
+ row = (struct cmdlist_row *)((char *)header + CL_HEADER_SZ + cl->nod_len * CL_ROW_SZ);
+ if (cl->next) {
+ if (dev_id == CMDLIST_CMP_INVALID) {
+ trace_u64_data("rch_id", rch_id);
+ row->module_cfg_addr = ((saturn_get_cl_chy_addr() + rch_id * 4) >> 2);
+ } else {
+ trace_u64_data("dev_id", dev_id);
+ row->module_cfg_addr = ((saturn_get_cl_chy_addr() + (5 + dev_id) * 4) >> 2);
+ }
+ row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+ if (cl->cmdlist_ch_y_other != CMDLIST_SENTINEL_POISON)
+ next_crtc_y = cl->cmdlist_ch_y_other;
+ else if (cl->next->type == CMDLIST_PLANE)
+ next_crtc_y = cl_to_spacemit_pstate(cl->next)->state.crtc_y;
+ else
+ next_crtc_y = 0;
+ row->module_regs[0] = next_crtc_y;
+ cl->nod_len++;
+ row++;
+ }
+
+ if (cl->type == CMDLIST_PLANE) {
+ if (cl->rch_start_cmps_y != CMDLIST_SENTINEL_POISON)
+ cur_crtc_y = cl->rch_start_cmps_y;
+ else
+ cur_crtc_y = cl_to_spacemit_pstate(cl)->state.crtc_y;
+ cur_crtc_h = cl_to_spacemit_pstate(cl)->state.crtc_h;
+ /* the cmdlist nodes don't belong to the same plane */
+ if (next_crtc_y != -1 && cl_to_spacemit_pstate(cl->next) != spacemit_pstate &&
+ (cur_crtc_y + cur_crtc_h + 24) > next_crtc_y) {
+ drm_err_ratelimited(spacemit_pstate->state.crtc->dev,
+ "invalid gap: cur_y=%d cur_h=%d next_y=%d\n",
+ cur_crtc_y, cur_crtc_h, next_crtc_y);
+ }
+ row->module_cfg_addr = (saturn_get_cl_start_cmps_y_addr() + rch_id * 4) >> 2;
+ row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+ row->module_regs[0] =
+ (cur_crtc_y >= 24) ? (cur_crtc_y - 24) : 0;
+ cl->nod_len++;
+ row++;
+ }
+
+ if (cl->mode_mask & CMDLIST_MOD_RDMA) {
+ wait_event_bits |= CMDLIST_WAIT_EVENT_BIT_RDMA_RELOAD;
+ cfg_rdy_bits |= CMDLIST_RDMA_CFG_RDY(rch_id);
+ }
+ if (cl->mode_mask & CMDLIST_MOD_COMP) {
+ wait_event_bits |= CMDLIST_WAIT_EVENT_BIT_CMPS_RELOAD;
+ cfg_rdy_bits |= CMDLIST_CMPS_CFG_RDY(dev_id);
+ }
+ row->module_cfg_addr = (saturn_get_cl_cfg_rdy_addr()) >> 2;
+ row->module_cfg_strobe = CMDLIST_REG_STROBE(0);
+ row->module_regs[0] = cfg_rdy_bits;
+ row->row_eof_tag = 1;
+ cl->nod_len++;
+ row++;
+
+ if (cl->next)
+ header->next_list_addr = cl->next->pa;
+ else
+ header->nod_type = 1;
+ header->nod_len = cl->nod_len + 1; /* include header */
+ header->wait_event_low = wait_event_bits & 0x1F;
+ header->wait_event = wait_event_bits >> 5;
+}
+
+void saturn_hee_crtc_cmdlist(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
+ struct spacemit_drm_private *priv, u8 crtc_id, u8 dev_id)
+{
+ u32 val = 0;
+
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_48,
+ hwdev->rdma_nums + 2 + crtc_id, cmdlist_ch_y_first,
+ val);
+
+ val = (lower_32_bits(priv->cmdlist_groups[hwdev->rdma_nums + crtc_id]->pa) &
+ CMDLIST_ADDRL_ALIGN_MASK) >> CMDLIST_ADDRL_ALIGN_BITS;
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_0,
+ hwdev->rdma_nums + 2 + crtc_id,
+ cmdlist_ch_start_addrl, val);
+ val = upper_32_bits(priv->cmdlist_groups[hwdev->rdma_nums + crtc_id]->pa);
+ dpu_write_reg_arr(hwdev, CMDLIST_REG, CMDLIST_BASE_ADDR, cmdlist_reg_16,
+ hwdev->rdma_nums + 2 + crtc_id,
+ cmdlist_ch_start_addrh, val);
+ dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+ dpu_ctl_top_reg_10, dev_id, cmdlist_cmps_top_en, 1);
+ dpu_write_reg_arr(hwdev, DPU_CTL_TOP_REG, DPU_CTRL_BASE_ADDR,
+ dpu_ctl_top_reg_10, dev_id, cmdlist_cmps_other_en, 1);
+
+ priv->cmdlist_groups[hwdev->rdma_nums + crtc_id] = NULL;
+}
+
+#define DUMP_BUF_LEN 100
+void saturn_hee_cmdlist_dump_node(struct cmdlist *cl)
+{
+ struct cmdlist_header *header = (struct cmdlist_header *)(cl->va);
+ ssize_t ret = 0;
+ char tmp[DUMP_BUF_LEN] = {0x0};
+
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "header: ");
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "next_addr:0x%llx ",
+ (unsigned long long)header->next_list_addr);
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "rows:%d ",
+ header->nod_len);
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "type:%d ",
+ header->nod_type);
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "event_low:0x%llx ",
+ (unsigned long long)header->wait_event_low);
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret, "event:0x%llx ",
+ (unsigned long long)header->wait_event);
+ trace_plat_cmdlist_dump_node(tmp);
+
+ for (int i = 0; i < header->nod_len - 1; i++) {
+ struct cmdlist_row *row = (struct cmdlist_row *)((char *)header + CL_HEADER_SZ) + i;
+ u32 addr = row->module_cfg_addr << 2;
+
+ ret = 0;
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+ "row[%4d:tag:%d]: ", i, row->row_eof_tag);
+
+ for (int j = 0; j < CMDLIST_ROW_REGS; j++) {
+ if (row->module_cfg_strobe & CMDLIST_REG_STROBE(j))
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+ "[%#10x:%#10x]", addr + j * 4,
+ row->module_regs[j]);
+ else
+ ret += scnprintf(tmp + ret, sizeof(tmp) - ret,
+ "%#11x:%#11x", addr + j * 4,
+ row->module_regs[j]);
+ }
+ trace_plat_cmdlist_dump_node(tmp);
+ }
+}
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
new file mode 100644
index 000000000000..144716ec9b80
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/stddef.h>
+#include <linux/export.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_mode.h>
+#include <drm/drm_fourcc.h>
+#include "../spacemit_cmdlist.h"
+#include "saturn_fbcmem.h"
+
+int get_raw_data_plane_rdma_mem_size(u32 drm_4cc_fmt, u32 plane_crop_width,
+ u32 *output_mem_size)
+{
+ u32 ret_mem_size = 0;
+ const struct drm_format_info *info = NULL;
+
+ info = drm_format_info(drm_4cc_fmt);
+ if (info->cpp[0] == 0 || info->num_planes != 1 || info->is_yuv)
+ return -EINVAL;
+
+ /* 64-byte rounding is a dpu hardware request */
+ ret_mem_size = roundup(plane_crop_width * info->cpp[0], 64);
+ ret_mem_size = ret_mem_size / 32;
+
+ if (output_mem_size)
+ *output_mem_size = ret_mem_size;
+
+ return 0;
+}
+
+int saturn_cal_layer_fbcmem_size(struct drm_plane *plane,
+ struct drm_plane_state *state)
+{
+ struct spacemit_plane_state *pstate = to_spacemit_plane_state(state);
+ u32 drm_4cc_fmt = pstate->state.fb->format->format;
+ u32 crop_w = pstate->state.src_w >> 16;
+
+ /* Only linear sizing: the compressed path is not exposed. */
+ return get_raw_data_plane_rdma_mem_size(drm_4cc_fmt, crop_w,
+ &pstate->fbcmem_size);
+}
+
+/* Even RDMA (0, 2, ...): sole owner of the front of its fbc memory bank. */
+static int saturn_fbcmem_place_even(struct spacemit_hw_device *hwdev,
+ struct spacemit_crtc_rdma *rdmas,
+ u32 *fbc_mems_left, u8 index)
+{
+ u32 cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+ u32 pri_fbcmem_size = hwdev->fbcmem_sizes[index / 2] / FBCMEM_UNIT;
+
+ if (cur_rdma_fbcmem_size > pri_fbcmem_size)
+ return -ENOSPC;
+
+ /* The preceding odd RDMA may not already share this bank. */
+ if (index > 0 && rdmas[index - 1].fbcmem.map)
+ return -ENOSPC;
+
+ rdmas[index].fbcmem.start = 0;
+ rdmas[index].fbcmem.map = true;
+ fbc_mems_left[index / 2] -= cur_rdma_fbcmem_size;
+
+ return 0;
+}
+
+/* Odd RDMA takes what is left of its own bank, spilling into the next. */
+static int saturn_fbcmem_place_odd(struct spacemit_hw_device *hwdev,
+ struct spacemit_crtc_rdma *rdmas,
+ u32 *fbc_mems_left, u8 index)
+{
+ u32 cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+ u32 pri_fbcmem_size = hwdev->fbcmem_sizes[index / 2] / FBCMEM_UNIT;
+ u8 rdma_nums = hwdev->rdma_nums;
+ u8 sec_fbcmem_index;
+ u32 sec_fbcmem_size;
+
+ if (cur_rdma_fbcmem_size <= fbc_mems_left[index / 2]) { /* not share fbc mem */
+ rdmas[index].fbcmem.map = false;
+ rdmas[index].fbcmem.start =
+ pri_fbcmem_size - fbc_mems_left[index / 2];
+ rdmas[index].fbcmem.size = fbc_mems_left[index / 2]; /* use all the mem left */
+ fbc_mems_left[index / 2] = 0;
+ return 0;
+ }
+
+ /* Need to share the next bank's fbc mem. */
+ sec_fbcmem_index = (index / 2 + 1) % (rdma_nums / 2);
+ sec_fbcmem_size = hwdev->fbcmem_sizes[sec_fbcmem_index] / FBCMEM_UNIT;
+ if (cur_rdma_fbcmem_size > fbc_mems_left[index / 2] + sec_fbcmem_size)
+ return -ENOSPC;
+
+ if (index == rdma_nums - 1) { /* last rdma id */
+ /* bank 0 is already in use */
+ if (fbc_mems_left[0] != hwdev->fbcmem_sizes[0] / FBCMEM_UNIT)
+ return -ENOSPC;
+
+ rdmas[index].fbcmem.size =
+ fbc_mems_left[index / 2] + sec_fbcmem_size;
+ cur_rdma_fbcmem_size = rdmas[index].fbcmem.size;
+ }
+ fbc_mems_left[sec_fbcmem_index] -= (cur_rdma_fbcmem_size - fbc_mems_left[index / 2]);
+ rdmas[index].fbcmem.start = pri_fbcmem_size - fbc_mems_left[index / 2];
+ rdmas[index].fbcmem.map = true;
+ fbc_mems_left[index / 2] = 0;
+
+ return 0;
+}
+
+int saturn_adjust_rdma_fbcmem(struct spacemit_hw_device *hwdev,
+ struct spacemit_crtc_rdma *rdmas)
+{
+ int ret = -ENOSPC;
+ u8 index = 0;
+ u32 *fbc_mems_left = NULL;
+ u8 rdma_nums = hwdev->rdma_nums;
+
+ fbc_mems_left = kzalloc_objs(*fbc_mems_left, rdma_nums / 2);
+ if (!fbc_mems_left) {
+ ret = -ENOMEM;
+ goto free;
+ }
+ for (index = 0; index < rdma_nums / 2; index++)
+ fbc_mems_left[index] = hwdev->fbcmem_sizes[index] / FBCMEM_UNIT;
+
+ for (index = 0; index < rdma_nums; index++) {
+ int err;
+
+ if (rdmas[index].fbcmem.size == 0)
+ continue;
+
+ if (index % 2 == 0)
+ err = saturn_fbcmem_place_even(hwdev, rdmas,
+ fbc_mems_left, index);
+ else
+ err = saturn_fbcmem_place_odd(hwdev, rdmas,
+ fbc_mems_left, index);
+ if (err) {
+ ret = err;
+ goto free;
+ }
+ }
+ ret = 0;
+
+free:
+ kfree(fbc_mems_left);
+
+ return ret;
+}
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h
new file mode 100644
index 000000000000..a4998dbc682f
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SATURN_FBCMEM_H_
+#define _SATURN_FBCMEM_H_
+
+#include <drm/drm_plane.h>
+#include "../spacemit_crtc.h"
+#include "../spacemit_dpu_reg.h"
+#include "dpu_saturn.h"
+
+#define FBCMEM_UNIT (32) /* fbcmem is 32 bytes per unit */
+
+int get_raw_data_plane_rdma_mem_size(u32 drm_4cc_fmt, u32 plane_crop_width,
+ u32 *output_mem_size);
+
+int saturn_cal_layer_fbcmem_size(struct drm_plane *plane,
+ struct drm_plane_state *state);
+
+int saturn_adjust_rdma_fbcmem(struct spacemit_hw_device *hwdev,
+ struct spacemit_crtc_rdma *rdmas);
+
+#endif
diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h b/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h
new file mode 100644
index 000000000000..7f5f01966377
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef _SATURN_hee_OPS_H_
+#define _SATURN_hee_OPS_H_
+
+#include "../../spacemit_crtc.h"
+
+void saturn_hee_enable_vsync(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, bool enable);
+void saturn_hee_enable_cfg_irq(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, bool enable);
+void saturn_hee_cfg_ready(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+void saturn_hee_sw_start(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+void saturn_hee_dpu_init(struct spacemit_crtc *a_crtc);
+void saturn_hee_plane_update_hw_channel(struct drm_plane *plane);
+void saturn_hee_plane_disable_hw_channel(struct drm_plane *plane,
+ struct drm_plane_state *old_state);
+void saturn_hee_irq_enable(struct spacemit_crtc *a_crtc, bool enable);
+void saturn_hee_conf_dpuctrl(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state);
+u32 saturn_hee_get_cfg_rdy(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev);
+u32 saturn_hee_get_int_sts(struct spacemit_hw_device *hwdev, int dev_id);
+u32 saturn_hee_get_irq_bit(enum spacemit_dpu_irq irq_id, int dev_id);
+void saturn_hee_clr_int_sts(struct spacemit_crtc *a_crtc, u32 data, int dev_id);
+void saturn_hee_enable_cmdlist(struct spacemit_crtc *a_crtc,
+ struct spacemit_hw_device *hwdev, int id,
+ bool enable);
+void saturn_hee_cfg_cmdlist(struct spacemit_hw_device *hwdev, int id, u32 chy,
+ u32 addrl, u32 addrh);
+void saturn_hee_rdma_dmmu(struct spacemit_hw_device *hwdev, u8 tbu_id,
+ struct tbu_instance *tbu, struct drm_framebuffer *fb,
+ u32 val, struct cmdlist_regs *cl_rdma,
+ struct drm_plane *plane);
+int saturn_hee_get_cl_rdma_buf(struct spacemit_crtc *a_crtc);
+void saturn_hee_cmdlist_fill_data_row(struct cmdlist *cl, u32 strobe,
+ u32 offset, u32 value[]);
+void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl,
+ struct spacemit_hw_device *hwdev,
+ u8 dev_id);
+void saturn_hee_crtc_cmdlist(struct cmdlist *cl,
+ struct spacemit_hw_device *hwdev,
+ struct spacemit_drm_private *priv, u8 crtc_id,
+ u8 dev_id);
+void saturn_hee_cmdlist_dump_node(struct cmdlist *cl);
+#endif
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend
2026-07-25 3:16 ` [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang
@ 2026-07-25 5:22 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:22 UTC (permalink / raw)
To: Cody Kang
Cc: linux-phy, devicetree, vkoul, robh, dri-devel, olteanv,
neil.armstrong, conor+dt
Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] Usage of sleep-capable functions in atomic CRTC and plane hardware programming callbacks.
- [High] Unchecked return value of `clk_round_rate` leads to hardware misconfiguration.
- [High] Data race on `flip_done` between the timeout handler and ISR leading to vblank refcount underflow.
- [High] Unconditional dereference of `state->fb` triggers kernel panic for solid fill planes.
- [High] When the cmdlist buffer overflows, `cl->nod_len` is incremented despite the row write being aborted, causing the hardware to read out-of-bounds DMA memory.
- [High] The `cl_rdma` register buffer is never packed into the cmdlist and gets corrupted when shared across planes.
- [High] Signed plane coordinates are directly cast to u32, causing massive out-of-bounds register programming when off-screen.
- [Medium] The `INT_VSYNC_UPDATE` interrupt is missing from the `irq_list` mapping array, preventing its status from being cleared.
--
--- Patch [9]: [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend ---
commit 1cf09ad0cc7d13639bcbfc63e90d8f431590bfbd
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU hardware backend
Add the register-level programming of one Saturn instance: clock and
reset bring-up, scene control and timing generator setup, RDMA channel
and composer layer programming, FBC line-buffer budgeting, interrupt
dispatch and the cfg-ready/vsync handshake that paces commits. The
backend is exposed as an ops table and hardware descriptor to be
aggregated by the KMS layer in an upcoming patch.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
> index 0000000000000..8d5f13bc5373c
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
[ ... ]
> +static int dpu_enable_clocks(struct spacemit_crtc *a_crtc)
> +{
> + struct dpu_clk_context *clk_ctx = &a_crtc->clk_ctx;
> + struct drm_crtc *crtc = &a_crtc->crtc;
> + struct drm_display_mode *mode = &crtc->mode;
> + u64 clk_val;
> + u64 set_clk_val;
> + int ret;
> +
> + ret = clk_prepare_enable(clk_ctx->pxclk);
> + if (ret)
> + goto err;
> + set_clk_val = mode->clock * 1000;
> + if (set_clk_val) {
> + set_clk_val = clk_round_rate(clk_ctx->pxclk, set_clk_val);
[Severity: High]
Does this code properly handle negative error codes from clk_round_rate()?
If clk_round_rate() fails and returns an error like -EINVAL, the unchecked
value is cast directly to u64 set_clk_val. This will sign-extend the error
code into a massive positive integer before it gets passed to clk_set_rate(),
which can lead to hardware misconfiguration.
> + clk_val = clk_get_rate(clk_ctx->pxclk);
> + if (clk_val != set_clk_val) {
> + clk_set_rate(clk_ctx->pxclk, set_clk_val);
> + drm_dbg(crtc->dev, "pxclk=%lld\n", clk_val);
> + }
> + }
[ ... ]
> +static inline void dpu_isr_vblank(struct spacemit_crtc *a_crtc, bool *flip)
> +{
> + struct drm_crtc *crtc = &a_crtc->crtc;
> + struct drm_device *drm = crtc->dev;
> + struct drm_pending_vblank_event *event;
> +
> + drm_crtc_handle_vblank(crtc);
> +
> + if (*flip)
> + return;
> + *flip = true;
[Severity: High]
Can this race condition cause a vblank reference underflow?
The timer handler spacemit_cfg_rdy_timer_handler() sets flip_done to true and
calls drm_crtc_vblank_put() asynchronously.
If the ISR runs concurrently, both threads can observe flip_done as false
due to the lockless TOCTOU check here. They will both set it to true and
drop the vblank reference, putting the reference twice.
> +
> + /*
> + * event_lock serialises this against the commit arming
> + * crtc->state->event.
> + */
> + spin_lock(&drm->event_lock);
[ ... ]
> +static u32 dpu_online_isr(struct spacemit_crtc *a_crtc)
> +{
> + u32 irq_raw, irq_bit, irq_ur_bit;
> + struct spacemit_drm_private *priv = a_crtc->crtc.dev->dev_private;
> + struct spacemit_hw_device *hwdev = priv->hwdev;
> + int dev_id = a_crtc->dev_id;
> +
> + trace_dpu_isr(a_crtc->dev_id);
[ ... ]
> + irq_bit = hwdev->get_irq_bit(INT_REST, dev_id) | irq_ur_bit;
> + if (irq_raw & irq_bit)
> + hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
> +
> + irq_bit = hwdev->get_irq_bit(INT_VSYNC_UPDATE, dev_id);
> + if (irq_raw & irq_bit)
> + hwdev->clr_int_sts(a_crtc, irq_bit, dev_id);
[Severity: Medium]
Is it possible to clear the INT_VSYNC_UPDATE interrupt?
The irq_list array in dpu_saturn_hee.c only defines up to INT_REST.
When get_irq_bit() is called with INT_VSYNC_UPDATE (index 5), it triggers
the ARRAY_SIZE(irq_list) check and returns 0. Consequently, the interrupt
status can never be cleared by this handler.
> +
> + return 0;
> +}
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
> index 0000000000000..c23e4ca36d26b
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
[ ... ]
> +void saturn_hee_plane_update_hw_channel(struct drm_plane *plane)
> +{
> + struct drm_plane_state *state = plane->state;
> + struct spacemit_crtc *a_crtc = to_spacemit_crtc(state->crtc);
> + struct drm_framebuffer *fb = plane->state->fb;
[ ... ]
> + src_w = state->src_w >> 16;
> + src_h = state->src_h >> 16;
> + src_x = state->src_x >> 16;
> + src_y = state->src_y >> 16;
> +
> + crtc_w = state->crtc_w;
> + crtc_h = state->crtc_h;
> + crtc_x = state->crtc_x;
> + crtc_y = state->crtc_y;
[Severity: High]
Can this cause massive out-of-bounds register programming if the plane is
partially off-screen?
The plane coordinates state->crtc_x and state->crtc_y are signed 32-bit
integers. Directly assigning them to unsigned 32-bit variables will cause
negative values to overflow into extremely large positive integers.
When these are written to the hardware area control registers, it creates
invalid constraints like area_left being greater than area_right.
> +
> + drm_dbg(plane->dev, "crtc_x %u crtc_y %u\n", crtc_x, crtc_y);
> +
> + if (rdma_id == RDMA_INVALID_ID)
> + solid_en = true;
> +
> + trace_dpu_plane_info(state, fb, rdma_id, alpha, state->rotation);
> +
> + /* For solid color both src_w and src_h are 0 */
> + if (!solid_en) {
> + base = RDMA_BASE_ADDR[rdma_id];
> + /* linear scanout only: the FBC decode path is not exposed */
> + dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_mode, 0, cl_rdma);
[Severity: High]
Is the cl_rdma register buffer ever flushed to the command list?
RDMA configurations are written to a_crtc->cl_rdma, but cmdlist_regs_packing()
is never called for this buffer, meaning the configurations are discarded and
never reach the hardware.
Furthermore, does sharing a_crtc->cl_rdma across planes corrupt the state?
When dpu_write() is called, it unconditionally overwrites cl_p->base. If
multiple planes are updated concurrently, the last plane will overwrite the
base address for all prior planes' registers in this shared buffer.
> + dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_cmpsr_id,
> + a_crtc->dev_id, cl_rdma);
[ ... ]
> + } else {
> + /* Constant-fill layer: zero-size source, transparent black. */
> + solid_r = 0;
> + solid_g = 0;
> + solid_b = 0;
> + solid_a = 0;
> + }
> +
> + switch (pixel_alpha) {
[ ... ]
> + }
> +
> + if (state->fb->format && state->fb->format->has_alpha &&
[Severity: High]
Will this crash if the plane is a solid fill plane?
In DRM, solid fill planes do not require an attached framebuffer, meaning
state->fb can be NULL. Unconditionally dereferencing state->fb->format here
without verifying if state->fb is valid will trigger a kernel panic.
> + pixel_alpha != DRM_MODE_BLEND_PIXEL_NONE) {
> + if (alpha != 0xff)
> + alpha_sel = 0x2;
> + else
> + alpha_sel = 0x1;
> + } else {
> + blend_mode = 0x0;
> + alpha_sel = 0x0;
> + }
> +
> + cl_cmp = alloc_cmdlist_regs(CMPS_X_REG);
[Severity: High]
Since plane atomic update callbacks execute in atomic context during
non-blocking commits, is it safe to allocate memory here?
The alloc_cmdlist_regs() macro uses kzalloc() with GFP_KERNEL. Calling
sleeping functions from an atomic context causes kernel warnings,
system instability, and potential deadlocks.
> +
> + base = CMP_BASE_ADDR[a_crtc->dev_id];
> + dpu_write(hwdev, CMPS_X_REG, base, dst_w, mode->hdisplay, cl_cmp);
[ ... ]
> +void saturn_hee_cmdlist_fill_data_row(struct cmdlist *cl, u32 strobe,
> + u32 offset, u32 value[])
> +{
> + struct cmdlist_row *row;
> + u8 i;
> +
> + /*
> + * packing counts rows blindly; drop the row rather than the buffer end
> + */
> + if (WARN_ONCE(CL_HEADER_SZ + (cl->nod_len + 1) * CL_ROW_SZ > cl->size,
> + "cmdlist buffer full (rows %u)\n", cl->nod_len))
> + return;
[Severity: High]
Does this correctly handle command list buffer overflows?
When the cmdlist buffer is full, this function issues a warning and returns
early without writing the row.
However, its caller in cmdlist_regs_packing() ignores this failure and
still increments cl->nod_len. This artificially inflates the length in the
header. When the DPU hardware parses the cmdlist, it will attempt to read
out-of-bounds DMA memory.
> +
> + row = (struct cmdlist_row *)((char *)cl->va + CL_HEADER_SZ) + cl->nod_len;
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
> index 0000000000000..144716ec9b80c
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
[ ... ]
> +int saturn_cal_layer_fbcmem_size(struct drm_plane *plane,
> + struct drm_plane_state *state)
> +{
> + struct spacemit_plane_state *pstate = to_spacemit_plane_state(state);
> + u32 drm_4cc_fmt = pstate->state.fb->format->format;
[Severity: High]
Will this crash if the plane is a solid fill plane?
In DRM, solid fill planes do not require an attached framebuffer, meaning
state->fb can be NULL. Unconditionally dereferencing state->fb->format here
without verifying if state->fb is valid will trigger a kernel panic.
> + u32 crop_w = pstate->state.src_w >> 16;
> +
> + /* Only linear sizing: the compressed path is not exposed. */
> + return get_raw_data_plane_rdma_mem_size(drm_4cc_fmt, crop_w,
> + &pstate->fbcmem_size);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=9
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (8 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:18 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang
` (7 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Add the atomic KMS implementation on top of the hardware backend: one
CRTC and one primary plane per DPU instance. atomic_check validates
the plane rectangle 1:1 against the mode and sizes the per-channel FBC
line buffer; atomic commit builds the cmdlist batches, maps the
framebuffer through the DMMU and arms the cfg-ready handshake, with
vblank events driven from the DPU interrupt.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/spacemit/spacemit_crtc.c | 815 +++++++++++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_planes.c | 376 +++++++++++++
2 files changed, 1191 insertions(+)
diff --git a/drivers/gpu/drm/spacemit/spacemit_crtc.c b/drivers/gpu/drm/spacemit/spacemit_crtc.c
new file mode 100644
index 000000000000..b75ff6320501
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_crtc.c
@@ -0,0 +1,815 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <drm/drm_print.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_plane_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/clients/drm_client_setup.h>
+#include <linux/aperture.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/component.h>
+#include <linux/interrupt.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/of_irq.h>
+#include <linux/pm_qos.h>
+#include <linux/property.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+#include <linux/trace_events.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include "spacemit_dpu_ids.h"
+#include <linux/reset.h>
+#include "spacemit_cmdlist.h"
+#include "spacemit_dmmu.h"
+#include "spacemit_drm.h"
+#include "spacemit_crtc.h"
+#include "dpu/dpu_saturn.h"
+#include "dpu/dpu_trace.h"
+
+static int spacemit_crtc_init(struct spacemit_crtc *a_crtc);
+static int spacemit_crtc_uninit(struct spacemit_crtc *a_crtc);
+static int dpu_pm_suspend(struct device *dev);
+static int dpu_pm_resume(struct device *dev);
+static void dpu_reset_assert(struct device *dev, const char *name,
+ struct reset_control *rstc);
+
+static void saturn_count_rdma_use(struct drm_crtc_state *crtc_state)
+{
+ struct drm_plane *plane;
+ struct spacemit_crtc_state *spacemit_state =
+ to_spacemit_crtc_state(crtc_state);
+ struct spacemit_crtc_rdma *rdmas = spacemit_state->rdmas;
+ u32 rdma_id = 0;
+ const struct drm_plane_state *pstate;
+
+ drm_atomic_crtc_state_for_each_plane_state(plane, pstate, crtc_state) {
+ rdma_id = to_spacemit_plane_state(pstate)->rdma_id;
+ if (rdma_id != RDMA_INVALID_ID)
+ rdmas[rdma_id].use_cnt++;
+ }
+}
+
+static int spacemit_crtc_atomic_check_fbmem(struct drm_crtc *crtc,
+ struct drm_crtc_state *crtc_state)
+{
+ struct spacemit_crtc_rdma *rdmas =
+ to_spacemit_crtc_state(crtc_state)->rdmas;
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ const struct drm_plane_state *pstate;
+ struct drm_plane *plane;
+
+ drm_atomic_crtc_state_for_each_plane_state(plane, pstate, crtc_state) {
+ u32 rdma_id = to_spacemit_plane_state(pstate)->rdma_id;
+ u32 layer_fbcmem_size =
+ to_spacemit_plane_state(pstate)->fbcmem_size;
+
+ if (rdma_id != RDMA_INVALID_ID)
+ rdmas[rdma_id].fbcmem.size = max(layer_fbcmem_size,
+ rdmas[rdma_id].fbcmem.size);
+ }
+
+ return a_crtc->core->adjust_rdma_fbcmem(hwdev, rdmas);
+}
+
+static void spacemit_crtc_atomic_enable(struct drm_crtc *crtc,
+ struct drm_atomic_commit *old_state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ int idx;
+
+ if (!drm_dev_enter(crtc->dev, &idx))
+ return;
+
+ trace_spacemit_crtc_atomic_enable(a_crtc->dev_id);
+
+ /*
+ * U-Boot leaves its boot-logo pipeline running. Hard-stop it with the
+ * resets alone; the clocks are not ours to gate until we have enabled
+ * them.
+ */
+ if (unlikely(a_crtc->first_modeset)) {
+ dpu_reset_assert(a_crtc->dev, "lcd", a_crtc->lcd_reset);
+ dpu_reset_assert(a_crtc->dev, "esc", a_crtc->esc_reset);
+ dpu_reset_assert(a_crtc->dev, "mclk", a_crtc->mclk_reset);
+ dpu_reset_assert(a_crtc->dev, "aclk", a_crtc->aclk_reset);
+ dpu_reset_assert(a_crtc->dev, "dsc", a_crtc->dsc_reset);
+ usleep_range(10000, 20000);
+ a_crtc->first_modeset = false;
+ }
+
+ if (!a_crtc->clocks_on) {
+ a_crtc->clocks_on = true;
+ dpu_pm_resume(a_crtc->dev);
+
+ /*
+ * The DPU must settle after reset deassert or CFG_RDY races
+ * the flush.
+ */
+ usleep_range(10000, 20000);
+ }
+
+ drm_crtc_vblank_on(&a_crtc->crtc);
+
+ spacemit_crtc_init(a_crtc);
+
+ drm_dev_exit(idx);
+}
+
+static void spacemit_crtc_atomic_disable(struct drm_crtc *crtc,
+ struct drm_atomic_commit *old_state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ struct drm_device *drm = a_crtc->crtc.dev;
+ int idx;
+
+ if (!drm_dev_enter(drm, &idx))
+ return;
+
+ trace_spacemit_crtc_atomic_disable(a_crtc->dev_id);
+
+ spacemit_crtc_uninit(a_crtc);
+
+ drm_crtc_vblank_off(&a_crtc->crtc);
+
+ /*
+ * MMIO on a clock-gated DPU stalls the bus, so an in-flight handler
+ * must finish its status reads before dpu_pm_suspend() gates the
+ * clocks.
+ */
+ if (a_crtc->irq_online > 0)
+ synchronize_irq(a_crtc->irq_online);
+
+ /*
+ * Nothing else cancels the watchdog; it would fire against freed state.
+ */
+ timer_delete_sync(&a_crtc->cfg_rdy_timer);
+
+ /* Close the ISR's MMIO gate before the clocks actually stop. */
+ a_crtc->clocks_on = false;
+ dpu_pm_suspend(a_crtc->dev);
+
+ spin_lock_irq(&drm->event_lock);
+ if (crtc->state->event) {
+ drm_crtc_send_vblank_event(crtc, crtc->state->event);
+ crtc->state->event = NULL;
+ }
+ spin_unlock_irq(&drm->event_lock);
+
+ drm_dev_exit(idx);
+}
+
+static int spacemit_crtc_atomic_check(struct drm_crtc *crtc,
+ struct drm_atomic_commit *atomic_state)
+{
+ struct drm_crtc_state *state =
+ drm_atomic_get_new_crtc_state(atomic_state, crtc);
+ struct spacemit_crtc_state *cstate = to_spacemit_crtc_state(state);
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ int ret = 0;
+
+ trace_spacemit_crtc_atomic_check(a_crtc->dev_id);
+
+ ret = spacemit_crtc_atomic_check_fbmem(crtc, state);
+ if (ret) {
+ drm_dbg_kms(crtc->dev,
+ "failed to satisfy fbcmem size for all rdmas\n");
+ return ret;
+ }
+
+ saturn_count_rdma_use(state);
+
+ ret = spacemit_cmdlist_alloc(a_crtc->dev, &cstate->cl);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static void spacemit_crtc_atomic_begin(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+
+ trace_spacemit_crtc_atomic_begin(a_crtc->dev_id);
+}
+
+static void spacemit_crtc_atomic_flush(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+ struct drm_crtc_state *old_state = drm_atomic_get_old_crtc_state(state,
+ crtc);
+ int idx;
+
+ if (!drm_dev_enter(crtc->dev, &idx))
+ return;
+
+ trace_spacemit_crtc_atomic_flush(a_crtc->dev_id);
+
+ spacemit_crtc_run(crtc, old_state);
+
+ drm_dev_exit(idx);
+}
+
+static struct drm_crtc_state *
+spacemit_crtc_duplicate_state(struct drm_crtc *crtc)
+{
+ struct spacemit_crtc_state *state;
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ u8 n_rdma;
+
+ if (WARN_ON(!crtc->state))
+ return NULL;
+
+ state = kzalloc_obj(*state);
+ if (!state)
+ return NULL;
+
+ __drm_atomic_helper_crtc_duplicate_state(crtc, &state->base);
+
+ n_rdma = hwdev->rdma_nums;
+ state->rdmas = kzalloc_objs(*state->rdmas, n_rdma);
+ if (!state->rdmas) {
+ kfree(state);
+ return NULL;
+ }
+
+ state->cl.type = CMDLIST_CRTC;
+
+ return &state->base;
+}
+
+static void spacemit_crtc_destroy_state(struct drm_crtc *crtc,
+ struct drm_crtc_state *state)
+{
+ struct spacemit_crtc_state *spacemit_state = NULL;
+ struct spacemit_crtc *a_crtc = NULL;
+
+ if (state) {
+ spacemit_state = to_spacemit_crtc_state(state);
+ __drm_atomic_helper_crtc_destroy_state(state);
+ a_crtc = to_spacemit_crtc(crtc);
+ if (spacemit_state->cl.va)
+ dma_free_coherent(a_crtc->dev,
+ spacemit_state->cl.size,
+ spacemit_state->cl.va,
+ spacemit_state->cl.pa);
+ kfree(spacemit_state->rdmas);
+ kfree(spacemit_state);
+ }
+}
+
+static void spacemit_crtc_reset(struct drm_crtc *crtc)
+{
+ struct spacemit_drm_private *priv = crtc->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct spacemit_crtc_state *state;
+
+ if (crtc->state)
+ spacemit_crtc_destroy_state(crtc, crtc->state);
+
+ state = kzalloc_obj(*state);
+ if (!state) {
+ __drm_atomic_helper_crtc_reset(crtc, NULL);
+ return;
+ }
+
+ state->rdmas = kzalloc_objs(*state->rdmas, hwdev->rdma_nums);
+ if (!state->rdmas) {
+ kfree(state);
+ __drm_atomic_helper_crtc_reset(crtc, NULL);
+ return;
+ }
+
+ __drm_atomic_helper_crtc_reset(crtc, &state->base);
+}
+
+static int spacemit_crtc_enable_vblank(struct drm_crtc *crtc)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+
+ trace_spacemit_crtc_enable_vblank(a_crtc->dev_id);
+
+ if (a_crtc->core && a_crtc->core->enable_vsync)
+ a_crtc->core->enable_vsync(a_crtc);
+
+ return 0;
+}
+
+static void spacemit_crtc_disable_vblank(struct drm_crtc *crtc)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+
+ trace_spacemit_crtc_disable_vblank(a_crtc->dev_id);
+
+ if (a_crtc->core && a_crtc->core->disable_vsync)
+ a_crtc->core->disable_vsync(a_crtc);
+}
+
+static const struct drm_crtc_helper_funcs spacemit_crtc_helper_funcs = {
+ .atomic_check = spacemit_crtc_atomic_check,
+ .atomic_begin = spacemit_crtc_atomic_begin,
+ .atomic_flush = spacemit_crtc_atomic_flush,
+ .atomic_enable = spacemit_crtc_atomic_enable,
+ .atomic_disable = spacemit_crtc_atomic_disable,
+};
+
+static const struct drm_crtc_funcs spacemit_crtc_funcs = {
+ .destroy = drm_crtc_cleanup,
+ .set_config = drm_atomic_helper_set_config,
+ .page_flip = drm_atomic_helper_page_flip,
+ .reset = spacemit_crtc_reset,
+ .atomic_duplicate_state = spacemit_crtc_duplicate_state,
+ .atomic_destroy_state = spacemit_crtc_destroy_state,
+ .enable_vblank = spacemit_crtc_enable_vblank,
+ .disable_vblank = spacemit_crtc_disable_vblank,
+};
+
+static void spacemit_crtc_put_port(struct drm_device *drm, void *port)
+{
+ of_node_put(port);
+}
+
+static int spacemit_crtc_bind_init(struct drm_device *drm, struct drm_crtc *crtc,
+ struct drm_plane *primary, struct device_node *port)
+{
+ int err;
+
+ /*
+ * drm_of_find_possible_crtcs() walks crtc->port for the life of the
+ * drm_device.
+ */
+ crtc->port = of_node_get(port);
+ err = drmm_add_action_or_reset(drm, spacemit_crtc_put_port, port);
+ if (err)
+ return err;
+
+ err = drm_crtc_init_with_planes(drm, crtc, primary, NULL,
+ &spacemit_crtc_funcs, NULL);
+ if (err) {
+ drm_err(drm, "failed to init crtc\n");
+ return err;
+ }
+
+ drm_mode_crtc_set_gamma_size(crtc, 256);
+
+ drm_crtc_helper_add(crtc, &spacemit_crtc_helper_funcs);
+
+ return 0;
+}
+
+int spacemit_crtc_run(struct drm_crtc *crtc,
+ struct drm_crtc_state *old_state)
+{
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
+
+ trace_spacemit_crtc_run(a_crtc->dev_id);
+
+ if (a_crtc->core && a_crtc->core->run)
+ a_crtc->core->run(crtc, old_state);
+
+ return 0;
+}
+
+static int spacemit_crtc_init(struct spacemit_crtc *a_crtc)
+{
+ trace_spacemit_crtc_init(a_crtc->dev_id);
+
+ if (a_crtc->core && a_crtc->core->init)
+ a_crtc->core->init(a_crtc);
+
+ a_crtc->is_1st_f = true;
+
+ return 0;
+}
+
+static int spacemit_crtc_uninit(struct spacemit_crtc *a_crtc)
+{
+ trace_spacemit_crtc_uninit(a_crtc->dev_id);
+
+ if (a_crtc->core && a_crtc->core->uninit)
+ a_crtc->core->uninit(a_crtc);
+
+ return 0;
+}
+
+static irqreturn_t spacemit_dpu_isr(int irq, void *data)
+{
+ struct spacemit_drm_private *priv = data;
+ struct spacemit_crtc *a_crtc = priv->a_crtc;
+ int idx;
+
+ if (!a_crtc)
+ return IRQ_NONE;
+
+ if (!drm_dev_enter(a_crtc->crtc.dev, &idx))
+ return IRQ_NONE;
+
+ /* Reading a clock-gated DPU's status registers stalls the bus. */
+ if (unlikely(!a_crtc->clocks_on)) {
+ drm_dev_exit(idx);
+ return IRQ_NONE;
+ }
+
+ if (a_crtc->core && a_crtc->core->online_isr)
+ a_crtc->core->online_isr(a_crtc);
+
+ drm_dev_exit(idx);
+
+ return IRQ_HANDLED;
+}
+
+/*
+ * Acquire at probe, not at bind: the aggregate can rebind without this device
+ * detaching, and the exclusive resets would not be handed out a second time.
+ */
+static int spacemit_dpu_hw_resources_init(struct spacemit_drm_private *priv,
+ struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ int ret;
+
+ ret = dpu_parse_clocks(dev, &priv->clk_ctx);
+ if (ret)
+ return ret;
+
+ priv->mclk_reset = devm_reset_control_get_optional_exclusive(dev,
+ "mclk");
+ if (IS_ERR(priv->mclk_reset))
+ return dev_err_probe(dev, PTR_ERR(priv->mclk_reset),
+ "failed to get mclk reset\n");
+ priv->esc_reset = devm_reset_control_get_optional_exclusive(dev, "esc");
+ if (IS_ERR(priv->esc_reset))
+ return dev_err_probe(dev, PTR_ERR(priv->esc_reset),
+ "failed to get esc reset\n");
+ priv->lcd_reset = devm_reset_control_get_optional_exclusive(dev, "lcd");
+ if (IS_ERR(priv->lcd_reset))
+ return dev_err_probe(dev, PTR_ERR(priv->lcd_reset),
+ "failed to get lcd reset\n");
+ priv->aclk_reset = devm_reset_control_get_optional_exclusive(dev,
+ "aclk");
+ if (IS_ERR(priv->aclk_reset))
+ return dev_err_probe(dev, PTR_ERR(priv->aclk_reset),
+ "failed to get aclk reset\n");
+ priv->dsc_reset = devm_reset_control_get_optional_exclusive(dev, "dsc");
+ if (IS_ERR(priv->dsc_reset))
+ return dev_err_probe(dev, PTR_ERR(priv->dsc_reset),
+ "failed to get dsc reset\n");
+
+ priv->irq_online = platform_get_irq_byname(pdev, "online");
+ if (priv->irq_online < 0)
+ return priv->irq_online;
+
+ /*
+ * Keep the line disabled until bind: the handler needs bind-created
+ * state, and a bootloader-left display can raise an interrupt during
+ * probe.
+ */
+ ret = devm_request_irq(dev, priv->irq_online, spacemit_dpu_isr,
+ IRQF_NO_AUTOEN, "DPU_ONLINE", priv);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to request online IRQ\n");
+
+ return 0;
+}
+
+static const struct regmap_config spacemit_dpu_regmap_config = {
+ .name = "dpu",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = 0x53ffc, /* 0x54000-byte DPU MMIO from binding */
+};
+
+/* Bind-scope wrapper so devm_drm_dev_alloc has a struct to embed into. */
+struct spacemit_kms {
+ struct drm_device drm;
+};
+
+static int spacemit_drm_master_bind(struct device *dev)
+{
+ struct spacemit_drm_private *priv = dev_get_drvdata(dev);
+ struct device_node *np = dev->of_node;
+ struct device_node *ports = NULL, *port = NULL;
+ struct spacemit_crtc *a_crtc;
+ struct drm_plane *plane;
+ struct spacemit_kms *kms;
+ struct drm_device *drm;
+ int ret;
+
+ ret = aperture_remove_all_conflicting_devices(spacemit_drm_drv.name);
+ if (ret) {
+ dev_err(dev, "failed to remove existing framebuffers: %d\n",
+ ret);
+ return ret;
+ }
+
+ kms = devm_drm_dev_alloc(dev, &spacemit_drm_drv,
+ struct spacemit_kms, drm);
+ if (IS_ERR(kms))
+ return PTR_ERR(kms);
+ drm = &kms->drm;
+ priv->ddev = drm;
+ drm->dev_private = priv;
+
+ ret = spacemit_drm_mode_config_init(drm);
+ if (ret)
+ return ret;
+
+ a_crtc = drmm_kzalloc(drm, sizeof(*a_crtc), GFP_KERNEL);
+ if (!a_crtc) {
+ ret = -ENOMEM;
+ goto err_dc;
+ }
+ a_crtc->dev = dev;
+ a_crtc->first_modeset = true;
+ priv->a_crtc = a_crtc;
+
+ a_crtc->clk_ctx = priv->clk_ctx;
+ a_crtc->aclk = DPU_AXICLK_DEFAULT;
+ a_crtc->mclk_reset = priv->mclk_reset;
+ a_crtc->esc_reset = priv->esc_reset;
+ a_crtc->lcd_reset = priv->lcd_reset;
+ a_crtc->aclk_reset = priv->aclk_reset;
+ a_crtc->dsc_reset = priv->dsc_reset;
+ a_crtc->irq_online = priv->irq_online;
+
+ /* Both K3 DPU instances are COMPOSER1 in the Saturn IRQ tables. */
+ a_crtc->dev_id = COMPOSER1;
+
+ /* The K3 pipeline always hands RGB888 to the DP/eDP encoder. */
+ a_crtc->out_format = OUTFMT_RGB888;
+
+ a_crtc->core = &dpu_saturn_ops;
+
+ timer_setup(&a_crtc->cfg_rdy_timer, spacemit_cfg_rdy_timer_handler, 0);
+
+ plane = spacemit_plane_init(drm, a_crtc);
+ if (IS_ERR(plane)) {
+ ret = PTR_ERR(plane);
+ goto err_dc;
+ }
+
+ ports = of_get_child_by_name(np, "ports");
+ if (!ports) {
+ dev_err(dev, "CRTC %pOF has no ports node\n", np);
+ ret = -EINVAL;
+ goto err_dc;
+ }
+
+ port = of_get_child_by_name(ports, "port");
+ if (!port) {
+ dev_err(dev, "CRTC %pOF has no port@X node\n", np);
+ ret = -EINVAL;
+ goto err_ports_put;
+ }
+
+ ret = spacemit_crtc_bind_init(drm, &a_crtc->crtc, plane, port);
+ of_node_put(port);
+ of_node_put(ports);
+ port = NULL;
+ ports = NULL;
+ if (ret)
+ goto err_dc;
+
+ /*
+ * component_bind_all() unbinds what it bound before returning an error.
+ */
+ ret = component_bind_all(dev, drm);
+ if (ret) {
+ dev_err(dev, "failed to bind all components\n");
+ goto err_dc;
+ }
+
+ ret = drm_vblank_init(drm, drm->mode_config.num_crtc);
+ if (ret) {
+ dev_err(dev, "failed to initialize vblank\n");
+ goto err_unbind_all;
+ }
+
+ drm_mode_config_reset(drm);
+ drmm_kms_helper_poll_init(drm);
+ drm_helper_hpd_irq_event(drm);
+
+ enable_irq(priv->irq_online);
+
+ ret = drm_dev_register(drm, 0);
+ if (ret < 0)
+ goto err_disable_irq;
+ drm_client_setup(drm, NULL);
+
+ return 0;
+
+err_disable_irq:
+ disable_irq(priv->irq_online);
+
+err_unbind_all:
+ component_unbind_all(dev, drm);
+ goto err_dc;
+err_ports_put:
+ of_node_put(port);
+ of_node_put(ports);
+err_dc:
+ /*
+ * .shutdown tests priv->ddev; never leave it on a half-initialised
+ * device.
+ */
+ priv->ddev = NULL;
+ priv->a_crtc = NULL;
+ return ret;
+}
+
+static void spacemit_drm_master_unbind(struct device *dev)
+{
+ struct spacemit_drm_private *priv = dev_get_drvdata(dev);
+ struct drm_device *drm = priv->ddev;
+
+ disable_irq(priv->irq_online);
+
+ drm_dev_unregister(drm);
+ drm_atomic_helper_shutdown(drm);
+ component_unbind_all(dev, drm);
+
+ /* .shutdown and a later re-bind must not touch the stale instance. */
+ priv->ddev = NULL;
+ priv->a_crtc = NULL;
+}
+
+const struct component_master_ops spacemit_drm_master_ops = {
+ .bind = spacemit_drm_master_bind,
+ .unbind = spacemit_drm_master_unbind,
+};
+
+static int spacemit_dpu_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ const struct spacemit_hw_device *hwdev_template;
+ struct spacemit_drm_private *priv;
+ struct device_node *np = dev->of_node;
+ int ret;
+
+ if (!np) {
+ dev_err(dev, "no OF node for the DPU\n");
+ return -ENODEV;
+ }
+
+ ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(64));
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to set DMA mask\n");
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+ priv->dev = dev;
+ dev_set_drvdata(dev, priv);
+
+ /* The match data is shared const; the MMIO mapping is per-instance. */
+ hwdev_template = device_get_match_data(dev);
+ if (!hwdev_template)
+ return -ENODEV;
+ priv->hwdev = devm_kmemdup(dev, hwdev_template,
+ sizeof(*hwdev_template), GFP_KERNEL);
+ if (!priv->hwdev)
+ return -ENOMEM;
+
+ priv->cmdlist_groups = devm_kcalloc(dev,
+ priv->hwdev->rdma_nums + priv->hwdev->crtc_nums,
+ sizeof(*priv->cmdlist_groups), GFP_KERNEL);
+ if (!priv->cmdlist_groups)
+ return -ENOMEM;
+
+ priv->hwdev->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(priv->hwdev->base))
+ return PTR_ERR(priv->hwdev->base);
+
+ priv->hwdev->regmap = devm_regmap_init_mmio(dev, priv->hwdev->base,
+ &spacemit_dpu_regmap_config);
+ if (IS_ERR(priv->hwdev->regmap))
+ return dev_err_probe(dev, PTR_ERR(priv->hwdev->regmap),
+ "failed to init DPU regmap\n");
+
+ ret = spacemit_dpu_hw_resources_init(priv, pdev);
+ if (ret)
+ return ret;
+
+ return spacemit_drm_of_component_probe(dev);
+}
+
+static void spacemit_dpu_remove(struct platform_device *pdev)
+{
+ component_master_del(&pdev->dev, &spacemit_drm_master_ops);
+}
+
+static void spacemit_dpu_shutdown(struct platform_device *pdev)
+{
+ struct spacemit_drm_private *priv = dev_get_drvdata(&pdev->dev);
+ struct drm_device *drm = priv->ddev;
+
+ if (!drm) {
+ dev_warn(&pdev->dev,
+ "drm device is not available, no shutdown\n");
+ return;
+ }
+
+ drm_atomic_helper_shutdown(drm);
+}
+
+/*
+ * The assert and deassert orders are not mirror images: the hardware wants
+ * these.
+ */
+static void dpu_reset_assert(struct device *dev, const char *name,
+ struct reset_control *rstc)
+{
+ int ret = reset_control_assert(rstc);
+
+ if (ret)
+ dev_warn(dev, "failed to assert %s reset: %d\n", name, ret);
+}
+
+static void dpu_reset_deassert(struct device *dev, const char *name,
+ struct reset_control *rstc)
+{
+ int ret = reset_control_deassert(rstc);
+
+ if (ret)
+ dev_warn(dev, "failed to deassert %s reset: %d\n", name, ret);
+}
+
+static int dpu_pm_suspend(struct device *dev)
+{
+ struct spacemit_drm_private *priv = dev_get_drvdata(dev);
+ struct spacemit_crtc *a_crtc = priv->a_crtc;
+
+ /*
+ * Assert before gating: a reset asserted into an already-gated block
+ * has no clock edges to propagate on and leaves the register file
+ * untouched.
+ */
+ dpu_reset_assert(dev, "lcd", a_crtc->lcd_reset);
+ dpu_reset_assert(dev, "esc", a_crtc->esc_reset);
+ dpu_reset_assert(dev, "mclk", a_crtc->mclk_reset);
+ dpu_reset_assert(dev, "aclk", a_crtc->aclk_reset);
+ dpu_reset_assert(dev, "dsc", a_crtc->dsc_reset);
+ usleep_range(10000, 20000);
+
+ if (a_crtc->core && a_crtc->core->disable_clk)
+ a_crtc->core->disable_clk(a_crtc);
+
+ return 0;
+}
+
+static int dpu_pm_resume(struct device *dev)
+{
+ struct spacemit_drm_private *priv = dev_get_drvdata(dev);
+ struct spacemit_crtc *a_crtc = priv->a_crtc;
+
+ dpu_reset_deassert(dev, "mclk", a_crtc->mclk_reset);
+ dpu_reset_deassert(dev, "esc", a_crtc->esc_reset);
+ dpu_reset_deassert(dev, "lcd", a_crtc->lcd_reset);
+ dpu_reset_deassert(dev, "aclk", a_crtc->aclk_reset);
+ dpu_reset_deassert(dev, "dsc", a_crtc->dsc_reset);
+
+ if (a_crtc->core && a_crtc->core->enable_clk)
+ a_crtc->core->enable_clk(a_crtc);
+
+ return 0;
+}
+
+static const struct of_device_id dpu_match_table[] = {
+ {
+ .compatible = "spacemit,k3-saturn-dpu",
+ .data = &k3_saturn_dpu,
+ },
+ {},
+};
+MODULE_DEVICE_TABLE(of, dpu_match_table);
+
+static struct platform_driver spacemit_dpu_driver = {
+ .probe = spacemit_dpu_probe,
+ .remove = spacemit_dpu_remove,
+ .shutdown = spacemit_dpu_shutdown,
+ .driver = {
+ .name = "spacemit-dpu-drv",
+ .of_match_table = dpu_match_table,
+ .suppress_bind_attrs = true,
+ },
+};
+module_platform_driver(spacemit_dpu_driver);
+
+MODULE_DESCRIPTION("SpacemiT DRM KMS master driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/spacemit/spacemit_planes.c b/drivers/gpu/drm/spacemit/spacemit_planes.c
new file mode 100644
index 000000000000..3442013b0c37
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_planes.c
@@ -0,0 +1,376 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <drm/drm_print.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_blend.h>
+#include <drm/drm_atomic_uapi.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_plane_helper.h>
+#include <drm/drm_color_mgmt.h>
+#include <drm/drm_gem.h>
+#include <linux/component.h>
+#include <linux/dma-mapping.h>
+#include "spacemit_cmdlist.h"
+#include "spacemit_dmmu.h"
+#include "spacemit_drm.h"
+#include "dpu/dpu_saturn.h"
+#include "dpu/dpu_trace.h"
+#include "dpu/saturn_fbcmem.h"
+
+/* The AFBC decoder exists but no modifier has been validated on hardware. */
+static const u64 supported_format_modifiers[] = {
+ DRM_FORMAT_MOD_LINEAR,
+ DRM_FORMAT_MOD_INVALID
+};
+
+struct spacemit_plane *to_spacemit_plane(struct drm_plane *plane)
+{
+ return container_of(plane, struct spacemit_plane, plane);
+}
+
+static int spacemit_plane_atomic_check(struct drm_plane *plane,
+ struct drm_atomic_commit *atomic_state)
+{
+ struct drm_plane_state *state =
+ drm_atomic_get_new_plane_state(atomic_state, plane);
+ struct drm_framebuffer *fb = state->fb;
+ u32 src_w, src_h, src_x, src_y;
+ u32 crtc_w, crtc_h;
+ struct spacemit_plane_state *cur_state = to_spacemit_plane_state(state);
+ u32 cur_rdma_id = cur_state->rdma_id;
+ struct spacemit_drm_private *priv = plane->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ struct spacemit_crtc *a_crtc = NULL;
+
+ struct drm_crtc_state *crtc_state = NULL;
+ int ret;
+
+ if (state->crtc)
+ crtc_state = drm_atomic_get_new_crtc_state(atomic_state,
+ state->crtc);
+
+ ret = drm_atomic_helper_check_plane_state(state, crtc_state,
+ DRM_PLANE_NO_SCALING,
+ DRM_PLANE_NO_SCALING,
+ true, false);
+ if (ret)
+ return ret;
+
+ if (!state->visible)
+ return 0;
+
+ a_crtc = to_spacemit_crtc(state->crtc);
+ trace_spacemit_plane_atomic_check(a_crtc->dev_id);
+
+ src_x = state->src_x >> 16;
+ src_y = state->src_y >> 16;
+ src_w = state->src_w >> 16;
+ src_h = state->src_h >> 16;
+ crtc_w = state->crtc_w;
+ crtc_h = state->crtc_h;
+
+ /* The programming paths use the raw rectangles, so refuse to clip. */
+ if ((state->src.x1 >> 16) != src_x || (state->src.y1 >> 16) != src_y ||
+ (drm_rect_width(&state->src) >> 16) != src_w ||
+ (drm_rect_height(&state->src) >> 16) != src_h ||
+ state->dst.x1 != state->crtc_x || state->dst.y1 != state->crtc_y ||
+ drm_rect_width(&state->dst) != crtc_w ||
+ drm_rect_height(&state->dst) != crtc_h) {
+ drm_dbg_kms(plane->dev,
+ "clipped plane rectangles not supported\n");
+ return -EINVAL;
+ }
+
+ /* One-dimensional sources are invalid (only 0x0 = constant fill) */
+ if ((src_w == 0) != (src_h == 0)) {
+ drm_dbg_kms(plane->dev, "unsupported src_w %d src_h %d\n",
+ src_w, src_h);
+ return -EINVAL;
+ }
+
+ if (src_w == 0 && src_h == 0)
+ cur_rdma_id = RDMA_INVALID_ID; /* constant-fill layer, no RDMA */
+ else if (cur_rdma_id == RDMA_INVALID_ID)
+ cur_rdma_id = state->zpos; /* first commit: bind channel by zpos */
+ cur_state->rdma_id = cur_rdma_id;
+
+ if (cur_rdma_id != RDMA_INVALID_ID) {
+ if (cur_rdma_id >= hwdev->rdma_nums) {
+ drm_dbg_kms(plane->dev, "invalid rdma id %d\n",
+ cur_rdma_id);
+ return -EINVAL;
+ }
+
+ if (a_crtc->core->cal_layer_fbcmem_size(plane, state)) {
+ drm_dbg_kms(plane->dev,
+ "plane %d: invalid fbcmem size\n",
+ state->zpos);
+ return -EINVAL;
+ }
+
+ cur_state->mmu_tbl.size =
+ ((PAGE_ALIGN(fb->obj[0]->size) >> PAGE_SHIFT) +
+ HW_ALIGN_TTB_NUM) * 4;
+ cur_state->mmu_tbl.va =
+ dma_alloc_coherent(a_crtc->dev, cur_state->mmu_tbl.size,
+ &cur_state->mmu_tbl.pa,
+ GFP_KERNEL | __GFP_ZERO);
+ if (!cur_state->mmu_tbl.va)
+ return -ENOMEM;
+ }
+
+ /*
+ * The commit cannot fail, so take the DMA buffer while -ENOMEM is an
+ * answer.
+ */
+ ret = spacemit_cmdlist_alloc(a_crtc->dev, &cur_state->cl);
+ if (ret)
+ return ret;
+
+ cur_state->format = spacemit_plane_hw_get_format_id(fb->format->format);
+ if (cur_state->format == SPACEMIT_DPU_INVALID_FORMAT_ID) {
+ drm_dbg_kms(plane->dev, "unsupported format %p4cc\n",
+ &fb->format->format);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void spacemit_plane_atomic_update(struct drm_plane *plane,
+ struct drm_atomic_commit *state)
+{
+ int ret = 0;
+ struct spacemit_crtc *a_crtc = to_spacemit_crtc(plane->state->crtc);
+ struct spacemit_plane_state *spacemit_pstate =
+ to_spacemit_plane_state(plane->state);
+ struct spacemit_drm_private *priv = plane->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ u32 rdma_id = spacemit_pstate->rdma_id;
+ struct cmdlist *cl = NULL;
+ int idx;
+
+ if (!drm_dev_enter(plane->dev, &idx))
+ return;
+
+ trace_spacemit_plane_atomic_update(a_crtc->dev_id);
+
+ hwdev->get_cl_rdma_buf(a_crtc);
+
+ hwdev->plane_update_hw_channel(plane);
+
+ /* Solid-fill layers have no RDMA and no page table */
+ if (rdma_id < hwdev->rdma_nums && spacemit_pstate->mmu_tbl.va) {
+ u8 tbu_id = rdma_id * 2;
+
+ ret = spacemit_dmmu_map(plane->state->fb,
+ &spacemit_pstate->mmu_tbl, tbu_id,
+ a_crtc->cl_rdma, plane);
+ if (!ret)
+ cmdlist_regs_packing(plane_to_cl(plane),
+ CMDLIST_MOD_RDMA, a_crtc->cl_rdma);
+ else
+ drm_err(plane->dev, "failed to map plane: %d\n", ret);
+ }
+
+ cl = &spacemit_pstate->cl;
+ cl->rch_start_cmps_y = CMDLIST_SENTINEL_POISON;
+ cl->cmdlist_ch_y_other = CMDLIST_SENTINEL_POISON;
+ free_cmdlist_regs(a_crtc->cl_rdma);
+
+ drm_dev_exit(idx);
+}
+
+static void spacemit_plane_atomic_disable(struct drm_plane *plane,
+ struct drm_atomic_commit *state)
+{
+ struct drm_plane_state *old_state =
+ drm_atomic_get_old_plane_state(state, plane);
+ struct spacemit_drm_private *priv = plane->dev->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ int idx;
+
+ if (!drm_dev_enter(plane->dev, &idx))
+ return;
+
+ hwdev->plane_disable_hw_channel(plane, old_state);
+
+ drm_dev_exit(idx);
+}
+
+static void spacemit_plane_reset(struct drm_plane *plane)
+{
+ struct spacemit_plane_state *s;
+
+ if (plane->state) {
+ s = to_spacemit_plane_state(plane->state);
+ __drm_atomic_helper_plane_destroy_state(plane->state);
+ kfree(s);
+ plane->state = NULL;
+ }
+
+ s = kzalloc(sizeof(*s), GFP_KERNEL);
+ if (s) {
+ __drm_atomic_helper_plane_reset(plane, &s->state);
+ s->rdma_id = RDMA_INVALID_ID;
+ }
+}
+
+static struct drm_plane_state *
+spacemit_plane_atomic_duplicate_state(struct drm_plane *plane)
+{
+ struct spacemit_plane_state *s;
+ struct spacemit_plane_state *old_state =
+ to_spacemit_plane_state(plane->state);
+ struct spacemit_crtc *a_crtc = NULL;
+
+ if (plane->crtc) {
+ a_crtc = to_spacemit_crtc(plane->crtc);
+ trace_spacemit_plane_atomic_duplicate_state(a_crtc->dev_id);
+ }
+
+ s = kzalloc(sizeof(*s), GFP_KERNEL);
+ if (!s)
+ return NULL;
+
+ __drm_atomic_helper_plane_duplicate_state(plane, &s->state);
+
+ WARN_ON(s->state.plane != plane);
+
+ s->cl.type = CMDLIST_PLANE;
+
+ s->rdma_id = old_state->rdma_id;
+ s->format = old_state->format;
+ s->fbcmem_size = 0;
+
+ return &s->state;
+}
+
+static void spacemit_plane_atomic_destroy_state(struct drm_plane *plane,
+ struct drm_plane_state *state)
+{
+ struct spacemit_plane_state *spacemit_pstate =
+ to_spacemit_plane_state(state);
+ struct spacemit_crtc *a_crtc = NULL;
+
+ if (state->crtc) {
+ a_crtc = to_spacemit_crtc(state->crtc);
+
+ if (spacemit_pstate->mmu_tbl.va)
+ dma_free_coherent(a_crtc->dev,
+ spacemit_pstate->mmu_tbl.size,
+ spacemit_pstate->mmu_tbl.va,
+ spacemit_pstate->mmu_tbl.pa);
+
+ if (spacemit_pstate->cl.va)
+ dma_free_coherent(a_crtc->dev,
+ spacemit_pstate->cl.size,
+ spacemit_pstate->cl.va,
+ spacemit_pstate->cl.pa);
+ trace_spacemit_plane_atomic_destroy_state(a_crtc->dev_id);
+ }
+ __drm_atomic_helper_plane_destroy_state(state);
+
+ kfree(to_spacemit_plane_state(state));
+}
+
+static void spacemit_plane_create_properties(struct spacemit_plane *p)
+{
+ unsigned int support_modes = BIT(DRM_MODE_BLEND_PIXEL_NONE) |
+ BIT(DRM_MODE_BLEND_PREMULTI) |
+ BIT(DRM_MODE_BLEND_COVERAGE);
+
+ drm_plane_create_alpha_property(&p->plane);
+
+ drm_plane_create_blend_mode_property(&p->plane, support_modes);
+}
+
+static const struct drm_plane_helper_funcs spacemit_plane_helper_funcs = {
+ .atomic_check = spacemit_plane_atomic_check,
+ .atomic_update = spacemit_plane_atomic_update,
+ .atomic_disable = spacemit_plane_atomic_disable,
+};
+
+static const struct drm_plane_funcs spacemit_plane_funcs = {
+ .update_plane = drm_atomic_helper_update_plane,
+ .disable_plane = drm_atomic_helper_disable_plane,
+ .destroy = drm_plane_cleanup,
+ .reset = spacemit_plane_reset,
+ .atomic_duplicate_state = spacemit_plane_atomic_duplicate_state,
+ .atomic_destroy_state = spacemit_plane_atomic_destroy_state,
+};
+
+struct drm_plane *spacemit_plane_init(struct drm_device *drm,
+ struct spacemit_crtc *a_crtc)
+{
+ struct spacemit_plane *p = NULL;
+ int err, j;
+ u32 *formats;
+ struct spacemit_drm_private *priv = drm->dev_private;
+ struct spacemit_hw_device *hwdev = priv->hwdev;
+ u8 n_formats = hwdev->n_formats;
+ u8 n_fbcmems = hwdev->n_fbcmems;
+ u8 n_rdmas = hwdev->rdma_nums;
+ u32 plane_crtc_mask;
+
+ trace_spacemit_plane_init(a_crtc->dev_id);
+ if (n_fbcmems * 2 != n_rdmas) {
+ drm_err(drm, "mismatched rdma/fbcmem counts: %d vs %d\n",
+ n_rdmas, n_fbcmems);
+ return ERR_PTR(-EINVAL);
+ }
+
+ for (j = 0; j < n_fbcmems; j++) {
+ if (hwdev->fbcmem_sizes[j] < FBCMEM_UNIT) {
+ drm_err(drm, "invalid fbcmem_sizes[%d] = %u\n",
+ j, hwdev->fbcmem_sizes[j]);
+ return ERR_PTR(-EINVAL);
+ }
+ }
+
+ formats = kcalloc(n_formats, sizeof(*formats), GFP_KERNEL);
+ if (!formats)
+ return ERR_PTR(-ENOMEM);
+
+ /*
+ * The CRTC is created right after this, so its index is the current
+ * count.
+ */
+ plane_crtc_mask = 1 << drm->mode_config.num_crtc;
+
+ /* drmm, not devm: an open DRM fd outlives the platform unbind. */
+ p = drmm_kzalloc(drm, sizeof(*p), GFP_KERNEL);
+ if (!p) {
+ kfree(formats);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ for (j = 0; j < n_formats; j++)
+ formats[j] = hwdev->formats[j].format;
+
+ err = drm_universal_plane_init(drm, &p->plane, plane_crtc_mask,
+ &spacemit_plane_funcs, formats,
+ n_formats, supported_format_modifiers,
+ DRM_PLANE_TYPE_PRIMARY, NULL);
+ kfree(formats);
+ if (err) {
+ drm_err(drm, "failed to init primary plane\n");
+ return ERR_PTR(err);
+ }
+
+ drm_plane_helper_add(&p->plane, &spacemit_plane_helper_funcs);
+
+ spacemit_plane_create_properties(p);
+
+ p->hwdev = hwdev;
+
+ return &p->plane;
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline
2026-07-25 3:16 ` [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang
@ 2026-07-25 5:18 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:18 UTC (permalink / raw)
To: Cody Kang
Cc: olteanv, neil.armstrong, conor+dt, dri-devel, robh, vkoul,
linux-phy, devicetree
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [Critical] Ignored return value of `get_cl_rdma_buf()` in `spacemit_plane_atomic_update` leads to Use-After-Free and Double-Free.
- [High] Sleeping functions called from CRTC atomic commit callbacks (`atomic_enable` / `atomic_disable`), violating atomic context requirements.
- [High] `spacemit_plane_reset` open-codes state destruction, permanently leaking the plane's DMA allocations.
- [High] Dynamic memory allocation occurs in the `atomic_update` fast path.
- [High] Enabling the DPU IRQ before hardware reset causes an interrupt storm.
- [High] TOCTOU race condition in `spacemit_crtc_atomic_disable` allows MMIO access while clocks are gated.
- [High] Unbounded DMA allocations in `atomic_check` lead to memory leaks on retries and unprivileged Denial of Service.
- [High] Incorrect teardown order in `spacemit_drm_master_unbind` causes VBLANK timeouts.
- [High] DRM ioctls can access freed `priv` and `hwdev` after driver unbind due to missing unplug state.
- [Medium] TOCTOU race setting `a_crtc->clocks_on = true` before the hardware clocks are actually resumed.
- [Medium] Unbounded accumulation of `use_cnt` in `saturn_count_rdma_use`.
- [Medium] `spacemit_crtc_duplicate_state` leaks DRM property blobs on error path.
- [Medium] Hardware resets are deasserted before clocks are enabled during PM resume.
--
--- Patch [10]: [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline ---
commit f6c949f848ca8fa43777ef224d0be429d2966ff6
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU KMS pipeline
Add the atomic KMS implementation on top of the hardware backend: one
CRTC and one primary plane per DPU instance. atomic_check validates
the plane rectangle 1:1 against the mode and sizes the per-channel FBC
line buffer; atomic commit builds the cmdlist batches, maps the
framebuffer through the DMMU and arms the cfg-ready handshake, with
vblank events driven from the DPU interrupt.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_crtc.c b/drivers/gpu/drm/spacemit/spacemit_crtc.c
> --- a/drivers/gpu/drm/spacemit/spacemit_crtc.c
> +++ b/drivers/gpu/drm/spacemit/spacemit_crtc.c
> @@ -43,18 +43,15 @@
> static void saturn_count_rdma_use(struct drm_crtc_state *crtc_state)
> {
> struct drm_plane *plane;
> struct spacemit_crtc_state *spacemit_state =
> to_spacemit_crtc_state(crtc_state);
> struct spacemit_crtc_rdma *rdmas = spacemit_state->rdmas;
> u32 rdma_id = 0;
> const struct drm_plane_state *pstate;
>
> drm_atomic_crtc_state_for_each_plane_state(plane, pstate, crtc_state) {
> rdma_id = to_spacemit_plane_state(pstate)->rdma_id;
> if (rdma_id != RDMA_INVALID_ID)
> rdmas[rdma_id].use_cnt++;
[Severity: Medium]
Will this counter accumulate values incorrectly if atomic_check is called
multiple times on the same CRTC state? If the DRM core retries the commit due
to -EDEADLK backoff, it seems the counter might increment repeatedly since it
is not cleared first.
> }
> }
>
[ ... ]
> @@ -95,21 +92,20 @@
> static void spacemit_crtc_atomic_enable(struct drm_crtc *crtc,
> struct drm_atomic_commit *old_state)
> {
> struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
> int idx;
>
> if (!drm_dev_enter(crtc->dev, &idx))
> return;
>
> trace_spacemit_crtc_atomic_enable(a_crtc->dev_id);
>
> /*
> * U-Boot leaves its boot-logo pipeline running. Hard-stop it with the
> * resets alone; the clocks are not ours to gate until we have enabled
> * them.
> */
> if (unlikely(a_crtc->first_modeset)) {
> dpu_reset_assert(a_crtc->dev, "lcd", a_crtc->lcd_reset);
> dpu_reset_assert(a_crtc->dev, "esc", a_crtc->esc_reset);
> dpu_reset_assert(a_crtc->dev, "mclk", a_crtc->mclk_reset);
> dpu_reset_assert(a_crtc->dev, "aclk", a_crtc->aclk_reset);
> dpu_reset_assert(a_crtc->dev, "dsc", a_crtc->dsc_reset);
> usleep_range(10000, 20000);
[Severity: High]
Is it safe to call usleep_range() here? CRTC atomic enable callbacks are
invoked in atomic context during non-blocking commits, where sleeping functions
are strictly forbidden and can cause kernel panics or system instability.
> a_crtc->first_modeset = false;
> }
>
> if (!a_crtc->clocks_on) {
> a_crtc->clocks_on = true;
> dpu_pm_resume(a_crtc->dev);
[Severity: Medium]
Does setting a_crtc->clocks_on to true before the hardware clocks are actually
resumed in dpu_pm_resume() introduce a TOCTOU race? If a spurious or pending
interrupt fires in this window, the ISR might read MMIO registers before the
clocks are fully resumed, which could lead to a bus stall.
>
> /*
> * The DPU must settle after reset deassert or CFG_RDY races
> * the flush.
> */
> usleep_range(10000, 20000);
> }
>
[ ... ]
> @@ -124,31 +120,29 @@
> static void spacemit_crtc_atomic_disable(struct drm_crtc *crtc,
> struct drm_atomic_commit *old_state)
> {
> struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
> struct drm_device *drm = a_crtc->crtc.dev;
> int idx;
>
> if (!drm_dev_enter(drm, &idx))
> return;
>
> trace_spacemit_crtc_atomic_disable(a_crtc->dev_id);
>
> spacemit_crtc_uninit(a_crtc);
>
> drm_crtc_vblank_off(&a_crtc->crtc);
>
> /*
> * MMIO on a clock-gated DPU stalls the bus, so an in-flight handler
> * must finish its status reads before dpu_pm_suspend() gates the
> * clocks.
> */
> if (a_crtc->irq_online > 0)
> synchronize_irq(a_crtc->irq_online);
>
> /*
> * Nothing else cancels the watchdog; it would fire against freed state.
> */
> timer_delete_sync(&a_crtc->cfg_rdy_timer);
>
> /* Close the ISR's MMIO gate before the clocks actually stop. */
> a_crtc->clocks_on = false;
> dpu_pm_suspend(a_crtc->dev);
[Severity: High]
Are there calls to multiple blocking and sleeping functions within this atomic
disable path? Functions like synchronize_irq(), timer_delete_sync(), and any
potential sleeping inside dpu_pm_suspend() violate atomic context requirements.
[Severity: High]
Could setting a_crtc->clocks_on = false after calling synchronize_irq() create
a TOCTOU race window? A new interrupt could fire immediately after
synchronize_irq() returns, and the new ISR instance would see clocks_on as
true and access MMIO concurrently while dpu_pm_suspend() gates the clocks,
potentially hanging the bus.
>
> spin_lock_irq(&drm->event_lock);
> if (crtc->state->event) {
> drm_crtc_send_vblank_event(crtc, crtc->state->event);
> crtc->state->event = NULL;
[ ... ]
> @@ -171,15 +165,14 @@
> static int spacemit_crtc_atomic_check(struct drm_crtc *crtc,
> struct drm_atomic_commit *atomic_state)
> {
> struct drm_crtc_state *state =
> drm_atomic_get_new_crtc_state(atomic_state, crtc);
> struct spacemit_crtc_state *cstate = to_spacemit_crtc_state(state);
> struct spacemit_crtc *a_crtc = to_spacemit_crtc(crtc);
> int ret = 0;
>
> trace_spacemit_crtc_atomic_check(a_crtc->dev_id);
>
> ret = spacemit_crtc_atomic_check_fbmem(crtc, state);
> if (ret) {
> drm_dbg_kms(crtc->dev,
> "failed to satisfy fbcmem size for all rdmas\n");
> return ret;
> }
>
> saturn_count_rdma_use(state);
>
> ret = spacemit_cmdlist_alloc(a_crtc->dev, &cstate->cl);
> if (ret)
> return ret;
[Severity: High]
Is it possible for this DMA allocation in atomic_check() to cause memory leaks?
If the DRM core retries the commit (for example, due to -EDEADLK backoff),
atomic_check() is called again on the same state. This would overwrite the
pointer and permanently leak the previous allocation, allowing unprivileged
userspace to trigger a denial of service via TEST_ONLY commits.
>
> return 0;
> }
>
[ ... ]
> @@ -211,21 +204,18 @@
> static struct drm_crtc_state *
> spacemit_crtc_duplicate_state(struct drm_crtc *crtc)
> {
> struct spacemit_crtc_state *state;
> struct spacemit_drm_private *priv = crtc->dev->dev_private;
> struct spacemit_hw_device *hwdev = priv->hwdev;
> u8 n_rdma;
>
> if (WARN_ON(!crtc->state))
> return NULL;
>
> state = kzalloc_obj(*state);
> if (!state)
> return NULL;
>
> __drm_atomic_helper_crtc_duplicate_state(crtc, &state->base);
>
> n_rdma = hwdev->rdma_nums;
> state->rdmas = kzalloc_objs(*state->rdmas, n_rdma);
> if (!state->rdmas) {
> kfree(state);
> return NULL;
> }
[Severity: Medium]
Will this error path leak DRM property blob references? The call to
__drm_atomic_helper_crtc_duplicate_state() increments blob refcounts, but if
kzalloc_objs() fails, kfree(state) is called without dropping those blob
references via __drm_atomic_helper_crtc_destroy_state().
>
> state->cl.type = CMDLIST_CRTC;
>
> return &state->base;
> }
[ ... ]
> @@ -481,26 +471,21 @@
> static int spacemit_drm_master_bind(struct device *dev)
> {
> struct spacemit_drm_private *priv = dev_get_drvdata(dev);
> struct device_node *np = dev->of_node;
> struct device_node *ports = NULL, *port = NULL;
> struct spacemit_crtc *a_crtc;
> struct drm_plane *plane;
> struct spacemit_kms *kms;
> struct drm_device *drm;
> int ret;
>
> ret = aperture_remove_all_conflicting_devices(spacemit_drm_drv.name);
> if (ret) {
> dev_err(dev, "failed to remove existing framebuffers: %d\n",
> ret);
> return ret;
> }
>
> kms = devm_drm_dev_alloc(dev, &spacemit_drm_drv,
> struct spacemit_kms, drm);
> if (IS_ERR(kms))
> return PTR_ERR(kms);
> drm = &kms->drm;
> priv->ddev = drm;
> drm->dev_private = priv;
>
> ret = spacemit_drm_mode_config_init(drm);
> if (ret)
> return ret;
>
> a_crtc = drmm_kzalloc(drm, sizeof(*a_crtc), GFP_KERNEL);
> if (!a_crtc) {
> ret = -ENOMEM;
> goto err_dc;
> }
> a_crtc->dev = dev;
> a_crtc->first_modeset = true;
> priv->a_crtc = a_crtc;
>
> a_crtc->clk_ctx = priv->clk_ctx;
> a_crtc->aclk = DPU_AXICLK_DEFAULT;
> a_crtc->mclk_reset = priv->mclk_reset;
> a_crtc->esc_reset = priv->esc_reset;
> a_crtc->lcd_reset = priv->lcd_reset;
> a_crtc->aclk_reset = priv->aclk_reset;
> a_crtc->dsc_reset = priv->dsc_reset;
> a_crtc->irq_online = priv->irq_online;
>
> /* Both K3 DPU instances are COMPOSER1 in the Saturn IRQ tables. */
> a_crtc->dev_id = COMPOSER1;
>
> /* The K3 pipeline always hands RGB888 to the DP/eDP encoder. */
> a_crtc->out_format = OUTFMT_RGB888;
>
> a_crtc->core = &dpu_saturn_ops;
>
> timer_setup(&a_crtc->cfg_rdy_timer, spacemit_cfg_rdy_timer_handler, 0);
>
> plane = spacemit_plane_init(drm, a_crtc);
> if (IS_ERR(plane)) {
> ret = PTR_ERR(plane);
> goto err_dc;
> }
>
> ports = of_get_child_by_name(np, "ports");
> if (!ports) {
> dev_err(dev, "CRTC %pOF has no ports node\n", np);
> ret = -EINVAL;
> goto err_dc;
> }
>
> port = of_get_child_by_name(ports, "port");
> if (!port) {
> dev_err(dev, "CRTC %pOF has no port@X node\n", np);
> ret = -EINVAL;
> goto err_ports_put;
> }
>
> ret = spacemit_crtc_bind_init(drm, &a_crtc->crtc, plane, port);
> of_node_put(port);
> of_node_put(ports);
> port = NULL;
> ports = NULL;
> if (ret)
> goto err_dc;
>
> /*
> * component_bind_all() unbinds what it bound before returning an error.
> */
> ret = component_bind_all(dev, drm);
> if (ret) {
> dev_err(dev, "failed to bind all components\n");
> goto err_dc;
> }
>
> ret = drm_vblank_init(drm, drm->mode_config.num_crtc);
> if (ret) {
> dev_err(dev, "failed to initialize vblank\n");
> goto err_unbind_all;
> }
>
> drm_mode_config_reset(drm);
> drmm_kms_helper_poll_init(drm);
> drm_helper_hpd_irq_event(drm);
>
> enable_irq(priv->irq_online);
[Severity: High]
Does unmasking the OS IRQ here before the hardware state is prepared cause an
interrupt storm? If the bootloader left the display pipeline active, an
interrupt fires immediately. The ISR in spacemit_dpu_isr() will see
clocks_on == false and bail out with IRQ_NONE without acknowledging the
interrupt, forcing the kernel to permanently disable the IRQ line.
>
> ret = drm_dev_register(drm, 0);
> if (ret < 0)
> goto err_disable_irq;
> drm_client_setup(drm, NULL);
>
> return 0;
[ ... ]
> @@ -591,16 +576,14 @@
> static void spacemit_drm_master_unbind(struct device *dev)
> {
> struct spacemit_drm_private *priv = dev_get_drvdata(dev);
> struct drm_device *drm = priv->ddev;
>
> disable_irq(priv->irq_online);
>
> drm_dev_unregister(drm);
> drm_atomic_helper_shutdown(drm);
[Severity: High]
Is the teardown order correct here? Calling disable_irq() before
drm_atomic_helper_shutdown() can cause multi-second stalls, as the shutdown
process issues a full modeset to disable CRTCs and waits for vblank interrupts
that will never arrive since the IRQ is disabled.
[Severity: High]
Could calling drm_dev_unregister() without setting the unplugged state via
drm_dev_unplug() leave ioctl entry paths open while devm resources are freed?
If userspace holds an open DRM file descriptor, ioctls can still execute
because drm_dev_enter() will succeed, leading to a Use-After-Free when they
dereference the freed priv pointer.
> component_unbind_all(dev, drm);
>
> /* .shutdown and a later re-bind must not touch the stale instance. */
> priv->ddev = NULL;
> priv->a_crtc = NULL;
> }
[ ... ]
> @@ -747,21 +730,17 @@
> static int dpu_pm_resume(struct device *dev)
> {
> struct spacemit_drm_private *priv = dev_get_drvdata(dev);
> struct spacemit_crtc *a_crtc = priv->a_crtc;
>
> dpu_reset_deassert(dev, "mclk", a_crtc->mclk_reset);
> dpu_reset_deassert(dev, "esc", a_crtc->esc_reset);
> dpu_reset_deassert(dev, "lcd", a_crtc->lcd_reset);
> dpu_reset_deassert(dev, "aclk", a_crtc->aclk_reset);
> dpu_reset_deassert(dev, "dsc", a_crtc->dsc_reset);
>
> if (a_crtc->core && a_crtc->core->enable_clk)
> a_crtc->core->enable_clk(a_crtc);
[Severity: Medium]
Are hardware resets being deasserted before clocks are enabled? Reset edges
cannot propagate through gated hardware blocks, so the clocks should likely be
enabled before deasserting resets to ensure a clean initialization and avoid
hardware state machine violations.
>
> return 0;
> }
>
> static const struct of_device_id dpu_match_table[] = {
> {
> .compatible = "spacemit,k3-saturn-dpu",
> .data = &k3_saturn_dpu,
> },
> {},
> };
> MODULE_DEVICE_TABLE(of, dpu_match_table);
>
> static struct platform_driver spacemit_dpu_driver = {
> .probe = spacemit_dpu_probe,
> .remove = spacemit_dpu_remove,
> .shutdown = spacemit_dpu_shutdown,
> .driver = {
> .name = "spacemit-dpu-drv",
> .of_match_table = dpu_match_table,
> .suppress_bind_attrs = true,
> },
> };
> module_platform_driver(spacemit_dpu_driver);
>
> MODULE_DESCRIPTION("SpacemiT DRM KMS master driver");
> MODULE_LICENSE("GPL");
> diff --git a/drivers/gpu/drm/spacemit/spacemit_planes.c b/drivers/gpu/drm/spacemit/spacemit_planes.c
> --- a/drivers/gpu/drm/spacemit/spacemit_planes.c
> +++ b/drivers/gpu/drm/spacemit/spacemit_planes.c
> @@ -107,31 +104,27 @@
> static int spacemit_plane_atomic_check(struct drm_plane *plane,
> struct drm_atomic_commit *atomic_state)
> {
> struct drm_plane_state *state =
> drm_atomic_get_new_plane_state(atomic_state, plane);
> struct drm_framebuffer *fb = state->fb;
> u32 src_w, src_h, src_x, src_y;
> u32 crtc_w, crtc_h;
> struct spacemit_plane_state *cur_state = to_spacemit_plane_state(state);
> u32 cur_rdma_id = cur_state->rdma_id;
> struct spacemit_drm_private *priv = plane->dev->dev_private;
> struct spacemit_hw_device *hwdev = priv->hwdev;
> struct spacemit_crtc *a_crtc = NULL;
>
> struct drm_crtc_state *crtc_state = NULL;
> int ret;
>
> if (state->crtc)
> crtc_state = drm_atomic_get_new_crtc_state(atomic_state,
> state->crtc);
>
> ret = drm_atomic_helper_check_plane_state(state, crtc_state,
> DRM_PLANE_NO_SCALING,
> DRM_PLANE_NO_SCALING,
> true, false);
> if (ret)
> return ret;
>
> if (!state->visible)
> return 0;
>
> a_crtc = to_spacemit_crtc(state->crtc);
> trace_spacemit_plane_atomic_check(a_crtc->dev_id);
>
> src_x = state->src_x >> 16;
> src_y = state->src_y >> 16;
> src_w = state->src_w >> 16;
> src_h = state->src_h >> 16;
> crtc_w = state->crtc_w;
> crtc_h = state->crtc_h;
>
> /* The programming paths use the raw rectangles, so refuse to clip. */
> if ((state->src.x1 >> 16) != src_x || (state->src.y1 >> 16) != src_y ||
> (drm_rect_width(&state->src) >> 16) != src_w ||
> (drm_rect_height(&state->src) >> 16) != src_h ||
> state->dst.x1 != state->crtc_x || state->dst.y1 != state->crtc_y ||
> drm_rect_width(&state->dst) != crtc_w ||
> drm_rect_height(&state->dst) != crtc_h) {
> drm_dbg_kms(plane->dev,
> "clipped plane rectangles not supported\n");
> return -EINVAL;
> }
>
> /* One-dimensional sources are invalid (only 0x0 = constant fill) */
> if ((src_w == 0) != (src_h == 0)) {
> drm_dbg_kms(plane->dev, "unsupported src_w %d src_h %d\n",
> src_w, src_h);
> return -EINVAL;
> }
>
> if (src_w == 0 && src_h == 0)
> cur_rdma_id = RDMA_INVALID_ID; /* constant-fill layer, no RDMA */
> else if (cur_rdma_id == RDMA_INVALID_ID)
> cur_rdma_id = state->zpos; /* first commit: bind channel by zpos */
> cur_state->rdma_id = cur_rdma_id;
>
> if (cur_rdma_id != RDMA_INVALID_ID) {
> if (cur_rdma_id >= hwdev->rdma_nums) {
> drm_dbg_kms(plane->dev, "invalid rdma id %d\n",
> cur_rdma_id);
> return -EINVAL;
> }
>
> if (a_crtc->core->cal_layer_fbcmem_size(plane, state)) {
> drm_dbg_kms(plane->dev,
> "plane %d: invalid fbcmem size\n",
> state->zpos);
> return -EINVAL;
> }
>
> cur_state->mmu_tbl.size =
> ((PAGE_ALIGN(fb->obj[0]->size) >> PAGE_SHIFT) +
> HW_ALIGN_TTB_NUM) * 4;
> cur_state->mmu_tbl.va =
> dma_alloc_coherent(a_crtc->dev, cur_state->mmu_tbl.size,
> &cur_state->mmu_tbl.pa,
> GFP_KERNEL | __GFP_ZERO);
> if (!cur_state->mmu_tbl.va)
> return -ENOMEM;
[Severity: High]
Does allocating DMA memory directly into the state here in atomic_check()
cause memory leaks on retries? Just like with spacemit_cmdlist_alloc(), if the
commit is retried due to -EDEADLK backoff, this will overwrite the pointer and
permanently leak the previous allocation.
> }
>
> /*
> * The commit cannot fail, so take the DMA buffer while -ENOMEM is an
> * answer.
> */
> ret = spacemit_cmdlist_alloc(a_crtc->dev, &cur_state->cl);
> if (ret)
> return ret;
>
> cur_state->format = spacemit_plane_hw_get_format_id(fb->format->format);
> if (cur_state->format == SPACEMIT_DPU_INVALID_FORMAT_ID) {
> drm_dbg_kms(plane->dev, "unsupported format %p4cc\n",
> &fb->format->format);
> return -EINVAL;
> }
>
> return 0;
> }
>
> static void spacemit_plane_atomic_update(struct drm_plane *plane,
> struct drm_atomic_commit *state)
> {
> int ret = 0;
> struct spacemit_crtc *a_crtc = to_spacemit_crtc(plane->state->crtc);
> struct spacemit_plane_state *spacemit_pstate =
> to_spacemit_plane_state(plane->state);
> struct spacemit_drm_private *priv = plane->dev->dev_private;
> struct spacemit_hw_device *hwdev = priv->hwdev;
> u32 rdma_id = spacemit_pstate->rdma_id;
> struct cmdlist *cl = NULL;
> int idx;
>
> if (!drm_dev_enter(plane->dev, &idx))
> return;
>
> trace_spacemit_plane_atomic_update(a_crtc->dev_id);
>
> hwdev->get_cl_rdma_buf(a_crtc);
[Severity: High]
Is it safe to perform dynamic memory allocation in the atomic_update fast path?
The DRM atomic update phase is forbidden to fail or sleep. Any temporary memory
should be pre-allocated in atomic_check() or prepare_fb().
[Severity: Critical]
Does ignoring the return value of get_cl_rdma_buf() lead to a Use-After-Free
or Double-Free? If it fails, a_crtc->cl_rdma retains its previous value. When
free_cmdlist_regs(a_crtc->cl_rdma) is called later, it frees the memory but
does not set the pointer to NULL, making a_crtc->cl_rdma a dangling pointer on
subsequent failures.
>
> hwdev->plane_update_hw_channel(plane);
>
> /* Solid-fill layers have no RDMA and no page table */
> if (rdma_id < hwdev->rdma_nums && spacemit_pstate->mmu_tbl.va) {
> u8 tbu_id = rdma_id * 2;
>
> ret = spacemit_dmmu_map(plane->state->fb,
> &spacemit_pstate->mmu_tbl, tbu_id,
> a_crtc->cl_rdma, plane);
> if (!ret)
> cmdlist_regs_packing(plane_to_cl(plane),
> CMDLIST_MOD_RDMA, a_crtc->cl_rdma);
> else
> drm_err(plane->dev, "failed to map plane: %d\n", ret);
> }
>
> cl = &spacemit_pstate->cl;
> cl->rch_start_cmps_y = CMDLIST_SENTINEL_POISON;
> cl->cmdlist_ch_y_other = CMDLIST_SENTINEL_POISON;
> free_cmdlist_regs(a_crtc->cl_rdma);
[Severity: Critical]
Does this free_cmdlist_regs() call leave a_crtc->cl_rdma as a dangling pointer?
As mentioned above, since the pointer is not cleared after being freed, a
subsequent allocation failure will leave the pointer pointing to freed memory.
>
> drm_dev_exit(idx);
> }
>
[ ... ]
> @@ -201,15 +190,14 @@
> static void spacemit_plane_reset(struct drm_plane *plane)
> {
> struct spacemit_plane_state *s;
>
> if (plane->state) {
> s = to_spacemit_plane_state(plane->state);
> __drm_atomic_helper_plane_destroy_state(plane->state);
> kfree(s);
> plane->state = NULL;
> }
[Severity: High]
Should this cleanup logic utilize the driver-specific custom cleanup hook? By
open-coding the state destruction and calling kfree(s) directly, it bypasses
spacemit_plane_atomic_destroy_state(). This permanently leaks the plane's DMA
allocations (mmu_tbl.va and cl.va) when spacemit_plane_reset() executes on a
plane that already has a state.
>
> s = kzalloc(sizeof(*s), GFP_KERNEL);
> if (s) {
> __drm_atomic_helper_plane_reset(plane, &s->state);
> s->rdma_id = RDMA_INVALID_ID;
> }
> }
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=10
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (9 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:17 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang
` (6 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Add the DRM device for the SpacemiT K3 Saturn display processing unit
and hook the driver up to the build. The SoC has two DPU instances;
each drives one CRTC that scans out a single primary plane in linear
RGB at up to 3840x2160@60 and feeds a downstream DP/eDP controller
over an OF-graph endpoint. Buffers use drm_gem_shmem_helper. The
AFBC/compressed decode path is not exposed and writeback is not
implemented.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/spacemit/Kconfig | 18 +++++
drivers/gpu/drm/spacemit/Makefile | 12 +++
drivers/gpu/drm/spacemit/spacemit_drm.c | 132 ++++++++++++++++++++++++++++++++
5 files changed, 164 insertions(+)
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 323422861e8f..81081562a6da 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -341,6 +341,7 @@ source "drivers/gpu/drm/renesas/Kconfig"
source "drivers/gpu/drm/rockchip/Kconfig"
source "drivers/gpu/drm/sitronix/Kconfig"
source "drivers/gpu/drm/solomon/Kconfig"
+source "drivers/gpu/drm/spacemit/Kconfig"
source "drivers/gpu/drm/sprd/Kconfig"
source "drivers/gpu/drm/sti/Kconfig"
source "drivers/gpu/drm/stm/Kconfig"
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index e97faabcd783..4a105520ee91 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -204,6 +204,7 @@ obj-$(CONFIG_DRM_MSM) += msm/
obj-$(CONFIG_DRM_TEGRA) += tegra/
obj-$(CONFIG_DRM_STM) += stm/
obj-$(CONFIG_DRM_STI) += sti/
+obj-$(CONFIG_DRM_SPACEMIT) += spacemit/
obj-y += imx/
obj-$(CONFIG_DRM_INGENIC) += ingenic/
obj-$(CONFIG_DRM_LOGICVC) += logicvc/
diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
new file mode 100644
index 000000000000..51e38dabfdba
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config DRM_SPACEMIT
+ tristate "DRM support for SpacemiT display controllers"
+ depends on DRM && OF && MMU
+ depends on ARCH_SPACEMIT || COMPILE_TEST
+ select DRM_CLIENT_SELECTION
+ select DRM_KMS_HELPER
+ select DRM_GEM_SHMEM_HELPER
+ select REGMAP_MMIO
+ select VIDEOMODE_HELPERS
+ help
+ Choose this option if you have a SpacemiT SoC with the Saturn
+ display processing unit, such as the K3. The DPU scans out
+ framebuffers through its private display MMU and feeds a
+ DisplayPort or embedded DisplayPort controller.
+
+ If M is selected the module will be called spacemit-drm.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
new file mode 100644
index 000000000000..b3b148000cf7
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+
+spacemit-drm-y := spacemit_drm.o \
+ spacemit_crtc.o \
+ spacemit_cmdlist.o \
+ spacemit_planes.o \
+ spacemit_dmmu.o \
+ dpu/dpu_saturn.o \
+ dpu/dpu_saturn_hee.o \
+ dpu/saturn_fbcmem.o
+
+obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_drm.c b/drivers/gpu/drm/spacemit/spacemit_drm.c
new file mode 100644
index 000000000000..9fb6a7661b48
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_drm.c
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_of.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <linux/component.h>
+#include <linux/of_graph.h>
+
+#include "spacemit_drm.h"
+#include "spacemit_crtc.h"
+
+#define DRIVER_NAME "spacemit"
+#define DRIVER_DESC "SpacemiT SoC DRM driver"
+#define DRIVER_MAJOR 1
+#define DRIVER_MINOR 0
+
+static void spacemit_drm_atomic_commit_tail(struct drm_atomic_commit *old_state)
+{
+ struct drm_device *dev = old_state->dev;
+
+ drm_atomic_helper_commit_modeset_disables(dev, old_state);
+
+ drm_atomic_helper_commit_modeset_enables(dev, old_state);
+
+ drm_atomic_helper_commit_planes(dev, old_state,
+ DRM_PLANE_COMMIT_ACTIVE_ONLY);
+
+ drm_atomic_helper_wait_for_flip_done(dev, old_state);
+
+ drm_atomic_helper_commit_hw_done(old_state);
+
+ drm_atomic_helper_cleanup_planes(dev, old_state);
+}
+
+static const struct drm_mode_config_helper_funcs spacemit_drm_mode_config_helper = {
+ .atomic_commit_tail = spacemit_drm_atomic_commit_tail,
+};
+
+static const struct drm_mode_config_funcs spacemit_drm_mode_config_funcs = {
+ .fb_create = drm_gem_fb_create,
+ .atomic_check = drm_atomic_helper_check,
+ .atomic_commit = drm_atomic_helper_commit,
+};
+
+int spacemit_drm_mode_config_init(struct drm_device *drm)
+{
+ int ret;
+
+ ret = drmm_mode_config_init(drm);
+ if (ret)
+ return ret;
+
+ drm->mode_config.min_width = 1;
+ drm->mode_config.min_height = 1;
+ /* K3 user manual: each DPU scans out at most 3840x2160@60 */
+ drm->mode_config.max_width = 3840;
+ drm->mode_config.max_height = 2160;
+
+ drm->mode_config.funcs = &spacemit_drm_mode_config_funcs;
+ drm->mode_config.helper_private = &spacemit_drm_mode_config_helper;
+
+ return 0;
+}
+
+DEFINE_DRM_GEM_FOPS(spacemit_drm_fops);
+
+struct drm_driver spacemit_drm_drv = {
+ .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
+ .fops = &spacemit_drm_fops,
+
+ DRM_GEM_SHMEM_DRIVER_OPS,
+ DRM_FBDEV_SHMEM_DRIVER_OPS,
+
+ .name = DRIVER_NAME,
+ .desc = DRIVER_DESC,
+ .major = DRIVER_MAJOR,
+ .minor = DRIVER_MINOR,
+};
+
+static int compare_of(struct device *dev, void *data)
+{
+ return dev->of_node == data;
+}
+
+int spacemit_drm_of_component_probe(struct device *dev)
+{
+ struct device_node *ports, *port, *ep;
+ struct device_node *remote;
+ struct component_match *match = NULL;
+
+ if (!dev->of_node)
+ return -EINVAL;
+
+ /* The DPU is the drm root: its OF graph names the DP/eDP components. */
+ ports = of_get_child_by_name(dev->of_node, "ports");
+ if (!ports) {
+ dev_err(dev, "missing 'ports' child node\n");
+ return -EINVAL;
+ }
+
+ for_each_child_of_node(ports, port) {
+ if (!of_node_name_eq(port, "port"))
+ continue;
+ for_each_child_of_node(port, ep) {
+ if (!of_node_name_eq(ep, "endpoint"))
+ continue;
+ remote = of_graph_get_remote_port_parent(ep);
+ if (!remote)
+ continue;
+ if (of_device_is_available(remote))
+ drm_of_component_match_add(dev, &match,
+ compare_of, remote);
+ of_node_put(remote);
+ }
+ }
+ of_node_put(ports);
+
+ if (!match) {
+ dev_err(dev, "no downstream component available\n");
+ return -ENODEV;
+ }
+
+ return component_master_add_with_match(dev, &spacemit_drm_master_ops,
+ match);
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver
2026-07-25 3:16 ` [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang
@ 2026-07-25 5:17 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:17 UTC (permalink / raw)
To: Cody Kang
Cc: vkoul, robh, conor+dt, dri-devel, olteanv, linux-phy,
neil.armstrong, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Missing IS_ERR() check for 'match' causes an ERR_PTR dereference inside component_master_add_with_match().
- [Medium] The atomic commit tail stalls the commit pipeline by waiting for flip completion before signaling hardware done.
--
--- Patch [11]: [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver ---
commit e12174b9614adeccb5a06f03b5944837004e96f4
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU DRM device driver
This commit adds the DRM device for the SpacemiT K3 Saturn display processing
unit. The SoC has two DPU instances, each driving one CRTC that scans out a
single primary plane in linear RGB, feeding a downstream DP/eDP controller
over an OF-graph endpoint.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_drm.c b/drivers/gpu/drm/spacemit/spacemit_drm.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_drm.c
> @@ -0,0 +1,132 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
> + *
> + */
[ ... ]
> +static void spacemit_drm_atomic_commit_tail(struct drm_atomic_commit *old_state)
> +{
> + struct drm_device *dev = old_state->dev;
> +
> + drm_atomic_helper_commit_modeset_disables(dev, old_state);
> +
> + drm_atomic_helper_commit_modeset_enables(dev, old_state);
> +
> + drm_atomic_helper_commit_planes(dev, old_state,
> + DRM_PLANE_COMMIT_ACTIVE_ONLY);
> +
> + drm_atomic_helper_wait_for_flip_done(dev, old_state);
> +
> + drm_atomic_helper_commit_hw_done(old_state);
[Severity: Medium]
Does calling drm_atomic_helper_wait_for_flip_done() before
drm_atomic_helper_commit_hw_done() stall the commit pipeline?
By waiting for the flip completion before signaling hardware done, the CPU work
for the next frame (such as state preparation and buffer pinning) is blocked
until the hardware flip completes.
Should commit_hw_done() be called before wait_for_flip_done() to avoid
degrading atomic commit performance?
> +
> + drm_atomic_helper_cleanup_planes(dev, old_state);
> +}
[ ... ]
> +int spacemit_drm_of_component_probe(struct device *dev)
> +{
> + struct device_node *ports, *port, *ep;
> + struct device_node *remote;
> + struct component_match *match = NULL;
[ ... ]
> + for_each_child_of_node(ports, port) {
> + if (!of_node_name_eq(port, "port"))
> + continue;
> + for_each_child_of_node(port, ep) {
> + if (!of_node_name_eq(ep, "endpoint"))
> + continue;
> + remote = of_graph_get_remote_port_parent(ep);
> + if (!remote)
> + continue;
> + if (of_device_is_available(remote))
> + drm_of_component_match_add(dev, &match,
> + compare_of, remote);
> + of_node_put(remote);
> + }
> + }
> + of_node_put(ports);
> +
> + if (!match) {
> + dev_err(dev, "no downstream component available\n");
> + return -ENODEV;
> + }
> +
> + return component_master_add_with_match(dev, &spacemit_drm_master_ops,
> + match);
[Severity: High]
Can match be an ERR_PTR here instead of just NULL?
If memory allocation fails inside drm_of_component_match_add(), it can set
the match pointer to an ERR_PTR. Since the code only checks if (!match), an
error pointer would bypass this check and be passed directly to
component_master_add_with_match().
Looking at component_master_add_with_match(), it immediately accesses the
pointer:
drivers/base/component.c:component_master_add_with_match() {
...
/* Reallocate the match array for its true size */
ret = component_match_realloc(match, match->num);
...
}
Should this code check IS_ERR(match) to prevent a potential kernel panic
under memory pressure?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=11
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (10 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:20 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang
` (5 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.
The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
drivers/gpu/drm/spacemit/Kconfig | 19 +
drivers/gpu/drm/spacemit/Makefile | 3 +
drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 +++++++++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_inno_dp.h | 328 ++++
4 files changed, 2793 insertions(+)
diff --git a/drivers/gpu/drm/spacemit/Kconfig b/drivers/gpu/drm/spacemit/Kconfig
index 51e38dabfdba..afd6cdea27fc 100644
--- a/drivers/gpu/drm/spacemit/Kconfig
+++ b/drivers/gpu/drm/spacemit/Kconfig
@@ -16,3 +16,22 @@ config DRM_SPACEMIT
DisplayPort or embedded DisplayPort controller.
If M is selected the module will be called spacemit-drm.
+
+config SPACEMIT_INNO_DP
+ tristate "SpacemiT Innosilicon DP/eDP controller support"
+ depends on DRM_SPACEMIT
+ imply PHY_SPACEMIT_K3_INNO_DP
+ select GENERIC_PHY
+ select DRM_PANEL
+ select DRM_BRIDGE_CONNECTOR
+ select DRM_DISPLAY_HELPER
+ select DRM_DISPLAY_DP_HELPER
+ select DRM_DISPLAY_DP_AUX_BUS
+ help
+ Choose this option to enable the Innosilicon DisplayPort /
+ embedded DisplayPort transmitter integrated in SpacemiT SoCs.
+ The controller sits downstream of the Saturn DPU and drives an
+ eDP panel (discovered over the DP AUX bus) or an external
+ DisplayPort connector.
+
+ If M is selected the module will be called spacemit-inno-dp.
diff --git a/drivers/gpu/drm/spacemit/Makefile b/drivers/gpu/drm/spacemit/Makefile
index b3b148000cf7..260d316a271b 100644
--- a/drivers/gpu/drm/spacemit/Makefile
+++ b/drivers/gpu/drm/spacemit/Makefile
@@ -9,4 +9,7 @@ spacemit-drm-y := spacemit_drm.o \
dpu/dpu_saturn_hee.o \
dpu/saturn_fbcmem.o
+spacemit-inno-dp-y := spacemit_inno_dp.o
+
obj-$(CONFIG_DRM_SPACEMIT) += spacemit-drm.o
+obj-$(CONFIG_SPACEMIT_INNO_DP) += spacemit-inno-dp.o
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
new file mode 100644
index 000000000000..e88731d9b18b
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
@@ -0,0 +1,2443 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/platform_device.h>
+#include <linux/component.h>
+#include <linux/clk.h>
+#include <linux/cleanup.h>
+#include <linux/math64.h>
+#include <linux/mutex.h>
+#include <linux/reset.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/media-bus-format.h>
+#include <linux/of_platform.h>
+#include <linux/phy/phy.h>
+#include <linux/phy/phy-dp.h>
+#include <linux/regmap.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/drm_of.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_device.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/display/drm_dp_aux_bus.h>
+#include <drm/display/drm_dp.h>
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_print.h>
+
+#include "spacemit_inno_dp.h"
+
+#define SPACEMIT_DP_SWING_MAX 2
+#define SPACEMIT_DP_PREEMP_MAX 2
+#define SPACEMIT_DP_AUX_MAX_RETRIES 3
+
+#define SPACEMIT_DP_EDID_CHUNK_SIZE 16
+#define SPACEMIT_DP_DDC_SEGMENT_ADDR 0x30
+#define SPACEMIT_DP_SINK_READY_DELAY_MS 120
+#define SPACEMIT_DP_SINK_READY_RETRIES 3
+
+/*
+ * With DP routed through a Type-C PD controller the HPD pin is virtual and
+ * can re-assert seconds after a transmitter power-cycle. Asserted HPD
+ * returns immediately; the full wait is paid only when no sink shows up.
+ */
+#define SPACEMIT_DP_HPD_POLL_US 1000
+#define SPACEMIT_DP_HPD_DEBOUNCE_US (1000 * USEC_PER_MSEC)
+
+enum spacemit_dp_link_rate {
+ SPACEMIT_DP_LINK_RATE_1_62 = 1620000, /* 1.62 Gbps */
+ SPACEMIT_DP_LINK_RATE_2_70 = 2700000, /* 2.70 Gbps */
+ SPACEMIT_DP_LINK_RATE_5_40 = 5400000, /* 5.40 Gbps */
+ SPACEMIT_DP_LINK_RATE_8_10 = 8100000, /* 8.10 Gbps */
+};
+
+/* The K3 source tops out at HBR2: the PHY's MPLL has no 8.1 Gbps config. */
+#define SPACEMIT_DP_SOURCE_MAX_RATE SPACEMIT_DP_LINK_RATE_5_40
+
+enum spacemit_dp_lane_count {
+ SPACEMIT_DP_LANE_1 = 1,
+ SPACEMIT_DP_LANE_2 = 2,
+ SPACEMIT_DP_LANE_4 = 4,
+};
+
+enum soc_video_format {
+ SOC_VIDEO_RGB_6BIT = 0,
+ SOC_VIDEO_RGB_8BIT = 1,
+ SOC_VIDEO_RGB_10BIT = 2,
+ SOC_VIDEO_RGB_12BIT = 3,
+ SOC_VIDEO_RGB_16BIT = 4,
+ SOC_VIDEO_YUV444_8BIT = 5,
+ SOC_VIDEO_YUV444_10BIT = 6,
+ SOC_VIDEO_YUV444_12BIT = 7,
+ SOC_VIDEO_YUV444_16BIT = 8,
+ SOC_VIDEO_YUV422_8BIT = 9,
+ SOC_VIDEO_YUV422_10BIT = 10,
+ SOC_VIDEO_YUV422_12BIT = 11,
+ SOC_VIDEO_YUV422_16BIT = 12,
+};
+
+enum spacemit_dp_ref_clk {
+ SPACEMIT_DP_REF_CLK_24M = 24000,
+};
+
+static const struct spacemit_dp_link_config {
+ enum spacemit_dp_link_rate rate;
+ enum spacemit_dp_lane_count lanes;
+} spacemit_dp_link_priority_table[] = {
+ {SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_1}, /* 1.62 Gbps */
+ {SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_1}, /* 2.70 Gbps */
+ {SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_2}, /* 3.24 Gbps */
+
+ {SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_2}, /* 5.40 Gbps */
+ {SPACEMIT_DP_LINK_RATE_1_62, SPACEMIT_DP_LANE_4}, /* 6.48 Gbps */
+
+ {SPACEMIT_DP_LINK_RATE_2_70, SPACEMIT_DP_LANE_4}, /* 10.8 Gbps */
+ {SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_2}, /* 10.8 Gbps */
+
+ {SPACEMIT_DP_LINK_RATE_5_40, SPACEMIT_DP_LANE_4}, /* 21.6 Gbps */
+};
+
+static const struct soc_format_info {
+ u8 bpp; /* Bits Per Pixel */
+} format_info_table[] = {
+ [SOC_VIDEO_RGB_6BIT] = { .bpp = 18 },
+ [SOC_VIDEO_RGB_8BIT] = { .bpp = 24 },
+ [SOC_VIDEO_RGB_10BIT] = { .bpp = 30 },
+ [SOC_VIDEO_RGB_12BIT] = { .bpp = 36 },
+ [SOC_VIDEO_RGB_16BIT] = { .bpp = 48 },
+
+ [SOC_VIDEO_YUV444_8BIT] = { .bpp = 24 },
+ [SOC_VIDEO_YUV444_10BIT] = { .bpp = 30 },
+ [SOC_VIDEO_YUV444_12BIT] = { .bpp = 36 },
+ [SOC_VIDEO_YUV444_16BIT] = { .bpp = 48 },
+
+ [SOC_VIDEO_YUV422_8BIT] = { .bpp = 16 },
+ [SOC_VIDEO_YUV422_10BIT] = { .bpp = 20 },
+ [SOC_VIDEO_YUV422_12BIT] = { .bpp = 24 },
+ [SOC_VIDEO_YUV422_16BIT] = { .bpp = 32 },
+};
+
+/**
+ * struct spacemit_dp_dev - Per-instance state for an Innosilicon DP/eDP
+ * controller
+ * @dev: Underlying platform device.
+ * @drm: Parent DRM device, set at component bind.
+ * @encoder: Passive encoder anchoring possible_crtcs; atomic goes through
+ * @bridge.
+ * @bridge: DRM bridge implementing this controller, attached NO_CONNECTOR.
+ * @connector: Connector built on top of @bridge by drm_bridge_connector_init().
+ * @connector_status: Last-known status, driven by the HPD ISR and by polling.
+ * @mode: Committed adjusted_mode, cached under @mode_lock for the async paths.
+ * @regs: Mapped MMIO base of the DP controller register block.
+ * @aux: DPCD AUX channel, registered at bind.
+ * @dpcd: Raw DPCD capability bytes.
+ * @lane_count: Lane count chosen by link training (1, 2 or 4).
+ * @link_rate: Link rate chosen by link training, in kHz.
+ * @link: Sink-advertised link capability snapshot.
+ * @link.revision: DPCD revision byte.
+ * @link.enhanced_framing: Whether the sink advertises enhanced framing.
+ * @link.max_rate: Sink-advertised maximum link rate, kHz.
+ * @link.max_num_lanes: Sink-advertised maximum lane count.
+ * @reset: Optional shared reset line.
+ * @pxclk: Optional external pixel clock.
+ * @pll_clk: The PHY's pixel PLL; clk_set_rate() programs it.
+ * @phy: DP PHY handle.
+ * @phy_powered: Controller-side mirror of the PHY framework's power refcount.
+ * @next_bridge: Downstream bridge; panel-edp's drm_panel wrapper on eDP.
+ * @edp_mode: True when this instance drives an embedded panel.
+ * @use_ext_pixel_clock: True when the pixel clock is sourced externally.
+ * @pixel_clock: Cached external pixel-clock rate, kHz.
+ * @mode_lock: Serialises mode_set / enable / hotplug against each other.
+ * @modeset_retry_work: Deferred "link is bad" notification, see its handler.
+ * @irq: Controller interrupt line.
+ */
+struct spacemit_dp_dev {
+ struct device *dev;
+
+ struct drm_device *drm;
+ struct drm_encoder encoder;
+ struct drm_bridge bridge;
+ struct drm_connector *connector;
+
+ enum drm_connector_status connector_status;
+ struct drm_display_mode mode;
+
+ struct regmap *regs;
+ struct drm_dp_aux aux;
+
+ u8 dpcd[DP_RECEIVER_CAP_SIZE];
+ int lane_count;
+ u32 link_rate;
+
+ struct {
+ u8 revision;
+ u8 enhanced_framing;
+ u32 max_rate;
+ u32 max_num_lanes;
+ } link;
+
+ struct reset_control *reset;
+ struct clk *pxclk;
+ struct clk *pll_clk;
+ struct phy *phy;
+ bool phy_powered;
+ struct drm_bridge *next_bridge;
+
+ bool edp_mode;
+ bool use_ext_pixel_clock;
+ int pixel_clock;
+ struct mutex mode_lock;
+
+ struct work_struct modeset_retry_work;
+
+ int irq;
+};
+
+static int spacemit_dp_get_bpp(struct spacemit_dp_dev *dp, u32 format)
+{
+ if (format >= ARRAY_SIZE(format_info_table)) {
+ dev_warn(dp->dev,
+ "invalid color format index %d, defaulting to RGB888\n",
+ format);
+ return 24;
+ }
+ return format_info_table[format].bpp;
+}
+
+/*
+ * drm_connector_set_link_status_property() takes connection_mutex, which the
+ * commit already holds: calling it from .atomic_enable deadlocks against
+ * itself.
+ */
+static void spacemit_dp_modeset_retry_work(struct work_struct *work)
+{
+ struct spacemit_dp_dev *dp = container_of(work, struct spacemit_dp_dev,
+ modeset_retry_work);
+ struct drm_connector *connector = dp->connector;
+
+ if (!connector)
+ return;
+
+ mutex_lock(&connector->dev->mode_config.mutex);
+ drm_connector_set_link_status_property(connector,
+ DRM_MODE_LINK_STATUS_BAD);
+ mutex_unlock(&connector->dev->mode_config.mutex);
+
+ drm_kms_helper_connector_hotplug_event(connector);
+}
+
+/**
+ * struct spacemit_dp_bridge_state - Per-state planned link configuration.
+ * @base: Parent &struct drm_bridge_state.
+ * @link_cfg_idx: Index into spacemit_dp_link_priority_table[] picked by
+ * ->atomic_check(); -1 means ->atomic_enable() walks the table from the start.
+ * @color_format: Output colour format selected by ->atomic_check().
+ */
+struct spacemit_dp_bridge_state {
+ struct drm_bridge_state base;
+ int link_cfg_idx;
+ u32 color_format;
+};
+
+static inline struct spacemit_dp_bridge_state *
+to_spacemit_dp_bridge_state(struct drm_bridge_state *state)
+{
+ return container_of(state, struct spacemit_dp_bridge_state, base);
+}
+
+static const struct regmap_config spacemit_dp_regmap_config = {
+ .name = "dp",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = 0x3ffc, /* full 0x4000 reg region from binding */
+};
+
+static u32 spacemit_dp_aux_get_cmd(struct drm_dp_aux_msg *msg)
+{
+ switch (msg->request & ~DP_AUX_I2C_MOT) {
+ case DP_AUX_NATIVE_WRITE:
+ case DP_AUX_I2C_WRITE:
+ case DP_AUX_I2C_WRITE_STATUS_UPDATE:
+ return msg->request;
+ case DP_AUX_NATIVE_READ:
+ case DP_AUX_I2C_READ:
+ return msg->request;
+ default:
+ return 0;
+ }
+
+ return 0;
+}
+
+static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
+{
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x1));
+ usleep_range(2000, 2500);
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x0));
+ usleep_range(2000, 2500);
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT,
+ DP_AUX_REPLY_EVENT_INT_STA,
+ FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+}
+
+static bool spacemit_dp_dpcd_caps_valid(const u8 *dpcd)
+{
+ u8 max_bw = dpcd[DP_MAX_LINK_RATE];
+ u8 max_lanes = dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+ if (!dpcd[DP_DPCD_REV])
+ return false;
+
+ switch (max_bw) {
+ case DP_LINK_BW_1_62:
+ case DP_LINK_BW_2_7:
+ case DP_LINK_BW_5_4:
+ case DP_LINK_BW_8_1:
+ break;
+ default:
+ return false;
+ }
+
+ return max_lanes == 1 || max_lanes == 2 || max_lanes == 4;
+}
+
+static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
+ struct drm_dp_aux_msg *msg)
+{
+ int ret, i, retries = 0;
+ unsigned long timeout;
+
+ struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+ aux);
+ u32 cmd, len, val, status;
+ u32 data[4] = {0};
+ u8 *buf = msg->buffer;
+ bool is_read = (msg->request & DP_AUX_I2C_READ) ||
+ ((msg->request & DP_AUX_NATIVE_READ) == DP_AUX_NATIVE_READ);
+
+ if (msg->size > 16)
+ return -EINVAL;
+
+ cmd = spacemit_dp_aux_get_cmd(msg);
+
+retry_eio:
+ if (!is_read) {
+ /* Pack bytes into 32-bit words (Little Endian packing) */
+ for (i = 0; i < msg->size; i++)
+ data[i / 4] |= buf[i] << ((i % 4) * 8);
+
+ /* Write data to registers: DATA1(LSB)..DATA4(MSB) */
+ regmap_write_bits(dp->regs, DP_AUX_DATA1_REG, DP_AUX_DATA1,
+ FIELD_PREP(DP_AUX_DATA1, data[0]));
+ regmap_write_bits(dp->regs, DP_AUX_DATA2_REG, DP_AUX_DATA2,
+ FIELD_PREP(DP_AUX_DATA2, data[1]));
+ regmap_write_bits(dp->regs, DP_AUX_DATA3_REG, DP_AUX_DATA3,
+ FIELD_PREP(DP_AUX_DATA3, data[2]));
+ regmap_write_bits(dp->regs, DP_AUX_DATA4_REG, DP_AUX_DATA4,
+ FIELD_PREP(DP_AUX_DATA4, data[3]));
+ }
+
+ /* HW expects Length - 1 */
+ len = msg->size > 0 ? msg->size - 1 : 0;
+
+ regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_LENGTH,
+ FIELD_PREP(DP_AUX_LENGTH, len));
+ regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_ADDR,
+ FIELD_PREP(DP_AUX_ADDR, msg->address));
+ regmap_write_bits(dp->regs, DP_AUX_CMD_REG, DP_AUX_CMD_TYPE,
+ FIELD_PREP(DP_AUX_CMD_TYPE, cmd));
+
+ regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+ FIELD_PREP(DP_AUX_START, 0));
+ regmap_write_bits(dp->regs, DP_AUX_START_REG, DP_AUX_START,
+ FIELD_PREP(DP_AUX_START, 1));
+
+ timeout = jiffies + msecs_to_jiffies(200);
+ ret = -ETIMEDOUT;
+
+ while (1) {
+ regmap_read(dp->regs, DP_GENERAL_INTERRUPT, &val);
+ val = FIELD_GET(DP_AUX_REPLY_EVENT_INT_STA, val);
+ if (val) {
+ ret = 0;
+ break;
+ }
+ if (time_after(jiffies, timeout))
+ break;
+ usleep_range(100, 110);
+ }
+
+ if (ret) {
+ if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+ retries++;
+ spacemit_dp_aux_hw_reset(dp);
+ dev_dbg(dp->dev,
+ "AUX timeout, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+ cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+ goto retry_eio;
+ }
+
+ dev_err(dp->dev,
+ "AUX transfer timeout, req: 0x%x, cmd: 0x%x, addr: 0x%x, size %zu\n",
+ msg->request, cmd, msg->address, msg->size);
+ return ret;
+ }
+
+ regmap_read(dp->regs, DP_AUX_STS_REG,
+ &status); status = FIELD_GET(DP_AUX_STATUS, status);
+
+ /* Write 1 to clear. */
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT,
+ DP_AUX_REPLY_EVENT_INT_STA,
+ FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+
+ /* Map HW status (the AUX reply command byte) to DRM reply codes */
+ switch (status) {
+ case 0: /* ACK */
+ msg->reply = DP_AUX_NATIVE_REPLY_ACK;
+ break;
+ case 1: /* NACK */
+ msg->reply = DP_AUX_NATIVE_REPLY_NACK;
+ return 0; /* Standard says return 0 on NACK for upper layer retry */
+ case 2: /* DEFER */
+ msg->reply = DP_AUX_NATIVE_REPLY_DEFER;
+ return 0;
+ case DP_AUX_I2C_REPLY_NACK: /* native ACK, I2C NACK */
+ msg->reply = DP_AUX_I2C_REPLY_NACK;
+ return 0;
+ case DP_AUX_I2C_REPLY_DEFER: /* native ACK, I2C DEFER */
+ msg->reply = DP_AUX_I2C_REPLY_DEFER;
+ return 0;
+ default:
+ regmap_read(dp->regs, DP_AUX_STS_REG, &val);
+ val = FIELD_GET(DP_AUX_REPLY_ERR_CODE, val);
+ if (retries < SPACEMIT_DP_AUX_MAX_RETRIES) {
+ retries++;
+ dev_dbg(dp->dev,
+ "AUX status, cmd=0x%x addr=0x%x size=%zu retry %d/%d\n",
+ cmd, msg->address, msg->size, retries, SPACEMIT_DP_AUX_MAX_RETRIES);
+ usleep_range(1000, 1100);
+ goto retry_eio;
+ }
+ dev_err(dp->dev,
+ "AUX error: cmd=0x%x addr=0x%x size=%zu status=0x%x code=0x%x\n",
+ cmd, msg->address, msg->size, status, val);
+ return -EIO;
+ }
+
+ if (is_read && msg->size > 0) {
+ regmap_read(dp->regs, DP_AUX_DATA1_REG, &data[0]);
+ data[0] = FIELD_GET(DP_AUX_DATA1, data[0]);
+ regmap_read(dp->regs, DP_AUX_DATA2_REG, &data[1]);
+ data[1] = FIELD_GET(DP_AUX_DATA2, data[1]);
+ regmap_read(dp->regs, DP_AUX_DATA3_REG, &data[2]);
+ data[2] = FIELD_GET(DP_AUX_DATA3, data[2]);
+ regmap_read(dp->regs, DP_AUX_DATA4_REG, &data[3]);
+ data[3] = FIELD_GET(DP_AUX_DATA4, data[3]);
+
+ /* Unpack 32-bit words back to bytes */
+ for (i = 0; i < msg->size; i++)
+ buf[i] = (data[i / 4] >> ((i % 4) * 8)) & 0xFF;
+ }
+
+ return msg->size;
+}
+
+static int update_edp_config(struct spacemit_dp_dev *dp, bool enable)
+{
+ u8 value;
+ int ret;
+
+ ret = drm_dp_dpcd_read_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, &value);
+ if (ret < 0) {
+ dev_err(dp->dev,
+ "failed to read DP_EDP_CONFIGURATION_SET: %d\n", ret);
+ return ret;
+ }
+
+ if (enable)
+ value |= 0x01;
+ else
+ value &= ~0x01;
+
+ ret = drm_dp_dpcd_write_byte(&dp->aux, DP_EDP_CONFIGURATION_SET, value);
+ if (ret < 0) {
+ dev_err(dp->dev,
+ "failed to write DP_EDP_CONFIGURATION_SET: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Read the sink's DPCD link-layer capabilities. EDID is handled separately. */
+static int spacemit_dp_hw_read_sink_caps(struct spacemit_dp_dev *dp)
+{
+ ssize_t ret;
+ u8 max_bw;
+ int retry;
+
+ for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+ if (retry) {
+ spacemit_dp_aux_hw_reset(dp);
+ msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+ }
+
+ ret = drm_dp_read_dpcd_caps(&dp->aux, dp->dpcd);
+ if (ret < 0)
+ continue;
+
+ if (!spacemit_dp_dpcd_caps_valid(dp->dpcd)) {
+ dev_dbg(dp->dev,
+ "DPCD caps not ready: rev=0x%02x bw=0x%02x lanes=0x%02x\n",
+ dp->dpcd[DP_DPCD_REV], dp->dpcd[DP_MAX_LINK_RATE],
+ dp->dpcd[DP_MAX_LANE_COUNT]);
+ ret = -EAGAIN;
+ continue;
+ }
+
+ break;
+ }
+
+ if (ret < 0) {
+ dev_err(dp->dev, "failed to read DPCD after %d retries: %zd\n",
+ SPACEMIT_DP_SINK_READY_RETRIES, ret);
+ dp->link.revision = 0x14;
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+ dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+ dp->link.enhanced_framing = 1;
+ return ret;
+ }
+
+ dp->link.revision = dp->dpcd[DP_DPCD_REV];
+
+ max_bw = dp->dpcd[DP_MAX_LINK_RATE];
+ switch (max_bw) {
+ case DP_LINK_BW_1_62:
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_1_62;
+ break;
+ case DP_LINK_BW_2_7:
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_2_70;
+ break;
+ case DP_LINK_BW_5_4:
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+ break;
+ case DP_LINK_BW_8_1:
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_8_10;
+ break;
+ default:
+ dev_warn(dp->dev, "unknown DPCD max link rate 0x%x, defaulting to 5.40 Gbps\n",
+ max_bw);
+ dp->link.revision = 0x14;
+ dp->link.max_rate = SPACEMIT_DP_LINK_RATE_5_40;
+ dp->link.max_num_lanes = SPACEMIT_DP_LANE_2;
+ dp->link.enhanced_framing = 1;
+ return -EINVAL;
+ }
+
+ /* Clamp to the source's actual ceiling (HBR2). */
+ dp->link.max_rate = min(dp->link.max_rate,
+ (u32)SPACEMIT_DP_SOURCE_MAX_RATE);
+
+ dp->link.max_num_lanes =
+ dp->dpcd[DP_MAX_LANE_COUNT] & DP_MAX_LANE_COUNT_MASK;
+
+ dp->link.enhanced_framing =
+ (dp->dpcd[DP_MAX_LANE_COUNT] & DP_ENHANCED_FRAME_CAP);
+
+ dev_dbg(dp->dev,
+ "DPCD: Rev %x.%x, MaxRate %d kHz, MaxLanes %d, EnhFrame %d\n",
+ dp->link.revision >> 4, dp->link.revision & 0xF,
+ dp->link.max_rate, dp->link.max_num_lanes,
+ dp->link.enhanced_framing);
+
+ return 0;
+}
+
+static int spacemit_dp_phy_set_lanes(struct spacemit_dp_dev *dp,
+ enum spacemit_dp_lane_count lanes)
+{
+ union phy_configure_opts opts = {
+ .dp = {
+ .lanes = lanes,
+ .set_lanes = true,
+ },
+ };
+ int ret;
+
+ ret = phy_configure(dp->phy, &opts);
+ if (ret)
+ return ret;
+
+ dp->lane_count = lanes;
+ return 0;
+}
+
+static int spacemit_dp_phy_set_rate(struct spacemit_dp_dev *dp,
+ enum spacemit_dp_link_rate rate)
+{
+ union phy_configure_opts opts = {
+ .dp = {
+ /*
+ * enum value is in kbit/s; PHY framework wants Mbit/s.
+ */
+ .link_rate = rate / 1000,
+ .set_rate = true,
+ },
+ };
+ int ret;
+
+ ret = phy_configure(dp->phy, &opts);
+ if (ret)
+ return ret;
+
+ dp->link_rate = rate;
+ return 0;
+}
+
+/*
+ * The link PLL rides phy_configure(); the pixel PLL is a clock the PHY
+ * registered. Link first: that is the order the analog blocks come up in.
+ */
+static int spacemit_dp_set_plls(struct spacemit_dp_dev *dp,
+ enum spacemit_dp_link_rate rate, u32 pclk_khz)
+{
+ int ret;
+
+ ret = spacemit_dp_phy_set_rate(dp, rate);
+ if (ret)
+ return ret;
+
+ return clk_set_rate(dp->pll_clk, (unsigned long)pclk_khz * 1000);
+}
+
+/*
+ * phy_power_on/off() are refcounted, but this driver's bring-up is idempotent
+ * rather than pair-balanced. Mirror the state so the refcount cannot go
+ * negative.
+ */
+static int spacemit_dp_phy_on(struct spacemit_dp_dev *dp)
+{
+ int ret;
+
+ if (dp->phy_powered)
+ return 0;
+
+ ret = phy_power_on(dp->phy);
+ if (ret)
+ return ret;
+
+ dp->phy_powered = true;
+ return 0;
+}
+
+static void spacemit_dp_phy_off(struct spacemit_dp_dev *dp)
+{
+ if (!dp->phy_powered)
+ return;
+
+ phy_power_off(dp->phy);
+ dp->phy_powered = false;
+}
+
+/*
+ * The AUX reset pulse is what leaves the next bring-up a clean state machine.
+ */
+static void spacemit_dp_link_disable(struct spacemit_dp_dev *dp)
+{
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+ DP_VIDEO_STREAM_ENABLE,
+ FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+ spacemit_dp_phy_off(dp);
+
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x1));
+ usleep_range(5000, 8000);
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x0));
+ usleep_range(2000, 4000);
+}
+
+static enum drm_connector_status
+spacemit_dp_hw_detect_hpd(struct spacemit_dp_dev *dp)
+{
+ u32 hpd_status;
+ enum drm_connector_status connector_status =
+ connector_status_disconnected;
+
+ regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &hpd_status);
+ hpd_status = FIELD_GET(DP_HPD_IN_STATUS, hpd_status);
+ if (hpd_status)
+ connector_status = connector_status_connected;
+ else
+ connector_status = connector_status_disconnected;
+
+ return connector_status;
+}
+
+static void spacemit_dp_hw_clean_hpd(struct spacemit_dp_dev *dp)
+{
+ u32 plug_event, unplug_event;
+
+ regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &plug_event);
+ plug_event = FIELD_GET(DP_HOT_PLUG_EVENT, plug_event);
+ regmap_read(dp->regs, DP_HPD_INTERRUPT_STATUS, &unplug_event);
+ unplug_event = FIELD_GET(DP_HOT_UNPLUG_EVENT, unplug_event);
+
+ if (plug_event)
+ regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+ FIELD_PREP(DP_HOT_PLUG_EVENT, 0x1));
+
+ if (unplug_event)
+ regmap_write(dp->regs, DP_HPD_INTERRUPT_STATUS,
+ FIELD_PREP(DP_HOT_UNPLUG_EVENT, 0x1));
+}
+
+static int spacemit_dp_set_training_pattern(struct spacemit_dp_dev *dp,
+ u8 pattern)
+{
+ u32 tps_sel = 0;
+ u8 dpcd_pattern = pattern;
+ int ret;
+
+ if (pattern != DP_TRAINING_PATTERN_DISABLE)
+ dpcd_pattern |= DP_LINK_SCRAMBLING_DISABLE;
+
+ switch (pattern) {
+ case DP_TRAINING_PATTERN_DISABLE:
+ tps_sel = 0;
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_SCRAMBLER_DISABLE,
+ FIELD_PREP(DP_SCRAMBLER_DISABLE, 0));
+ break;
+ case DP_TRAINING_PATTERN_1:
+ tps_sel = 1;
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_SCRAMBLER_DISABLE,
+ FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+ break;
+ case DP_TRAINING_PATTERN_2:
+ tps_sel = 2;
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_SCRAMBLER_DISABLE,
+ FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+ break;
+ case DP_TRAINING_PATTERN_3:
+ tps_sel = 3;
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_SCRAMBLER_DISABLE,
+ FIELD_PREP(DP_SCRAMBLER_DISABLE, 1));
+ break;
+ default:
+ dev_err(dp->dev, "Unsupported training pattern: 0x%x\n",
+ pattern);
+ return -EINVAL;
+ }
+
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_TPS_SEL,
+ FIELD_PREP(DP_TPS_SEL, tps_sel));
+
+ ret = drm_dp_dpcd_write_byte(&dp->aux, DP_TRAINING_PATTERN_SET,
+ dpcd_pattern);
+ if (ret < 0) {
+ dev_err(dp->dev, "failed to set DPCD training pattern: %d\n",
+ ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * The adjust request has to reach the source PHY, not just the sink's DPCD:
+ * a transmitter left at its reset default trains any level > 0 with a bad eye.
+ */
+static int spacemit_dp_link_apply_adjust(struct spacemit_dp_dev *dp,
+ const u8 *link_status,
+ enum spacemit_dp_lane_count lanes,
+ u8 *training_set)
+{
+ union phy_configure_opts opts = {
+ .dp = {
+ .lanes = lanes,
+ .set_voltages = true,
+ },
+ };
+ int i, ret;
+
+ for (i = 0; i < lanes; i++) {
+ u8 v = drm_dp_get_adjust_request_voltage(link_status, i);
+ u8 p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+ u8 v_bits;
+
+ if (v >= SPACEMIT_DP_SWING_MAX)
+ v = SPACEMIT_DP_SWING_MAX;
+ if (p >= SPACEMIT_DP_PREEMP_MAX)
+ p = SPACEMIT_DP_PREEMP_MAX;
+
+ opts.dp.voltage[i] = v;
+ opts.dp.pre[i] = p;
+
+ v_bits = v;
+ if (v == SPACEMIT_DP_SWING_MAX)
+ v_bits |= DP_TRAIN_MAX_SWING_REACHED;
+ if (p == SPACEMIT_DP_PREEMP_MAX)
+ v_bits |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+ training_set[i] = v_bits | (p << DP_TRAIN_PRE_EMPHASIS_SHIFT);
+ }
+
+ ret = phy_configure(dp->phy, &opts);
+ if (ret)
+ return ret;
+
+ return drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+ training_set, lanes);
+}
+
+static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
+ enum spacemit_dp_link_rate rate,
+ enum spacemit_dp_lane_count lanes)
+{
+ u8 link_status[DP_LINK_STATUS_SIZE];
+ u8 training_set[4] = {0};
+ int retries = 0;
+ int ret;
+
+ ret = drm_dp_dpcd_write_data(&dp->aux, DP_TRAINING_LANE0_SET,
+ training_set, lanes);
+ if (ret < 0)
+ return ret;
+
+ ret = spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_1);
+ if (ret < 0) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ while (retries < 8) {
+ drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+
+ ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+ if (ret < 0) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ if (drm_dp_clock_recovery_ok(link_status, lanes))
+ return 0;
+
+ ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+ training_set);
+ if (ret) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ retries++;
+ }
+
+ dev_err(dp->dev, "link training: clock recovery failed\n");
+ spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+ return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train_channel_eq(struct spacemit_dp_dev *dp,
+ enum spacemit_dp_link_rate rate,
+ enum spacemit_dp_lane_count lanes)
+{
+ u8 link_status[DP_LINK_STATUS_SIZE];
+ u8 training_set[4] = {0};
+ u8 training_pattern = DP_TRAINING_PATTERN_2;
+ int retries = 0;
+ int ret;
+
+ if (dp->dpcd[DP_MAX_LANE_COUNT] & DP_TPS3_SUPPORTED) {
+ training_pattern = DP_TRAINING_PATTERN_3;
+ dev_dbg(dp->dev, "link training: using TPS3\n");
+ } else {
+ dev_dbg(dp->dev, "link training: using TPS2\n");
+ }
+
+ ret = spacemit_dp_set_training_pattern(dp, training_pattern);
+ if (ret < 0) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ while (retries < 8) {
+ drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+
+ ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+ if (ret < 0) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ if (drm_dp_channel_eq_ok(link_status, lanes)) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return 0;
+ }
+
+ ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
+ training_set);
+ if (ret) {
+ spacemit_dp_set_training_pattern(dp,
+ DP_TRAINING_PATTERN_DISABLE);
+ return ret;
+ }
+
+ retries++;
+ }
+
+ dev_err(dp->dev, "link training: channel EQ failed\n");
+ spacemit_dp_set_training_pattern(dp, DP_TRAINING_PATTERN_DISABLE);
+ return -ETIMEDOUT;
+}
+
+static int spacemit_dp_link_train(struct spacemit_dp_dev *dp, enum spacemit_dp_link_rate rate,
+ enum spacemit_dp_lane_count lanes)
+{
+ int ret;
+ u8 link_config[1];
+ u8 bw_code;
+
+ /* Map Link Rate Enum to DPCD Bandwidth Code */
+ switch (rate) {
+ case SPACEMIT_DP_LINK_RATE_1_62:
+ bw_code = DP_LINK_BW_1_62;
+ break;
+ case SPACEMIT_DP_LINK_RATE_2_70:
+ bw_code = DP_LINK_BW_2_7;
+ break;
+ case SPACEMIT_DP_LINK_RATE_5_40:
+ bw_code = DP_LINK_BW_5_4;
+ break;
+ case SPACEMIT_DP_LINK_RATE_8_10:
+ bw_code = DP_LINK_BW_8_1;
+ break;
+ default:
+ bw_code = DP_LINK_BW_1_62;
+ break;
+ }
+
+ /*
+ * A sink may act on the DP_LINK_BW_SET write, so set the lane count
+ * first.
+ */
+ link_config[0] = lanes;
+ if (dp->link.enhanced_framing)
+ link_config[0] |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+
+ ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LANE_COUNT_SET,
+ link_config[0]);
+ if (ret < 0) {
+ dev_err(dp->dev, "failed to set DPCD lane count\n");
+ return ret;
+ }
+
+ ret = drm_dp_dpcd_write_byte(&dp->aux, DP_LINK_BW_SET, bw_code);
+ if (ret < 0) {
+ dev_err(dp->dev, "failed to set DPCD link rate\n");
+ return ret;
+ }
+
+ ret = spacemit_dp_link_train_clock_recovery(dp, rate, lanes);
+ if (ret)
+ return ret;
+
+ ret = spacemit_dp_link_train_channel_eq(dp, rate, lanes);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
+ const struct drm_display_mode *mode,
+ enum spacemit_dp_link_rate rate,
+ enum spacemit_dp_lane_count lanes,
+ u32 color_format)
+{
+ u64 hb_num;
+ u32 link_rate;
+ u32 fp; /* Pixel clock in MHz */
+ u32 bpp, misc0;
+ u32 tu, tu_frac, tu_int, rd_thres;
+ u32 hsync_len;
+
+ /* mode->clock unit is kHz, fp unit is MHz */
+ if (dp->use_ext_pixel_clock)
+ fp = dp->pixel_clock / 1000;
+ else
+ fp = mode->clock / 1000;
+
+ if (fp == 0)
+ fp = 1; /* Prevent division by zero */
+
+ bpp = spacemit_dp_get_bpp(dp, color_format);
+
+ /* Calculate MISC0 */
+ /* bit0: 0 (Sync Clock) */
+ /* bits1-7: Color Format (000=RGB, 001=YCbCr422, 010=YCbCr444) */
+ /* bits5-7: BPC (001=8bpc, 010=10bpc, etc) */
+ switch (color_format) {
+ case SOC_VIDEO_RGB_6BIT:
+ misc0 = 0x00;
+ break;
+ case SOC_VIDEO_RGB_8BIT:
+ misc0 = 0x20;
+ break;
+ case SOC_VIDEO_RGB_10BIT:
+ misc0 = 0x40;
+ break;
+ case SOC_VIDEO_RGB_12BIT:
+ misc0 = 0x60;
+ break;
+ case SOC_VIDEO_RGB_16BIT:
+ misc0 = 0x80;
+ break;
+ case SOC_VIDEO_YUV422_8BIT:
+ misc0 = 0x22;
+ break;
+ case SOC_VIDEO_YUV422_10BIT:
+ misc0 = 0x42;
+ break;
+ case SOC_VIDEO_YUV422_12BIT:
+ misc0 = 0x62;
+ break;
+ case SOC_VIDEO_YUV422_16BIT:
+ misc0 = 0x82;
+ break;
+ case SOC_VIDEO_YUV444_8BIT:
+ misc0 = 0x24;
+ break;
+ case SOC_VIDEO_YUV444_10BIT:
+ misc0 = 0x44;
+ break;
+ case SOC_VIDEO_YUV444_12BIT:
+ misc0 = 0x64;
+ break;
+ case SOC_VIDEO_YUV444_16BIT:
+ misc0 = 0x84;
+ break;
+ default:
+ misc0 = 0x20;
+ break;
+ }
+
+ /*
+ * HBlank interval = (htotal - hactive) * (LinkSymbolClock / 4) /
+ * PixelClock, where LinkSymbolClock = LinkRate * 100 (1.62G -> 162MHz).
+ * rate is in kHz, so link_rate = rate / 10000 gives the 162, 270, 540
+ * form used below.
+ */
+ link_rate = rate / 10000;
+
+ /* Multiply before dividing: hb_num = hblank * (link_rate / 4) / fp */
+ hb_num = (u64)(mode->htotal - mode->hdisplay) * link_rate;
+ do_div(hb_num, 4 * fp);
+
+ /* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
+ {
+ u64 temp_tu = (u64)fp * bpp * 640;
+ u32 den = 8 * lanes * link_rate;
+
+ do_div(temp_tu, den);
+ tu = temp_tu;
+ }
+ tu_frac = tu % 10;
+ tu_int = tu / 10;
+
+ /* FIFO read threshold. */
+ if (tu_int < 6)
+ rd_thres = 32;
+ else if ((mode->htotal - mode->hdisplay) < 80)
+ rd_thres = 12;
+ else
+ rd_thres = 16;
+
+ dev_dbg(dp->dev,
+ "MSA: %dx%d, Rate:%d kHz, Lanes:%d, BPP:%d, TU:%d.%d\n",
+ mode->hdisplay, mode->vdisplay, rate, lanes, bpp, tu_int,
+ tu_frac);
+
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG, DP_VIDEO_MAPPING,
+ FIELD_PREP(DP_VIDEO_MAPPING, color_format));
+
+ /*
+ * The Saturn TMG raster is always positive-sync (hsp/vsp fixed to 1).
+ */
+ regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_HSYNC_IN_POLARITY,
+ FIELD_PREP(DP_HSYNC_IN_POLARITY, 1));
+ regmap_write_bits(dp->regs, DP_VINPUT_POLARITY, DP_VSYNC_IN_POLARITY,
+ FIELD_PREP(DP_VSYNC_IN_POLARITY, 1));
+
+ /* Basic timing */
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HACTIVE,
+ FIELD_PREP(DP_HACTIVE, mode->hdisplay));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VACTIVE,
+ FIELD_PREP(DP_VACTIVE, mode->vdisplay));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG1, DP_HBLANK,
+ FIELD_PREP(DP_HBLANK, mode->htotal - mode->hdisplay));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG2, DP_VBLANK,
+ FIELD_PREP(DP_VBLANK, mode->vtotal - mode->vdisplay));
+
+ u32 hstart = mode->htotal - mode->hsync_end +
+ (mode->hsync_end - mode->hsync_start);
+ u32 vstart = mode->vtotal - mode->vsync_end +
+ (mode->vsync_end - mode->vsync_start);
+
+ regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_HSTART,
+ FIELD_PREP(DP_HSTART, hstart));
+ regmap_write_bits(dp->regs, DP_VIDEO_MSA1, DP_VSTART,
+ FIELD_PREP(DP_VSTART, vstart));
+
+ hsync_len = mode->hsync_end - mode->hsync_start;
+
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_SYNC_WIDTH,
+ FIELD_PREP(DP_H_SYNC_WIDTH, hsync_len));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_SYNC_WIDTH,
+ FIELD_PREP(DP_V_SYNC_WIDTH, mode->vsync_end - mode->vsync_start));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG3, DP_H_FRONT_PORCH,
+ FIELD_PREP(DP_H_FRONT_PORCH, mode->hsync_start - mode->hdisplay));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG4, DP_V_FRONT_PORCH,
+ FIELD_PREP(DP_V_FRONT_PORCH, mode->vsync_start - mode->vdisplay));
+
+ /* MSA and MISC */
+ regmap_write_bits(dp->regs, DP_VIDEO_MSA2, DP_MISC0,
+ FIELD_PREP(DP_MISC0, misc0));
+ regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_MISC1,
+ FIELD_PREP(DP_MISC1, 0));
+ regmap_write_bits(dp->regs, DP_VIDEO_MSA3, DP_NVID,
+ FIELD_PREP(DP_NVID, 0));
+
+ /* Link layer parameters */
+ regmap_write_bits(dp->regs, DP_VIDEO_HBLANK_INTERVAL,
+ DP_HBLANK_INTERVAL,
+ FIELD_PREP(DP_HBLANK_INTERVAL, (u32)hb_num));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_AVERAGE_BYTES_PER_TU,
+ FIELD_PREP(DP_AVERAGE_BYTES_PER_TU, tu_int));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5,
+ DP_AVERAGE_BYTES_PER_TU_FRAC,
+ FIELD_PREP(DP_AVERAGE_BYTES_PER_TU_FRAC, tu_frac));
+ regmap_write_bits(dp->regs, DP_VIDEO_CONFIG5, DP_INIT_THRESHOLD,
+ FIELD_PREP(DP_INIT_THRESHOLD, rd_thres));
+
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_SSC_DIS,
+ FIELD_PREP(DP_PHY_SSC_DIS, 1));
+ regmap_write_bits(dp->regs, DP_CLK_INV_MUX, DP_REG_VID_CLK_SEL,
+ FIELD_PREP(DP_REG_VID_CLK_SEL, 0));
+ regmap_write_bits(dp->regs, DP_VIDEO_COLORBAR_CFG, DP_VID_BIST_EN,
+ FIELD_PREP(DP_VID_BIST_EN, 0));
+
+ dev_dbg(dp->dev, "enabling video stream\n");
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+ DP_VIDEO_STREAM_ENABLE,
+ FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 1));
+}
+
+static void spacemit_dp_hw_disable(struct spacemit_dp_dev *dp)
+{
+ dev_dbg(dp->dev, "disabling video and PHY\n");
+
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+ DP_VIDEO_STREAM_ENABLE,
+ FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+ FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D0,
+ FIELD_PREP(DP_ANA_TX_EN_LDO_D0, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D1,
+ FIELD_PREP(DP_ANA_TX_EN_LDO_D1, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D2,
+ FIELD_PREP(DP_ANA_TX_EN_LDO_D2, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_LDO_D3,
+ FIELD_PREP(DP_ANA_TX_EN_LDO_D3, 0));
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D0,
+ FIELD_PREP(DP_ANA_TX_EN_DRV_D0, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D1,
+ FIELD_PREP(DP_ANA_TX_EN_DRV_D1, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D2,
+ FIELD_PREP(DP_ANA_TX_EN_DRV_D2, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_EN_DRV_D3,
+ FIELD_PREP(DP_ANA_TX_EN_DRV_D3, 0));
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D0,
+ FIELD_PREP(DP_ANA_TX_EN_P2S_D0, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D1,
+ FIELD_PREP(DP_ANA_TX_EN_P2S_D1, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D2,
+ FIELD_PREP(DP_ANA_TX_EN_P2S_D2, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_EN_P2S_D3,
+ FIELD_PREP(DP_ANA_TX_EN_P2S_D3, 0));
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL6, DP_ANA_BG_EN,
+ FIELD_PREP(DP_ANA_BG_EN, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_EN_RCAL,
+ FIELD_PREP(DP_ANA_BG_EN_RCAL, 0));
+
+ /* Both PLLs. */
+ regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+ FIELD_PREP(DP_ANA_MPLL_PD, 1));
+ regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+ FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+}
+
+/* Calculate required bandwidth in kbps (Pixel Clock * Bits Per Pixel) */
+static u32 spacemit_dp_calc_required_bw(const struct drm_display_mode *mode,
+ int bpp)
+{
+ return mode->clock * bpp;
+}
+
+/*
+ * Calculate available link capacity in kbps (taking 8b/10b overhead into
+ * account)
+ */
+static u32 spacemit_dp_calc_link_capacity(enum spacemit_dp_link_rate rate,
+ enum spacemit_dp_lane_count lanes)
+{
+ u32 rate_khz = rate;
+ u32 lane_count = lanes;
+
+ /* Capacity = Rate(kHz) * Lanes * 0.8 */
+ return (rate_khz * lane_count * 8) / 10;
+}
+
+static enum drm_connector_status
+spacemit_dp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+ enum drm_connector_status status;
+
+ /*
+ * The eDP panel is fixed, and DP_HPD_IN_STATUS does not read
+ * meaningfully on that path. Report CONNECTED only once panel-edp has
+ * joined the chain.
+ */
+ if (dp->edp_mode) {
+ if (!dp->next_bridge)
+ return connector_status_disconnected;
+ return connector_status_connected;
+ }
+
+ /*
+ * The HPD interrupt is gated by the DP pixel clock, which is off
+ * exactly when a plug must be caught. Read the level register live
+ * instead.
+ */
+ guard(mutex)(&dp->mode_lock);
+ status = spacemit_dp_hw_detect_hpd(dp);
+
+ /*
+ * The bind-time cap read only covers a sink that was already there.
+ * Without a re-read here, link.max_rate stays 0 and every link config
+ * is filtered out.
+ */
+ if (status == connector_status_connected &&
+ dp->connector_status != connector_status_connected)
+ spacemit_dp_hw_read_sink_caps(dp);
+
+ dp->connector_status = status;
+
+ return status;
+}
+
+static int spacemit_dp_aux_i2c_write(struct spacemit_dp_dev *dp, u32 address,
+ const u8 *buf, size_t len)
+{
+ struct drm_dp_aux_msg msg = {
+ .address = address,
+ .request = DP_AUX_I2C_WRITE,
+ .buffer = (u8 *)buf,
+ .size = len,
+ };
+ int ret;
+
+ ret = spacemit_dp_aux_transfer(&dp->aux, &msg);
+ if (ret < 0)
+ return ret;
+
+ return ret == len ? 0 : -EIO;
+}
+
+static int spacemit_dp_aux_i2c_read(struct spacemit_dp_dev *dp, u32 address,
+ u8 *buf, size_t len)
+{
+ struct drm_dp_aux_msg msg = {
+ .address = address,
+ .request = DP_AUX_I2C_READ,
+ .buffer = buf,
+ .size = len,
+ };
+ int ret;
+
+ ret = spacemit_dp_aux_transfer(&dp->aux, &msg);
+ if (ret < 0)
+ return ret;
+
+ return ret == len ? 0 : -EIO;
+}
+
+static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
+ unsigned int block, size_t len)
+{
+ struct spacemit_dp_dev *dp = data;
+ unsigned int start = block * EDID_LENGTH;
+ u8 segment = block >> 1;
+ int ret, retry;
+ size_t offset;
+
+ if (segment) {
+ for (retry = 0; retry < 3; retry++) {
+ ret = spacemit_dp_aux_i2c_write(dp,
+ SPACEMIT_DP_DDC_SEGMENT_ADDR, &segment, 1);
+ if (!ret)
+ break;
+ }
+
+ if (ret) {
+ dev_err(dp->dev,
+ "EDID segment write failed, block %u segment %u: %d\n",
+ block, segment, ret);
+ return ret;
+ }
+ }
+
+ for (offset = 0; offset < len; offset += SPACEMIT_DP_EDID_CHUNK_SIZE) {
+ u8 edid_offset = (start + offset) & 0xff;
+ size_t chunk = min_t(size_t, SPACEMIT_DP_EDID_CHUNK_SIZE,
+ len - offset);
+
+ for (retry = 0; retry < 3; retry++) {
+ ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
+ &edid_offset, 1);
+ if (ret)
+ continue;
+
+ ret = spacemit_dp_aux_i2c_read(dp, DDC_ADDR,
+ buf + offset, chunk);
+ if (!ret)
+ break;
+ }
+
+ if (ret) {
+ dev_err(dp->dev,
+ "EDID read failed, block %u offset 0x%02x len %zu: %d\n",
+ block, edid_offset, chunk, ret);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static const struct drm_edid *
+spacemit_dp_bridge_edid_read(struct drm_bridge *bridge, struct drm_connector *connector)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+ const struct drm_edid *edid = NULL;
+ int retry;
+
+ scoped_guard(mutex, &dp->mode_lock) {
+ for (retry = 0; retry < SPACEMIT_DP_SINK_READY_RETRIES; retry++) {
+ if (retry) {
+ spacemit_dp_aux_hw_reset(dp);
+ msleep(SPACEMIT_DP_SINK_READY_DELAY_MS);
+ }
+
+ edid = drm_edid_read_custom(connector,
+ spacemit_dp_conn_get_edid_block, dp);
+ if (edid)
+ break;
+ }
+ }
+
+ if (!edid)
+ dev_err(dp->dev, "failed to read EDID\n");
+
+ return edid;
+}
+
+static enum drm_mode_status
+spacemit_dp_bridge_mode_valid(struct drm_bridge *bridge,
+ const struct drm_display_info *info,
+ const struct drm_display_mode *mode)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+ unsigned int max_link_rate, max_lanes;
+ u64 max_bw, req_bw;
+ const unsigned int bpp = 24;
+
+ /*
+ * drm_dp_max_link_rate() is in 10 kHz units; clamp to what the PHY can
+ * do.
+ */
+ max_link_rate = min_t(unsigned int, drm_dp_max_link_rate(dp->dpcd),
+ SPACEMIT_DP_SOURCE_MAX_RATE / 10);
+ max_lanes = drm_dp_max_lane_count(dp->dpcd);
+ if (!max_link_rate || !max_lanes) {
+ /* Sink not enumerated yet -- accept and re-check at modeset. */
+ return MODE_OK;
+ }
+
+ /*
+ * max_link_rate is per lane in 10 kHz units. The x10 to reach kHz and
+ * the 8b/10b coding overhead cancel down to a plain "* 8".
+ */
+ max_bw = (u64)max_link_rate * max_lanes * 8;
+
+ /* Required bandwidth: pixel clock (kHz) * bits per pixel. */
+ req_bw = (u64)mode->clock * bpp;
+
+ if (req_bw > max_bw) {
+ drm_dbg_kms(bridge->dev,
+ "rejecting %dx%d@%d: %llu > %llu kbit/s (%u kHz x%u lanes)\n",
+ mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+ req_bw, max_bw, max_link_rate * 10, max_lanes);
+ return MODE_CLOCK_HIGH;
+ }
+
+ return MODE_OK;
+}
+
+static const struct drm_encoder_funcs spacemit_dp_encoder_funcs = {
+ .destroy = drm_encoder_cleanup,
+};
+
+static void spacemit_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+ struct drm_bridge_state *new_bridge_state =
+ drm_atomic_get_new_bridge_state(state, bridge);
+ struct spacemit_dp_bridge_state *st =
+ to_spacemit_dp_bridge_state(new_bridge_state);
+ struct drm_connector_state *conn_state =
+ drm_atomic_get_new_connector_state(state, dp->connector);
+ struct drm_crtc_state *crtc_state =
+ drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+ struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+ const struct spacemit_dp_link_config *cfg;
+ bool trained;
+ u64 clk_val;
+ u64 set_clk_val;
+ u32 req_bw;
+ u32 hpd;
+ int start;
+ int bpp;
+ int i;
+
+ guard(mutex)(&dp->mode_lock);
+ drm_mode_copy(&dp->mode, adjusted_mode);
+
+ /* Enable-time retry for the caps .detect() could not read. */
+ if (!dp->link.max_rate || !dp->link.max_num_lanes)
+ spacemit_dp_hw_read_sink_caps(dp);
+
+ if (dp->use_ext_pixel_clock && dp->pxclk) {
+ set_clk_val = adjusted_mode->clock * 1000;
+ if (set_clk_val) {
+ set_clk_val = clk_round_rate(dp->pxclk, set_clk_val);
+ clk_val = clk_get_rate(dp->pxclk);
+ if (clk_val != set_clk_val) {
+ clk_set_rate(dp->pxclk, set_clk_val);
+ dev_dbg(dp->dev, "set dp pxclk=%lld\n",
+ set_clk_val);
+ }
+ }
+ clk_val = clk_get_rate(dp->pxclk);
+ dp->pixel_clock = clk_val / 1000;
+ }
+
+ bpp = spacemit_dp_get_bpp(dp, st->color_format);
+ req_bw = spacemit_dp_calc_required_bw(adjusted_mode, bpp);
+
+ /*
+ * Start where atomic_check() left off, but keep the upgrade walk as
+ * fallback.
+ */
+ start = st->link_cfg_idx >= 0 ? st->link_cfg_idx : 0;
+
+ for (trained = false, i = start;
+ i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+ u32 capacity;
+
+ cfg = &spacemit_dp_link_priority_table[i];
+
+ /* Source and sink limits */
+ if (cfg->rate > dp->link.max_rate || cfg->lanes > dp->link.max_num_lanes)
+ continue;
+
+ /* Bandwidth requirement */
+ capacity = spacemit_dp_calc_link_capacity(cfg->rate,
+ cfg->lanes);
+ if (capacity < req_bw)
+ continue;
+
+ dev_dbg(dp->dev, "trying link config: rate %d lanes %d (capacity %d, required %d)\n",
+ cfg->rate, cfg->lanes, capacity, req_bw);
+
+ spacemit_dp_link_disable(dp);
+
+ /* Apply Hardware Settings */
+ if (spacemit_dp_set_plls(dp, cfg->rate,
+ dp->use_ext_pixel_clock ? dp->pixel_clock
+ : adjusted_mode->clock))
+ continue;
+
+ if (spacemit_dp_phy_set_lanes(dp, cfg->lanes))
+ continue;
+
+ if (spacemit_dp_phy_on(dp))
+ continue;
+
+ /*
+ * Debounce: the transmitter was power-cycled just now and a
+ * sink may legitimately drop HPD across a link-down. eDP is
+ * hardwired and noisy.
+ */
+ if (!dp->edp_mode &&
+ regmap_read_poll_timeout(dp->regs, DP_HPD_INTERRUPT_STATUS, hpd,
+ hpd & DP_HPD_IN_STATUS,
+ SPACEMIT_DP_HPD_POLL_US,
+ SPACEMIT_DP_HPD_DEBOUNCE_US)) {
+ dev_warn(dp->dev,
+ "sink is not asserting HPD; not training\n");
+ break;
+ }
+
+ if (dp->edp_mode) {
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_ENABLE_EDP,
+ FIELD_PREP(DP_ENABLE_EDP, 0x1));
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+ DP_STREAM_ENC_EN,
+ FIELD_PREP(DP_STREAM_ENC_EN, 0x1));
+ update_edp_config(dp, true);
+ }
+
+ /* Execute Link Training */
+ if (spacemit_dp_link_train(dp, cfg->rate, cfg->lanes) == 0) {
+ dev_dbg(dp->dev,
+ "link training succeeded: rate %d lanes %d\n",
+ cfg->rate, cfg->lanes);
+ trained = true;
+ break;
+ }
+
+ dev_warn(dp->dev, "link training failed: rate %d lanes %d, trying next config\n",
+ cfg->rate, cfg->lanes);
+ }
+
+ /*
+ * The DPU's pixel clock comes from the PHY PLL, so scanning out into an
+ * untrained link never reaches CFG_RDY and wedges the CRTC on
+ * flip_done.
+ */
+ if (!trained) {
+ dev_err(dp->dev,
+ "no usable link config; leaving video disabled (%u kHz x%u lanes)\n",
+ dp->link.max_rate, dp->link.max_num_lanes);
+ /*
+ * The caps may be from a sink that has since been swapped out.
+ */
+ dp->link.max_rate = 0;
+ dp->link.max_num_lanes = 0;
+
+ /*
+ * Deferred: we are inside the commit and hold the modeset
+ * locks.
+ */
+ schedule_work(&dp->modeset_retry_work);
+ return;
+ }
+
+ spacemit_dp_hw_set_msa_and_enable_video(dp, adjusted_mode,
+ dp->link_rate, dp->lane_count,
+ st->color_format);
+}
+
+static void spacemit_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+
+ guard(mutex)(&dp->mode_lock);
+
+ spacemit_dp_link_disable(dp);
+ memset(&dp->mode, 0, sizeof(dp->mode));
+}
+
+static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
+ struct drm_bridge_state *bridge_state,
+ struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+ struct spacemit_dp_bridge_state *st =
+ to_spacemit_dp_bridge_state(bridge_state);
+ const struct drm_display_mode *mode = &crtc_state->adjusted_mode;
+ int bpp;
+ u32 req_bw;
+ int i;
+
+ /* A single output bus format, see ->atomic_get_output_bus_fmts(). */
+ st->color_format = SOC_VIDEO_RGB_8BIT;
+ bpp = spacemit_dp_get_bpp(dp, st->color_format);
+ req_bw = spacemit_dp_calc_required_bw(mode, bpp);
+
+ /* No caps yet: leave idx unset and let ->atomic_enable() retry. */
+ if (!dp->link.max_rate || !dp->link.max_num_lanes) {
+ st->link_cfg_idx = -1;
+ return 0;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
+ const struct spacemit_dp_link_config *cfg =
+ &spacemit_dp_link_priority_table[i];
+
+ if (cfg->rate > dp->link.max_rate ||
+ cfg->lanes > dp->link.max_num_lanes)
+ continue;
+ if (spacemit_dp_calc_link_capacity(cfg->rate, cfg->lanes) < req_bw)
+ continue;
+
+ st->link_cfg_idx = i;
+ return 0;
+ }
+
+ drm_dbg_kms(bridge->dev,
+ "no DP link cfg fits %dx%d@%d (need %u kbit/s; sink %u kHz x %u lanes)\n",
+ mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
+ req_bw, dp->link.max_rate * 10, dp->link.max_num_lanes);
+ return -EINVAL;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge)
+{
+ struct spacemit_dp_bridge_state *cur =
+ to_spacemit_dp_bridge_state(drm_priv_to_bridge_state(bridge->base.state));
+ struct spacemit_dp_bridge_state *new;
+
+ new = kzalloc_obj(*new);
+ if (!new)
+ return NULL;
+
+ __drm_atomic_helper_bridge_duplicate_state(bridge, &new->base);
+ new->link_cfg_idx = cur->link_cfg_idx;
+ new->color_format = cur->color_format;
+ return &new->base;
+}
+
+static void
+spacemit_dp_bridge_atomic_destroy_state(struct drm_bridge *bridge,
+ struct drm_bridge_state *state)
+{
+ kfree(to_spacemit_dp_bridge_state(state));
+}
+
+static u32 *spacemit_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge,
+ struct drm_bridge_state *bridge_state,
+ struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state,
+ unsigned int *num_output_fmts)
+{
+ u32 *output_fmts;
+
+ output_fmts = kmalloc_obj(*output_fmts, GFP_KERNEL);
+ if (!output_fmts) {
+ *num_output_fmts = 0;
+ return NULL;
+ }
+
+ output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
+ *num_output_fmts = 1;
+ return output_fmts;
+}
+
+static struct drm_bridge_state *
+spacemit_dp_bridge_atomic_create_state(struct drm_bridge *bridge)
+{
+ struct spacemit_dp_bridge_state *new;
+
+ new = kzalloc_obj(*new);
+ if (!new)
+ return ERR_PTR(-ENOMEM);
+
+ __drm_atomic_helper_bridge_state_init(&new->base, bridge);
+ new->link_cfg_idx = -1;
+ new->color_format = SOC_VIDEO_RGB_8BIT;
+ return &new->base;
+}
+
+static int spacemit_dp_bridge_attach(struct drm_bridge *bridge,
+ struct drm_encoder *encoder,
+ enum drm_bridge_attach_flags flags)
+{
+ struct spacemit_dp_dev *dp = container_of(bridge,
+ struct spacemit_dp_dev,
+ bridge);
+
+ if (!dp->next_bridge)
+ return 0;
+
+ return drm_bridge_attach(encoder, dp->next_bridge, bridge, flags);
+}
+
+/*
+ * AUX-bus done-probing callback. panel-edp may probe inline (before .attach) or
+ * off the deferred-probe queue (after it), so handle both orders.
+ */
+static int spacemit_dp_edp_link_panel(struct drm_dp_aux *aux)
+{
+ struct spacemit_dp_dev *dp = container_of(aux, struct spacemit_dp_dev,
+ aux);
+ struct drm_bridge *next;
+ int ret;
+
+ next = devm_drm_of_get_bridge(dp->dev, dp->dev->of_node, 1, 0);
+ if (IS_ERR(next))
+ return PTR_ERR(next);
+
+ dp->next_bridge = next;
+
+ /*
+ * A non-NULL bridge.encoder means .attach already ran: late-chain
+ * panel-edp.
+ */
+ if (dp->bridge.encoder) {
+ ret = drm_bridge_attach(dp->bridge.encoder, next,
+ &dp->bridge,
+ DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+ if (ret)
+ return ret;
+
+ /*
+ * eDP HPD-IN is unreliable, so bind may have skipped the cap
+ * read and left link.max_rate at 0. The AUX bus just worked;
+ * read them here.
+ */
+ if (dp->edp_mode) {
+ guard(mutex)(&dp->mode_lock);
+ spacemit_dp_hw_read_sink_caps(dp);
+ }
+
+ drm_bridge_hpd_notify(&dp->bridge,
+ spacemit_dp_bridge_detect(&dp->bridge,
+ dp->connector));
+ }
+
+ return 0;
+}
+
+static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
+ .attach = spacemit_dp_bridge_attach,
+ .atomic_enable = spacemit_dp_bridge_atomic_enable,
+ .atomic_disable = spacemit_dp_bridge_atomic_disable,
+ .atomic_check = spacemit_dp_bridge_atomic_check,
+ .atomic_duplicate_state = spacemit_dp_bridge_atomic_duplicate_state,
+ .atomic_destroy_state = spacemit_dp_bridge_atomic_destroy_state,
+ .atomic_create_state = spacemit_dp_bridge_atomic_create_state,
+ .atomic_get_output_bus_fmts = spacemit_dp_bridge_atomic_get_output_bus_fmts,
+ .mode_valid = spacemit_dp_bridge_mode_valid,
+ .detect = spacemit_dp_bridge_detect,
+ .edid_read = spacemit_dp_bridge_edid_read,
+};
+
+static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
+{
+ struct spacemit_dp_dev *dp = data;
+ irqreturn_t ret = IRQ_NONE;
+ int idx;
+
+ if (!drm_dev_enter(dp->drm, &idx))
+ return IRQ_NONE;
+
+ /*
+ * eDP .detect() ignores the HPD level (and never records it), so a
+ * level flip would wake the thread forever; ack and ignore.
+ */
+ if (dp->edp_mode) {
+ spacemit_dp_hw_clean_hpd(dp);
+ drm_dev_exit(idx);
+ return IRQ_HANDLED;
+ }
+
+ /*
+ * Only compare here: the authoritative connector_status update
+ * happens in the thread under mode_lock, via .detect().
+ */
+ if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
+ ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
+ spacemit_dp_hw_clean_hpd(dp);
+
+ drm_dev_exit(idx);
+ return ret;
+}
+
+static irqreturn_t spacemit_dp_hotplug_event_handler(int irq, void *data)
+{
+ struct spacemit_dp_dev *dp = data;
+ enum drm_connector_status status;
+ int idx;
+
+ if (!drm_dev_enter(dp->drm, &idx))
+ return IRQ_HANDLED;
+
+ /*
+ * .detect() owns mode_lock and re-reads the sink caps on a fresh plug
+ */
+ status = spacemit_dp_bridge_detect(&dp->bridge, dp->connector);
+ drm_bridge_hpd_notify(&dp->bridge, status);
+
+ drm_dev_exit(idx);
+ return IRQ_HANDLED;
+}
+
+static int spacemit_dp_resource_init(struct spacemit_dp_dev *dp,
+ struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ void __iomem *base;
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return dev_err_probe(dev, PTR_ERR(base),
+ "failed to map registers\n");
+ dp->regs = devm_regmap_init_mmio(dev, base, &spacemit_dp_regmap_config);
+ if (IS_ERR(dp->regs))
+ return dev_err_probe(dev, PTR_ERR(dp->regs),
+ "failed to init DP regmap\n");
+
+ /*
+ * The pixel clock comes from the PHY PLL, so the controller never sets
+ * it.
+ */
+ dp->use_ext_pixel_clock = false;
+
+ dp->irq = platform_get_irq(pdev, 0);
+ if (dp->irq < 0) {
+ dev_err(dev, "failed to get IRQ\n");
+ return dp->irq;
+ }
+
+ return 0;
+}
+
+static int spacemit_dp_dev_init(struct spacemit_dp_dev *dp)
+{
+ int ret;
+ u32 m_isel = 0x5, m_mainsel = 0x19;
+ u32 m_pre = 0x0, m_post = 0x2;
+ u32 tx_mode = 0x1, tx_pre = 0x0;
+ u32 clk_div = 24 * 1000 / 100;
+
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+ FIELD_PREP(DP_XMIT_ENABLE, 0));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(dp->regs, DP_MPLL_CTRL0, DP_ANA_MPLL_PD,
+ FIELD_PREP(DP_ANA_MPLL_PD, 1));
+ regmap_write_bits(dp->regs, DP_PREPLL_CTRL0, DP_ANA_PREPLL_PD,
+ FIELD_PREP(DP_ANA_PREPLL_PD, 1));
+ usleep_range(2000, 4000);
+
+ /* Reset Controller and PHY */
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+ FIELD_PREP(DP_CONTROLLER_RESET, 0x1));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+ FIELD_PREP(DP_PHY_RESET, 0x1));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+ FIELD_PREP(DP_HDCP_RESET, 0x1));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x1));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+ FIELD_PREP(DP_VIDEO_RESET, 0x1));
+ usleep_range(5000, 8000);
+
+ /* Release the resets */
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_CONTROLLER_RESET,
+ FIELD_PREP(DP_CONTROLLER_RESET, 0x0));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_PHY_RESET,
+ FIELD_PREP(DP_PHY_RESET, 0x0));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_HDCP_RESET,
+ FIELD_PREP(DP_HDCP_RESET, 0x0));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
+ FIELD_PREP(DP_AUX_RESET, 0x0));
+ regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_VIDEO_RESET,
+ FIELD_PREP(DP_VIDEO_RESET, 0x0));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT,
+ DP_AUX_REPLY_EVENT_INT_STA,
+ FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
+
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL,
+ DP_DEFAULT_FAST_LINK_TRAIN_EN,
+ FIELD_PREP(DP_DEFAULT_FAST_LINK_TRAIN_EN, 0x0));
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCRAMBLER_DISABLE,
+ FIELD_PREP(DP_SCRAMBLER_DISABLE, 0x0));
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_SCALE_DOWN_MODE,
+ FIELD_PREP(DP_SCALE_DOWN_MODE, 0x0));
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_XMIT_ENABLE,
+ FIELD_PREP(DP_XMIT_ENABLE, 0));
+
+ regmap_write_bits(dp->regs, DP_VIDEO_VSAMPLE_REG,
+ DP_VIDEO_STREAM_ENABLE,
+ FIELD_PREP(DP_VIDEO_STREAM_ENABLE, 0));
+
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_AUX_REPLY_EVENT_INT_STA_MSK,
+ FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_HDCP_INT_STA_MSK,
+ FIELD_PREP(DP_HDCP_INT_STA_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_ILLEGAL_AUX_CMD_INT_STA_MSK,
+ FIELD_PREP(DP_ILLEGAL_AUX_CMD_INT_STA_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_TYPE_C_EVENT_MSK,
+ FIELD_PREP(DP_TYPE_C_EVENT_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK, DP_DSC_EVENT_MSK,
+ FIELD_PREP(DP_DSC_EVENT_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_SDP_INT_STA_S3_MSK,
+ FIELD_PREP(DP_SDP_INT_STA_S3_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_SDP_INT_STA_S2_MSK,
+ FIELD_PREP(DP_SDP_INT_STA_S2_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_SDP_INT_STA_S1_MSK,
+ FIELD_PREP(DP_SDP_INT_STA_S1_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_SDP_INT_STA_S0_MSK,
+ FIELD_PREP(DP_SDP_INT_STA_S0_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK,
+ FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK,
+ FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK,
+ FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK,
+ FIELD_PREP(DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+ DP_SINK_IRQ_EVENT_MSK,
+ FIELD_PREP(DP_SINK_IRQ_EVENT_MSK, 0x0));
+ regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT_MASK,
+ DP_HPD_INT_STA_MSK,
+ FIELD_PREP(DP_HPD_INT_STA_MSK, 0x1));
+ regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+ DP_HOT_PLUG_EVENT_MSK,
+ FIELD_PREP(DP_HOT_PLUG_EVENT_MSK, 0x1));
+ regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+ DP_HOT_UNPLUG_EVENT_MSK,
+ FIELD_PREP(DP_HOT_UNPLUG_EVENT_MSK, 0x1));
+ regmap_write_bits(dp->regs, DP_HPD_INTERRUPT_ENABLE,
+ DP_SINK_UNPLUG_ERROR_EVENT_MSK,
+ FIELD_PREP(DP_SINK_UNPLUG_ERROR_EVENT_MSK, 0x0));
+ usleep_range(2000, 4000);
+
+ regmap_write_bits(dp->regs, DP_PHYIF_CTRL_ADDR, DP_PHY_BUSY_BYP,
+ FIELD_PREP(DP_PHY_BUSY_BYP, 0x1));
+
+ /* Enable enhanced framing */
+ regmap_write_bits(dp->regs, DP_CORE_CONTROL, DP_ENHANCE_FRAMING_EN,
+ FIELD_PREP(DP_ENHANCE_FRAMING_EN, 0x1));
+
+ ret = spacemit_dp_set_plls(dp, SPACEMIT_DP_LINK_RATE_2_70,
+ dp->use_ext_pixel_clock ? 150000 : 148500);
+ if (ret)
+ return ret;
+
+ ret = spacemit_dp_phy_set_lanes(dp, SPACEMIT_DP_LANE_2);
+ if (ret)
+ return ret;
+
+ ret = spacemit_dp_phy_on(dp);
+ if (ret)
+ return ret;
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+ FIELD_PREP(DP_ANA_TX_MODE_D0, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+ FIELD_PREP(DP_ANA_TX_MODE_D1, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+ FIELD_PREP(DP_ANA_TX_MODE_D2, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+ FIELD_PREP(DP_ANA_TX_MODE_D3, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_HBIT,
+ FIELD_PREP(DP_ANA_RTCAL_FREQDIV_HBIT, (clk_div >> 8) & 0x7f));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+ FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_FREQDIV_LBIT,
+ FIELD_PREP(DP_ANA_RTCAL_FREQDIV_LBIT, clk_div & 0xff));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_BG_RCAL_SEL,
+ FIELD_PREP(DP_ANA_BG_RCAL_SEL, 0));
+
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D3,
+ FIELD_PREP(DP_ANA_TX_RTM_D3, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D2,
+ FIELD_PREP(DP_ANA_TX_RTM_D2, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D1,
+ FIELD_PREP(DP_ANA_TX_RTM_D1, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL8, DP_ANA_TX_RTM_D0,
+ FIELD_PREP(DP_ANA_TX_RTM_D0, 0));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+ FIELD_PREP(DP_ANA_RTCAL_BYPASS, 1));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL7, DP_ANA_RTCAL_BYPASS,
+ FIELD_PREP(DP_ANA_RTCAL_BYPASS, 0));
+ msleep(100);
+
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D3,
+ FIELD_PREP(DP_ANA_TX_MODE_PRE_D3, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D2,
+ FIELD_PREP(DP_ANA_TX_MODE_PRE_D2, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D1,
+ FIELD_PREP(DP_ANA_TX_MODE_PRE_D1, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_PRE_D0,
+ FIELD_PREP(DP_ANA_TX_MODE_PRE_D0, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D3,
+ FIELD_PREP(DP_ANA_TX_MODE_DE_D3, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D2,
+ FIELD_PREP(DP_ANA_TX_MODE_DE_D2, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D1,
+ FIELD_PREP(DP_ANA_TX_MODE_DE_D1, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_MODE_DE_D0,
+ FIELD_PREP(DP_ANA_TX_MODE_DE_D0, 1));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D3,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D3, tx_pre));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D2,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D2, tx_pre));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D1,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D1, tx_pre));
+ regmap_write_bits(dp->regs, DP_ANA_LANE_EMPH, DP_ANA_TX_POSTSEL_PRE_D0,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_PRE_D0, tx_pre));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D3,
+ FIELD_PREP(DP_ANA_TX_ISEL_DRV_D3, m_isel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_ENABLE, DP_ANA_TX_ISEL_DRV_D2,
+ FIELD_PREP(DP_ANA_TX_ISEL_DRV_D2, m_isel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D2,
+ FIELD_PREP(DP_ANA_TX_MAINSEL_D2, m_mainsel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_MAINSEL_D3,
+ FIELD_PREP(DP_ANA_TX_MAINSEL_D3, m_mainsel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D1,
+ FIELD_PREP(DP_ANA_TX_ISEL_DRV_D1, m_isel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL1, DP_ANA_TX_ISEL_DRV_D0,
+ FIELD_PREP(DP_ANA_TX_ISEL_DRV_D0, m_isel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D1,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_D1, m_post));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D0,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_D0, m_post));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D3,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_D3, m_post));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_POSTSEL_D2,
+ FIELD_PREP(DP_ANA_TX_POSTSEL_D2, m_post));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_DA_TX_MAINSEL_D0_4_0,
+ FIELD_PREP(DP_DA_TX_MAINSEL_D0_4_0, m_mainsel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL2, DP_ANA_TX_MAINSEL_D1,
+ FIELD_PREP(DP_ANA_TX_MAINSEL_D1, m_mainsel));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D1,
+ FIELD_PREP(DP_ANA_TX_PRESEL_D1, m_pre));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D0,
+ FIELD_PREP(DP_ANA_TX_PRESEL_D0, m_pre));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D3,
+ FIELD_PREP(DP_ANA_TX_PRESEL_D3, m_pre));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_PRESEL_D2,
+ FIELD_PREP(DP_ANA_TX_PRESEL_D2, m_pre));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D3,
+ FIELD_PREP(DP_ANA_TX_MODE_D3, tx_mode));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D2,
+ FIELD_PREP(DP_ANA_TX_MODE_D2, tx_mode));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D1,
+ FIELD_PREP(DP_ANA_TX_MODE_D1, tx_mode));
+ regmap_write_bits(dp->regs, DP_ANA_TX_CTRL3, DP_ANA_TX_MODE_D0,
+ FIELD_PREP(DP_ANA_TX_MODE_D0, tx_mode));
+ regmap_write_bits(dp->regs, DP_ANA_TX_AUX_CTRL, DP_ANA_TX_AUX_RX_VSEL,
+ FIELD_PREP(DP_ANA_TX_AUX_RX_VSEL, 0x0));
+
+ return 0;
+}
+
+static int spacemit_dp_bind(struct device *dev, struct device *master,
+ void *data)
+{
+ struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+ struct drm_device *drm = data;
+ int ret;
+
+ dp->drm = drm;
+ dp->aux.drm_dev = drm;
+ ret = drm_dp_aux_register(&dp->aux);
+ if (ret) {
+ dev_err(dev, "failed to register DP AUX: %d\n", ret);
+ return ret;
+ }
+
+ ret = drm_encoder_init(drm, &dp->encoder, &spacemit_dp_encoder_funcs,
+ DRM_MODE_ENCODER_NONE, NULL);
+ if (ret) {
+ dev_err(dev, "failed to init encoder\n");
+ goto err_aux;
+ }
+
+ dp->encoder.possible_crtcs = drm_of_find_possible_crtcs(drm,
+ dev->of_node);
+
+ ret = drm_bridge_attach(&dp->encoder, &dp->bridge, NULL,
+ DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+ if (ret) {
+ dev_err(dev, "failed to attach bridge: %d\n", ret);
+ goto err_encoder;
+ }
+
+ dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
+ if (IS_ERR(dp->connector)) {
+ ret = PTR_ERR(dp->connector);
+ dev_err(dev, "failed to init bridge connector: %d\n", ret);
+ goto err_encoder;
+ }
+
+ drm_connector_attach_encoder(dp->connector, &dp->encoder);
+
+ ret = spacemit_dp_dev_init(dp);
+ if (ret)
+ goto err_encoder;
+
+ spacemit_dp_link_disable(dp);
+
+ dp->connector_status = spacemit_dp_hw_detect_hpd(dp);
+ spacemit_dp_hw_clean_hpd(dp);
+
+ if (dp->connector_status == connector_status_connected)
+ spacemit_dp_hw_read_sink_caps(dp);
+
+ /*
+ * With panel-edp still deferred the chain is incomplete: a CONNECTED
+ * notification here makes fbdev commit a modeset that wedges on
+ * flip_done.
+ */
+ if (dp->next_bridge || !dp->edp_mode)
+ drm_bridge_hpd_notify(&dp->bridge,
+ spacemit_dp_bridge_detect(&dp->bridge,
+ dp->connector));
+
+ enable_irq(dp->irq);
+
+ return 0;
+
+err_encoder:
+ drm_encoder_cleanup(&dp->encoder);
+err_aux:
+ drm_dp_aux_unregister(&dp->aux);
+ return ret;
+}
+
+static void spacemit_dp_unbind(struct device *dev, struct device *master,
+ void *data)
+{
+ struct spacemit_dp_dev *dp = dev_get_drvdata(dev);
+
+ disable_irq(dp->irq);
+
+ /* The connector this work touches dies with the DRM device. */
+ cancel_work_sync(&dp->modeset_retry_work);
+
+ drm_dp_aux_unregister(&dp->aux);
+
+ spacemit_dp_hw_disable(dp);
+
+ drm_encoder_cleanup(&dp->encoder);
+
+ dp->drm = NULL;
+ dp->connector = NULL;
+ dp->aux.drm_dev = NULL;
+}
+
+static const struct component_ops spacemit_dp_ops = {
+ .bind = spacemit_dp_bind,
+ .unbind = spacemit_dp_unbind,
+};
+
+static int inno_dp_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct device_node *aux_bus_np;
+ struct spacemit_dp_dev *dp;
+ struct resource *res;
+ int ret;
+
+ dp = devm_drm_bridge_alloc(dev, struct spacemit_dp_dev, bridge,
+ &spacemit_dp_bridge_funcs);
+ if (IS_ERR(dp))
+ return PTR_ERR(dp);
+
+ res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+ if (!res)
+ return dev_err_probe(dev, -ENODEV,
+ "no MMIO resource\n");
+
+ dp->dev = dev;
+ INIT_WORK(&dp->modeset_retry_work, spacemit_dp_modeset_retry_work);
+
+ /* eDP instances have an aux-bus child node; DP instances do not. */
+ aux_bus_np = of_get_child_by_name(dev->of_node, "aux-bus");
+ dp->edp_mode = !!aux_bus_np;
+ of_node_put(aux_bus_np);
+
+ dp->bridge.of_node = dev->of_node;
+ dp->connector_status = connector_status_disconnected;
+ ret = devm_mutex_init(dev, &dp->mode_lock);
+ if (ret)
+ return ret;
+
+ dp->reset = devm_reset_control_get_optional_exclusive(&pdev->dev, NULL);
+ if (IS_ERR(dp->reset)) {
+ ret = dev_err_probe(dev, PTR_ERR(dp->reset),
+ "failed to get reset\n");
+ return ret;
+ }
+
+ dp->pxclk = devm_clk_get(dev, "pxclk");
+ if (IS_ERR(dp->pxclk)) {
+ ret = dev_err_probe(dev, PTR_ERR(dp->pxclk),
+ "failed to get pxclk\n");
+ return ret;
+ }
+
+ if (dp->edp_mode) {
+ dp->bridge.type = DRM_MODE_CONNECTOR_eDP;
+ /*
+ * OP_DETECT is required: without it
+ * drm_bridge_connector_detect() hard-codes CONNECTED for any
+ * eDP connector and fbdev wedges at bind.
+ */
+ dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+ DRM_BRIDGE_OP_HPD;
+ } else {
+ dp->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+ /*
+ * No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
+ * Leaving it out marks the connector POLL_CONNECT |
+ * POLL_DISCONNECT instead.
+ */
+ dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
+ DRM_BRIDGE_OP_MODES;
+ }
+
+ if (dp->reset) {
+ ret = reset_control_deassert(dp->reset);
+ if (ret) {
+ dev_err(dev, "failed to deassert reset: %d\n", ret);
+ return ret;
+ }
+ }
+
+ if (dp->pxclk) {
+ ret = clk_prepare_enable(dp->pxclk);
+ if (ret) {
+ dev_err(dev, "failed to enable pxclk: %d\n", ret);
+ goto err_reset;
+ }
+ }
+
+ /*
+ * The PHY child shares this device's regmap, so the regmap must exist
+ * before devm_of_platform_populate() lets the PHY probe and
+ * devm_phy_get() succeed.
+ */
+ ret = spacemit_dp_resource_init(dp, pdev);
+ if (ret)
+ goto err_clk;
+
+ ret = devm_of_platform_populate(dev);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to populate child devices\n");
+ goto err_clk;
+ }
+
+ dp->phy = devm_phy_get(dev, "dp");
+ if (IS_ERR(dp->phy)) {
+ ret = dev_err_probe(dev, PTR_ERR(dp->phy),
+ "failed to get DP PHY\n");
+ goto err_clk;
+ }
+
+ /*
+ * The PHY exposes its PLL as the APMU pixel-clock mux's external
+ * parent.
+ */
+ dp->pll_clk = devm_clk_get(dev, "pll");
+ if (IS_ERR(dp->pll_clk)) {
+ ret = dev_err_probe(dev, PTR_ERR(dp->pll_clk),
+ "failed to get PHY pixel clock\n");
+ goto err_clk;
+ }
+
+ if (dp->pxclk) {
+ ret = clk_set_parent(dp->pxclk, dp->pll_clk);
+ if (ret) {
+ dev_err(dev, "failed to route eDP pixel mux to PHY PLL: %d\n", ret);
+ goto err_clk;
+ }
+ }
+
+ /*
+ * Panel discovery needs the AUX channel here; registration waits for
+ * bind.
+ */
+ dp->aux.name = "spacemit-dp-aux";
+ dp->aux.dev = dev;
+ dp->aux.transfer = spacemit_dp_aux_transfer;
+ dp->aux.no_zero_sized = true;
+ drm_dp_aux_init(&dp->aux);
+ dp->bridge.ddc = &dp->aux.ddc;
+
+ /*
+ * eDP hands panel power, enable-GPIO and backlight to panel-edp over
+ * the AUX bus. A DP instance has no aux-bus child and takes the
+ * -ENODEV path.
+ */
+ if (dp->edp_mode) {
+ ret = devm_of_dp_aux_populate_bus(&dp->aux,
+ spacemit_dp_edp_link_panel);
+ if (ret) {
+ dev_err_probe(dev, ret,
+ "eDP: failed to populate aux-bus panel\n");
+ goto err_clk;
+ }
+ }
+
+ /*
+ * drm_bridge_attach() expects the bridge to already be on the global
+ * list.
+ */
+ ret = devm_drm_bridge_add(dev, &dp->bridge);
+ if (ret) {
+ dev_err(dev, "failed to register DP bridge: %d\n", ret);
+ goto err_clk;
+ }
+
+ platform_set_drvdata(pdev, dp);
+
+ /*
+ * The handler needs bind-created state, so keep the line disabled until
+ * bind.
+ */
+ ret = devm_request_threaded_irq(dev, dp->irq, spacemit_dp_irq_handler,
+ spacemit_dp_hotplug_event_handler,
+ IRQF_NO_AUTOEN, dev_name(dev), dp);
+ if (ret) {
+ dev_err(dev, "failed to request irq %d: %d\n", dp->irq, ret);
+ goto err_clk;
+ }
+
+ ret = component_add(dev, &spacemit_dp_ops);
+ if (ret) {
+ dev_err(dev, "failed to add component: %d\n", ret);
+ goto err_clk;
+ }
+
+ return 0;
+
+err_clk:
+ clk_disable_unprepare(dp->pxclk);
+err_reset:
+ if (dp->reset)
+ reset_control_assert(dp->reset);
+
+ return ret;
+}
+
+static void inno_dp_remove(struct platform_device *pdev)
+{
+ struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+ component_del(&pdev->dev, &spacemit_dp_ops);
+
+ if (dp) {
+ clk_disable_unprepare(dp->pxclk);
+ if (dp->reset)
+ reset_control_assert(dp->reset);
+ }
+}
+
+static void inno_dp_shutdown(struct platform_device *pdev)
+{
+ struct spacemit_dp_dev *dp = platform_get_drvdata(pdev);
+
+ /* A deferred or failed bind leaves drvdata NULL. */
+ if (!dp)
+ return;
+
+ spacemit_dp_hw_disable(dp);
+}
+
+static const struct of_device_id spacemit_dp_match[] = {
+ { .compatible = "spacemit,k3-inno-dp" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, spacemit_dp_match);
+
+static struct platform_driver inno_dp_driver = {
+ .probe = inno_dp_probe,
+ .remove = inno_dp_remove,
+ .shutdown = inno_dp_shutdown,
+ .driver = {
+ .name = "spacemit-inno-dp-drv",
+ .of_match_table = spacemit_dp_match,
+ },
+};
+
+module_platform_driver(inno_dp_driver);
+
+MODULE_DESCRIPTION("SpacemiT K3 Innosilicon DP/eDP bridge driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: phy-k3-inno-dp");
diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.h b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
new file mode 100644
index 000000000000..412ef0c0f27e
--- /dev/null
+++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.h
@@ -0,0 +1,328 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026 SpacemiT Co., Ltd.
+ *
+ */
+
+#ifndef __SPACEMIT_INNO_DP_H__
+#define __SPACEMIT_INNO_DP_H__
+
+/* Register field access uses GENMASK/BIT + FIELD_PREP/FIELD_GET. */
+#include <linux/bits.h>
+
+#define DP_CORE_CONTROL 0x0018
+#define DP_SCRAMBLER_DISABLE BIT(31)
+#define DP_ENHANCE_FRAMING_EN BIT(30)
+#define DP_DEFAULT_FAST_LINK_TRAIN_EN BIT(29)
+#define DP_SCALE_DOWN_MODE BIT(28)
+#define DP_FORCE_HPD BIT(27)
+#define DP_ENABLE_EDP BIT(4)
+
+#define DP_SOFT_RESET 0x001c
+#define DP_CONTROLLER_RESET BIT(31)
+#define DP_PHY_RESET BIT(30)
+#define DP_HDCP_RESET BIT(29)
+#define DP_AUDIO_RESET BIT(28)
+#define DP_AUX_RESET BIT(27)
+#define DP_VIDEO_RESET GENMASK(3, 0)
+
+#define DP_CLK_INV_MUX 0x0028
+#define DP_REG_VID_CLK_SEL BIT(0)
+
+#define DP_GENERAL_INTERRUPT 0x0080
+#define DP_AUX_REPLY_EVENT_INT_STA BIT(16)
+
+#define DP_GENERAL_INTERRUPT_MASK 0x0084
+#define DP_HPD_INT_STA_MSK BIT(17)
+#define DP_AUX_REPLY_EVENT_INT_STA_MSK BIT(16)
+#define DP_HDCP_INT_STA_MSK BIT(15)
+#define DP_ILLEGAL_AUX_CMD_INT_STA_MSK BIT(14)
+#define DP_TYPE_C_EVENT_MSK BIT(13)
+#define DP_DSC_EVENT_MSK BIT(12)
+#define DP_SDP_INT_STA_S3_MSK BIT(11)
+#define DP_SDP_INT_STA_S2_MSK BIT(10)
+#define DP_SDP_INT_STA_S1_MSK BIT(9)
+#define DP_SDP_INT_STA_S0_MSK BIT(8)
+#define DP_VIDEO_FIFO_OVERFLOW_INT_STA_S3_MSK BIT(7)
+#define DP_VIDEO_FIFO_OVERFLOW_INT_STA_S2_MSK BIT(6)
+#define DP_VIDEO_FIFO_OVERFLOW_INT_STA_S1_MSK BIT(5)
+#define DP_VIDEO_FIFO_OVERFLOW_INT_STA_S0_MSK BIT(4)
+
+#define DP_HPD_INTERRUPT_STATUS 0x0088
+#define DP_HOT_PLUG_EVENT BIT(29)
+#define DP_HOT_UNPLUG_EVENT BIT(28)
+#define DP_HPD_IN_STATUS BIT(26)
+
+#define DP_HPD_INTERRUPT_ENABLE 0x008c
+#define DP_SINK_IRQ_EVENT_MSK BIT(31)
+#define DP_HOT_PLUG_EVENT_MSK BIT(29)
+#define DP_HOT_UNPLUG_EVENT_MSK BIT(28)
+#define DP_SINK_UNPLUG_ERROR_EVENT_MSK BIT(27)
+
+#define DP_PHYIF_CTRL_ADDR 0x0100
+#define DP_PHY_BUSY_BYP BIT(31)
+#define DP_TPS_SEL GENMASK(28, 25)
+#define DP_XMIT_ENABLE GENMASK(20, 17)
+#define DP_PHY_POWERDOWN GENMASK(16, 13)
+#define DP_PHY_SSC_DIS BIT(8)
+#define DP_PHY_NUM_LANES GENMASK(6, 5)
+#define DP_PHY_RATE GENMASK(1, 0)
+
+#define DP_PHYIF_TX_EQ_ADDR 0x0104
+#define DP_PHY_LANE3_TX_VSWING GENMASK(21, 20)
+#define DP_PHY_LANE3_TX_PREEMP GENMASK(19, 18)
+#define DP_PHY_LANE2_TX_VSWING GENMASK(15, 14)
+#define DP_PHY_LANE2_TX_PREEMP GENMASK(13, 12)
+#define DP_PHY_LANE1_TX_VSWING GENMASK(9, 8)
+#define DP_PHY_LANE1_TX_PREEMP GENMASK(7, 6)
+#define DP_PHY_LANE0_TX_VSWING GENMASK(3, 2)
+#define DP_PHY_LANE0_TX_PREEMP GENMASK(1, 0)
+
+#define DP_MPLL_CTRL0 0x0180
+#define DP_ANA_MPLL_FBDIV_LBIT GENMASK(31, 24)
+#define DP_ANA_MPLL_SEL_EXTWAVE BIT(22)
+#define DP_ANA_MPLL_DISABLE_SSCG BIT(21)
+#define DP_ANA_MPLL_DOWNSPREAD BIT(20)
+#define DP_ANA_MPLL_FBDIV_HBIT GENMASK(19, 16)
+#define DP_ANA_MPLL_OBSEN BIT(15)
+#define DP_ANA_MPLL_OBSSEL BIT(14)
+#define DP_ANA_MPLL_PREDIV GENMASK(13, 8)
+#define DP_AD_LOCK_COREPLL BIT(7)
+#define DP_ANA_MPLL_DACPD BIT(5)
+#define DP_ANA_MPLL_DSMPD BIT(4)
+#define DP_ANA_MPLL_PD BIT(0)
+
+#define DP_MPLL_CTRL1 0x0184
+#define DP_ANA_MPLL_FRAC_LBIT GENMASK(23, 16)
+#define DP_ANA_MPLL_FRAC_MBIT GENMASK(15, 8)
+#define DP_ANA_MPLL_FRAC_HBIT GENMASK(7, 0)
+
+#define DP_MPLL_CTRL2 0x0188
+#define DP_ANA_MPLL_VCOCLK_DIV8_EN BIT(16)
+#define DP_ANA_MPLL_POSTDIVEN BIT(11)
+#define DP_ANA_MPLL_POSTDIV GENMASK(10, 8)
+
+#define DP_PREPLL_CTRL0 0x0190
+#define DP_ANA_PREPLL_FBDIV2_LBIT GENMASK(31, 24)
+#define DP_ANA_PREPLL_FBDIV2_HBIT GENMASK(19, 16)
+#define DP_ANA_PREPLL_PREDIV GENMASK(13, 8)
+#define DP_AD_LOCK_PIXELPLL BIT(7)
+#define DP_ANA_PREPLL_DACPD BIT(5)
+#define DP_ANA_PREPLL_DSMPD BIT(4)
+#define DP_REG_PCLK_OUTPUT_NORMAL BIT(1)
+#define DP_ANA_PREPLL_PD BIT(0)
+
+#define DP_PREPLL_CTRL1 0x0194
+#define DP_ANA_PREPLL_PRECLK_DIVM GENMASK(17, 16)
+#define DP_ANA_PREPLL_PRECLK_DIVAUX GENMASK(12, 8)
+
+#define DP_PREPLL_CTRL2 0x0198
+#define DP_ANA_PREPLL_PCLKDIV5_EN BIT(31)
+#define DP_ANA_PREPLL_PCLK_DIVAUX GENMASK(28, 24)
+#define DP_ANA_PREPLL_LOCK_BYPEN BIT(17)
+#define DP_ANA_PREPLL_LOWFRE_EN BIT(16)
+#define DP_ANA_PREPLL_TESTEN BIT(15)
+#define DP_ANA_PREPLL_TESTSEL GENMASK(14, 12)
+#define DP_ANA_PREPLL_HDMI_EN BIT(9)
+#define DP_ANA_PREPLL_DP_EN BIT(8)
+
+#define DP_PREPLL_SSC_CTRL 0x019c
+#define DP_ANA_PREPLL_FRAC2_LBIT GENMASK(23, 16)
+#define DP_ANA_PREPLL_FRAC2_MBIT GENMASK(15, 8)
+#define DP_ANA_PREPLL_FRAC2_HBIT GENMASK(7, 0)
+
+#define DP_ANA_TX_CTRL0 0x01a0
+#define DP_ANA_MPLL_CLKDIV_16M GENMASK(13, 8)
+
+#define DP_ANA_TX_ENABLE 0x01a4
+#define DP_ANA_TX_ISEL_DRV_D3 GENMASK(31, 28)
+#define DP_ANA_TX_ISEL_DRV_D2 GENMASK(27, 24)
+#define DP_AD_TX_ESDSENSE_D3 GENMASK(23, 22)
+#define DP_AD_TX_ESDSENSE_D2 GENMASK(21, 20)
+#define DP_AD_TX_ESDSENSE_D1 GENMASK(19, 18)
+#define DP_AD_TX_ESDSENSE_D0 GENMASK(17, 16)
+#define DP_ANA_TX_EN_LDO_D3 BIT(11)
+#define DP_ANA_TX_EN_LDO_D2 BIT(10)
+#define DP_ANA_TX_EN_LDO_D1 BIT(9)
+#define DP_ANA_TX_EN_LDO_D0 BIT(8)
+#define DP_ANA_TX_EN_BYP_D3 BIT(7)
+#define DP_ANA_TX_EN_BYP_D2 BIT(6)
+#define DP_ANA_TX_EN_BYP_D1 BIT(5)
+#define DP_ANA_TX_EN_BYP_D0 BIT(4)
+#define DP_ANA_TX_EN_DRV_D3 BIT(3)
+#define DP_ANA_TX_EN_DRV_D2 BIT(2)
+#define DP_ANA_TX_EN_DRV_D1 BIT(1)
+#define DP_ANA_TX_EN_DRV_D0 BIT(0)
+
+#define DP_ANA_TX_CTRL1 0x01a8
+#define DP_ANA_TX_PHASE_D2 GENMASK(31, 30)
+#define DP_ANA_TX_MAINSEL_D2 GENMASK(28, 24)
+#define DP_ANA_TX_PHASE_D3 GENMASK(23, 22)
+#define DP_ANA_TX_MAINSEL_D3 GENMASK(20, 16)
+#define DP_ANA_TX_ISEL_LDO_D3 BIT(11)
+#define DP_ANA_TX_ISEL_LDO_D2 BIT(10)
+#define DP_ANA_TX_ISEL_LDO_D1 BIT(9)
+#define DP_ANA_TX_ISEL_LDO_D0 BIT(8)
+#define DP_ANA_TX_ISEL_DRV_D1 GENMASK(7, 4)
+#define DP_ANA_TX_ISEL_DRV_D0 GENMASK(3, 0)
+
+#define DP_ANA_TX_CTRL2 0x01ac
+#define DP_ANA_TX_POSTSEL_D1 GENMASK(31, 28)
+#define DP_ANA_TX_POSTSEL_D0 GENMASK(27, 24)
+#define DP_ANA_TX_POSTSEL_D3 GENMASK(23, 20)
+#define DP_ANA_TX_POSTSEL_D2 GENMASK(19, 16)
+#define DP_ANA_TX_PHASE_D0 GENMASK(15, 14)
+#define DP_DA_TX_MAINSEL_D0_4_0 GENMASK(12, 8)
+#define DP_ANA_TX_PHASE_D1 GENMASK(7, 6)
+#define DP_ANA_TX_MAINSEL_D1 GENMASK(4, 0)
+
+#define DP_ANA_TX_CTRL3 0x01b0
+#define DP_ANA_TX_EN_P2S_D3 BIT(31)
+#define DP_ANA_TX_EN_P2S_D2 BIT(30)
+#define DP_ANA_TX_EN_P2S_D1 BIT(29)
+#define DP_ANA_TX_EN_P2S_D0 BIT(28)
+#define DP_ANA_TX_MODE_D3 BIT(27)
+#define DP_ANA_TX_MODE_D2 BIT(26)
+#define DP_ANA_TX_MODE_D1 BIT(25)
+#define DP_ANA_TX_MODE_D0 BIT(24)
+#define DP_ANA_TX_PRESEL_D1 GENMASK(14, 12)
+#define DP_ANA_TX_PRESEL_D0 GENMASK(10, 8)
+#define DP_ANA_TX_PRESEL_D3 GENMASK(6, 4)
+#define DP_ANA_TX_PRESEL_D2 GENMASK(2, 0)
+
+#define DP_ANA_TX_CTRL6 0x01bc
+#define DP_ANA_BG_VREF5 GENMASK(15, 14)
+#define DP_ANA_BG_VREF4 GENMASK(13, 12)
+#define DP_ANA_BG_VREF3 GENMASK(11, 10)
+#define DP_ANA_BG_VREF2 GENMASK(9, 8)
+#define DP_ANA_BG_VREF GENMASK(7, 6)
+#define DP_ANA_BG_VREF1 GENMASK(5, 4)
+#define DP_ANA_BG_EN_CHOP BIT(3)
+#define DP_ANA_BG_EN BIT(2)
+#define DP_ANA_BG_ISEL BIT(1)
+#define DP_ANA_BG_VSEL BIT(0)
+
+#define DP_ANA_TX_CTRL7 0x01c0
+#define DP_ANA_BG_EN_RCAL BIT(27)
+#define DP_ANA_BG_RCAL_SEL GENMASK(26, 25)
+#define DP_AD_BG_RCAL_OUT BIT(24)
+#define DP_ANA_RTCAL_FREQDIV_LBIT GENMASK(23, 16)
+#define DP_ANA_RTCAL_BYPASS BIT(15)
+#define DP_ANA_RTCAL_FREQDIV_HBIT GENMASK(14, 8)
+#define DP_ANA_BG_RCAL_VAL GENMASK(5, 0)
+
+#define DP_ANA_TX_CTRL8 0x01c4
+#define DP_ANA_TX_RTM_D3 GENMASK(29, 24)
+#define DP_ANA_TX_RTM_D2 GENMASK(21, 16)
+#define DP_ANA_TX_RTM_D1 GENMASK(13, 8)
+#define DP_ANA_TX_RTM_D0 GENMASK(5, 0)
+
+#define DP_ANA_TX_AUX_CTRL 0x01d0
+#define DP_ANA_TX_AUX_RX_VSEL GENMASK(13, 12)
+
+#define DP_ANA_LANE_EMPH 0x01dc
+#define DP_ANA_TX_POSTSEL_PRE_D3 GENMASK(23, 20)
+#define DP_ANA_TX_POSTSEL_PRE_D2 GENMASK(19, 16)
+#define DP_ANA_TX_POSTSEL_PRE_D1 GENMASK(15, 12)
+#define DP_ANA_TX_POSTSEL_PRE_D0 GENMASK(11, 8)
+#define DP_ANA_TX_MODE_DE_D3 BIT(7)
+#define DP_ANA_TX_MODE_DE_D2 BIT(6)
+#define DP_ANA_TX_MODE_DE_D1 BIT(5)
+#define DP_ANA_TX_MODE_DE_D0 BIT(4)
+#define DP_ANA_TX_MODE_PRE_D3 BIT(3)
+#define DP_ANA_TX_MODE_PRE_D2 BIT(2)
+#define DP_ANA_TX_MODE_PRE_D1 BIT(1)
+#define DP_ANA_TX_MODE_PRE_D0 BIT(0)
+
+#define DP_VIDEO_VSAMPLE_REG 0x0200
+#define DP_VIDEO_STREAM_ENABLE BIT(28)
+#define DP_VIDEO_MAPPING GENMASK(26, 22)
+#define DP_STREAM_ENC_EN BIT(18)
+
+#define DP_VINPUT_POLARITY 0x020c
+#define DP_HSYNC_IN_POLARITY BIT(29)
+#define DP_VSYNC_IN_POLARITY BIT(28)
+
+#define DP_VIDEO_CONFIG2 0x0210
+#define DP_VACTIVE GENMASK(31, 16)
+#define DP_VBLANK GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG1 0x0214
+#define DP_HBLANK GENMASK(29, 16)
+#define DP_HACTIVE GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG5 0x0218
+#define DP_AVERAGE_BYTES_PER_TU GENMASK(31, 25)
+#define DP_AVERAGE_BYTES_PER_TU_FRAC GENMASK(24, 21)
+#define DP_INIT_THRESHOLD GENMASK(18, 10)
+
+#define DP_VIDEO_CONFIG3 0x021c
+#define DP_H_FRONT_PORCH GENMASK(31, 16)
+#define DP_H_SYNC_WIDTH GENMASK(15, 0)
+
+#define DP_VIDEO_CONFIG4 0x0220
+#define DP_V_FRONT_PORCH GENMASK(31, 16)
+#define DP_V_SYNC_WIDTH GENMASK(15, 0)
+
+#define DP_VIDEO_MSA1 0x0224
+#define DP_HSTART GENMASK(31, 16)
+#define DP_VSTART GENMASK(15, 0)
+
+#define DP_VIDEO_MSA2 0x0228
+#define DP_MISC0 GENMASK(7, 0)
+
+#define DP_VIDEO_MSA3 0x022c
+#define DP_NVID GENMASK(31, 8)
+#define DP_MISC1 GENMASK(7, 0)
+
+#define DP_VIDEO_HBLANK_INTERVAL 0x0230
+#define DP_HBLANK_INTERVAL GENMASK(31, 16)
+
+#define DP_VIDEO_COLORBAR_CFG 0x0238
+#define DP_VID_BIST_EN BIT(0)
+
+#define DP_AUDIO_CONFIG1 0x0300
+#define DP_AUDIO_INF_SELECT BIT(31)
+#define DP_AUDIO_MUTE BIT(30)
+#define DP_HBR_MODE_ENABLE BIT(29)
+#define DP_AUDIO_DATA_IN_EN GENMASK(28, 25)
+#define DP_I2S_AUDIO_MODE GENMASK(24, 23)
+#define DP_AUD_ADJUST_SEL BIT(22)
+#define DP_AUDIO_DATA_WIDTH GENMASK(21, 17)
+#define DP_AUDIO_NUM_CHANNELS GENMASK(16, 14)
+#define DP_AUDIO_PACKET_ID GENMASK(13, 6)
+#define DP_AUDIO_TIMESTAMP_VERSION_NUM GENMASK(5, 0)
+
+#define DP_AUX_STS_REG 0x0400
+#define DP_AUX_STATUS GENMASK(31, 24)
+#define DP_AUX_REPLY_ERR_CODE GENMASK(6, 4)
+
+#define DP_AUX_CMD_REG 0x0404
+#define DP_AUX_LENGTH GENMASK(27, 24)
+#define DP_AUX_ADDR GENMASK(23, 4)
+#define DP_AUX_CMD_TYPE GENMASK(3, 0)
+
+#define DP_AUX_START_REG 0x0408
+#define DP_AUX_START BIT(31)
+
+#define DP_AUX_DATA4_REG 0x040c
+#define DP_AUX_DATA4 GENMASK(31, 0)
+
+#define DP_AUX_DATA3_REG 0x0410
+#define DP_AUX_DATA3 GENMASK(31, 0)
+
+#define DP_AUX_DATA2_REG 0x0414
+#define DP_AUX_DATA2 GENMASK(31, 0)
+
+#define DP_AUX_DATA1_REG 0x0418
+#define DP_AUX_DATA1 GENMASK(31, 0)
+
+#define DP_SDP_HORIZONTAL_CTRL 0x0420
+#define DP_AUD_STREAM_HORIZONTAL_EN BIT(13)
+#define DP_AUD_TIMESTAMP_HORIZONTAL_EN BIT(12)
+
+#define DP_SDP_VERTICAL_CTRL 0x0424
+#define DP_AUD_STREAM_VERTICAL_EN BIT(13)
+#define DP_AUD_TIMESTAMP_VERTICAL_EN BIT(12)
+
+#endif /* __SPACEMIT_INNO_DP_H__ */
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
2026-07-25 3:16 ` [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang
@ 2026-07-25 5:20 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:20 UTC (permalink / raw)
To: Cody Kang
Cc: vkoul, devicetree, linux-phy, olteanv, conor+dt, robh, dri-devel,
neil.armstrong
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] Missing W1C handling on DP_GENERAL_INTERRUPT causes unintentionally cleared interrupts.
- [High] Late dynamic bridge attachment breaks `drm_bridge_connector_init` for eDP panels, resulting in 0 modes.
- [High] Custom EDID reading bypasses standard DRM core helpers and breaks I2C MOT semantics.
- [High] Integer truncation of the pixel clock causes inaccurate DisplayPort Transfer Unit (TU) and HBLANK calculations.
- [High] Link training aborts prematurely because successful DPCD writes are evaluated as errors.
- [High] Asynchronous panel probe causes Use-After-Free due to improper unbind/error cleanup order.
- [Medium] Returning IRQ_NONE after actively clearing pending hardware interrupts violates the IRQ API.
- [Medium] Sink capabilities are read concurrently without synchronization.
- [Medium] Hardware shutdown sequence modifies registers without acquiring `dp->mode_lock`.
--
--- Patch [12]: [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver ---
commit 7a2976d5d6f50aaede0b18a3f5e99c3cb67bd5c1
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Innosilicon DP/eDP controller bridge driver
Add the DP/eDP controller that sits downstream of the Saturn DPU. Two
identical instances share one compatible; the eDP-vs-DP role is board
wiring, so it is taken from the devicetree: an eDP panel always sits
under an aux-bus child node, an external DP connector never does.
The link is driven through the generic PHY framework, so the controller
never touches a PLL register. The controller's HPD interrupt is gated by
the DP pixel clock, which can be off exactly when a plug has to be
caught, so the connector is also polled and the interrupt path re-reads
the live level when it does fire.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
> index 0000000000000..e88731d9b18bf
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
[ ... ]
> @@ -290,6 +290,9 @@ static void spacemit_dp_aux_hw_reset(struct spacemit_dp_dev *dp)
> usleep_range(2000, 2500);
> regmap_write_bits(dp->regs, DP_SOFT_RESET, DP_AUX_RESET,
> FIELD_PREP(DP_AUX_RESET, 0x0));
> usleep_range(2000, 2500);
> regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT,
> DP_AUX_REPLY_EVENT_INT_STA,
> FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
> }
[Severity: High]
Does this unintentionally clear other pending interrupts?
DP_GENERAL_INTERRUPT appears to be a write-1-to-clear register. Calling
regmap_write_bits() performs a read-modify-write operation, which reads all
currently asserted interrupt bits and writes them back as 1s.
This drops any unrelated pending interrupts (such as HDCP, overflow events,
and hotplugs) that arrived concurrently.
[ ... ]
> @@ -401,9 +401,12 @@ static ssize_t spacemit_dp_aux_transfer(struct drm_dp_aux *aux,
> regmap_read(dp->regs, DP_AUX_STS_REG,
> &status); status = FIELD_GET(DP_AUX_STATUS, status);
>
> /* Write 1 to clear. */
> regmap_write_bits(dp->regs, DP_GENERAL_INTERRUPT,
> DP_AUX_REPLY_EVENT_INT_STA,
> FIELD_PREP(DP_AUX_REPLY_EVENT_INT_STA, 1));
[Severity: High]
Can this read-modify-write on a write-1-to-clear register silently drop
other pending hardware interrupts that arrive during the AUX transfer?
[ ... ]
> @@ -806,6 +806,8 @@ static int spacemit_dp_aux_i2c_write(struct spacemit_dp_dev *dp, u32 address,
> .buffer = (u8 *)buf,
> .size = len,
> };
> int ret;
>
> ret = spacemit_dp_aux_transfer(&dp->aux, &msg);
> if (ret < 0)
> return ret;
[Severity: High]
Does this custom I2C over AUX implementation bypass synchronization?
By directly invoking spacemit_dp_aux_transfer() here rather than using standard
DRM core helpers, the code bypasses aux->hw_mutex, allowing concurrent AUX
transactions on the bus.
[ ... ]
> @@ -831,6 +831,10 @@ static int spacemit_dp_link_train_clock_recovery(struct spacemit_dp_dev *dp,
>
> if (drm_dp_clock_recovery_ok(link_status, lanes))
> return 0;
>
> ret = spacemit_dp_link_apply_adjust(dp, link_status, lanes,
> training_set);
> if (ret) {
> spacemit_dp_set_training_pattern(dp,
> DP_TRAINING_PATTERN_DISABLE);
> return ret;
> }
[Severity: High]
Could this return value check cause link training to fail?
drm_dp_dpcd_write_data() returns the number of bytes written on success
(a positive value). Since spacemit_dp_link_apply_adjust() returns that value
directly, this "if (ret)" check will evaluate to true for successful writes.
This causes the driver to interpret successful DPCD writes as failures and
immediately abort the training sequence.
[ ... ]
> @@ -854,6 +854,10 @@ static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
> for (retry = 0; retry < 3; retry++) {
> ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
> &edid_offset, 1);
> if (ret)
> continue;
>
> ret = spacemit_dp_aux_i2c_read(dp, DDC_ADDR,
> buf + offset, chunk);
> if (!ret)
> break;
[Severity: High]
Can splitting the EDID write and read cause corrupted EDID reads?
The I2C offset write and data read are split into two separate transactions
without the DP_AUX_I2C_MOT (Middle-Of-Transaction) flag.
This generates an I2C STOP condition between them, which causes many DP sinks
to reset their internal EDID offset to 0.
[ ... ]
> @@ -954,6 +954,9 @@ static void spacemit_dp_hw_set_msa_and_enable_video(struct spacemit_dp_dev *dp,
>
> /* mode->clock unit is kHz, fp unit is MHz */
> if (dp->use_ext_pixel_clock)
> fp = dp->pixel_clock / 1000;
> else
> fp = mode->clock / 1000;
>
> if (fp == 0)
[Severity: High]
Can this integer division truncate fractional pixel clocks?
If mode->clock is a fractional value like 148500 (148.5 MHz), dividing by 1000
truncates it to 148.
This loss of precision skews the subsequent DisplayPort Transfer Unit (tu) and
horizontal blanking interval (hb_num) calculations, which could cause display
output corruption or video FIFO underflows.
[ ... ]
> @@ -1650,6 +1650,9 @@ static int spacemit_dp_bridge_atomic_check(struct drm_bridge *bridge,
>
> /* No caps yet: leave idx unset and let ->atomic_enable() retry. */
> if (!dp->link.max_rate || !dp->link.max_num_lanes) {
> st->link_cfg_idx = -1;
> return 0;
> }
>
> for (i = 0; i < ARRAY_SIZE(spacemit_dp_link_priority_table); i++) {
[Severity: Medium]
Is it safe to read dp->link capabilities here without holding dp->mode_lock?
This read races with spacemit_dp_bridge_detect(), which updates these fields
during a hotplug event. A concurrent hotplug could cause the validation path
to see torn or inconsistent sink capabilities.
[ ... ]
> @@ -1777,6 +1777,10 @@ static irqreturn_t spacemit_dp_irq_handler(int irq, void *data)
>
> /*
> * Only compare here: the authoritative connector_status update
> * happens in the thread under mode_lock, via .detect().
> */
> if (spacemit_dp_hw_detect_hpd(dp) != READ_ONCE(dp->connector_status))
> ret = IRQ_WAKE_THREAD; /* Call hotplug_event */
> spacemit_dp_hw_clean_hpd(dp);
>
> drm_dev_exit(idx);
> return ret;
> }
[Severity: Medium]
Should this return IRQ_HANDLED when interrupts are explicitly cleared?
The handler calls spacemit_dp_hw_clean_hpd(), which reads and clears pending
hardware interrupt bits. If the current HPD level matches the cached state
(which can happen during physical contact bounce), the handler returns IRQ_NONE.
Returning IRQ_NONE after servicing and clearing a hardware interrupt violates
the IRQ API and can cause the kernel's spurious interrupt detector to permanently
disable the DP interrupt line.
[ ... ]
> @@ -2129,6 +2129,9 @@ static int spacemit_dp_bind(struct device *dev, struct device *master,
>
> dp->connector = drm_bridge_connector_init(drm, &dp->encoder);
> if (IS_ERR(dp->connector)) {
> ret = PTR_ERR(dp->connector);
> dev_err(dev, "failed to init bridge connector: %d\n", ret);
> goto err_encoder;
> }
[Severity: High]
Does this initialize the connector before the panel bridge is attached?
For eDP, the downstream panel bridge is attached dynamically later via the
async probe callback spacemit_dp_edp_link_panel().
Calling drm_bridge_connector_init() here before the panel is attached means
the connector is built without the panel's mode-getting function, resulting
in it reporting 0 modes to userspace.
[ ... ]
> @@ -2160,6 +2160,9 @@ static int spacemit_dp_bind(struct device *dev, struct device *master,
> return 0;
>
> err_encoder:
> drm_encoder_cleanup(&dp->encoder);
> err_aux:
> drm_dp_aux_unregister(&dp->aux);
> return ret;
> }
[Severity: High]
Can this cleanup order cause a use-after-free?
drm_encoder_cleanup() is called before drm_dp_aux_unregister().
This leaves a window where the eDP panel's asynchronous done_probing callback
(spacemit_dp_edp_link_panel) can still execute. It will attempt to dereference
dp->bridge.encoder (which has been zeroed here) and the parent DRM device,
leading to memory corruption.
[ ... ]
> @@ -2327,6 +2327,8 @@ static void inno_dp_shutdown(struct platform_device *pdev)
> /* A deferred or failed bind leaves drvdata NULL. */
> if (!dp)
> return;
>
> spacemit_dp_hw_disable(dp);
> }
[Severity: Medium]
Is it safe to modify hardware registers here without acquiring dp->mode_lock?
spacemit_dp_hw_disable() modifies numerous DP configuration registers. Running
this during system shutdown without holding the lock allows it to race with
concurrent atomic modesets executed by the DRM core, potentially causing
hardware lockups.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=12
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 13/17] MAINTAINERS: add SpacemiT K3 display driver entry
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (11 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang
` (4 subsequent siblings)
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Cover the Saturn DPU, the Innosilicon DP/eDP controller, the DP PHY and
their bindings.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
MAINTAINERS | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 6dea93a41962..9f2910a17c38 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25436,6 +25436,16 @@ S: Maintained
F: Documentation/devicetree/bindings/i2c/spacemit,k1-i2c.yaml
F: drivers/i2c/busses/i2c-k1.c
+SPACEMIT K3 DISPLAY DRIVER
+M: Cody Kang <codykang.hk@gmail.com>
+L: dri-devel@lists.freedesktop.org
+L: linux-riscv@lists.infradead.org
+S: Maintained
+F: Documentation/devicetree/bindings/display/spacemit/
+F: Documentation/devicetree/bindings/phy/spacemit,k3-inno-dp-phy.yaml
+F: drivers/gpu/drm/spacemit/
+F: drivers/phy/spacemit/phy-k3-inno-dp.c
+
SPANISH DOCUMENTATION
M: Carlos Bilbao <carlos.bilbao@kernel.org>
R: Avadhut Naik <avadhut.naik@amd.com>
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (12 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:20 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang
` (3 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Add the SoC-level display nodes: the two Saturn DPUs, the two DP/eDP
controllers with their PHY children.
The PHY PLLs are fed back into the APMU as pixel-clock mux parents, so
updated the &syscon_apmu node in the display dtsi.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
arch/riscv/boot/dts/spacemit/k3-display.dtsi | 156 +++++++++++++++++++++++++++
1 file changed, 156 insertions(+)
diff --git a/arch/riscv/boot/dts/spacemit/k3-display.dtsi b/arch/riscv/boot/dts/spacemit/k3-display.dtsi
new file mode 100644
index 000000000000..4892ee6acda2
--- /dev/null
+++ b/arch/riscv/boot/dts/spacemit/k3-display.dtsi
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+/*
+ * SpacemiT K3 display subsystem (DPU0/eDP0 + DPU1/DP1) shared
+ * device-tree definitions for inclusion alongside k3.dtsi.
+ *
+ * Copyright (C) 2026 SpacemiT Co., Ltd.
+ */
+
+#include <dt-bindings/clock/spacemit,k3-clocks.h>
+#include <dt-bindings/reset/spacemit,k3-resets.h>
+#include <dt-bindings/interrupt-controller/irq.h>
+
+&soc {
+ dpu0_crtc0: dpu@c0340000 {
+ compatible = "spacemit,k3-saturn-dpu";
+ reg = <0x0 0xc0340000 0x0 0x54000>;
+ interrupts = <90 IRQ_TYPE_LEVEL_HIGH>,
+ <89 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "online", "offline";
+ clocks = <&syscon_apmu CLK_APMU_LCD_PXCLK>,
+ <&syscon_apmu CLK_APMU_LCD_MCLK>,
+ <&syscon_apmu CLK_APMU_DPU_ACLK>,
+ <&syscon_apmu CLK_APMU_DSI_ESC>,
+ <&syscon_apmu CLK_APMU_LCD_DSC>;
+ clock-names = "pxclk", "mclk", "aclk", "escclk", "dscclk";
+ resets = <&syscon_apmu RESET_APMU_LCD_MCLK>,
+ <&syscon_apmu RESET_APMU_LCD>,
+ <&syscon_apmu RESET_APMU_DSI_ESC>,
+ <&syscon_apmu RESET_APMU_DPU_ACLK>,
+ <&syscon_apmu RESET_APMU_LCD_DSCCLK>;
+ reset-names = "mclk", "lcd", "esc",
+ "aclk", "dsc";
+ status = "disabled";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ dpu0_out: endpoint {
+ remote-endpoint = <&edp0_in>;
+ };
+ };
+ };
+ };
+
+ edp0: edp@cac84000 {
+ compatible = "spacemit,k3-inno-dp";
+ reg = <0x0 0xcac84000 0x0 0x4000>;
+ interrupts = <132 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&syscon_apmu CLK_APMU_EDP0_PXCLK>, <&edp0_phy>;
+ clock-names = "pxclk", "pll";
+ resets = <&syscon_apmu RESET_APMU_EDP0>;
+ phys = <&edp0_phy>;
+ phy-names = "dp";
+ status = "disabled";
+
+ edp0_phy: phy {
+ compatible = "spacemit,k3-inno-dp-phy";
+ #phy-cells = <0>;
+ #clock-cells = <0>;
+ clock-output-names = "edp0_pll_pxclk";
+ };
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ edp0_in: endpoint {
+ remote-endpoint = <&dpu0_out>;
+ };
+ };
+ /* port@1 (panel output) is added by boards fitting an eDP panel */
+ };
+ };
+
+ dpu1_crtc0: dpu@c0440000 {
+ compatible = "spacemit,k3-saturn-dpu";
+ reg = <0x0 0xc0440000 0x0 0x54000>;
+ interrupts = <139 IRQ_TYPE_LEVEL_HIGH>,
+ <138 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-names = "online", "offline";
+ clocks = <&syscon_apmu CLK_APMU_DSI4LN2_LCD_PXCLK>,
+ <&syscon_apmu CLK_APMU_DSI4LN2_LCD_MCLK>,
+ <&syscon_apmu CLK_APMU_DSI4LN2_DPU_ACLK>,
+ <&syscon_apmu CLK_APMU_DSI4LN2_DSI_ESC>,
+ <&syscon_apmu CLK_APMU_DSI4LN2_LCD_DSC>;
+ clock-names = "pxclk", "mclk", "aclk", "escclk", "dscclk";
+ resets = <&syscon_apmu RESET_APMU_DSI4LN2_LCD_MCLK>,
+ <&syscon_apmu RESET_APMU_DSI4LN2_LCD_SW>,
+ <&syscon_apmu RESET_APMU_DSI4LN2_ESCCLK>,
+ <&syscon_apmu RESET_APMU_DSI4LN2_DPU_ACLK>,
+ <&syscon_apmu RESET_APMU_DSI4LN2_LCD_DSCCLK>;
+ reset-names = "mclk", "lcd", "esc",
+ "aclk", "dsc";
+ status = "disabled";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ dpu1_out: endpoint {
+ remote-endpoint = <&dp1_in>;
+ };
+ };
+ };
+ };
+
+ dp1: dp@cac88000 {
+ compatible = "spacemit,k3-inno-dp";
+ reg = <0x0 0xcac88000 0x0 0x4000>;
+ interrupts = <140 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&syscon_apmu CLK_APMU_EDP1_PXCLK>, <&dp1_phy>;
+ clock-names = "pxclk", "pll";
+ resets = <&syscon_apmu RESET_APMU_EDP1>;
+ phys = <&dp1_phy>;
+ phy-names = "dp";
+ status = "disabled";
+
+ dp1_phy: phy {
+ compatible = "spacemit,k3-inno-dp-phy";
+ #phy-cells = <0>;
+ #clock-cells = <0>;
+ clock-output-names = "edp1_pll_pxclk";
+ };
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+ dp1_in: endpoint {
+ remote-endpoint = <&dpu1_out>;
+ };
+ };
+ /* no port@1: the DP TX drives the external connector directly */
+ };
+ };
+};
+
+/*
+ * Feed the PHY PLL pixel clocks back in as the APMU mux parents. This lives
+ * here, not in k3.dtsi: the PHYs only exist where this file is included.
+ */
+&syscon_apmu {
+ clocks = <&osc_32k>, <&vctcxo_1m>, <&vctcxo_3m>, <&vctcxo_24m>,
+ <&edp0_phy>, <&dp1_phy>;
+ clock-names = "osc", "vctcxo_1m", "vctcxo_3m", "vctcxo_24m",
+ "edp0_pll_pxclk", "edp1_pll_pxclk";
+};
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes
2026-07-25 3:16 ` [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang
@ 2026-07-25 5:20 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:20 UTC (permalink / raw)
To: Cody Kang
Cc: neil.armstrong, linux-phy, olteanv, dri-devel, vkoul, robh,
conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Cyclic DT dependency between DP controllers, their child PHY nodes, and the APMU syscon will cause a fw_devlink warning and a potential -EPROBE_DEFER deadlock.
- [Medium] Fragile array override of `syscon_apmu` clocks creates a severe maintenance hazard.
--
--- Patch [14]: [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes ---
commit c12c6ab932374f52bfc3ce3d04640d8193a9bfea
Author: Cody Kang <codykang.hk@gmail.com>
riscv: dts: spacemit: k3: add display nodes
Add the SoC-level display nodes: the two Saturn DPUs, the two DP/eDP
controllers with their PHY children.
> diff --git a/arch/riscv/boot/dts/spacemit/k3-display.dtsi b/arch/riscv/boot/dts/spacemit/k3-display.dtsi
> new file mode 100644
> index 0000000000000..4892ee6acda22
> --- /dev/null
> +++ b/arch/riscv/boot/dts/spacemit/k3-display.dtsi
[ ... ]
> + edp0: edp@cac84000 {
> + compatible = "spacemit,k3-inno-dp";
> + reg = <0x0 0xcac84000 0x0 0x4000>;
> + interrupts = <132 IRQ_TYPE_LEVEL_HIGH>;
> + clocks = <&syscon_apmu CLK_APMU_EDP0_PXCLK>, <&edp0_phy>;
[Severity: High]
Does this create a cyclical dependency that could cause a fw_devlink warning
or a permanent probe deadlock?
edp0 depends on syscon_apmu here, while syscon_apmu depends on the edp0_phy
child node below. Since edp0_phy is a child of edp0, it won't be populated
until edp0 probes. If fw_devlink breaks the cycle by dropping the edp0 ->
syscon_apmu link, edp0 will attempt to probe first, fail to get its clock, and
defer forever because it will not proceed to populate the child PHY.
> + clock-names = "pxclk", "pll";
> + resets = <&syscon_apmu RESET_APMU_EDP0>;
> + phys = <&edp0_phy>;
> + phy-names = "dp";
> + status = "disabled";
> +
> + edp0_phy: phy {
> + compatible = "spacemit,k3-inno-dp-phy";
> + #phy-cells = <0>;
> + #clock-cells = <0>;
> + clock-output-names = "edp0_pll_pxclk";
> + };
[ ... ]
> +/*
> + * Feed the PHY PLL pixel clocks back in as the APMU mux parents. This lives
> + * here, not in k3.dtsi: the PHYs only exist where this file is included.
> + */
> +&syscon_apmu {
> + clocks = <&osc_32k>, <&vctcxo_1m>, <&vctcxo_3m>, <&vctcxo_24m>,
> + <&edp0_phy>, <&dp1_phy>;
> + clock-names = "osc", "vctcxo_1m", "vctcxo_3m", "vctcxo_24m",
> + "edp0_pll_pxclk", "edp1_pll_pxclk";
[Severity: Medium]
Will overriding the clocks property completely drop any future additions made to
the syscon_apmu node in the base k3.dtsi?
Device tree doesn't support appending to array properties, so if any new clocks
are added to the base syscon_apmu node later, this file will silently override
them and break other subsystems relying on those clocks.
> +};
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=14
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (13 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 5:23 ` sashiko-bot
2026-07-25 3:16 ` [PATCH 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang
` (2 subsequent siblings)
17 siblings, 1 reply; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Pico-ITX brings the second DPU's output to an external DisplayPort
connector. Enable that DPU and the DisplayPort controller it feeds.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
arch/riscv/boot/dts/spacemit/k3-pico-itx.dts | 11 +++++++++++
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 11 +++++++++++
2 files changed, 22 insertions(+)
diff --git a/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts b/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
index b89c1521e664..6c31460990a7 100644
--- a/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
+++ b/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
@@ -6,6 +6,7 @@
#include <dt-bindings/gpio/gpio.h>
#include "k3.dtsi"
+#include "k3-display.dtsi"
#include "k3-pinctrl.dtsi"
/ {
@@ -221,3 +222,13 @@ hub@1 {
&usb2_phy {
status = "okay";
};
+
+&dpu1_crtc0 {
+ status = "okay";
+};
+
+&dp1 {
+ pinctrl-names = "default";
+ pinctrl-0 = <&dp1_1_cfg>;
+ status = "okay";
+};
diff --git a/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi b/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
index 3ee1471f3798..2f36eba20f1e 100644
--- a/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
+++ b/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
@@ -11,6 +11,17 @@
#define K3_GPIO(x) (x / 32) (x % 32)
&pinctrl {
+ /omit-if-no-ref/
+ dp1_1_cfg: dp1-1-cfg {
+ dp1-pins {
+ pinmux = <K3_PADCONF(24, 6)>; /* dp1 hpd */
+
+ bias-disable;
+ drive-strength = <25>;
+ power-source = <3300>;
+ };
+ };
+
gmac0_rgmii_0_cfg: gmac0-rgmii-0-cfg {
gmac0-rgmii-0-pins {
pinmux = <K3_PADCONF(0, 1)>, /* gmac0_rxdv */
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
2026-07-25 3:16 ` [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang
@ 2026-07-25 5:23 ` sashiko-bot
0 siblings, 0 replies; 28+ messages in thread
From: sashiko-bot @ 2026-07-25 5:23 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, neil.armstrong, robh, conor+dt, vkoul, linux-phy,
olteanv, devicetree
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Pixel clock truncation causes DisplayPort Transfer Unit (TU) miscalculation.
- [High] EDID extension block reads fail due to omitted DP_AUX_I2C_MOT bit in I2C-over-AUX transactions.
- [Medium] Missing get_modes implementation for DRM_BRIDGE_OP_MODES capability.
- [Medium] Dropped hotplug interrupt notifications due to omitted DRM_BRIDGE_OP_HPD.
--
--- Patch [15]: [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output ---
commit 81b9c7b43f433a109ef12eb306af123e3e5941fc
Author: Cody Kang <codykang.hk@gmail.com>
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
Pico-ITX brings the second DPU's output to an external DisplayPort
connector. Enable that DPU and the DisplayPort controller it feeds.
> diff --git a/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts b/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
> index b89c1521e6649..6c31460990a75 100644
> --- a/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
> +++ b/arch/riscv/boot/dts/spacemit/k3-pico-itx.dts
[ ... ]
> @@ -221,3 +222,13 @@ hub@1 {
> &usb2_phy {
> status = "okay";
> };
> +
> +&dpu1_crtc0 {
> + status = "okay";
> +};
> +
> +&dp1 {
> + pinctrl-names = "default";
> + pinctrl-0 = <&dp1_1_cfg>;
> + status = "okay";
> +};
[Severity: Medium]
Enabling this DisplayPort output exposes some issues in the underlying driver.
Does the DisplayPort driver incorrectly declare support for
DRM_BRIDGE_OP_MODES?
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_bind()
dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
DRM_BRIDGE_OP_MODES;
The callback structure omits the logically required .get_modes callback:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_bridge_funcs
static const struct drm_bridge_funcs spacemit_dp_bridge_funcs = {
.attach = spacemit_dp_bridge_attach,
...
[Severity: Medium]
Will hotplug events be silently dropped here? The driver omits the
DRM_BRIDGE_OP_HPD flag:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_bind()
/*
* No OP_HPD: the HPD interrupt is gated by the DP pixel clock.
* Leaving it out marks the connector POLL_CONNECT |
* POLL_DISCONNECT instead.
*/
dp->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID |
DRM_BRIDGE_OP_MODES;
However, the hardware interrupt is fully enabled:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_dev_init()
enable_irq(dp->irq);
And the handler invokes the notification function:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_hotplug_event_handler()
drm_bridge_hpd_notify(&dp->bridge, status);
Because DRM_BRIDGE_OP_HPD is omitted, the DRM core never registers a hotplug
callback. This turns the notification into a no-op and forces the system to
fall back to a 10-second polling interval.
[Severity: High]
Could this calculation result in an incorrect DisplayPort Transfer Unit (TU)?
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_hw_set_msa_and_enable_video()
fp = mode->clock / 1000;
The pixel clock is truncated from kHz to an integer MHz value, losing
precision for standard clocks like 148.5 MHz. This truncated value is then
used directly to compute the Transfer Unit:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_hw_set_msa_and_enable_video()
/* Transfer unit: tu = fp * bpp * 640 / (8 * lanes * link_rate) */
{
u64 temp_tu = (u64)fp * bpp * 640;
This math error produces a systematically incorrect Transfer Unit which can
lead to FIFO underflows and video corruption.
[Severity: High]
Can EDID extension block reads fail due to protocol violations on the
I2C-over-AUX transactions?
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_conn_get_edid_block()
ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
&edid_offset, 1);
if (ret)
continue;
ret = spacemit_dp_aux_i2c_read(dp, DDC_ADDR,
buf + offset, chunk);
The custom message request explicitly lacks the DP_AUX_I2C_MOT (Middle Of
Transaction) bit:
drivers/gpu/drm/spacemit/spacemit_inno_dp.c:spacemit_dp_aux_i2c_write()
.request = DP_AUX_I2C_WRITE,
Issuing a write and a read separately without maintaining I2C transaction
continuity triggers an I2C STOP condition. This can cause I2C EEPROMs to
reset their address pointers, preventing the reading of EDID extension blocks.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260724-k3-display-v1-0-f22a10a52867@gmail.com?part=15
^ permalink raw reply [flat|nested] 28+ messages in thread
* [PATCH 16/17] riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (14 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 3:16 ` [PATCH 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang
2026-07-25 6:36 ` [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
CoM260-IFX carries a single DisplayPort output, so the second DPU and
the DisplayPort controller it feeds are enabled.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts | 11 +++++++++++
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 11 +++++++++++
2 files changed, 22 insertions(+)
diff --git a/arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts b/arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts
index 238bb03d0e9e..114d0e02fed9 100644
--- a/arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts
+++ b/arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts
@@ -5,6 +5,7 @@
*/
#include "k3-com260.dtsi"
+#include "k3-display.dtsi"
/ {
model = "SpacemiT K3 CoM260 IFX";
@@ -19,3 +20,13 @@ chosen {
stdout-path = "serial0:115200n8";
};
};
+
+&dpu1_crtc0 {
+ status = "okay";
+};
+
+&dp1 {
+ pinctrl-names = "default";
+ pinctrl-0 = <&dp1_3_cfg>;
+ status = "okay";
+};
diff --git a/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi b/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
index 2f36eba20f1e..dbe8db4a8b76 100644
--- a/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
+++ b/arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi
@@ -22,6 +22,17 @@ dp1-pins {
};
};
+ /omit-if-no-ref/
+ dp1_3_cfg: dp1-3-cfg {
+ dp1-pins {
+ pinmux = <K3_PADCONF(72, 4)>; /* dp1 hpd */
+
+ bias-disable;
+ drive-strength = <25>;
+ power-source = <3300>;
+ };
+ };
+
gmac0_rgmii_0_cfg: gmac0-rgmii-0-cfg {
gmac0-rgmii-0-pins {
pinmux = <K3_PADCONF(0, 1)>, /* gmac0_rxdv */
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* [PATCH 17/17] riscv: defconfig: spacemit: k3: enable display driver
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (15 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang
@ 2026-07-25 3:16 ` Cody Kang
2026-07-25 6:36 ` [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 3:16 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: dri-devel, linux-riscv, devicetree, spacemit, linux-kernel,
linux-phy, linux-clk, Cody Kang
Enable the Saturn DPU and the DP/eDP controller as modules, matching the
other DRM drivers in this defconfig; the DP PHY follows through the
controller's imply.
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
arch/riscv/configs/defconfig | 2 ++
1 file changed, 2 insertions(+)
diff --git a/arch/riscv/configs/defconfig b/arch/riscv/configs/defconfig
index 74ba5acc12a4..c38853d69d55 100644
--- a/arch/riscv/configs/defconfig
+++ b/arch/riscv/configs/defconfig
@@ -191,6 +191,8 @@ CONFIG_VIDEO_CADENCE_CSI2RX=m
CONFIG_DRM=m
CONFIG_DRM_RADEON=m
CONFIG_DRM_NOUVEAU=m
+CONFIG_DRM_SPACEMIT=m
+CONFIG_SPACEMIT_INNO_DP=m
CONFIG_DRM_SUN4I=m
CONFIG_DRM_VIRTIO_GPU=m
CONFIG_FB=y
--
2.43.0
^ permalink raw reply related [flat|nested] 28+ messages in thread* Re: [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support
2026-07-25 3:16 [PATCH 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang
` (16 preceding siblings ...)
2026-07-25 3:16 ` [PATCH 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang
@ 2026-07-25 6:36 ` Cody Kang
17 siblings, 0 replies; 28+ messages in thread
From: Cody Kang @ 2026-07-25 6:36 UTC (permalink / raw)
To: David Airlie, Simona Vetter, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Vinod Koul, Neil Armstrong, Haylen Chu,
Michael Turquette, Stephen Boyd, Brian Masney, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: Cody Kang, dri-devel, linux-riscv, devicetree, spacemit,
linux-kernel, linux-phy, linux-clk
Apologies for the duplicate posting. The patches are identical in both
threads, this series and a resend series, see [1].
Please use the resend [1] for furthing review comments:
[1] https://lore.kernel.org/r/20260725-k3-display-v1-0-6de34d80e86c@gmail.com
A delivery failure on gmail side appeared to have lost the cover letter
and eight of the patches from this thread. For about two hours I can
only find 9 (4, 6, 7, 11, 13, 14, 15, 16, 17) of the 17 emails in the lore
link. So I decided to resend the series with a different mail server.
Sorry for the noise.
Thanks,
Cody
^ permalink raw reply [flat|nested] 28+ messages in thread