* [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
This is a resend of today's earlier posting, which was only partially
delivered because of a mail failure on the sending side: the cover
letter and eight of the patches never reached the lists. The patches
are unchanged. Apologies for the duplicates.
This series adds display support for the SpacemiT K3 [2], a RISC-V SoC:
a new DRM driver, a new PHY driver and three new bindings.
The display path is three blocks deep:
Saturn DPU -> Innosilicon DP/eDP controller -> Innosilicon DP PHY
The Saturn DPU is SpacemiT's own display processing unit. It has a private
address translation unit and a command-list composer that replays a batch
of register writes when a flush is latched. It exposes one CRTC with one
primary plane. The K3 instantiates two DPUs, one per display path.
The DPU driver arrives as five staged patches to keep each one a
reasonable size. The driver only becomes buildable with that last patch.
Bisect safe.
The DP/eDP controller and its PHY are Innosilicon IP, also two instances
of each. An instance behaves as embedded DisplayPort or as external
DisplayPort purely according to what its output port is wired to, so the
bindings do not distinguish them. The PHY's registers are interleaved
with the controller's inside one MMIO window, which is why it is
described as a child node and shares the controller's regmap.
The pixel clock is generated by the DP PHY's PLL, and the SoC's APMU
pixel-clock mux takes that PLL as an input. Rather than let the display
driver reach into PHY registers, the PHY registers its PLL as a clock
provider and the APMU mux is parented to it through the clock framework.
That is what patches 2, 4, 5 and 6 are for, and it is also what keeps the
APMU and the DP/eDP controller out of a devicetree provider cycle.
As a first posting of the K3 display subsystem, this patchset supports:
a single primary plane per CRTC, linear RGB scanout (no plane scaling,
rotation, YUV or writeback). It does not support DSI, DSC or DP audio,
and link rates are capped at HBR2.
Based on drm-misc-next at commit 5b9af25f2d0b ("accel/ivpu: Remove
unnecessary min_t()/max_t() usage"), v7.2-rc2 based.
Testing this series needs a SpacemiT u-boot carrying display support
[1]. SpacemiT expects to add that support into an official u-boot
release in August 2026.
Tested on two K3 boards:
- Pico-ITX: an eDP panel and an external DisplayPort monitor.
- CoM260-IFX: DisplayPort only, no eDP.
Note: The eDP panel DTS enablement is not part of this series; a separate
series will follow showing how eDP can be verified.
What was run on both boards, with modetest from libdrm and with kmscube:
- modetest -s, looped over 1920x1080, 1280x720 and 3840x2160
on DisplayPort and 2560x1600 on eDP
- kmscube, on each head and on both at once
- hot-plug and sink swap on the DisplayPort connector
Both built-in and modular configurations were tested, and the runs above
were repeated in both. Patch 17 defconfig adds them as modules.
[1] https://github.com/spacemit-com/uboot-for-k3-display-verify/tree/k3-br-v1.0.y
[2] https://www.spacemit.com/community/document/info?lang=en&nodepath=hardware/key_stone/k3
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
Cody Kang (17):
dt-bindings: display: spacemit: add K3 Saturn DPU controller
dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
phy: spacemit: add Innosilicon DP TX PHY driver
clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
drm/spacemit: add Saturn DPU register model
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
drm/spacemit: add Saturn DPU hardware backend
drm/spacemit: add Saturn DPU KMS pipeline
drm/spacemit: add Saturn DPU DRM device driver
drm/spacemit: add Innosilicon DP/eDP controller bridge driver
MAINTAINERS: add SpacemiT K3 display driver entry
riscv: dts: spacemit: k3: add display nodes
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
riscv: defconfig: spacemit: k3: enable display driver
.../display/spacemit/spacemit,k3-inno-dp.yaml | 140 ++
.../display/spacemit/spacemit,k3-saturn-dpu.yaml | 132 ++
.../bindings/phy/spacemit,k3-inno-dp-phy.yaml | 48 +
.../bindings/soc/spacemit/spacemit,k1-syscon.yaml | 30 +-
MAINTAINERS | 10 +
arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-display.dtsi | 156 ++
arch/riscv/boot/dts/spacemit/k3-pico-itx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 22 +
arch/riscv/configs/defconfig | 2 +
drivers/clk/spacemit/ccu-k3.c | 4 +-
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/spacemit/Kconfig | 37 +
drivers/gpu/drm/spacemit/Makefile | 15 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn.c | 483 ++++
drivers/gpu/drm/spacemit/dpu/dpu_saturn.h | 35 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c | 896 +++++++
drivers/gpu/drm/spacemit/dpu/dpu_trace.h | 350 +++
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/dpu_int.h | 25 +
drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h | 52 +
.../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_cmdlist.c | 304 +++
drivers/gpu/drm/spacemit/spacemit_cmdlist.h | 146 ++
drivers/gpu/drm/spacemit/spacemit_crtc.c | 815 +++++++
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_ids.h | 22 +
drivers/gpu/drm/spacemit/spacemit_dpu_reg.h | 108 +
drivers/gpu/drm/spacemit/spacemit_drm.c | 132 ++
drivers/gpu/drm/spacemit/spacemit_drm.h | 68 +
drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 ++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_inno_dp.h | 328 +++
drivers/gpu/drm/spacemit/spacemit_planes.c | 376 +++
drivers/phy/spacemit/Kconfig | 13 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k3-inno-dp.c | 948 ++++++++
41 files changed, 9099 insertions(+), 3 deletions(-)
---
base-commit: 5b9af25f2d0b574b4058c9f8c84486a597168116
change-id: 20260713-k3-display-e8ffed67e936
--
Cody Kang <codykang.hk@gmail.com>
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
This is a resend of today's earlier posting, which was only partially
delivered because of a mail failure on the sending side: the cover
letter and eight of the patches never reached the lists. The patches
are unchanged. Apologies for the duplicates.
This series adds display support for the SpacemiT K3 [2], a RISC-V SoC:
a new DRM driver, a new PHY driver and three new bindings.
The display path is three blocks deep:
Saturn DPU -> Innosilicon DP/eDP controller -> Innosilicon DP PHY
The Saturn DPU is SpacemiT's own display processing unit. It has a private
address translation unit and a command-list composer that replays a batch
of register writes when a flush is latched. It exposes one CRTC with one
primary plane. The K3 instantiates two DPUs, one per display path.
The DPU driver arrives as five staged patches to keep each one a
reasonable size. The driver only becomes buildable with that last patch.
Bisect safe.
The DP/eDP controller and its PHY are Innosilicon IP, also two instances
of each. An instance behaves as embedded DisplayPort or as external
DisplayPort purely according to what its output port is wired to, so the
bindings do not distinguish them. The PHY's registers are interleaved
with the controller's inside one MMIO window, which is why it is
described as a child node and shares the controller's regmap.
The pixel clock is generated by the DP PHY's PLL, and the SoC's APMU
pixel-clock mux takes that PLL as an input. Rather than let the display
driver reach into PHY registers, the PHY registers its PLL as a clock
provider and the APMU mux is parented to it through the clock framework.
That is what patches 2, 4, 5 and 6 are for, and it is also what keeps the
APMU and the DP/eDP controller out of a devicetree provider cycle.
As a first posting of the K3 display subsystem, this patchset supports:
a single primary plane per CRTC, linear RGB scanout (no plane scaling,
rotation, YUV or writeback). It does not support DSI, DSC or DP audio,
and link rates are capped at HBR2.
Based on drm-misc-next at commit 5b9af25f2d0b ("accel/ivpu: Remove
unnecessary min_t()/max_t() usage"), v7.2-rc2 based.
Testing this series needs a SpacemiT u-boot carrying display support
[1]. SpacemiT expects to add that support into an official u-boot
release in August 2026.
Tested on two K3 boards:
- Pico-ITX: an eDP panel and an external DisplayPort monitor.
- CoM260-IFX: DisplayPort only, no eDP.
Note: The eDP panel DTS enablement is not part of this series; a separate
series will follow showing how eDP can be verified.
What was run on both boards, with modetest from libdrm and with kmscube:
- modetest -s, looped over 1920x1080, 1280x720 and 3840x2160
on DisplayPort and 2560x1600 on eDP
- kmscube, on each head and on both at once
- hot-plug and sink swap on the DisplayPort connector
Both built-in and modular configurations were tested, and the runs above
were repeated in both. Patch 17 defconfig adds them as modules.
[1] https://github.com/spacemit-com/uboot-for-k3-display-verify/tree/k3-br-v1.0.y
[2] https://www.spacemit.com/community/document/info?lang=en&nodepath=hardware/key_stone/k3
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
Cody Kang (17):
dt-bindings: display: spacemit: add K3 Saturn DPU controller
dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
phy: spacemit: add Innosilicon DP TX PHY driver
clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
drm/spacemit: add Saturn DPU register model
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
drm/spacemit: add Saturn DPU hardware backend
drm/spacemit: add Saturn DPU KMS pipeline
drm/spacemit: add Saturn DPU DRM device driver
drm/spacemit: add Innosilicon DP/eDP controller bridge driver
MAINTAINERS: add SpacemiT K3 display driver entry
riscv: dts: spacemit: k3: add display nodes
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
riscv: defconfig: spacemit: k3: enable display driver
.../display/spacemit/spacemit,k3-inno-dp.yaml | 140 ++
.../display/spacemit/spacemit,k3-saturn-dpu.yaml | 132 ++
.../bindings/phy/spacemit,k3-inno-dp-phy.yaml | 48 +
.../bindings/soc/spacemit/spacemit,k1-syscon.yaml | 30 +-
MAINTAINERS | 10 +
arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-display.dtsi | 156 ++
arch/riscv/boot/dts/spacemit/k3-pico-itx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 22 +
arch/riscv/configs/defconfig | 2 +
drivers/clk/spacemit/ccu-k3.c | 4 +-
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/spacemit/Kconfig | 37 +
drivers/gpu/drm/spacemit/Makefile | 15 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn.c | 483 ++++
drivers/gpu/drm/spacemit/dpu/dpu_saturn.h | 35 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c | 896 +++++++
drivers/gpu/drm/spacemit/dpu/dpu_trace.h | 350 +++
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/dpu_int.h | 25 +
drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h | 52 +
.../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_cmdlist.c | 304 +++
drivers/gpu/drm/spacemit/spacemit_cmdlist.h | 146 ++
drivers/gpu/drm/spacemit/spacemit_crtc.c | 815 +++++++
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_ids.h | 22 +
drivers/gpu/drm/spacemit/spacemit_dpu_reg.h | 108 +
drivers/gpu/drm/spacemit/spacemit_drm.c | 132 ++
drivers/gpu/drm/spacemit/spacemit_drm.h | 68 +
drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 ++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_inno_dp.h | 328 +++
drivers/gpu/drm/spacemit/spacemit_planes.c | 376 +++
drivers/phy/spacemit/Kconfig | 13 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k3-inno-dp.c | 948 ++++++++
41 files changed, 9099 insertions(+), 3 deletions(-)
---
base-commit: 5b9af25f2d0b574b4058c9f8c84486a597168116
change-id: 20260713-k3-display-e8ffed67e936
--
Cody Kang <codykang.hk@gmail.com>
^ permalink raw reply [flat|nested] 88+ messages in thread
* [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
This is a resend of today's earlier posting, which was only partially
delivered because of a mail failure on the sending side: the cover
letter and eight of the patches never reached the lists. The patches
are unchanged. Apologies for the duplicates.
This series adds display support for the SpacemiT K3 [2], a RISC-V SoC:
a new DRM driver, a new PHY driver and three new bindings.
The display path is three blocks deep:
Saturn DPU -> Innosilicon DP/eDP controller -> Innosilicon DP PHY
The Saturn DPU is SpacemiT's own display processing unit. It has a private
address translation unit and a command-list composer that replays a batch
of register writes when a flush is latched. It exposes one CRTC with one
primary plane. The K3 instantiates two DPUs, one per display path.
The DPU driver arrives as five staged patches to keep each one a
reasonable size. The driver only becomes buildable with that last patch.
Bisect safe.
The DP/eDP controller and its PHY are Innosilicon IP, also two instances
of each. An instance behaves as embedded DisplayPort or as external
DisplayPort purely according to what its output port is wired to, so the
bindings do not distinguish them. The PHY's registers are interleaved
with the controller's inside one MMIO window, which is why it is
described as a child node and shares the controller's regmap.
The pixel clock is generated by the DP PHY's PLL, and the SoC's APMU
pixel-clock mux takes that PLL as an input. Rather than let the display
driver reach into PHY registers, the PHY registers its PLL as a clock
provider and the APMU mux is parented to it through the clock framework.
That is what patches 2, 4, 5 and 6 are for, and it is also what keeps the
APMU and the DP/eDP controller out of a devicetree provider cycle.
As a first posting of the K3 display subsystem, this patchset supports:
a single primary plane per CRTC, linear RGB scanout (no plane scaling,
rotation, YUV or writeback). It does not support DSI, DSC or DP audio,
and link rates are capped at HBR2.
Based on drm-misc-next at commit 5b9af25f2d0b ("accel/ivpu: Remove
unnecessary min_t()/max_t() usage"), v7.2-rc2 based.
Testing this series needs a SpacemiT u-boot carrying display support
[1]. SpacemiT expects to add that support into an official u-boot
release in August 2026.
Tested on two K3 boards:
- Pico-ITX: an eDP panel and an external DisplayPort monitor.
- CoM260-IFX: DisplayPort only, no eDP.
Note: The eDP panel DTS enablement is not part of this series; a separate
series will follow showing how eDP can be verified.
What was run on both boards, with modetest from libdrm and with kmscube:
- modetest -s, looped over 1920x1080, 1280x720 and 3840x2160
on DisplayPort and 2560x1600 on eDP
- kmscube, on each head and on both at once
- hot-plug and sink swap on the DisplayPort connector
Both built-in and modular configurations were tested, and the runs above
were repeated in both. Patch 17 defconfig adds them as modules.
[1] https://github.com/spacemit-com/uboot-for-k3-display-verify/tree/k3-br-v1.0.y
[2] https://www.spacemit.com/community/document/info?lang=en&nodepath=hardware/key_stone/k3
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
Cody Kang (17):
dt-bindings: display: spacemit: add K3 Saturn DPU controller
dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
phy: spacemit: add Innosilicon DP TX PHY driver
clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
drm/spacemit: add Saturn DPU register model
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
drm/spacemit: add Saturn DPU hardware backend
drm/spacemit: add Saturn DPU KMS pipeline
drm/spacemit: add Saturn DPU DRM device driver
drm/spacemit: add Innosilicon DP/eDP controller bridge driver
MAINTAINERS: add SpacemiT K3 display driver entry
riscv: dts: spacemit: k3: add display nodes
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
riscv: defconfig: spacemit: k3: enable display driver
.../display/spacemit/spacemit,k3-inno-dp.yaml | 140 ++
.../display/spacemit/spacemit,k3-saturn-dpu.yaml | 132 ++
.../bindings/phy/spacemit,k3-inno-dp-phy.yaml | 48 +
.../bindings/soc/spacemit/spacemit,k1-syscon.yaml | 30 +-
MAINTAINERS | 10 +
arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-display.dtsi | 156 ++
arch/riscv/boot/dts/spacemit/k3-pico-itx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 22 +
arch/riscv/configs/defconfig | 2 +
drivers/clk/spacemit/ccu-k3.c | 4 +-
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/spacemit/Kconfig | 37 +
drivers/gpu/drm/spacemit/Makefile | 15 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn.c | 483 ++++
drivers/gpu/drm/spacemit/dpu/dpu_saturn.h | 35 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c | 896 +++++++
drivers/gpu/drm/spacemit/dpu/dpu_trace.h | 350 +++
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/dpu_int.h | 25 +
drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h | 52 +
.../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_cmdlist.c | 304 +++
drivers/gpu/drm/spacemit/spacemit_cmdlist.h | 146 ++
drivers/gpu/drm/spacemit/spacemit_crtc.c | 815 +++++++
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_ids.h | 22 +
drivers/gpu/drm/spacemit/spacemit_dpu_reg.h | 108 +
drivers/gpu/drm/spacemit/spacemit_drm.c | 132 ++
drivers/gpu/drm/spacemit/spacemit_drm.h | 68 +
drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 ++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_inno_dp.h | 328 +++
drivers/gpu/drm/spacemit/spacemit_planes.c | 376 +++
drivers/phy/spacemit/Kconfig | 13 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k3-inno-dp.c | 948 ++++++++
41 files changed, 9099 insertions(+), 3 deletions(-)
---
base-commit: 5b9af25f2d0b574b4058c9f8c84486a597168116
change-id: 20260713-k3-display-e8ffed67e936
--
Cody Kang <codykang.hk@gmail.com>
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply [flat|nested] 88+ messages in thread
* [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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
This is a resend of today's earlier posting, which was only partially
delivered because of a mail failure on the sending side: the cover
letter and eight of the patches never reached the lists. The patches
are unchanged. Apologies for the duplicates.
This series adds display support for the SpacemiT K3 [2], a RISC-V SoC:
a new DRM driver, a new PHY driver and three new bindings.
The display path is three blocks deep:
Saturn DPU -> Innosilicon DP/eDP controller -> Innosilicon DP PHY
The Saturn DPU is SpacemiT's own display processing unit. It has a private
address translation unit and a command-list composer that replays a batch
of register writes when a flush is latched. It exposes one CRTC with one
primary plane. The K3 instantiates two DPUs, one per display path.
The DPU driver arrives as five staged patches to keep each one a
reasonable size. The driver only becomes buildable with that last patch.
Bisect safe.
The DP/eDP controller and its PHY are Innosilicon IP, also two instances
of each. An instance behaves as embedded DisplayPort or as external
DisplayPort purely according to what its output port is wired to, so the
bindings do not distinguish them. The PHY's registers are interleaved
with the controller's inside one MMIO window, which is why it is
described as a child node and shares the controller's regmap.
The pixel clock is generated by the DP PHY's PLL, and the SoC's APMU
pixel-clock mux takes that PLL as an input. Rather than let the display
driver reach into PHY registers, the PHY registers its PLL as a clock
provider and the APMU mux is parented to it through the clock framework.
That is what patches 2, 4, 5 and 6 are for, and it is also what keeps the
APMU and the DP/eDP controller out of a devicetree provider cycle.
As a first posting of the K3 display subsystem, this patchset supports:
a single primary plane per CRTC, linear RGB scanout (no plane scaling,
rotation, YUV or writeback). It does not support DSI, DSC or DP audio,
and link rates are capped at HBR2.
Based on drm-misc-next at commit 5b9af25f2d0b ("accel/ivpu: Remove
unnecessary min_t()/max_t() usage"), v7.2-rc2 based.
Testing this series needs a SpacemiT u-boot carrying display support
[1]. SpacemiT expects to add that support into an official u-boot
release in August 2026.
Tested on two K3 boards:
- Pico-ITX: an eDP panel and an external DisplayPort monitor.
- CoM260-IFX: DisplayPort only, no eDP.
Note: The eDP panel DTS enablement is not part of this series; a separate
series will follow showing how eDP can be verified.
What was run on both boards, with modetest from libdrm and with kmscube:
- modetest -s, looped over 1920x1080, 1280x720 and 3840x2160
on DisplayPort and 2560x1600 on eDP
- kmscube, on each head and on both at once
- hot-plug and sink swap on the DisplayPort connector
Both built-in and modular configurations were tested, and the runs above
were repeated in both. Patch 17 defconfig adds them as modules.
[1] https://github.com/spacemit-com/uboot-for-k3-display-verify/tree/k3-br-v1.0.y
[2] https://www.spacemit.com/community/document/info?lang=en&nodepath=hardware/key_stone/k3
Signed-off-by: Cody Kang <codykang.hk@gmail.com>
---
Cody Kang (17):
dt-bindings: display: spacemit: add K3 Saturn DPU controller
dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
phy: spacemit: add Innosilicon DP TX PHY driver
clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
drm/spacemit: add Saturn DPU register model
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
drm/spacemit: add Saturn DPU hardware backend
drm/spacemit: add Saturn DPU KMS pipeline
drm/spacemit: add Saturn DPU DRM device driver
drm/spacemit: add Innosilicon DP/eDP controller bridge driver
MAINTAINERS: add SpacemiT K3 display driver entry
riscv: dts: spacemit: k3: add display nodes
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
riscv: defconfig: spacemit: k3: enable display driver
.../display/spacemit/spacemit,k3-inno-dp.yaml | 140 ++
.../display/spacemit/spacemit,k3-saturn-dpu.yaml | 132 ++
.../bindings/phy/spacemit,k3-inno-dp-phy.yaml | 48 +
.../bindings/soc/spacemit/spacemit,k1-syscon.yaml | 30 +-
MAINTAINERS | 10 +
arch/riscv/boot/dts/spacemit/k3-com260-ifx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-display.dtsi | 156 ++
arch/riscv/boot/dts/spacemit/k3-pico-itx.dts | 11 +
arch/riscv/boot/dts/spacemit/k3-pinctrl.dtsi | 22 +
arch/riscv/configs/defconfig | 2 +
drivers/clk/spacemit/ccu-k3.c | 4 +-
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/spacemit/Kconfig | 37 +
drivers/gpu/drm/spacemit/Makefile | 15 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn.c | 483 ++++
drivers/gpu/drm/spacemit/dpu/dpu_saturn.h | 35 +
drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c | 896 +++++++
drivers/gpu/drm/spacemit/dpu/dpu_trace.h | 350 +++
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/dpu_int.h | 25 +
drivers/gpu/drm/spacemit/dpu/saturn_regs/ops_hee.h | 52 +
.../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_cmdlist.c | 304 +++
drivers/gpu/drm/spacemit/spacemit_cmdlist.h | 146 ++
drivers/gpu/drm/spacemit/spacemit_crtc.c | 815 +++++++
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_ids.h | 22 +
drivers/gpu/drm/spacemit/spacemit_dpu_reg.h | 108 +
drivers/gpu/drm/spacemit/spacemit_drm.c | 132 ++
drivers/gpu/drm/spacemit/spacemit_drm.h | 68 +
drivers/gpu/drm/spacemit/spacemit_inno_dp.c | 2443 ++++++++++++++++++++
drivers/gpu/drm/spacemit/spacemit_inno_dp.h | 328 +++
drivers/gpu/drm/spacemit/spacemit_planes.c | 376 +++
drivers/phy/spacemit/Kconfig | 13 +
drivers/phy/spacemit/Makefile | 1 +
drivers/phy/spacemit/phy-k3-inno-dp.c | 948 ++++++++
41 files changed, 9099 insertions(+), 3 deletions(-)
---
base-commit: 5b9af25f2d0b574b4058c9f8c84486a597168116
change-id: 20260713-k3-display-e8ffed67e936
--
Cody Kang <codykang.hk@gmail.com>
^ permalink raw reply [flat|nested] 88+ messages in thread
* [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: enable the DisplayPort output
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
(?)
(?)
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
-1 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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] 88+ messages in thread
* [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang via B4 Relay @ 2026-07-25 4:51 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
From: Cody Kang <codykang.hk@gmail.com>
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
_______________________________________________
linux-riscv mailing list
linux-riscv@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-riscv
^ permalink raw reply related [flat|nested] 88+ messages in thread
* [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver
@ 2026-07-25 4:51 ` Cody Kang via B4 Relay
0 siblings, 0 replies; 88+ messages in thread
From: Cody Kang @ 2026-07-25 4:51 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] 88+ messages in thread
* Re: [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:02 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:02 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, vkoul, linux-phy, neil.armstrong, robh, devicetree,
olteanv, conor+dt
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] PHY provider is registered before probe finishes, exposing it to early concurrent access and potential Use-After-Free (UAF) on probe failure.
- [High] Lack of driver-level synchronization between CCF and PHY frameworks leads to concurrent hardware state corruption.
- [Medium] The MPLL (main link PLL) is powered on during PHY initialization but is never powered off in the power down sequence.
- [Medium] Missing hardware state cleanup on error paths in `k3_inno_dp_phy_power_on` leaves the PHY powered on.
- [Medium] Rate configuration functions unconditionally power on the PLLs, overriding the logical PHY power state.
- [Medium] PLL programming functions do not poll for hardware lock before proceeding.
--
--- Patch [5]: [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver ---
commit 31db74eae7dc59ddc61373dbe3e2c6c1e6966ccd
Author: Cody Kang <codykang.hk@gmail.com>
Subject: phy: spacemit: add Innosilicon DP TX PHY driver
This commit introduces the SpacemiT K3 Innosilicon DisplayPort PHY driver.
It integrates the PHY as a child of the DP controller, registering a PHY
provider and exposing the 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
> @@ -0,0 +1,948 @@
[ ... ]
> +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;
[ ... ]
> + 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 the MPLL or PREPLL fails to lock within 10ms, this immediately returns an
error without restoring the power down bits or XMIT_EN.
Since the generic PHY framework does not automatically call phy_power_off()
when phy_power_on() fails, could this leave the hardware running in an
inconsistent state and leak power?
> +
> + 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]
Does this power down sequence omit the MPLL?
The k3_inno_dp_phy_power_on() function powers up the MPLL by clearing
DPTX_ANA_MPLL_PD, but this power_off function does not appear to re-assert
it. Could this leave the main link PLL running indefinitely when the DP
controller is suspended or torn down?
[ ... ]
> +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));
[Severity: Medium]
Does unconditionally writing 0 to DPTX_ANA_MPLL_PD override the logical
power state of the PHY?
If a DRM driver configures the link rate (calling phy_configure()) while the
PHY is logically powered off, this will force the PLL to run and
desynchronize the hardware from the PHY framework's power state.
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
Is it safe to rely on a fixed 2-4ms sleep here instead of polling
DPTX_ANA_MPLL_LOCKED?
The power_on() function polls the locked bit for up to 10ms. A fixed delay
here might proceed with an unstable clock if the hardware takes longer to
lock, potentially causing display failures or link training issues.
[ ... ]
> +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);
[Severity: High]
Is a driver-level lock needed to synchronize access to the PHY registers
between the CCF and PHY frameworks?
Since this driver exposes both a clock (via CCF) and a PHY that share the
same hardware registers, concurrent calls to clk_set_rate() and
phy_power_on() can interleave.
If clk_set_rate() begins this multi-step programming sequence, a concurrent
phy_power_on() could clear the PD bit while the clock is still being
configured, corrupting the hardware state.
[ ... ]
> + 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));
[Severity: Medium]
Similar to the MPLL configuration, does unconditionally clearing
DPTX_ANA_PREPLL_PD here override the PHY power state if the clock rate is
set while the PHY is logically off?
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
Is it safe to rely on a fixed delay here instead of polling the
DPTX_ANA_PREPLL_LOCKED bit, which could lead to proceeding with an unstable
pixel clock?
[ ... ]
> +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;
[ ... ]
> + 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");
[Severity: High]
Can the PHY provider be accessed by consumers before probe completes
successfully?
The PHY provider is registered via devm_of_phy_provider_register() before
the pixel clock is registered. If devm_clk_hw_register() fails, probe will
return an error and devres will free the driver context struct.
If a consumer driver acquires a reference to the PHY during this race
window, it could retain the reference after probe fails, leading to a
use-after-free when it attempts PHY operations on the freed pointer.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=5
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver
@ 2026-07-25 5:02 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:02 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, vkoul, linux-phy, neil.armstrong, robh, devicetree,
olteanv, conor+dt
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] PHY provider is registered before probe finishes, exposing it to early concurrent access and potential Use-After-Free (UAF) on probe failure.
- [High] Lack of driver-level synchronization between CCF and PHY frameworks leads to concurrent hardware state corruption.
- [Medium] The MPLL (main link PLL) is powered on during PHY initialization but is never powered off in the power down sequence.
- [Medium] Missing hardware state cleanup on error paths in `k3_inno_dp_phy_power_on` leaves the PHY powered on.
- [Medium] Rate configuration functions unconditionally power on the PLLs, overriding the logical PHY power state.
- [Medium] PLL programming functions do not poll for hardware lock before proceeding.
--
--- Patch [5]: [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver ---
commit 31db74eae7dc59ddc61373dbe3e2c6c1e6966ccd
Author: Cody Kang <codykang.hk@gmail.com>
Subject: phy: spacemit: add Innosilicon DP TX PHY driver
This commit introduces the SpacemiT K3 Innosilicon DisplayPort PHY driver.
It integrates the PHY as a child of the DP controller, registering a PHY
provider and exposing the 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
> @@ -0,0 +1,948 @@
[ ... ]
> +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;
[ ... ]
> + 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 the MPLL or PREPLL fails to lock within 10ms, this immediately returns an
error without restoring the power down bits or XMIT_EN.
Since the generic PHY framework does not automatically call phy_power_off()
when phy_power_on() fails, could this leave the hardware running in an
inconsistent state and leak power?
> +
> + 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]
Does this power down sequence omit the MPLL?
The k3_inno_dp_phy_power_on() function powers up the MPLL by clearing
DPTX_ANA_MPLL_PD, but this power_off function does not appear to re-assert
it. Could this leave the main link PLL running indefinitely when the DP
controller is suspended or torn down?
[ ... ]
> +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));
[Severity: Medium]
Does unconditionally writing 0 to DPTX_ANA_MPLL_PD override the logical
power state of the PHY?
If a DRM driver configures the link rate (calling phy_configure()) while the
PHY is logically powered off, this will force the PLL to run and
desynchronize the hardware from the PHY framework's power state.
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
Is it safe to rely on a fixed 2-4ms sleep here instead of polling
DPTX_ANA_MPLL_LOCKED?
The power_on() function polls the locked bit for up to 10ms. A fixed delay
here might proceed with an unstable clock if the hardware takes longer to
lock, potentially causing display failures or link training issues.
[ ... ]
> +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);
[Severity: High]
Is a driver-level lock needed to synchronize access to the PHY registers
between the CCF and PHY frameworks?
Since this driver exposes both a clock (via CCF) and a PHY that share the
same hardware registers, concurrent calls to clk_set_rate() and
phy_power_on() can interleave.
If clk_set_rate() begins this multi-step programming sequence, a concurrent
phy_power_on() could clear the PD bit while the clock is still being
configured, corrupting the hardware state.
[ ... ]
> + 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));
[Severity: Medium]
Similar to the MPLL configuration, does unconditionally clearing
DPTX_ANA_PREPLL_PD here override the PHY power state if the clock rate is
set while the PHY is logically off?
> + usleep_range(2000, 4000);
> +}
[Severity: Medium]
Is it safe to rely on a fixed delay here instead of polling the
DPTX_ANA_PREPLL_LOCKED bit, which could lead to proceeding with an unstable
pixel clock?
[ ... ]
> +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;
[ ... ]
> + 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");
[Severity: High]
Can the PHY provider be accessed by consumers before probe completes
successfully?
The PHY provider is registered via devm_of_phy_provider_register() before
the pixel clock is registered. If devm_clk_hw_register() fails, probe will
return an error and devres will free the driver context struct.
If a consumer driver acquires a reference to the PHY during this race
window, it could retain the reference after probe fails, leading to a
use-after-free when it attempts PHY operations on the freed pointer.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=5
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:04 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, linux-phy, olteanv, conor+dt, vkoul, neil.armstrong,
robh, devicetree
Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Sleeping functions are called from CRTC atomic enable and disable callbacks, which execute in atomic context.
- [High] TOCTOU race when updating the `clocks_on` flag and gating hardware clocks leads to MMIO stalls.
- [High] Calling `devm_drm_dev_alloc` inside a component `bind` callback leaks the DRM device on rebinds.
- [High] Enabling the DPU interrupt before hardware is initialized leads to an unhandled IRQ storm.
- [High] `spacemit_plane_reset` bypasses the custom state destructor, leaking DMA coherent memory.
- [High] `disable_irq()` is called before `drm_atomic_helper_shutdown()` during driver unbind, causing shutdown to hang.
- [High] `spacemit_plane_atomic_check()` assigns RDMA channels based on unvalidated `zpos`, leading to channel collisions.
- [High] Unconditional `spin_unlock_irq` used in atomic context improperly re-enables interrupts.
- [Medium] Leaking DRM property blob references in `spacemit_crtc_duplicate_state` on error.
- [Medium] Missing NULL check on `plane->state` in `spacemit_plane_atomic_duplicate_state` causes a crash if the state is missing.
--
--- Patch [10]: [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline ---
commit cf6665928c2c5fcf07a88c5cd2141feb18245e17
Author: Cody Kang <codykang.hk@gmail.com>
Subject: 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.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_crtc.c b/drivers/gpu/drm/spacemit/spacemit_crtc.c
> new file mode 100644
> index 0000000000000..b75ff63205011
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_crtc.c
[ ... ]
> @@ -91,7 +91,7 @@ static void spacemit_crtc_atomic_enable(struct drm_crtc *crtc,
> 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]
Does this sleep in an atomic context? The spacemit_crtc_atomic_enable
callback is executed during a non-blocking DRM atomic commit, which runs
in an atomic context. Calling usleep_range() here might violate atomic
context rules and lead to scheduling-while-atomic warnings.
[ ... ]
> @@ -145,13 +145,13 @@ static void spacemit_crtc_atomic_disable(struct drm_crtc *crtc,
> * must finish its status reads before dpu_pm_suspend() gates the
> * clocks.
> */
> if (a_crtc->irq_online > 0)
> synchronize_irq(a_crtc->irq_online);
[Severity: High]
Can synchronize_irq() safely execute here? This callback runs in an atomic
context, but synchronize_irq() blocks waiting for executing handlers.
[Severity: High]
Does this code leave a race window before the flags are updated? An interrupt
could fire immediately after synchronize_irq() returns but before clocks_on
is toggled to false, leading the ISR to access MMIO while dpu_pm_suspend()
is shutting down the clocks.
>
> /*
> * 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]
Does dpu_pm_suspend() sleep in this atomic context? It calls usleep_range()
which could trigger kernel warnings and deadlocks when reached via
spacemit_crtc_atomic_disable().
>
> 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);
[Severity: High]
Is it safe to use unconditional spin_unlock_irq() here? Since this callback
executes in an atomic context (e.g. from drm_atomic_helper_commit_tail),
unconditionally re-enabling interrupts breaks the caller's atomic assumptions.
Should this use spin_lock_irqsave() and spin_unlock_irqrestore() instead?
[ ... ]
> @@ -231,6 +231,5 @@ static struct drm_crtc_state *
> __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]
Does this error path leak DRM property blob references?
__drm_atomic_helper_crtc_duplicate_state() increments references on blobs
like mode_blob, ctm, and gamma_lut. Returning NULL here without releasing
them could leak those references.
[ ... ]
> @@ -418,6 +418,5 @@ static irqreturn_t spacemit_dpu_isr(int irq, void *data)
> /* Reading a clock-gated DPU's status registers stalls the bus. */
> if (unlikely(!a_crtc->clocks_on)) {
> drm_dev_exit(idx);
> return IRQ_NONE;
> }
[Severity: High]
Does this code cause an IRQ storm? By returning IRQ_NONE without clearing
the hardware interrupt status when clocks_on is false, the interrupt
controller might continuously re-trigger the ISR if the bootloader left the
interrupt asserted.
[ ... ]
> @@ -496,6 +496,5 @@ static int spacemit_drm_master_bind(struct device *dev)
> }
>
> kms = devm_drm_dev_alloc(dev, &spacemit_drm_drv,
> struct spacemit_kms, drm);
> if (IS_ERR(kms))
[Severity: High]
Will this leak the DRM device on rebind? devm_drm_dev_alloc ties the
allocation lifetime to the platform device's probe/remove cycle. If the
component unbinds and rebinds, new DRM devices are allocated but the old
ones are never freed.
[ ... ]
> @@ -573,6 +573,5 @@ static int spacemit_drm_master_bind(struct device *dev)
> drm_mode_config_reset(drm);
> drmm_kms_helper_poll_init(drm);
> drm_helper_hpd_irq_event(drm);
>
> enable_irq(priv->irq_online);
[Severity: High]
Is it safe to enable the IRQ before the hardware state is fully initialized?
If the bootloader left the display hardware active, the ISR might fire
immediately while a_crtc->clocks_on is still false, leading to the IRQ storm
mentioned above.
[ ... ]
> @@ -594,6 +594,5 @@ static void spacemit_drm_master_unbind(struct device *dev)
>
> disable_irq(priv->irq_online);
>
> drm_dev_unregister(drm);
> drm_atomic_helper_shutdown(drm);
> component_unbind_all(dev, drm);
[Severity: High]
Will calling drm_atomic_helper_shutdown() hang if the IRQ is already
disabled? The shutdown helper flushes pending atomic commits and disables
CRTCs, which requires the hardware vblank interrupt to signal completion.
Without it, the commit wait could time out.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_planes.c b/drivers/gpu/drm/spacemit/spacemit_planes.c
> new file mode 100644
> index 0000000000000..3442013b0c37f
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_planes.c
[ ... ]
> @@ -98,6 +98,5 @@ static int spacemit_plane_atomic_check(struct drm_plane *plane,
> 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;
[Severity: High]
Does this code assign RDMA channels based on unvalidated zpos? Userspace
could submit a configuration with duplicate zpos values across multiple
planes, resulting in channel collisions where planes overwrite each other's
hardware registers during spacemit_plane_atomic_update().
[ ... ]
> @@ -193,7 +193,6 @@ static void spacemit_plane_reset(struct drm_plane *plane)
> 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]
Does freeing the state directly bypass the driver's custom destructor?
Calling kfree(s) directly in spacemit_plane_reset() skips
spacemit_plane_atomic_destroy_state(), which means the DMA coherent buffers
for mmu_tbl.va and cl.va could be permanently leaked when a plane is reset.
[ ... ]
> @@ -211,6 +211,5 @@ 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;
[Severity: Medium]
Will this crash if plane->state is NULL? The to_spacemit_plane_state()
macro uses container_of, which will produce a negative pointer if
plane->state is NULL. Dereferencing old_state->rdma_id later would trigger
a fault.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=10
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline
@ 2026-07-25 5:04 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: dri-devel, linux-phy, olteanv, conor+dt, vkoul, neil.armstrong,
robh, devicetree
Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Sleeping functions are called from CRTC atomic enable and disable callbacks, which execute in atomic context.
- [High] TOCTOU race when updating the `clocks_on` flag and gating hardware clocks leads to MMIO stalls.
- [High] Calling `devm_drm_dev_alloc` inside a component `bind` callback leaks the DRM device on rebinds.
- [High] Enabling the DPU interrupt before hardware is initialized leads to an unhandled IRQ storm.
- [High] `spacemit_plane_reset` bypasses the custom state destructor, leaking DMA coherent memory.
- [High] `disable_irq()` is called before `drm_atomic_helper_shutdown()` during driver unbind, causing shutdown to hang.
- [High] `spacemit_plane_atomic_check()` assigns RDMA channels based on unvalidated `zpos`, leading to channel collisions.
- [High] Unconditional `spin_unlock_irq` used in atomic context improperly re-enables interrupts.
- [Medium] Leaking DRM property blob references in `spacemit_crtc_duplicate_state` on error.
- [Medium] Missing NULL check on `plane->state` in `spacemit_plane_atomic_duplicate_state` causes a crash if the state is missing.
--
--- Patch [10]: [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline ---
commit cf6665928c2c5fcf07a88c5cd2141feb18245e17
Author: Cody Kang <codykang.hk@gmail.com>
Subject: 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.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_crtc.c b/drivers/gpu/drm/spacemit/spacemit_crtc.c
> new file mode 100644
> index 0000000000000..b75ff63205011
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_crtc.c
[ ... ]
> @@ -91,7 +91,7 @@ static void spacemit_crtc_atomic_enable(struct drm_crtc *crtc,
> 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]
Does this sleep in an atomic context? The spacemit_crtc_atomic_enable
callback is executed during a non-blocking DRM atomic commit, which runs
in an atomic context. Calling usleep_range() here might violate atomic
context rules and lead to scheduling-while-atomic warnings.
[ ... ]
> @@ -145,13 +145,13 @@ static void spacemit_crtc_atomic_disable(struct drm_crtc *crtc,
> * must finish its status reads before dpu_pm_suspend() gates the
> * clocks.
> */
> if (a_crtc->irq_online > 0)
> synchronize_irq(a_crtc->irq_online);
[Severity: High]
Can synchronize_irq() safely execute here? This callback runs in an atomic
context, but synchronize_irq() blocks waiting for executing handlers.
[Severity: High]
Does this code leave a race window before the flags are updated? An interrupt
could fire immediately after synchronize_irq() returns but before clocks_on
is toggled to false, leading the ISR to access MMIO while dpu_pm_suspend()
is shutting down the clocks.
>
> /*
> * 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]
Does dpu_pm_suspend() sleep in this atomic context? It calls usleep_range()
which could trigger kernel warnings and deadlocks when reached via
spacemit_crtc_atomic_disable().
>
> 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);
[Severity: High]
Is it safe to use unconditional spin_unlock_irq() here? Since this callback
executes in an atomic context (e.g. from drm_atomic_helper_commit_tail),
unconditionally re-enabling interrupts breaks the caller's atomic assumptions.
Should this use spin_lock_irqsave() and spin_unlock_irqrestore() instead?
[ ... ]
> @@ -231,6 +231,5 @@ static struct drm_crtc_state *
> __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]
Does this error path leak DRM property blob references?
__drm_atomic_helper_crtc_duplicate_state() increments references on blobs
like mode_blob, ctm, and gamma_lut. Returning NULL here without releasing
them could leak those references.
[ ... ]
> @@ -418,6 +418,5 @@ static irqreturn_t spacemit_dpu_isr(int irq, void *data)
> /* Reading a clock-gated DPU's status registers stalls the bus. */
> if (unlikely(!a_crtc->clocks_on)) {
> drm_dev_exit(idx);
> return IRQ_NONE;
> }
[Severity: High]
Does this code cause an IRQ storm? By returning IRQ_NONE without clearing
the hardware interrupt status when clocks_on is false, the interrupt
controller might continuously re-trigger the ISR if the bootloader left the
interrupt asserted.
[ ... ]
> @@ -496,6 +496,5 @@ static int spacemit_drm_master_bind(struct device *dev)
> }
>
> kms = devm_drm_dev_alloc(dev, &spacemit_drm_drv,
> struct spacemit_kms, drm);
> if (IS_ERR(kms))
[Severity: High]
Will this leak the DRM device on rebind? devm_drm_dev_alloc ties the
allocation lifetime to the platform device's probe/remove cycle. If the
component unbinds and rebinds, new DRM devices are allocated but the old
ones are never freed.
[ ... ]
> @@ -573,6 +573,5 @@ static int spacemit_drm_master_bind(struct device *dev)
> drm_mode_config_reset(drm);
> drmm_kms_helper_poll_init(drm);
> drm_helper_hpd_irq_event(drm);
>
> enable_irq(priv->irq_online);
[Severity: High]
Is it safe to enable the IRQ before the hardware state is fully initialized?
If the bootloader left the display hardware active, the ISR might fire
immediately while a_crtc->clocks_on is still false, leading to the IRQ storm
mentioned above.
[ ... ]
> @@ -594,6 +594,5 @@ static void spacemit_drm_master_unbind(struct device *dev)
>
> disable_irq(priv->irq_online);
>
> drm_dev_unregister(drm);
> drm_atomic_helper_shutdown(drm);
> component_unbind_all(dev, drm);
[Severity: High]
Will calling drm_atomic_helper_shutdown() hang if the IRQ is already
disabled? The shutdown helper flushes pending atomic commits and disables
CRTCs, which requires the hardware vblank interrupt to signal completion.
Without it, the commit wait could time out.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_planes.c b/drivers/gpu/drm/spacemit/spacemit_planes.c
> new file mode 100644
> index 0000000000000..3442013b0c37f
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_planes.c
[ ... ]
> @@ -98,6 +98,5 @@ static int spacemit_plane_atomic_check(struct drm_plane *plane,
> 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;
[Severity: High]
Does this code assign RDMA channels based on unvalidated zpos? Userspace
could submit a configuration with duplicate zpos values across multiple
planes, resulting in channel collisions where planes overwrite each other's
hardware registers during spacemit_plane_atomic_update().
[ ... ]
> @@ -193,7 +193,6 @@ static void spacemit_plane_reset(struct drm_plane *plane)
> 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]
Does freeing the state directly bypass the driver's custom destructor?
Calling kfree(s) directly in spacemit_plane_reset() skips
spacemit_plane_atomic_destroy_state(), which means the DMA coherent buffers
for mmu_tbl.va and cl.va could be permanently leaked when a plane is reset.
[ ... ]
> @@ -211,6 +211,5 @@ 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;
[Severity: Medium]
Will this crash if plane->state is NULL? The to_spacemit_plane_state()
macro uses container_of, which will produce a negative pointer if
plane->state is NULL. Dereferencing old_state->rdma_id later would trigger
a fault.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=10
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:04 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: neil.armstrong, conor+dt, devicetree, robh, linux-phy, olteanv,
dri-devel, vkoul
Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [High] Custom EDID read function bypasses DRM core and breaks I2C repeated-start (MOT) rules, corrupting EDID reads for blocks > 0 or on active adapters.
- [High] The hardirq handler returns `IRQ_NONE` after clearing hardware interrupt flags, permanently disabling the IRQ line.
- [High] Late attachment of the eDP panel bridge violates drm_bridge_connector architecture and breaks panel support if the panel probes asynchronously.
- [High] DP Transfer Unit (TU) miscalculation due to pixel clock truncation.
- [High] The IRQ handler returns `IRQ_NONE` without clearing the hardware interrupt when `drm_dev_enter()` fails, leading to an interrupt storm.
- [High] `spacemit_dp_bridge_atomic_create_state` returns an error pointer instead of `NULL` on allocation failure, causing a kernel crash in the DRM core.
- [High] The hardirq handler calls regmap functions which use a sleepable lock on PREEMPT_RT.
- [Medium] The atomic_check and mode_valid callbacks access DPCD link capabilities without holding mode_lock, leading to data races with the hotplug IRQ handler.
- [Medium] The DP IRQ handler silently drops hotplug notifications because drm_bridge_hpd_notify() is a no-op when DRM_BRIDGE_OP_HPD is omitted.
- [Medium] Calling clk_set_parent() on an already enabled clock.
- [Medium] Resetting the AUX controller without holding aux->hw_mutex races with concurrent AUX transfers.
--
--- Patch [12]: [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver ---
commit b7eace372254ab27e120213d38c3a656b2a195b8
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.
> 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
> @@ -248,16 +248,10 @@
> 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 */
> };
[Severity: High]
Is it safe to omit use_raw_spinlock from the regmap configuration? Because
this is not set to true, the regmap defaults to a sleepable lock on
PREEMPT_RT. This causes a BUG when regmap read/write functions are called
from the hardirq handler spacemit_dp_irq_handler().
> [ ... ]
> @@ -661,16 +661,10 @@
> 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));
[Severity: Medium]
Could this reset corrupt concurrent AUX transfers? Modifying the AUX
hardware in spacemit_dp_link_disable() without holding the aux->hw_mutex
could race against a userspace or deferred transfer, potentially wedging
the controller.
> [ ... ]
> @@ -936,15 +936,10 @@
> 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;
[Severity: High]
Can this truncation of the pixel clock to an integer MHz value cause DP
FIFO underflows or overflows? For non-integer MHz pixel clocks like
148.5 MHz, this truncates to 148, introducing precision errors in the
Transfer Unit (TU) calculations which results in corrupted video.
> [ ... ]
> @@ -1286,13 +1286,10 @@
> 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);
[Severity: High]
Does this custom EDID read function bypass the Middle-Of-Transaction (MOT)
rules for I2C repeated-start? Issuing writes and reads without the MOT flag
resets the DDC segment pointer to 0, breaking EDID reads for displays with
multiple EDID blocks (block >= 1) or on active adapters.
> [ ... ]
> @@ -1632,15 +1632,10 @@
> 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) {
[Severity: Medium]
Could reading the DPCD link capabilities here race with the hotplug IRQ
handler? Accessing the dp->link capabilities in
spacemit_dp_bridge_atomic_check() without holding mode_lock might observe
intermediate or torn states if a concurrent interrupt triggers a capability
re-read, leading to transient rejections of valid modes.
> [ ... ]
> @@ -1705,10 +1705,10 @@
> 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);
[Severity: High]
Should this return NULL on allocation failure instead of an error pointer?
The DRM core's reset helper assigns the returned state directly without
checking for errors. Returning an error pointer here causes a fatal page
fault when the DRM core later dereferences it.
> [ ... ]
> @@ -1742,16 +1742,10 @@
> 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);
[Severity: High]
Does dynamically attaching the panel bridge late bypass connector
initialization? If the eDP panel defers its probe,
drm_bridge_connector_init() builds the connector without the panel bridge.
Attaching it here later violates the drm_bridge_connector architecture and
breaks panel support, because the connector will lack the panel's mode or
EDID operations.
> [ ... ]
> @@ -1782,10 +1782,10 @@
> 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;
[Severity: High]
Will returning IRQ_NONE here without clearing the hardware interrupt cause
an interrupt storm? If an interrupt fires while the DRM device is
unregistered (such as during module unload), the handler exits without
acknowledging the hardware interrupt, causing it to continuously fire and
stall the CPU.
> [ ... ]
> @@ -1796,11 +1796,10 @@
> /*
> * 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: High]
Is it safe to return IRQ_NONE after clearing the hardware interrupt flags?
If the logical state has not changed (e.g. from a transient hardware
glitch), returning IRQ_NONE causes the kernel's spurious interrupt detector
to eventually disable the IRQ line, which results in all future hotplug
events being permanently missed.
> [ ... ]
> @@ -1813,11 +1813,10 @@
> 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);
[Severity: Medium]
Will this hotplug notification be silently dropped? Since
DRM_BRIDGE_OP_HPD is not set for the DP bridge during inno_dp_probe(), the
bridge->hpd_cb is NULL, making drm_bridge_hpd_notify() a no-op and forcing
the DRM core to rely entirely on periodic polling.
> [ ... ]
> @@ -2338,10 +2338,15 @@
> static int inno_dp_probe(struct platform_device *pdev)
> {
> struct device *dev = &pdev->dev;
> [ ... ]
> if (dp->pxclk) {
> ret = clk_prepare_enable(dp->pxclk);
> if (ret) {
> dev_err(dev, "failed to enable pxclk: %d\n", ret);
> goto err_reset;
> }
> }
> [ ... ]
> if (dp->pxclk) {
> ret = clk_set_parent(dp->pxclk, dp->pll_clk);
[Severity: Medium]
Can calling clk_set_parent() on an already enabled clock cause hardware
glitches? The pxclk is prepared and enabled earlier in inno_dp_probe(), and
reparenting a running clock without hardware-specific glitch-free muxing
can lock up downstream logic.
> [ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=12
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver
@ 2026-07-25 5:04 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: neil.armstrong, conor+dt, devicetree, robh, linux-phy, olteanv,
dri-devel, vkoul
Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [High] Custom EDID read function bypasses DRM core and breaks I2C repeated-start (MOT) rules, corrupting EDID reads for blocks > 0 or on active adapters.
- [High] The hardirq handler returns `IRQ_NONE` after clearing hardware interrupt flags, permanently disabling the IRQ line.
- [High] Late attachment of the eDP panel bridge violates drm_bridge_connector architecture and breaks panel support if the panel probes asynchronously.
- [High] DP Transfer Unit (TU) miscalculation due to pixel clock truncation.
- [High] The IRQ handler returns `IRQ_NONE` without clearing the hardware interrupt when `drm_dev_enter()` fails, leading to an interrupt storm.
- [High] `spacemit_dp_bridge_atomic_create_state` returns an error pointer instead of `NULL` on allocation failure, causing a kernel crash in the DRM core.
- [High] The hardirq handler calls regmap functions which use a sleepable lock on PREEMPT_RT.
- [Medium] The atomic_check and mode_valid callbacks access DPCD link capabilities without holding mode_lock, leading to data races with the hotplug IRQ handler.
- [Medium] The DP IRQ handler silently drops hotplug notifications because drm_bridge_hpd_notify() is a no-op when DRM_BRIDGE_OP_HPD is omitted.
- [Medium] Calling clk_set_parent() on an already enabled clock.
- [Medium] Resetting the AUX controller without holding aux->hw_mutex races with concurrent AUX transfers.
--
--- Patch [12]: [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver ---
commit b7eace372254ab27e120213d38c3a656b2a195b8
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.
> 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
> @@ -248,16 +248,10 @@
> 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 */
> };
[Severity: High]
Is it safe to omit use_raw_spinlock from the regmap configuration? Because
this is not set to true, the regmap defaults to a sleepable lock on
PREEMPT_RT. This causes a BUG when regmap read/write functions are called
from the hardirq handler spacemit_dp_irq_handler().
> [ ... ]
> @@ -661,16 +661,10 @@
> 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));
[Severity: Medium]
Could this reset corrupt concurrent AUX transfers? Modifying the AUX
hardware in spacemit_dp_link_disable() without holding the aux->hw_mutex
could race against a userspace or deferred transfer, potentially wedging
the controller.
> [ ... ]
> @@ -936,15 +936,10 @@
> 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;
[Severity: High]
Can this truncation of the pixel clock to an integer MHz value cause DP
FIFO underflows or overflows? For non-integer MHz pixel clocks like
148.5 MHz, this truncates to 148, introducing precision errors in the
Transfer Unit (TU) calculations which results in corrupted video.
> [ ... ]
> @@ -1286,13 +1286,10 @@
> 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);
[Severity: High]
Does this custom EDID read function bypass the Middle-Of-Transaction (MOT)
rules for I2C repeated-start? Issuing writes and reads without the MOT flag
resets the DDC segment pointer to 0, breaking EDID reads for displays with
multiple EDID blocks (block >= 1) or on active adapters.
> [ ... ]
> @@ -1632,15 +1632,10 @@
> 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) {
[Severity: Medium]
Could reading the DPCD link capabilities here race with the hotplug IRQ
handler? Accessing the dp->link capabilities in
spacemit_dp_bridge_atomic_check() without holding mode_lock might observe
intermediate or torn states if a concurrent interrupt triggers a capability
re-read, leading to transient rejections of valid modes.
> [ ... ]
> @@ -1705,10 +1705,10 @@
> 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);
[Severity: High]
Should this return NULL on allocation failure instead of an error pointer?
The DRM core's reset helper assigns the returned state directly without
checking for errors. Returning an error pointer here causes a fatal page
fault when the DRM core later dereferences it.
> [ ... ]
> @@ -1742,16 +1742,10 @@
> 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);
[Severity: High]
Does dynamically attaching the panel bridge late bypass connector
initialization? If the eDP panel defers its probe,
drm_bridge_connector_init() builds the connector without the panel bridge.
Attaching it here later violates the drm_bridge_connector architecture and
breaks panel support, because the connector will lack the panel's mode or
EDID operations.
> [ ... ]
> @@ -1782,10 +1782,10 @@
> 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;
[Severity: High]
Will returning IRQ_NONE here without clearing the hardware interrupt cause
an interrupt storm? If an interrupt fires while the DRM device is
unregistered (such as during module unload), the handler exits without
acknowledging the hardware interrupt, causing it to continuously fire and
stall the CPU.
> [ ... ]
> @@ -1796,11 +1796,10 @@
> /*
> * 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: High]
Is it safe to return IRQ_NONE after clearing the hardware interrupt flags?
If the logical state has not changed (e.g. from a transient hardware
glitch), returning IRQ_NONE causes the kernel's spurious interrupt detector
to eventually disable the IRQ line, which results in all future hotplug
events being permanently missed.
> [ ... ]
> @@ -1813,11 +1813,10 @@
> 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);
[Severity: Medium]
Will this hotplug notification be silently dropped? Since
DRM_BRIDGE_OP_HPD is not set for the DP bridge during inno_dp_probe(), the
bridge->hpd_cb is NULL, making drm_bridge_hpd_notify() a no-op and forcing
the DRM core to rely entirely on periodic polling.
> [ ... ]
> @@ -2338,10 +2338,15 @@
> static int inno_dp_probe(struct platform_device *pdev)
> {
> struct device *dev = &pdev->dev;
> [ ... ]
> if (dp->pxclk) {
> ret = clk_prepare_enable(dp->pxclk);
> if (ret) {
> dev_err(dev, "failed to enable pxclk: %d\n", ret);
> goto err_reset;
> }
> }
> [ ... ]
> if (dp->pxclk) {
> ret = clk_set_parent(dp->pxclk, dp->pll_clk);
[Severity: Medium]
Can calling clk_set_parent() on an already enabled clock cause hardware
glitches? The pxclk is prepared and enabled earlier in inno_dp_probe(), and
reparenting a running clock without hardware-specific glitch-free muxing
can lock up downstream logic.
> [ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=12
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:04 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: conor+dt, linux-phy, vkoul, neil.armstrong, dri-devel, devicetree,
robh, olteanv
Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] DMMU page table size calculation underflow allows arbitrary physical memory access.
- [Critical] Shallow copy of DMA buffers in atomic state causes active display corruption.
- [High] crtc_cmdlist_atomic_commit() passes a guaranteed NULL pointer to the hardware command list execution callback.
- [High] Global `cmdlist_groups` array causes data races across concurrent atomic commits.
- [High] Unconditional dereference of `first_cl` in `cmdlist_atomic_commit()` causes a kernel panic if the command list group is empty.
- [High] DMMU maps only the first GEM object, breaking multi-planar framebuffers.
- [High] NULL pointer dereference in `dpu_plane_info` tracepoint.
- [High] Sleeping in atomic context via `drm_gem_shmem_get_pages_sgt()`.
- [High] Missing bounds check on `nod_len` causes DMA buffer overflow.
- [Medium] Unaligned base virtual addresses are used for DMMU translation regions.
- [Medium] Memory leak of DMA buffers upon atomic state destruction.
- [Medium] Speculative array out-of-bounds access in `cmdlist_sort_by_group()`.
--
--- Patch [8]: [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU ---
commit dc983d68951c3577447bb897e17c833e557da908
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
This commit introduces the shared driver types, MMIO accessors, trace
events, command-list builder, and DMMU table code for the Saturn DPU.
It enables batched register programming by building per-module register
lists in memory, which the hardware command-list engine replays.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_trace.h b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
> index 0000000000000..abfe34a67a4bd 100644
> --- /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: High]
Can this dereference a NULL pointer? When a plane is disabled during an
atomic commit, state->fb might be NULL. Unconditionally reading fb->width
here could cause a kernel panic if tracing is enabled.
> + __entry->blend_mode = state->pixel_blend_mode;
> + __entry->alpha = alpha;
> + __entry->zpos = state->zpos;
> + __entry->rotation = rotation;
> + ),
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.c b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
> index 0000000000000..787d38e9e135f 100644
> --- /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)
> +{
[ ... ]
> + 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++;
[Severity: High]
Is there a risk of overflowing the DMA command list buffer here? The loop
increments cl->nod_len and writes to memory, but there doesn't seem to be
a bounds check to ensure the packed rows do not exceed the allocated
capacity in cl->size.
> + } 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;
[Severity: Medium]
Could rdma_id exceed the bounds of the array? It is used directly as an
index for the rdmas array without checking if it is within hwdev->rdma_nums.
> + if (priv->cmdlist_groups[rdma_id]) {
[Severity: High]
Does modifying the global priv->cmdlist_groups array here cause data races?
Since this array is stored in the shared spacemit_drm_private structure,
concurrent atomic commits on multiple CRTCs might overwrite each other's
command lists without locking.
> + 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)
> +{
[ ... ]
> + 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? If cur_rdmas[i].in_use is true but
priv->cmdlist_groups[i] is empty, first_cl would be NULL. This can happen
if planes are removed from the state but in_use was copied as true from the
old state. The cl_to_spacemit_pstate macro would yield a negative offset
from NULL, resulting in a crash.
> + 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 pass a guaranteed NULL pointer to the hardware callback?
The while loop exhausts cur_cl to NULL, which is then immediately passed
to hwdev->crtc_cmdlist. It looks like the head of the list should be
passed instead.
> +}
> +
> +/* 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;
[Severity: Critical]
Does returning early when cl->va is present cause active display corruption?
Because struct cmdlist is embedded directly in the plane state, DRM's state
duplication will shallow-copy cl->va. If atomic_update mutates this same
virtual address while the hardware is actively reading it from the old
state, it could lead to screen tearing or DPU hangs. DMA buffers for
command lists generally need to be multi-buffered or allocated per-state.
> +
> + cl->size = PER_CMDLIST_SIZE;
> + cl->va = dma_alloc_coherent(dev, cl->size, &cl->pa,
> + GFP_KERNEL | __GFP_ZERO);
> + if (!cl->va)
> + return -ENOMEM;
[Severity: Medium]
Is there a corresponding dma_free_coherent() during state destruction?
If the allocated cl->va is never freed when the atomic state is destroyed,
this might cause a steady leak of DMA memory.
> +
> + return 0;
> +}
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.c b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
> index 0000000000000..2ae30dd1a4f23 100644
> --- /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]));
[Severity: High]
Will this sleep in atomic context? This function appears to be called
during hardware programming in the atomic commit phase. Calling
drm_gem_shmem_get_pages_sgt allocates memory and can acquire a mutex,
which is forbidden in atomic context. Buffer mapping typically needs to be
deferred to the sleepable prepare_fb callback.
[Severity: High]
Does mapping only fb->obj[0] break multi-planar framebuffers? If userspace
provides a multi-planar framebuffer where the planes reside in different GEM
objects, extracting the SG table only for obj[0] will result in incorrect
physical memory pages for the secondary planes.
> + 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;
[Severity: Medium]
Could adding offset2 directly to TBU_BASE_VA result in unaligned base
virtual addresses? If userspace supplies an offset that is not a multiple
of PAGE_SIZE, programming an unaligned virtual address might confuse the
hardware MMU.
> + tbu.ttb_size[2] = PAGE_ALIGN(total_size - rounddown(offset2, PAGE_SIZE))
> + >> PAGE_SHIFT;
[Severity: Critical]
Can this size calculation underflow? If a malicious userspace client
provides offset2 smaller than offset1, the unsigned subtraction
offset2 - rounddown(offset1, PAGE_SIZE) will underflow to a massive
positive value. This could program the DMMU with a huge translation block,
allowing arbitrary out-of-bounds physical memory reads.
> + 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;
> + }
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=8
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
@ 2026-07-25 5:04 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:04 UTC (permalink / raw)
To: Cody Kang
Cc: conor+dt, linux-phy, vkoul, neil.armstrong, dri-devel, devicetree,
robh, olteanv
Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [Critical] DMMU page table size calculation underflow allows arbitrary physical memory access.
- [Critical] Shallow copy of DMA buffers in atomic state causes active display corruption.
- [High] crtc_cmdlist_atomic_commit() passes a guaranteed NULL pointer to the hardware command list execution callback.
- [High] Global `cmdlist_groups` array causes data races across concurrent atomic commits.
- [High] Unconditional dereference of `first_cl` in `cmdlist_atomic_commit()` causes a kernel panic if the command list group is empty.
- [High] DMMU maps only the first GEM object, breaking multi-planar framebuffers.
- [High] NULL pointer dereference in `dpu_plane_info` tracepoint.
- [High] Sleeping in atomic context via `drm_gem_shmem_get_pages_sgt()`.
- [High] Missing bounds check on `nod_len` causes DMA buffer overflow.
- [Medium] Unaligned base virtual addresses are used for DMMU translation regions.
- [Medium] Memory leak of DMA buffers upon atomic state destruction.
- [Medium] Speculative array out-of-bounds access in `cmdlist_sort_by_group()`.
--
--- Patch [8]: [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU ---
commit dc983d68951c3577447bb897e17c833e557da908
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
This commit introduces the shared driver types, MMIO accessors, trace
events, command-list builder, and DMMU table code for the Saturn DPU.
It enables batched register programming by building per-module register
lists in memory, which the hardware command-list engine replays.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_trace.h b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
> index 0000000000000..abfe34a67a4bd 100644
> --- /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: High]
Can this dereference a NULL pointer? When a plane is disabled during an
atomic commit, state->fb might be NULL. Unconditionally reading fb->width
here could cause a kernel panic if tracing is enabled.
> + __entry->blend_mode = state->pixel_blend_mode;
> + __entry->alpha = alpha;
> + __entry->zpos = state->zpos;
> + __entry->rotation = rotation;
> + ),
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.c b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
> index 0000000000000..787d38e9e135f 100644
> --- /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)
> +{
[ ... ]
> + 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++;
[Severity: High]
Is there a risk of overflowing the DMA command list buffer here? The loop
increments cl->nod_len and writes to memory, but there doesn't seem to be
a bounds check to ensure the packed rows do not exceed the allocated
capacity in cl->size.
> + } 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;
[Severity: Medium]
Could rdma_id exceed the bounds of the array? It is used directly as an
index for the rdmas array without checking if it is within hwdev->rdma_nums.
> + if (priv->cmdlist_groups[rdma_id]) {
[Severity: High]
Does modifying the global priv->cmdlist_groups array here cause data races?
Since this array is stored in the shared spacemit_drm_private structure,
concurrent atomic commits on multiple CRTCs might overwrite each other's
command lists without locking.
> + 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)
> +{
[ ... ]
> + 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? If cur_rdmas[i].in_use is true but
priv->cmdlist_groups[i] is empty, first_cl would be NULL. This can happen
if planes are removed from the state but in_use was copied as true from the
old state. The cl_to_spacemit_pstate macro would yield a negative offset
from NULL, resulting in a crash.
> + 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 pass a guaranteed NULL pointer to the hardware callback?
The while loop exhausts cur_cl to NULL, which is then immediately passed
to hwdev->crtc_cmdlist. It looks like the head of the list should be
passed instead.
> +}
> +
> +/* 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;
[Severity: Critical]
Does returning early when cl->va is present cause active display corruption?
Because struct cmdlist is embedded directly in the plane state, DRM's state
duplication will shallow-copy cl->va. If atomic_update mutates this same
virtual address while the hardware is actively reading it from the old
state, it could lead to screen tearing or DPU hangs. DMA buffers for
command lists generally need to be multi-buffered or allocated per-state.
> +
> + cl->size = PER_CMDLIST_SIZE;
> + cl->va = dma_alloc_coherent(dev, cl->size, &cl->pa,
> + GFP_KERNEL | __GFP_ZERO);
> + if (!cl->va)
> + return -ENOMEM;
[Severity: Medium]
Is there a corresponding dma_free_coherent() during state destruction?
If the allocated cl->va is never freed when the atomic state is destroyed,
this might cause a steady leak of DMA memory.
> +
> + return 0;
> +}
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.c b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
> index 0000000000000..2ae30dd1a4f23 100644
> --- /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]));
[Severity: High]
Will this sleep in atomic context? This function appears to be called
during hardware programming in the atomic commit phase. Calling
drm_gem_shmem_get_pages_sgt allocates memory and can acquire a mutex,
which is forbidden in atomic context. Buffer mapping typically needs to be
deferred to the sleepable prepare_fb callback.
[Severity: High]
Does mapping only fb->obj[0] break multi-planar framebuffers? If userspace
provides a multi-planar framebuffer where the planes reside in different GEM
objects, extracting the SG table only for obj[0] will result in incorrect
physical memory pages for the secondary planes.
> + 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;
[Severity: Medium]
Could adding offset2 directly to TBU_BASE_VA result in unaligned base
virtual addresses? If userspace supplies an offset that is not a multiple
of PAGE_SIZE, programming an unaligned virtual address might confuse the
hardware MMU.
> + tbu.ttb_size[2] = PAGE_ALIGN(total_size - rounddown(offset2, PAGE_SIZE))
> + >> PAGE_SHIFT;
[Severity: Critical]
Can this size calculation underflow? If a malicious userspace client
provides offset2 smaller than offset1, the unsigned subtraction
offset2 - rounddown(offset1, PAGE_SIZE) will underflow to a massive
positive value. This could program the DMMU with a huge translation block,
allowing arbitrary out-of-bounds physical memory reads.
> + 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;
> + }
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=8
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:05 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:05 UTC (permalink / raw)
To: Cody Kang
Cc: robh, linux-phy, devicetree, dri-devel, neil.armstrong, olteanv,
vkoul, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Stalling the atomic commit pipeline by calling wait_for_flip_done() before commit_hw_done()
- [High] Sleeping functions (usleep_range) called from atomic context in CRTC enable/disable
- [High] Sleeping function called from atomic context in plane update due to missing prepare_fb
--
--- Patch [11]: [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver ---
commit db43bd6ae677a859cea76ccf2390ca3d109d0fe0
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU DRM device driver
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.
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_drm.c b/drivers/gpu/drm/spacemit/spacemit_drm.c
> new file mode 100644
> index 0000000000000..9fb6a7661b484
> --- /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: High]
Does calling drm_atomic_helper_wait_for_flip_done() before
drm_atomic_helper_commit_hw_done() stall the commit pipeline?
drm_atomic_helper_commit_hw_done() signals that the driver has finished
programming the hardware, which unblocks subsequent non-blocking atomic commits.
Waiting for the VBLANK page flip to complete before signaling hw_done blocks
all subsequent commits, artificially serializing the commit pipeline and
defeating the non-blocking design of atomic KMS.
Could this lead to UI stutter and performance regressions?
> +
> + drm_atomic_helper_cleanup_planes(dev, old_state);
> +}
I also noticed a few additional regressions in files that appear to be part of
this driver series:
[Severity: High]
Are these usleep_range() calls safe in atomic context?
According to DRM subsystem guidelines, CRTC atomic enable/disable callbacks
execute in atomic context during non-blocking commits where sleeping is
forbidden.
Both spacemit_crtc_atomic_enable() and dpu_pm_suspend() (called from
spacemit_crtc_atomic_disable()) invoke usleep_range():
drivers/gpu/drm/spacemit/spacemit_crtc.c:spacemit_crtc_atomic_enable() {
...
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);
...
}
drivers/gpu/drm/spacemit/spacemit_crtc.c:dpu_pm_suspend() {
...
dpu_reset_assert(dev, "aclk", a_crtc->aclk_reset);
dpu_reset_assert(dev, "dsc", a_crtc->dsc_reset);
usleep_range(10000, 20000);
...
}
Calling sleeping functions in this context causes kernel warnings and system
instability.
Should non-sleeping delays like udelay() be used instead?
[Severity: High]
Does spacemit_plane_atomic_update() sleep in atomic context?
The DRM subsystem guidelines explicitly state that Plane atomic update callbacks
execute in atomic context where sleeping is forbidden.
spacemit_plane_atomic_update() calls spacemit_dmmu_map():
drivers/gpu/drm/spacemit/spacemit_planes.c:spacemit_plane_atomic_update() {
...
ret = spacemit_dmmu_map(plane->state->fb,
&spacemit_pstate->mmu_tbl, tbu_id,
a_crtc->cl_rdma, plane);
...
}
which then calls drm_gem_shmem_get_pages_sgt():
drivers/gpu/drm/spacemit/spacemit_dmmu.c:spacemit_dmmu_map() {
...
sgt = drm_gem_shmem_get_pages_sgt(to_drm_gem_shmem_obj(fb->obj[0]));
if (IS_ERR(sgt))
...
}
This helper acquires a sleeping mutex via dma_resv_lock_interruptible(). This
causes a sleep in atomic context, risking kernel panics.
Should the driver implement a prepare_fb hook (e.g.
drm_gem_shmem_plane_helper_prepare_fb) to pin the pages and obtain the SGT
outside of the atomic update phase?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=11
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver
@ 2026-07-25 5:05 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:05 UTC (permalink / raw)
To: Cody Kang
Cc: robh, linux-phy, devicetree, dri-devel, neil.armstrong, olteanv,
vkoul, conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Stalling the atomic commit pipeline by calling wait_for_flip_done() before commit_hw_done()
- [High] Sleeping functions (usleep_range) called from atomic context in CRTC enable/disable
- [High] Sleeping function called from atomic context in plane update due to missing prepare_fb
--
--- Patch [11]: [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver ---
commit db43bd6ae677a859cea76ccf2390ca3d109d0fe0
Author: Cody Kang <codykang.hk@gmail.com>
drm/spacemit: add Saturn DPU DRM device driver
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.
[ ... ]
> diff --git a/drivers/gpu/drm/spacemit/spacemit_drm.c b/drivers/gpu/drm/spacemit/spacemit_drm.c
> new file mode 100644
> index 0000000000000..9fb6a7661b484
> --- /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: High]
Does calling drm_atomic_helper_wait_for_flip_done() before
drm_atomic_helper_commit_hw_done() stall the commit pipeline?
drm_atomic_helper_commit_hw_done() signals that the driver has finished
programming the hardware, which unblocks subsequent non-blocking atomic commits.
Waiting for the VBLANK page flip to complete before signaling hw_done blocks
all subsequent commits, artificially serializing the commit pipeline and
defeating the non-blocking design of atomic KMS.
Could this lead to UI stutter and performance regressions?
> +
> + drm_atomic_helper_cleanup_planes(dev, old_state);
> +}
I also noticed a few additional regressions in files that appear to be part of
this driver series:
[Severity: High]
Are these usleep_range() calls safe in atomic context?
According to DRM subsystem guidelines, CRTC atomic enable/disable callbacks
execute in atomic context during non-blocking commits where sleeping is
forbidden.
Both spacemit_crtc_atomic_enable() and dpu_pm_suspend() (called from
spacemit_crtc_atomic_disable()) invoke usleep_range():
drivers/gpu/drm/spacemit/spacemit_crtc.c:spacemit_crtc_atomic_enable() {
...
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);
...
}
drivers/gpu/drm/spacemit/spacemit_crtc.c:dpu_pm_suspend() {
...
dpu_reset_assert(dev, "aclk", a_crtc->aclk_reset);
dpu_reset_assert(dev, "dsc", a_crtc->dsc_reset);
usleep_range(10000, 20000);
...
}
Calling sleeping functions in this context causes kernel warnings and system
instability.
Should non-sleeping delays like udelay() be used instead?
[Severity: High]
Does spacemit_plane_atomic_update() sleep in atomic context?
The DRM subsystem guidelines explicitly state that Plane atomic update callbacks
execute in atomic context where sleeping is forbidden.
spacemit_plane_atomic_update() calls spacemit_dmmu_map():
drivers/gpu/drm/spacemit/spacemit_planes.c:spacemit_plane_atomic_update() {
...
ret = spacemit_dmmu_map(plane->state->fb,
&spacemit_pstate->mmu_tbl, tbu_id,
a_crtc->cl_rdma, plane);
...
}
which then calls drm_gem_shmem_get_pages_sgt():
drivers/gpu/drm/spacemit/spacemit_dmmu.c:spacemit_dmmu_map() {
...
sgt = drm_gem_shmem_get_pages_sgt(to_drm_gem_shmem_obj(fb->obj[0]));
if (IS_ERR(sgt))
...
}
This helper acquires a sleeping mutex via dma_resv_lock_interruptible(). This
causes a sleep in atomic context, risking kernel panics.
Should the driver implement a prepare_fb hook (e.g.
drm_gem_shmem_plane_helper_prepare_fb) to pin the pages and obtain the SGT
outside of the atomic update phase?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=11
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:08 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:08 UTC (permalink / raw)
To: Cody Kang
Cc: robh, olteanv, linux-phy, conor+dt, vkoul, dri-devel,
neil.armstrong, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The DP controller hardware is not initialized before the eDP panel probe is triggered, causing synchronous AUX transfers to fail or hang.
- [High] The custom EDID read function violates the I2C EEPROM protocol by omitting the Middle-of-Transaction (MOT) bit, breaking EDID reads on sinks that require a repeated START.
--
--- Patch [14]: [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes ---
commit 9b9ee39195ad90a76f056f9bddbdabb8377b67ec
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
> @@ -0,0 +1,156 @@
[ ... ]
> +&soc {
[ ... ]
> + edp0: edp@cac84000 {
> + compatible = "spacemit,k3-inno-dp";
[Severity: High]
Does enabling this device node cause the eDP panel probe to fail?
Looking at inno_dp_probe() in drivers/gpu/drm/spacemit/spacemit_inno_dp.c, it
populates the eDP panel device on the AUX bus:
ret = devm_of_dp_aux_populate_bus(&dp->aux,
spacemit_dp_edp_link_panel);
The panel driver probes synchronously and attempts an AUX transfer to read the
EDID.
However, the DP controller's hardware registers (including the AUX clock
divider DP_AUX_CTRL_CLK_DIV and soft resets) are not initialized until later:
spacemit_inno_dp.c:spacemit_dp_bind() {
...
ret = spacemit_dp_dev_init(dp);
...
}
The uninitialized controller ignores the transfer or the AUX state machine
does not start, causing an unavoidable timeout.
> + reg = <0x0 0xcac84000 0x0 0x4000>;
[ ... ]
> + dp1: dp@cac88000 {
> + compatible = "spacemit,k3-inno-dp";
[Severity: High]
Will EDID reads fail for sinks connected to this node?
When reading EDID, the driver splits the transaction into a write and a read:
spacemit_inno_dp.c:spacemit_dp_conn_get_edid_block() {
...
ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
&edid_offset, 1);
...
}
It calls spacemit_dp_aux_i2c_write() which hardcodes the request:
spacemit_inno_dp.c:spacemit_dp_aux_i2c_write() {
...
.request = DP_AUX_I2C_WRITE,
...
}
The missing MOT (Middle-of-Transaction) bit guarantees an I2C STOP condition
after writing the offset address. This resets the internal address pointer of
many standard EEPROMs, causing the subsequent read to repeatedly fetch block 0
instead of the requested extension block.
> + reg = <0x0 0xcac88000 0x0 0x4000>;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=14
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes
@ 2026-07-25 5:08 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:08 UTC (permalink / raw)
To: Cody Kang
Cc: robh, olteanv, linux-phy, conor+dt, vkoul, dri-devel,
neil.armstrong, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The DP controller hardware is not initialized before the eDP panel probe is triggered, causing synchronous AUX transfers to fail or hang.
- [High] The custom EDID read function violates the I2C EEPROM protocol by omitting the Middle-of-Transaction (MOT) bit, breaking EDID reads on sinks that require a repeated START.
--
--- Patch [14]: [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes ---
commit 9b9ee39195ad90a76f056f9bddbdabb8377b67ec
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
> @@ -0,0 +1,156 @@
[ ... ]
> +&soc {
[ ... ]
> + edp0: edp@cac84000 {
> + compatible = "spacemit,k3-inno-dp";
[Severity: High]
Does enabling this device node cause the eDP panel probe to fail?
Looking at inno_dp_probe() in drivers/gpu/drm/spacemit/spacemit_inno_dp.c, it
populates the eDP panel device on the AUX bus:
ret = devm_of_dp_aux_populate_bus(&dp->aux,
spacemit_dp_edp_link_panel);
The panel driver probes synchronously and attempts an AUX transfer to read the
EDID.
However, the DP controller's hardware registers (including the AUX clock
divider DP_AUX_CTRL_CLK_DIV and soft resets) are not initialized until later:
spacemit_inno_dp.c:spacemit_dp_bind() {
...
ret = spacemit_dp_dev_init(dp);
...
}
The uninitialized controller ignores the transfer or the AUX state machine
does not start, causing an unavoidable timeout.
> + reg = <0x0 0xcac84000 0x0 0x4000>;
[ ... ]
> + dp1: dp@cac88000 {
> + compatible = "spacemit,k3-inno-dp";
[Severity: High]
Will EDID reads fail for sinks connected to this node?
When reading EDID, the driver splits the transaction into a write and a read:
spacemit_inno_dp.c:spacemit_dp_conn_get_edid_block() {
...
ret = spacemit_dp_aux_i2c_write(dp, DDC_ADDR,
&edid_offset, 1);
...
}
It calls spacemit_dp_aux_i2c_write() which hardcodes the request:
spacemit_inno_dp.c:spacemit_dp_aux_i2c_write() {
...
.request = DP_AUX_I2C_WRITE,
...
}
The missing MOT (Middle-of-Transaction) bit guarantees an I2C STOP condition
after writing the offset address. This resets the internal address pointer of
many standard EEPROMs, causing the subsequent read to repeatedly fetch block 0
instead of the requested extension block.
> + reg = <0x0 0xcac88000 0x0 0x4000>;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=14
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:12 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:12 UTC (permalink / raw)
To: Cody Kang
Cc: vkoul, dri-devel, devicetree, olteanv, neil.armstrong, linux-phy,
robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Precision Loss in DP Timing Configuration due to integer division
- [High] Protocol Violation in I2C-over-AUX EDID Read (Missing MOT flag)
--
--- Patch [15]: [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output ---
commit 8521347c30ea3dc057bb9d8b08c1de584bd6ba67
Author: Cody Kang <codykang.hk@gmail.com>
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
This commit enables the second Saturn DPU and the corresponding DisplayPort
controller on the Pico-ITX board. While this patch configures the devicetree
to route the DPU output, it exposes regressions in the newly added underlying
Innosilicon DP/eDP driver.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
[ ... ]
> @@ -982,19 +982,19 @@ 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 */
[Severity: High]
Will dividing by 1000 here truncate necessary precision?
Since mode->clock / 1000 discards the fractional part (for example, a 74.25
MHz clock becomes 74 MHz), this truncated value is used in calculating the
Transfer Unit size and horizontal blanking interval. This discrepancy can
cause the display controller and the DP transmitter to disagree on the
required bandwidth, resulting in the DP TX FIFO overflowing and corrupting
the display for common standard resolutions like 720p and 1080p.
[ ... ]
> @@ -1278,9 +1278,9 @@ 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,
[Severity: High]
Is it intentional to hardcode DP_AUX_I2C_WRITE here without allowing the
DP_AUX_I2C_MOT (Middle Of Transaction) flag to be appended?
[ ... ]
> @@ -1320,11 +1320,11 @@ static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
> 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;
> }
[Severity: High]
Can this segment write cause the hardware to generate an I2C STOP condition?
Because spacemit_dp_aux_i2c_write() lacks the MOT flag, writing the E-DDC
segment pointer without it directs the hardware to generate a STOP condition.
According to the E-DDC specification, a STOP condition immediately after
writing the segment pointer forces the display EEPROM to reset its segment
pointer to 0.
As a result, subsequent reads might always fetch EDID blocks 0 and 1, making
it impossible to correctly read EDID blocks >= 2 (such as CTA-861 extension
blocks) and potentially breaking support for advanced resolutions and audio
and HDR features.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=15
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
@ 2026-07-25 5:12 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:12 UTC (permalink / raw)
To: Cody Kang
Cc: vkoul, dri-devel, devicetree, olteanv, neil.armstrong, linux-phy,
robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Precision Loss in DP Timing Configuration due to integer division
- [High] Protocol Violation in I2C-over-AUX EDID Read (Missing MOT flag)
--
--- Patch [15]: [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output ---
commit 8521347c30ea3dc057bb9d8b08c1de584bd6ba67
Author: Cody Kang <codykang.hk@gmail.com>
riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output
This commit enables the second Saturn DPU and the corresponding DisplayPort
controller on the Pico-ITX board. While this patch configures the devicetree
to route the DPU output, it exposes regressions in the newly added underlying
Innosilicon DP/eDP driver.
> diff --git a/drivers/gpu/drm/spacemit/spacemit_inno_dp.c b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/spacemit_inno_dp.c
[ ... ]
> @@ -982,19 +982,19 @@ 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 */
[Severity: High]
Will dividing by 1000 here truncate necessary precision?
Since mode->clock / 1000 discards the fractional part (for example, a 74.25
MHz clock becomes 74 MHz), this truncated value is used in calculating the
Transfer Unit size and horizontal blanking interval. This discrepancy can
cause the display controller and the DP transmitter to disagree on the
required bandwidth, resulting in the DP TX FIFO overflowing and corrupting
the display for common standard resolutions like 720p and 1080p.
[ ... ]
> @@ -1278,9 +1278,9 @@ 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,
[Severity: High]
Is it intentional to hardcode DP_AUX_I2C_WRITE here without allowing the
DP_AUX_I2C_MOT (Middle Of Transaction) flag to be appended?
[ ... ]
> @@ -1320,11 +1320,11 @@ static int spacemit_dp_conn_get_edid_block(void *data, u8 *buf,
> 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;
> }
[Severity: High]
Can this segment write cause the hardware to generate an I2C STOP condition?
Because spacemit_dp_aux_i2c_write() lacks the MOT flag, writing the E-DDC
segment pointer without it directs the hardware to generate a STOP condition.
According to the E-DDC specification, a STOP condition immediately after
writing the segment pointer forces the display EEPROM to reset its segment
pointer to 0.
As a result, subsequent reads might always fetch EDID blocks 0 and 1, making
it impossible to correctly read EDID blocks >= 2 (such as CTA-861 extension
blocks) and potentially breaking support for advanced resolutions and audio
and HDR features.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=15
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
2026-07-25 4:51 ` Cody Kang via B4 Relay
@ 2026-07-25 5:12 ` sashiko-bot
-1 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:12 UTC (permalink / raw)
To: Cody Kang
Cc: olteanv, devicetree, robh, conor+dt, neil.armstrong, vkoul,
dri-devel, linux-phy
Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [High] Sleeping memory allocations (`GFP_KERNEL`) in atomic context plane and CRTC update paths, with a silent fallback that breaks atomic update semantics.
- [High] Sleeping delay function (`usleep_range`) used in CRTC atomic enable context.
- [High] Unconditional dereference of `state->fb` for disabled or solid-fill planes.
- [High] Race condition between the configuration timer handler and the VSYNC ISR leads to a double `drm_crtc_vblank_put` and refcount underflow.
- [High] The `cfg_rdy_timer` is not synchronously canceled when the DPU hardware is uninitialized or disabled.
- [High] `row_eof_tag` and un-strobed `module_regs` are left uninitialized in `saturn_hee_cmdlist_fill_data_row()`.
- [High] Accessing `crtc->state->event` from the VBLANK interrupt handler races with concurrent non-blocking atomic commits, causing leaked and premature events.
- [High] Command list length counter desynchronization leads to missing EOF tags and display hangs.
- [High] RDMA hardware configuration is written to a shared staging buffer but is never packed into the DMA command list, causing updates to be dropped or overwritten.
- [Medium] `dpu_run()` ignores the return value of `drm_crtc_vblank_get()`, potentially leading to refcount underflow.
- [Medium] Using C bitfields for DMA-shared hardware descriptors (`struct cmdlist_header` and `struct cmdlist_row`) leads to compiler-dependent memory layout and breaks on different endiannesses.
--
--- Patch [9]: [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend ---
commit 51bff8176b75d13f3aa75c39d52083ed25e121c0
Author: Cody Kang <codykang.hk@gmail.com>
Date: Sat Jul 25 04:51:18 2026 +0000
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.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
> index 8d5f13bc5373c.....
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
[ ... ]
> +void spacemit_cfg_rdy_timer_handler(struct timer_list *t)
> +{
[ ... ]
> + spin_lock_irqsave(&drm->event_lock, flags);
> + event = crtc->state->event;
[Severity: High]
Does this race with concurrent non-blocking atomic commits?
The DRM core's atomic helpers synchronously swap the crtc->state pointer
during the commit phase before the hardware's VBLANK interrupt fires. If two
non-blocking commits are issued back-to-back, the second commit swaps
crtc->state to its new state.
When the first commit's VBLANK fires (or this timer handler executes), reading
crtc->state->event prematurely consumes the second commit's event, leaking
the first commit's event. Should the event be stored in a driver-private CRTC
structure instead?
> + 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 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);
[Severity: High]
Is it safe to use a sleeping delay function here?
dpu_init() is hooked to the .init callback of dpu_core_ops, which
executes during the CRTC atomic enable phase. The DRM subsystem guidelines
explicitly state that CRTC atomic enable callbacks run in atomic context
where sleeping is strictly forbidden.
Will this trigger a "scheduling while atomic" panic?
> + 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);
> +}
[Severity: High]
Does this uninitialization path need to cancel the configuration timer?
saturn_ctrl_cfg_ready() arms a_crtc->cfg_rdy_timer when the DPU is started.
If the display pipeline is disabled before the hardware raises the CFG_RDY
interrupt, this timer will remain active and could fire asynchronously after
shutdown.
Can this lead to spurious timeout errors, incorrect vblank reference decrements,
or a use-after-free if the CRTC object is destroyed before the timer is
canceled?
> +static inline void dpu_isr_vblank(struct spacemit_crtc *a_crtc, bool *flip)
> +{
[ ... ]
> + spin_lock(&drm->event_lock);
> + event = crtc->state->event;
[Severity: High]
Can this read the wrong event pointer?
Similar to the timer handler above, accessing crtc->state->event races with
concurrent non-blocking atomic commits that swap the crtc->state pointer.
If a new commit is queued, this reads the new state's event and leaks the
current flip's event.
> + if (event) {
[ ... ]
> +static u32 dpu_online_isr(struct spacemit_crtc *a_crtc)
> +{
[ ... ]
> + 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;
[Severity: High]
Does using the non-blocking timer_delete() return value create a race
condition?
If timer_delete() returns 0 (e.g., because the callback is actively executing
on another CPU), it leaves a_crtc->flip_done unmodified.
The ISR then processes INT_VSYNC and calls dpu_isr_vblank(), which reads
flip_done == false, sets it to true, and calls drm_crtc_vblank_put().
Concurrently, the timer handler runs, sets flip_done = true, and also
unconditionally calls drm_crtc_vblank_put().
Could this lead to a double vblank put and refcount underflow?
> + a_crtc->ur_reported = false;
[ ... ]
> +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);
[Severity: Medium]
Should the return value of drm_crtc_vblank_get() be checked?
drm_crtc_vblank_get() can fail (e.g., if vblank interrupts are uninitialized
or disabled), returning a negative error code without incrementing the vblank
reference count.
By ignoring this return value, this function proceeds as if the reference was
successfully acquired. When the hardware interrupt or timeout later triggers,
they unconditionally call drm_crtc_vblank_put(), which could cause a refcount
underflow.
> + if (hwdev->enable_cfg_irq)
> + hwdev->enable_cfg_irq(a_crtc, hwdev, true);
> +
> + saturn_ctrl_cfg_ready(a_crtc, true);
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
> index c23e4ca36d26b.....
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
[ ... ]
> +void saturn_hee_plane_update_hw_channel(struct drm_plane *plane)
> +{
[ ... ]
> + /* 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]
Are these RDMA hardware configuration writes dropped because they are never
packed into the DMA command list?
This uses a_crtc->cl_rdma to stage hardware writes for the RDMA block.
However, there is no corresponding call to cmdlist_regs_packing() for
cl_rdma to commit these writes to the DMA-backed command list.
Since a_crtc->cl_rdma is a single shared buffer, do subsequent plane updates
overwrite the staged settings of previous planes, silently corrupting the
configuration and failing to program the hardware?
> + dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_cmpsr_id,
[ ... ]
> + 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 &&
[Severity: High]
Will this unconditionally dereference a NULL pointer for solid-fill planes?
During DRM atomic commits, a plane's state->fb is frequently NULL (e.g., when
a plane is being disabled, or for solid color fill planes).
While the earlier code handles solid fills by skipping the RDMA block
(if (!solid_en)), this block evaluates state->fb->format unconditionally,
which will cause a kernel panic if state->fb is NULL.
> + 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]
Can this trigger a "scheduling while atomic" panic or silent fallback
failures?
alloc_cmdlist_regs() is a macro that uses GFP_KERNEL allocations
(kzalloc(sizeof(*cl), GFP_KERNEL)). This plane update callback runs in atomic
context where sleeping is forbidden.
Additionally, if the allocation fails and returns NULL, the dpu_write() macro
silently falls back to immediate MMIO writes (dpu_write_reg_word). This
bypasses the command list's VBLANK synchronization, applying partial hardware
state immediately and causing visual tearing or lockups.
> +
> + base = CMP_BASE_ADDR[a_crtc->dev_id];
[ ... ]
> +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];
> +};
[Severity: Medium]
Does using C bitfields for DMA-shared hardware descriptors break on different
compilers or endiannesses?
These structures format command list nodes directly into a DMA buffer consumed
by the hardware. The C standard leaves bitfield packing order and alignment
entirely implementation-defined.
Mixing u64 and u32 bitfields makes the exact memory layout highly sensitive to
compiler ABIs and versions, and it will not work correctly across different
endiannesses. Could you use explicit bitwise operations with fixed-width types
instead?
> +
> +/* DMA stream layout consumed by the cmdlist engine; sizes are load-bearing. */
[ ... ]
> +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++)
[Severity: High]
Are row_eof_tag and un-strobed module_regs left uninitialized here?
This overlays a struct cmdlist_row onto the cl->va DMA buffer without
explicitly clearing or setting row->row_eof_tag. If the buffer is reused
across frames and the current frame is shorter than the previous one,
row_eof_tag could contain a stale 1 from the end of a previous commit's
list.
If the hardware sees this uninitialized bitfield as 1, it will terminate
command processing prematurely, ignoring all subsequent commands in the current
frame and causing display hangs.
> + if (strobe & CMDLIST_REG_STROBE(i))
[ ... ]
> +void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
> + u8 dev_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;
[Severity: High]
Does a desynchronized cl->nod_len cause the EOF tag to be dropped?
The calling code in cmdlist_regs_packing() unconditionally increments
cl->nod_len after calling saturn_hee_cmdlist_fill_data_row(), even if that
function silently dropped the row due to the buffer being full.
This causes cl->nod_len to artificially inflate beyond the actual rows
written. If it hits this WARN_ONCE, it aborts before appending the critical
configuration row containing row_eof_tag = 1.
Could the hardware process an incomplete command list lacking an EOF tag,
thereby failing to signal the CFG_RDY interrupt?
> +
> + header = (struct cmdlist_header *)(cl->va);
> diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
> index 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 dereference a NULL pointer when pstate->state.fb is NULL?
Similar to the issue in saturn_hee_plane_update_hw_channel(), state->fb
can be NULL during plane disablement or solid-fill scenarios. This
unconditional dereference will cause a kernel panic in those cases.
> + u32 crop_w = pstate->state.src_w >> 16;
> +
> + /* Only linear sizing: the compressed path is not exposed. */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=9
^ permalink raw reply [flat|nested] 88+ messages in thread
* Re: [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend
@ 2026-07-25 5:12 ` sashiko-bot
0 siblings, 0 replies; 88+ messages in thread
From: sashiko-bot @ 2026-07-25 5:12 UTC (permalink / raw)
To: Cody Kang
Cc: olteanv, devicetree, robh, conor+dt, neil.armstrong, vkoul,
dri-devel, linux-phy
Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [High] Sleeping memory allocations (`GFP_KERNEL`) in atomic context plane and CRTC update paths, with a silent fallback that breaks atomic update semantics.
- [High] Sleeping delay function (`usleep_range`) used in CRTC atomic enable context.
- [High] Unconditional dereference of `state->fb` for disabled or solid-fill planes.
- [High] Race condition between the configuration timer handler and the VSYNC ISR leads to a double `drm_crtc_vblank_put` and refcount underflow.
- [High] The `cfg_rdy_timer` is not synchronously canceled when the DPU hardware is uninitialized or disabled.
- [High] `row_eof_tag` and un-strobed `module_regs` are left uninitialized in `saturn_hee_cmdlist_fill_data_row()`.
- [High] Accessing `crtc->state->event` from the VBLANK interrupt handler races with concurrent non-blocking atomic commits, causing leaked and premature events.
- [High] Command list length counter desynchronization leads to missing EOF tags and display hangs.
- [High] RDMA hardware configuration is written to a shared staging buffer but is never packed into the DMA command list, causing updates to be dropped or overwritten.
- [Medium] `dpu_run()` ignores the return value of `drm_crtc_vblank_get()`, potentially leading to refcount underflow.
- [Medium] Using C bitfields for DMA-shared hardware descriptors (`struct cmdlist_header` and `struct cmdlist_row`) leads to compiler-dependent memory layout and breaks on different endiannesses.
--
--- Patch [9]: [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend ---
commit 51bff8176b75d13f3aa75c39d52083ed25e121c0
Author: Cody Kang <codykang.hk@gmail.com>
Date: Sat Jul 25 04:51:18 2026 +0000
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.
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
> index 8d5f13bc5373c.....
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn.c
[ ... ]
> +void spacemit_cfg_rdy_timer_handler(struct timer_list *t)
> +{
[ ... ]
> + spin_lock_irqsave(&drm->event_lock, flags);
> + event = crtc->state->event;
[Severity: High]
Does this race with concurrent non-blocking atomic commits?
The DRM core's atomic helpers synchronously swap the crtc->state pointer
during the commit phase before the hardware's VBLANK interrupt fires. If two
non-blocking commits are issued back-to-back, the second commit swaps
crtc->state to its new state.
When the first commit's VBLANK fires (or this timer handler executes), reading
crtc->state->event prematurely consumes the second commit's event, leaking
the first commit's event. Should the event be stored in a driver-private CRTC
structure instead?
> + 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 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);
[Severity: High]
Is it safe to use a sleeping delay function here?
dpu_init() is hooked to the .init callback of dpu_core_ops, which
executes during the CRTC atomic enable phase. The DRM subsystem guidelines
explicitly state that CRTC atomic enable callbacks run in atomic context
where sleeping is strictly forbidden.
Will this trigger a "scheduling while atomic" panic?
> + 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);
> +}
[Severity: High]
Does this uninitialization path need to cancel the configuration timer?
saturn_ctrl_cfg_ready() arms a_crtc->cfg_rdy_timer when the DPU is started.
If the display pipeline is disabled before the hardware raises the CFG_RDY
interrupt, this timer will remain active and could fire asynchronously after
shutdown.
Can this lead to spurious timeout errors, incorrect vblank reference decrements,
or a use-after-free if the CRTC object is destroyed before the timer is
canceled?
> +static inline void dpu_isr_vblank(struct spacemit_crtc *a_crtc, bool *flip)
> +{
[ ... ]
> + spin_lock(&drm->event_lock);
> + event = crtc->state->event;
[Severity: High]
Can this read the wrong event pointer?
Similar to the timer handler above, accessing crtc->state->event races with
concurrent non-blocking atomic commits that swap the crtc->state pointer.
If a new commit is queued, this reads the new state's event and leaks the
current flip's event.
> + if (event) {
[ ... ]
> +static u32 dpu_online_isr(struct spacemit_crtc *a_crtc)
> +{
[ ... ]
> + 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;
[Severity: High]
Does using the non-blocking timer_delete() return value create a race
condition?
If timer_delete() returns 0 (e.g., because the callback is actively executing
on another CPU), it leaves a_crtc->flip_done unmodified.
The ISR then processes INT_VSYNC and calls dpu_isr_vblank(), which reads
flip_done == false, sets it to true, and calls drm_crtc_vblank_put().
Concurrently, the timer handler runs, sets flip_done = true, and also
unconditionally calls drm_crtc_vblank_put().
Could this lead to a double vblank put and refcount underflow?
> + a_crtc->ur_reported = false;
[ ... ]
> +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);
[Severity: Medium]
Should the return value of drm_crtc_vblank_get() be checked?
drm_crtc_vblank_get() can fail (e.g., if vblank interrupts are uninitialized
or disabled), returning a negative error code without incrementing the vblank
reference count.
By ignoring this return value, this function proceeds as if the reference was
successfully acquired. When the hardware interrupt or timeout later triggers,
they unconditionally call drm_crtc_vblank_put(), which could cause a refcount
underflow.
> + if (hwdev->enable_cfg_irq)
> + hwdev->enable_cfg_irq(a_crtc, hwdev, true);
> +
> + saturn_ctrl_cfg_ready(a_crtc, true);
> diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
> index c23e4ca36d26b.....
> --- /dev/null
> +++ b/drivers/gpu/drm/spacemit/dpu/dpu_saturn_hee.c
[ ... ]
> +void saturn_hee_plane_update_hw_channel(struct drm_plane *plane)
> +{
[ ... ]
> + /* 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]
Are these RDMA hardware configuration writes dropped because they are never
packed into the DMA command list?
This uses a_crtc->cl_rdma to stage hardware writes for the RDMA block.
However, there is no corresponding call to cmdlist_regs_packing() for
cl_rdma to commit these writes to the DMA-backed command list.
Since a_crtc->cl_rdma is a single shared buffer, do subsequent plane updates
overwrite the staged settings of previous planes, silently corrupting the
configuration and failing to program the hardware?
> + dpu_write(hwdev, RDMA_PATH_X_REG, base, layer_cmpsr_id,
[ ... ]
> + 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 &&
[Severity: High]
Will this unconditionally dereference a NULL pointer for solid-fill planes?
During DRM atomic commits, a plane's state->fb is frequently NULL (e.g., when
a plane is being disabled, or for solid color fill planes).
While the earlier code handles solid fills by skipping the RDMA block
(if (!solid_en)), this block evaluates state->fb->format unconditionally,
which will cause a kernel panic if state->fb is NULL.
> + 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]
Can this trigger a "scheduling while atomic" panic or silent fallback
failures?
alloc_cmdlist_regs() is a macro that uses GFP_KERNEL allocations
(kzalloc(sizeof(*cl), GFP_KERNEL)). This plane update callback runs in atomic
context where sleeping is forbidden.
Additionally, if the allocation fails and returns NULL, the dpu_write() macro
silently falls back to immediate MMIO writes (dpu_write_reg_word). This
bypasses the command list's VBLANK synchronization, applying partial hardware
state immediately and causing visual tearing or lockups.
> +
> + base = CMP_BASE_ADDR[a_crtc->dev_id];
[ ... ]
> +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];
> +};
[Severity: Medium]
Does using C bitfields for DMA-shared hardware descriptors break on different
compilers or endiannesses?
These structures format command list nodes directly into a DMA buffer consumed
by the hardware. The C standard leaves bitfield packing order and alignment
entirely implementation-defined.
Mixing u64 and u32 bitfields makes the exact memory layout highly sensitive to
compiler ABIs and versions, and it will not work correctly across different
endiannesses. Could you use explicit bitwise operations with fixed-width types
instead?
> +
> +/* DMA stream layout consumed by the cmdlist engine; sizes are load-bearing. */
[ ... ]
> +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++)
[Severity: High]
Are row_eof_tag and un-strobed module_regs left uninitialized here?
This overlays a struct cmdlist_row onto the cl->va DMA buffer without
explicitly clearing or setting row->row_eof_tag. If the buffer is reused
across frames and the current frame is shorter than the previous one,
row_eof_tag could contain a stale 1 from the end of a previous commit's
list.
If the hardware sees this uninitialized bitfield as 1, it will terminate
command processing prematurely, ignoring all subsequent commands in the current
frame and causing display hangs.
> + if (strobe & CMDLIST_REG_STROBE(i))
[ ... ]
> +void saturn_hee_cmdlist_fill_conf_row(struct cmdlist *cl, struct spacemit_hw_device *hwdev,
> + u8 dev_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;
[Severity: High]
Does a desynchronized cl->nod_len cause the EOF tag to be dropped?
The calling code in cmdlist_regs_packing() unconditionally increments
cl->nod_len after calling saturn_hee_cmdlist_fill_data_row(), even if that
function silently dropped the row due to the buffer being full.
This causes cl->nod_len to artificially inflate beyond the actual rows
written. If it hits this WARN_ONCE, it aborts before appending the critical
configuration row containing row_eof_tag = 1.
Could the hardware process an incomplete command list lacking an EOF tag,
thereby failing to signal the CFG_RDY interrupt?
> +
> + header = (struct cmdlist_header *)(cl->va);
> diff --git a/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c b/drivers/gpu/drm/spacemit/dpu/saturn_fbcmem.c
> index 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 dereference a NULL pointer when pstate->state.fb is NULL?
Similar to the issue in saturn_hee_plane_update_hw_channel(), state->fb
can be NULL during plane disablement or solid-fill scenarios. This
unconditional dereference will cause a kernel panic in those cases.
> + u32 crop_w = pstate->state.src_w >> 16;
> +
> + /* Only linear sizing: the compressed path is not exposed. */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260725-k3-display-v1-0-6de34d80e86c@gmail.com?part=9
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
^ permalink raw reply [flat|nested] 88+ messages in thread
end of thread, other threads:[~2026-07-25 5:12 UTC | newest]
Thread overview: 88+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-25 4:51 [PATCH RESEND 00/17] drm/spacemit: add SpacemiT K3 display support Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 01/17] dt-bindings: display: spacemit: add K3 Saturn DPU controller Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 02/17] dt-bindings: phy: add SpacemiT K3 Innosilicon DP PHY Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 03/17] dt-bindings: display: spacemit: add K3 Innosilicon DP/eDP controller Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 04/17] dt-bindings: soc: spacemit: allow eDP/DP PHY PLL pixel clocks on K3 APMU Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 05/17] phy: spacemit: add Innosilicon DP TX PHY driver Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:02 ` sashiko-bot
2026-07-25 5:02 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 06/17] clk: spacemit: k3: parent eDP/DP pixel clock to the PHY PLL Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 07/17] drm/spacemit: add Saturn DPU register model Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, cmdlist and display MMU Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:04 ` sashiko-bot
2026-07-25 5:04 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 09/17] drm/spacemit: add Saturn DPU hardware backend Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:12 ` sashiko-bot
2026-07-25 5:12 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 10/17] drm/spacemit: add Saturn DPU KMS pipeline Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:04 ` sashiko-bot
2026-07-25 5:04 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 11/17] drm/spacemit: add Saturn DPU DRM device driver Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:05 ` sashiko-bot
2026-07-25 5:05 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 12/17] drm/spacemit: add Innosilicon DP/eDP controller bridge driver Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:04 ` sashiko-bot
2026-07-25 5:04 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 13/17] MAINTAINERS: add SpacemiT K3 display driver entry Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 14/17] riscv: dts: spacemit: k3: add display nodes Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:08 ` sashiko-bot
2026-07-25 5:08 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 15/17] riscv: dts: spacemit: k3-pico-itx: enable the DisplayPort output Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 5:12 ` sashiko-bot
2026-07-25 5:12 ` sashiko-bot
2026-07-25 4:51 ` [PATCH RESEND 16/17] riscv: dts: spacemit: k3-com260-ifx: " Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` [PATCH RESEND 17/17] riscv: defconfig: spacemit: k3: enable display driver Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang
2026-07-25 4:51 ` Cody Kang via B4 Relay
2026-07-25 4:51 ` Cody Kang via B4 Relay
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.