From d31e4a4add77d280c51a1aaba94cc9839cb28310 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:18:14 +0800 Subject: [PATCH 01/16] dt-bindings: clock: add Allwinner A733 DE352 CCU Describe the DE352 clock controller, its four parent clocks and its system and bus resets. Define clock and reset identifiers for the two mixers and their shared memory interface. --- .../allwinner,sun60i-a733-de352-clk.yaml | 97 +++++++++++++++++++ include/dt-bindings/clock/sun60i-de352.h | 14 +++ include/dt-bindings/reset/sun60i-de352.h | 13 +++ 3 files changed, 124 insertions(+) create mode 100644 Documentation/devicetree/bindings/clock/allwinner,sun60i-a733-de352-clk.yaml create mode 100644 include/dt-bindings/clock/sun60i-de352.h create mode 100644 include/dt-bindings/reset/sun60i-de352.h diff --git a/Documentation/devicetree/bindings/clock/allwinner,sun60i-a733-de352-clk.yaml b/Documentation/devicetree/bindings/clock/allwinner,sun60i-a733-de352-clk.yaml new file mode 100644 index 00000000000000..063b69717267e3 --- /dev/null +++ b/Documentation/devicetree/bindings/clock/allwinner,sun60i-a733-de352-clk.yaml @@ -0,0 +1,97 @@ +# SPDX-License-Identifier: GPL-2.0 +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/clock/allwinner,sun60i-a733-de352-clk.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Allwinner A733 DE352 DETOP Clock Controller + +maintainers: + - Chen-Yu Tsai + +description: | + DE352 keeps mixer/display module clocks in the DETOP register block at + DE + 0x8000 (RESET/CLK/MBUS), not at the DE2/DE3 clock@0 layout. + +properties: + "#clock-cells": + const: 1 + + "#reset-cells": + const: 1 + + compatible: + const: allwinner,sun60i-a733-de352-clk + + reg: + maxItems: 1 + + clocks: + items: + - description: DE AHB fabric gate (CLK_DE_AHB_GATE) + - description: DE MBUS fabric gate (CLK_MBUS_DESYS_GATE) + - description: DE bus clock (CLK_BUS_DE0) + - description: DE module clock (CLK_DE0) + + clock-names: + items: + - const: ahb + - const: mbus + - const: bus + - const: mod + + resets: + items: + - description: DE system reset (RST_BUS_DE_SY) + - description: DE0 bus reset (RST_BUS_DE0) + + reset-names: + items: + - const: sys + - const: bus + + power-domains: + maxItems: 1 + +required: + - "#clock-cells" + - "#reset-cells" + - compatible + - reg + - clocks + - clock-names + - resets + - reset-names + +additionalProperties: false + +examples: + - | + #include + #include + + bus@5000000 { + #address-cells = <1>; + #size-cells = <1>; + ranges = <0x0 0x0 0x05000000 0x400000>; + + de352_clocks: clock@8000 { + compatible = "allwinner,sun60i-a733-de352-clk"; + reg = <0x8000 0x100>; + clocks = <&ccu CLK_DE_AHB_GATE>, + <&ccu CLK_MBUS_DESYS_GATE>, + <&ccu CLK_BUS_DE0>, + <&ccu CLK_DE0>; + clock-names = "ahb", + "mbus", + "bus", + "mod"; + resets = <&ccu RST_BUS_DE_SY>, + <&ccu RST_BUS_DE0>; + reset-names = "sys", "bus"; + #clock-cells = <1>; + #reset-cells = <1>; + }; + }; + +... diff --git a/include/dt-bindings/clock/sun60i-de352.h b/include/dt-bindings/clock/sun60i-de352.h new file mode 100644 index 00000000000000..59dab9de696abb --- /dev/null +++ b/include/dt-bindings/clock/sun60i-de352.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * Clock indices for Allwinner DE352 DETOP (A733): regs at DE+0x8000. + */ + +#ifndef _DT_BINDINGS_CLOCK_SUN60I_DE352_H_ +#define _DT_BINDINGS_CLOCK_SUN60I_DE352_H_ + +#define CLK_MIXER0 0 +#define CLK_BUS_MIXER0 1 +#define CLK_MIXER1 2 + +#endif /* _DT_BINDINGS_CLOCK_SUN60I_DE352_H_ */ diff --git a/include/dt-bindings/reset/sun60i-de352.h b/include/dt-bindings/reset/sun60i-de352.h new file mode 100644 index 00000000000000..05586076c18aaf --- /dev/null +++ b/include/dt-bindings/reset/sun60i-de352.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: (GPL-2.0-only OR MIT) */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * Reset indices for Allwinner DE352 DETOP (A733): regs at DE+0x8000. + */ + +#ifndef _DT_BINDINGS_RESET_SUN60I_DE352_H_ +#define _DT_BINDINGS_RESET_SUN60I_DE352_H_ + +#define RST_MIXER0 0 +#define RST_MIXER1 1 + +#endif /* _DT_BINDINGS_RESET_SUN60I_DE352_H_ */ From 03ac42e3bbb9069241dd77d8bf99e97301505ecc Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:18:42 +0800 Subject: [PATCH 02/16] dt-bindings: display: add Allwinner A733 DE352 mixer Describe the DE352 mixer register regions, clocks, reset, memory interface and output graph. Document the A733 timing-controller selection used to route mixer 0 to a display output. --- .../allwinner,sun60i-a733-de352-mixer.yaml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Documentation/devicetree/bindings/display/allwinner,sun60i-a733-de352-mixer.yaml diff --git a/Documentation/devicetree/bindings/display/allwinner,sun60i-a733-de352-mixer.yaml b/Documentation/devicetree/bindings/display/allwinner,sun60i-a733-de352-mixer.yaml new file mode 100644 index 00000000000000..134b44b86c8d07 --- /dev/null +++ b/Documentation/devicetree/bindings/display/allwinner,sun60i-a733-de352-mixer.yaml @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: GPL-2.0 +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/display/allwinner,sun60i-a733-de352-mixer.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Allwinner A733 DE352 Mixer + +maintainers: + - Chen-Yu Tsai + +description: | + DE352 mixer / display pipe for A733. DT shape follows the T527 A523 + de33-mixer-0 node (engine/top/display/detop), but the driver is sun60i_de + (DE352 RCQ), not sun8i_mixer. + +properties: + compatible: + const: allwinner,sun60i-a733-de352-mixer-0 + + reg: + items: + - description: Channel / overlay engine (DE + 0x100000) + - description: RTMX top (DE + 0x8100) + - description: Display blender / formatter (DE + 0x280000) + - description: DETOP mux block (DE + 0x8000); overlaps de352-clk + + reg-names: + items: + - const: engine + - const: top + - const: display + - const: detop + + clocks: + minItems: 2 + maxItems: 2 + + clock-names: + items: + - const: bus + - const: mod + + resets: + maxItems: 1 + + reset-names: + items: + - const: mod + + iommus: + maxItems: 1 + + power-domains: + maxItems: 1 + + chn_cfg_mode: + $ref: /schemas/types.yaml#/definitions/uint32 + description: DE352 channel topology selected for this mixer. + + allwinner,tcon-id: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 15 + description: | + Hardware TCON device index programmed into the DE2TCON mux for display + pipe 0. This is a hardware device index, not an OF graph endpoint ID. + A733 TCON-LCD0/1/2 use 0/1/2 respectively. + + ports: + $ref: /schemas/graph.yaml#/properties/ports + +required: + - compatible + - reg + - reg-names + - clocks + - clock-names + - resets + - reset-names + +additionalProperties: true + +... From 21a517a9b89e1d66007ba6fb11f7f96e46dd30f3 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:19:35 +0800 Subject: [PATCH 03/16] dt-bindings: display: describe A733 TV timing controllers Describe the A733 TCON-TV and Video Output 1 top controller. Account for the extra fabric reset, AHB gate and power domain while preserving the requirements of existing timing controllers. Leave the LCD controller and external panel PHY descriptions to the separate panel-output series. --- .../display/allwinner,sun4i-a10-tcon.yaml | 2 ++ .../display/allwinner,sun8i-r40-tcon-top.yaml | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/display/allwinner,sun4i-a10-tcon.yaml b/Documentation/devicetree/bindings/display/allwinner,sun4i-a10-tcon.yaml index 724d93b9193bc7..00557a1ca183db 100644 --- a/Documentation/devicetree/bindings/display/allwinner,sun4i-a10-tcon.yaml +++ b/Documentation/devicetree/bindings/display/allwinner,sun4i-a10-tcon.yaml @@ -35,6 +35,7 @@ properties: - const: allwinner,sun9i-a80-tcon-tv - const: allwinner,sun20i-d1-tcon-lcd - const: allwinner,sun20i-d1-tcon-tv + - const: allwinner,sun60i-a733-tcon-tv - items: - enum: @@ -254,6 +255,7 @@ allOf: - allwinner,sun8i-r40-tcon-tv - allwinner,sun9i-a80-tcon-tv - allwinner,sun20i-d1-tcon-tv + - allwinner,sun60i-a733-tcon-tv then: properties: diff --git a/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml b/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml index 7d849c4095a36c..1c2839579ed5ea 100644 --- a/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml +++ b/Documentation/devicetree/bindings/display/allwinner,sun8i-r40-tcon-top.yaml @@ -43,6 +43,7 @@ properties: - allwinner,sun8i-r40-tcon-top - allwinner,sun20i-d1-tcon-top - allwinner,sun50i-h6-tcon-top + - allwinner,sun60i-a733-tcon-top reg: maxItems: 1 @@ -60,6 +61,14 @@ properties: maxItems: 3 resets: + minItems: 1 + maxItems: 2 + + reset-names: + minItems: 1 + maxItems: 2 + + power-domains: maxItems: 1 ports: @@ -99,16 +108,12 @@ properties: required: - port@0 - port@1 - - port@4 - - port@5 required: - - "#clock-cells" - compatible - reg - clocks - clock-names - - clock-output-names - resets - ports @@ -152,6 +157,27 @@ allOf: - port@2 - port@3 + - if: + properties: + compatible: + contains: + enum: + - allwinner,sun8i-r40-tcon-top + - allwinner,sun20i-d1-tcon-top + - allwinner,sun50i-h6-tcon-top + - allwinner,sun60i-a733-tcon-top + + then: + required: + - "#clock-cells" + - clock-output-names + + properties: + ports: + required: + - port@4 + - port@5 + - if: properties: compatible: From 2b566507719d2d1812ca24e12cdbfc5cf77415ec Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:18:30 +0800 Subject: [PATCH 04/16] clk: sunxi-ng: preserve required NM PLL enable bits Some PLLs require their output, lock detector and LDO bits to remain set while the factors are updated. Add an opt-in NM clock feature that restores these bits together with the enable bit during a rate change. Leave existing NM clocks unchanged unless they select this feature. This provides the common clock support required by the A733 DE PLL. --- drivers/clk/sunxi-ng/ccu_common.h | 1 + drivers/clk/sunxi-ng/ccu_nm.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/drivers/clk/sunxi-ng/ccu_common.h b/drivers/clk/sunxi-ng/ccu_common.h index b2994f7bae7c64..7ba517797fe67f 100644 --- a/drivers/clk/sunxi-ng/ccu_common.h +++ b/drivers/clk/sunxi-ng/ccu_common.h @@ -22,6 +22,7 @@ #define CCU_FEATURE_DUAL_DIV BIT(10) #define CCU_FEATURE_UPDATE_BIT BIT(11) #define CCU_FEATURE_CLEAR_MOD BIT(12) +#define CCU_FEATURE_KEEP_PLL_ENABLED BIT(13) /* MMC timing mode switch bit */ #define CCU_MMC_NEW_TIMING_MODE BIT(30) diff --git a/drivers/clk/sunxi-ng/ccu_nm.c b/drivers/clk/sunxi-ng/ccu_nm.c index 08ae359c191963..5a4214b8a501da 100644 --- a/drivers/clk/sunxi-ng/ccu_nm.c +++ b/drivers/clk/sunxi-ng/ccu_nm.c @@ -229,6 +229,8 @@ static int ccu_nm_set_rate(struct clk_hw *hw, unsigned long rate, reg |= (_nm.n - nm->n.offset) << nm->n.shift; reg |= (_nm.m - nm->m.offset) << nm->m.shift; + if (nm->common.features & CCU_FEATURE_KEEP_PLL_ENABLED) + reg |= nm->enable | nm->output | nm->lock_enable | nm->ldo_en; writel(reg, nm->common.base + nm->common.reg); spin_unlock_irqrestore(nm->common.lock, flags); From 567c73bbc706257bec567bf61d1f31d676eb54ba Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:20:40 +0800 Subject: [PATCH 05/16] clk: sunxi-ng: keep the A733 DE clock chain enabled Describe the separate enable, LDO, lock and output bits of the DE PLL and preserve them across rate changes. Mark its divided outputs and the DE0 functional and bus clocks as critical. DETOP register accesses can stall when this parent clock chain is gated. Keep the parent chain available while leaving the per-mixer gates under the DE352 clock controller. Retain the initial shared-output setup. --- drivers/clk/sunxi-ng/ccu-sun60i-a733.c | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/drivers/clk/sunxi-ng/ccu-sun60i-a733.c b/drivers/clk/sunxi-ng/ccu-sun60i-a733.c index 8de32b1db11359..c4ccf038883c89 100644 --- a/drivers/clk/sunxi-ng/ccu-sun60i-a733.c +++ b/drivers/clk/sunxi-ng/ccu-sun60i-a733.c @@ -382,9 +382,17 @@ static struct ccu_nm pll_npu_clk = { }; #define SUN60IW2_PLL_DE_CTRL_REG 0x02E0 +/* + * The PLL and both divided outputs share one control register. Keep an + * explicit critical reference at every level: relying only on the DE0 child + * left the CCF enable count live while the hardware bits were incomplete. + */ static struct ccu_nm pll_de_clk = { + .output = BIT(27) | BIT(26), .lock = BIT(28), - .enable = BIT(27), + .lock_enable = BIT(29), + .ldo_en = BIT(30), + .enable = BIT(31), .n = _SUNXI_CCU_MULT_MIN_MAX(8, 8, 53, 105), .min_rate = 1272000000, .max_rate = 2520000000, @@ -393,10 +401,11 @@ static struct ccu_nm pll_de_clk = { 0x02EC, BIT(27)), /* pattern1 */ .common = { .reg = 0x02E0, + .features = CCU_FEATURE_KEEP_PLL_ENABLED, .hw.init = CLK_HW_INIT("pll-de", "pll-ref", &ccu_nm_ops, CLK_SET_RATE_UNGATE | - CLK_IGNORE_UNUSED), + CLK_IS_CRITICAL), }, }; @@ -404,13 +413,13 @@ static SUNXI_CCU_M_WITH_GATE(pll_de_4x_clk, "pll-de-4x", "pll-de", 0x02E0, 20, 3, BIT(27), - CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED); + CLK_SET_RATE_PARENT | CLK_IS_CRITICAL); static SUNXI_CCU_M_WITH_GATE(pll_de_3x_clk, "pll-de-3x", "pll-de", 0x02E0, 16, 3, BIT(26), - CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED); + CLK_SET_RATE_PARENT | CLK_IS_CRITICAL); static const char * const apb0_parents[] = { "sys24M", "osc32k", "iosc", "pll-peri0-600m" }; SUNXI_CCU_M_WITH_MUX(ahb_clk, "ahb", apb0_parents, @@ -879,16 +888,20 @@ static SUNXI_CCU_MUX_WITH_GATE(avs_clk, "avs", static const char * const de0_parents[] = { "pll-de-3x", "pll-de-4x", "pll-peri0-480m", "pll-peri0-400m", "pll-peri0-300m", "pll-video0-4x", "pll-video2-4x" }; +/* + * DETOP MMIO stalls when this chain is gated. Keep the main DE0 functional + * and bus clocks running; the DE352 CCU still owns the per-mixer gates. + */ static SUNXI_CCU_M_WITH_MUX_GATE(de0_clk, "de0", de0_parents, 0x0A00, 0, 5, /* M */ 24, 3, /* mux */ BIT(31), /* gate */ - CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED); + CLK_SET_RATE_PARENT | CLK_IS_CRITICAL); static SUNXI_CCU_GATE(de0_gate_clk, "de0-gate", "ahb", - 0x0A04, BIT(0), CLK_IGNORE_UNUSED); + 0x0A04, BIT(0), CLK_IS_CRITICAL); static const char * const di_parents[] = { "pll-peri0-600m", "pll-peri0-480m", "pll-peri0-400m", "pll-video0-4x", "pll-video2-4x" }; @@ -2926,6 +2939,11 @@ static int sun60iw2_ccu_really_probe(struct platform_device *pdev) if (ret) return ret; + /* Enable the shared DE PLL outputs after clock registration. */ + val = readl(reg + SUN60IW2_PLL_DE_CTRL_REG); + val |= BIT(31) | BIT(30) | BIT(29) | BIT(27) | BIT(26); + writel(val, reg + SUN60IW2_PLL_DE_CTRL_REG); + return 0; } From 7e3edecd806f0804a109ee1d6cd460aebbf5ca7b Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:20:53 +0800 Subject: [PATCH 06/16] clk: sunxi-ng: model the A733 HDMI CEC gate and divider Replace the fixed-factor CEC clock with the gated predivider output of pll-peri0-2x. Register its common clock data so the HDMI consumer can control the gate as well as obtain the divided clock rate. --- drivers/clk/sunxi-ng/ccu-sun60i-a733.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/drivers/clk/sunxi-ng/ccu-sun60i-a733.c b/drivers/clk/sunxi-ng/ccu-sun60i-a733.c index c4ccf038883c89..8daac6d3aff855 100644 --- a/drivers/clk/sunxi-ng/ccu-sun60i-a733.c +++ b/drivers/clk/sunxi-ng/ccu-sun60i-a733.c @@ -98,6 +98,10 @@ static SUNXI_CCU_M(pll_peri0_2x_clk, "pll-peri0-2x", 20, 3, 0); +static const struct clk_hw *pll_peri0_2x_hws[] = { + &pll_peri0_2x_clk.common.hw, +}; + static SUNXI_CCU_M(pll_peri0_800m_clk, "pll-peri0-800m", "pll-peri0", 0x00A0, 16, 3, @@ -155,7 +159,9 @@ static CLK_FIXED_FACTOR(pll_peri1_300m_clk, "pll-peri1-300m", "pll-peri1-600m", static CLK_FIXED_FACTOR(pll_peri1_200m_clk, "pll-peri1-200m", "pll-peri1-400m", 2, 1, 0); static CLK_FIXED_FACTOR(pll_peri1_160m_clk, "pll-peri1-160m", "pll-peri1-480m", 3, 1, 0); static CLK_FIXED_FACTOR(pll_peri1_150m_clk, "pll-peri1-150m", "pll-peri1-300m", 2, 1, 0); -static CLK_FIXED_FACTOR(hdmi_cec_32k_clk, "hdmi-cec-clk32k", "pll-peri0-2x", 1, 36621, 0); +static SUNXI_CCU_GATE_HWS_WITH_PREDIV(hdmi_cec_32k_clk, + "hdmi-cec-clk32k", pll_peri0_2x_hws, + 0x1680, BIT(30), 36621, 0); #define SUN60IW2_PLL_GPU0_CTRL_REG 0x00E0 @@ -2267,7 +2273,7 @@ static struct clk_hw_onecell_data sun60iw2_hw_clks = { [CLK_PLL_PERI1_200M] = &pll_peri1_200m_clk.hw, [CLK_PLL_PERI1_160M] = &pll_peri1_160m_clk.hw, [CLK_PLL_PERI1_150M] = &pll_peri1_150m_clk.hw, - [CLK_HDMI_CEC_32K] = &hdmi_cec_32k_clk.hw, + [CLK_HDMI_CEC_32K] = &hdmi_cec_32k_clk.common.hw, [CLK_PLL_GPU0] = &pll_gpu0_clk.common.hw, [CLK_PLL_VIDEO0] = &pll_video0_clk.common.hw, [CLK_PLL_VIDEO0_4X] = &pll_video0_4x_clk.common.hw, @@ -2580,6 +2586,7 @@ static struct ccu_common *sun60iw2_ccu_clks[] = { &pll_peri0_2x_clk.common, &pll_peri0_800m_clk.common, &pll_peri0_480m_clk.common, + &hdmi_cec_32k_clk.common, &pll_peri1_clk.common, &pll_peri1_2x_clk.common, &pll_peri1_800m_clk.common, From 492c1132b031862c9ed4df6005c16cb99c52a4b7 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:18:30 +0800 Subject: [PATCH 07/16] clk: sunxi-ng: add A733 DE352 clock and reset control Register the mixer clocks, shared MBUS gate and keyed mixer resets provided by the DE352 top block. Enable the parent clocks before setting the module rate and releasing the system and instance resets. Use the common clock and reset frameworks to keep the shared resources owned by this provider. Unwind registrations and parent references on probe failure and provider removal. --- drivers/clk/sunxi-ng/Kconfig | 10 + drivers/clk/sunxi-ng/Makefile | 2 + drivers/clk/sunxi-ng/ccu-sun60i-de352.c | 302 ++++++++++++++++++++++++ drivers/clk/sunxi-ng/ccu-sun60i-de352.h | 12 + 4 files changed, 326 insertions(+) create mode 100644 drivers/clk/sunxi-ng/ccu-sun60i-de352.c create mode 100644 drivers/clk/sunxi-ng/ccu-sun60i-de352.h diff --git a/drivers/clk/sunxi-ng/Kconfig b/drivers/clk/sunxi-ng/Kconfig index 293aaeaacfbccf..3ce99e40b19d84 100644 --- a/drivers/clk/sunxi-ng/Kconfig +++ b/drivers/clk/sunxi-ng/Kconfig @@ -87,6 +87,16 @@ config SUN60I_A733_CPUPLL_CCU default ARCH_SUNXI depends on ARM64 || COMPILE_TEST +config SUN60I_DE352_CCU + tristate "Support for the Allwinner A733 DE352 DETOP CCU" + default ARCH_SUNXI + depends on ARM64 || COMPILE_TEST + help + Enable support for the clock and reset controller in the DE352 + display engine top block found in the Allwinner A733 SoC. It controls + the mixer module clocks, display gates and shared memory bus clock. + This controller is required by the A733 display engine driver. + config SUN4I_A10_CCU tristate "Support for the Allwinner A10/A20 CCU" default ARCH_SUNXI diff --git a/drivers/clk/sunxi-ng/Makefile b/drivers/clk/sunxi-ng/Makefile index b32ba2fea40f1e..7dbf8b3a6cd4d2 100644 --- a/drivers/clk/sunxi-ng/Makefile +++ b/drivers/clk/sunxi-ng/Makefile @@ -40,6 +40,7 @@ obj-$(CONFIG_SUN60I_A733_CCU) += sun60i-a733-ccu.o obj-$(CONFIG_SUN60I_A733_RTC_CCU) += ccu-sun60i-a733-rtc.o obj-$(CONFIG_SUN60I_A733_R_CCU) += sun60i-a733-r-ccu.o obj-$(CONFIG_SUN60I_A733_CPUPLL_CCU) += sun60i-a733-cpupll-ccu.o +obj-$(CONFIG_SUN60I_DE352_CCU) += sun60i-de352-ccu.o obj-$(CONFIG_SUN4I_A10_CCU) += sun4i-a10-ccu.o obj-$(CONFIG_SUN5I_CCU) += sun5i-ccu.o obj-$(CONFIG_SUN6I_A31_CCU) += sun6i-a31-ccu.o @@ -71,6 +72,7 @@ sun55i-a523-r-ccu-y += ccu-sun55i-a523-r.o sun60i-a733-ccu-y += ccu-sun60i-a733.o sun60i-a733-r-ccu-y += ccu-sun60i-a733-r.o sun60i-a733-cpupll-ccu-y += ccu-sun60i-a733-cpupll.o +sun60i-de352-ccu-y += ccu-sun60i-de352.o sun4i-a10-ccu-y += ccu-sun4i-a10.o sun5i-ccu-y += ccu-sun5i.o sun6i-a31-ccu-y += ccu-sun6i-a31.o diff --git a/drivers/clk/sunxi-ng/ccu-sun60i-de352.c b/drivers/clk/sunxi-ng/ccu-sun60i-de352.c new file mode 100644 index 00000000000000..e996740876ecc0 --- /dev/null +++ b/drivers/clk/sunxi-ng/ccu-sun60i-de352.c @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * Allwinner DE352 DETOP clock/reset controller (A733). + * + * Unlike DE2/DE3 (gates at DE+0x0), DE352 module clocks live in DETOP: + * +0x00 RESET — displayN gate at bit (N * 4), writes need BIT(16) key + * +0x04 CLK — same bit layout / key + * +0x08 MBUS — bit0 clk_en, bit4 reset deassert (no key; never set bit8) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ccu_common.h" +#include "ccu_gate.h" +#include "ccu_reset.h" + +#include "ccu-sun60i-de352.h" + +#define DE352_RESET_REG 0x00 +#define DE352_CLK_REG 0x04 +#define DE352_MBUS_REG 0x08 + +#define DE352_KEY BIT(16) +#define DE352_DISP_SHIFT 4 +#define DE352_MBUS_CLK_EN BIT(0) +#define DE352_MBUS_RST_N BIT(4) +#define DE352_MOD_RATE 600000000UL + +static SUNXI_CCU_GATE_WITH_KEY(mixer0_clk, "mixer0", "de0", + DE352_CLK_REG, DE352_KEY, BIT(0), + CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED); +static SUNXI_CCU_GATE_WITH_KEY(mixer1_clk, "mixer1", "de0", + DE352_CLK_REG, DE352_KEY, + BIT(1 * DE352_DISP_SHIFT), + CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED); + +/* + * MBUS is shared by all mixers. Export as bus-mixer0; enabling sets clk_en and + * reset-deassert together (BSP __de_mbus_*). Auto-gate bit8 must stay clear. + */ +static SUNXI_CCU_GATE(bus_mixer0_clk, "bus-mixer0", "de0-gate", + DE352_MBUS_REG, DE352_MBUS_CLK_EN | DE352_MBUS_RST_N, + CLK_IGNORE_UNUSED); + +static struct ccu_common *sun60i_de352_ccu_clks[] = { + &mixer0_clk.common, + &mixer1_clk.common, + &bus_mixer0_clk.common, +}; + +static struct clk_hw_onecell_data sun60i_de352_hw_clks = { + .hws = { + [CLK_MIXER0] = &mixer0_clk.common.hw, + [CLK_BUS_MIXER0] = &bus_mixer0_clk.common.hw, + [CLK_MIXER1] = &mixer1_clk.common.hw, + }, + .num = CLK_NUMBER, +}; + +static const struct ccu_reset_map sun60i_de352_resets[] = { + [RST_MIXER0] = { DE352_RESET_REG, BIT(0) }, + [RST_MIXER1] = { DE352_RESET_REG, BIT(1 * DE352_DISP_SHIFT) }, +}; + +struct sun60i_de352_clk { + struct device *dev; + void __iomem *base; + spinlock_t lock; + struct ccu_reset reset; + struct clk_bulk_data parents[4]; + struct reset_control *rst_sys; + struct reset_control *rst_bus; + int num_registered; +}; + +static int sun60i_de352_reset_assert(struct reset_controller_dev *rcdev, + unsigned long id) +{ + struct ccu_reset *ccu = rcdev_to_ccu_reset(rcdev); + const struct ccu_reset_map *map = &ccu->reset_map[id]; + unsigned long flags; + u32 reg; + + spin_lock_irqsave(ccu->lock, flags); + reg = readl(ccu->base + map->reg); + writel((reg & ~map->bit) | DE352_KEY, ccu->base + map->reg); + spin_unlock_irqrestore(ccu->lock, flags); + + return 0; +} + +static int sun60i_de352_reset_deassert(struct reset_controller_dev *rcdev, + unsigned long id) +{ + struct ccu_reset *ccu = rcdev_to_ccu_reset(rcdev); + const struct ccu_reset_map *map = &ccu->reset_map[id]; + unsigned long flags; + u32 reg; + + spin_lock_irqsave(ccu->lock, flags); + reg = readl(ccu->base + map->reg); + writel(reg | map->bit | DE352_KEY, ccu->base + map->reg); + spin_unlock_irqrestore(ccu->lock, flags); + + return 0; +} + +static int sun60i_de352_reset_status(struct reset_controller_dev *rcdev, + unsigned long id) +{ + struct ccu_reset *ccu = rcdev_to_ccu_reset(rcdev); + const struct ccu_reset_map *map = &ccu->reset_map[id]; + + /* 0 = deasserted (bit set), matching ccu_reset_status polarity */ + return !(map->bit & readl(ccu->base + map->reg)); +} + +static const struct reset_control_ops sun60i_de352_reset_ops = { + .assert = sun60i_de352_reset_assert, + .deassert = sun60i_de352_reset_deassert, + .status = sun60i_de352_reset_status, +}; + +static void sun60i_de352_clk_remove_action(void *data) +{ + struct sun60i_de352_clk *de352 = data; + int i; + + reset_controller_unregister(&de352->reset.rcdev); + of_clk_del_provider(de352->dev->of_node); + + for (i = 0; i < de352->num_registered; i++) { + struct clk_hw *hw = sun60i_de352_hw_clks.hws[i]; + + if (hw) + clk_hw_unregister(hw); + } + + reset_control_assert(de352->rst_bus); + reset_control_assert(de352->rst_sys); + clk_bulk_disable_unprepare(ARRAY_SIZE(de352->parents), de352->parents); +} + +static int sun60i_de352_clk_probe(struct platform_device *pdev) +{ + struct sun60i_de352_clk *de352; + void __iomem *reg; + int i, ret; + + de352 = devm_kzalloc(&pdev->dev, sizeof(*de352), GFP_KERNEL); + if (!de352) + return -ENOMEM; + + de352->dev = &pdev->dev; + de352->parents[0].id = "ahb"; + de352->parents[1].id = "mbus"; + de352->parents[2].id = "bus"; + de352->parents[3].id = "mod"; + + reg = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(reg)) + return PTR_ERR(reg); + + ret = devm_clk_bulk_get(&pdev->dev, ARRAY_SIZE(de352->parents), + de352->parents); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Couldn't get DE parent clocks\n"); + + de352->rst_sys = devm_reset_control_get_shared(&pdev->dev, "sys"); + if (IS_ERR(de352->rst_sys)) + return dev_err_probe(&pdev->dev, PTR_ERR(de352->rst_sys), + "Couldn't get DE system reset\n"); + + de352->rst_bus = devm_reset_control_get_shared(&pdev->dev, "bus"); + if (IS_ERR(de352->rst_bus)) + return dev_err_probe(&pdev->dev, PTR_ERR(de352->rst_bus), + "Couldn't get DE0 bus reset\n"); + + /* + * The DETOP register interface stalls when DE0 has no functional clock. + * Keep the complete parent chain running before changing its rate or + * releasing either DE reset. + */ + ret = clk_bulk_prepare_enable(ARRAY_SIZE(de352->parents), de352->parents); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Couldn't enable DE parent clocks\n"); + + ret = clk_set_rate(de352->parents[3].clk, DE352_MOD_RATE); + if (ret) { + dev_err(&pdev->dev, "Couldn't set DE module rate: %d\n", ret); + goto err_disable_clks; + } + + /* A733 BSP order: release the DE system first, then the DE0 instance. */ + ret = reset_control_deassert(de352->rst_sys); + if (ret) { + dev_err(&pdev->dev, "Couldn't deassert DE system reset: %d\n", + ret); + goto err_disable_clks; + } + + ret = reset_control_deassert(de352->rst_bus); + if (ret) { + dev_err(&pdev->dev, "Couldn't deassert DE0 bus reset: %d\n", + ret); + goto err_assert_sys_reset; + } + + spin_lock_init(&de352->lock); + de352->base = reg; + + for (i = 0; i < ARRAY_SIZE(sun60i_de352_ccu_clks); i++) { + sun60i_de352_ccu_clks[i]->base = reg; + sun60i_de352_ccu_clks[i]->lock = &de352->lock; + } + + for (i = 0; i < sun60i_de352_hw_clks.num; i++) { + struct clk_hw *hw = sun60i_de352_hw_clks.hws[i]; + + if (!hw) + continue; + + ret = clk_hw_register(&pdev->dev, hw); + if (ret) { + dev_err(&pdev->dev, "Couldn't register clock %s: %d\n", + clk_hw_get_name(hw), ret); + de352->num_registered = i; + goto err_unreg_clks; + } + } + de352->num_registered = sun60i_de352_hw_clks.num; + + ret = of_clk_add_hw_provider(pdev->dev.of_node, of_clk_hw_onecell_get, + &sun60i_de352_hw_clks); + if (ret) + goto err_unreg_clks; + + de352->reset.base = reg; + de352->reset.lock = &de352->lock; + de352->reset.reset_map = sun60i_de352_resets; + de352->reset.rcdev.of_node = pdev->dev.of_node; + de352->reset.rcdev.ops = &sun60i_de352_reset_ops; + de352->reset.rcdev.owner = THIS_MODULE; + de352->reset.rcdev.nr_resets = ARRAY_SIZE(sun60i_de352_resets); + + ret = reset_controller_register(&de352->reset.rcdev); + if (ret) { + of_clk_del_provider(pdev->dev.of_node); + goto err_unreg_clks; + } + + ret = devm_add_action_or_reset(&pdev->dev, + sun60i_de352_clk_remove_action, de352); + if (ret) + return ret; + + return 0; + +err_unreg_clks: + for (i = de352->num_registered - 1; i >= 0; i--) { + struct clk_hw *hw = sun60i_de352_hw_clks.hws[i]; + + if (hw) + clk_hw_unregister(hw); + } + reset_control_assert(de352->rst_bus); +err_assert_sys_reset: + reset_control_assert(de352->rst_sys); +err_disable_clks: + clk_bulk_disable_unprepare(ARRAY_SIZE(de352->parents), de352->parents); + return ret; +} + +static const struct of_device_id sun60i_de352_clk_ids[] = { + { .compatible = "allwinner,sun60i-a733-de352-clk" }, + { } +}; +MODULE_DEVICE_TABLE(of, sun60i_de352_clk_ids); + +static struct platform_driver sun60i_de352_clk_driver = { + .probe = sun60i_de352_clk_probe, + .driver = { + .name = "sun60i-de352-clks", + .of_match_table = sun60i_de352_clk_ids, + }, +}; +module_platform_driver(sun60i_de352_clk_driver); + +MODULE_IMPORT_NS("SUNXI_CCU"); +MODULE_DESCRIPTION("Allwinner DE352 DETOP CCU"); +MODULE_LICENSE("GPL"); diff --git a/drivers/clk/sunxi-ng/ccu-sun60i-de352.h b/drivers/clk/sunxi-ng/ccu-sun60i-de352.h new file mode 100644 index 00000000000000..74b00f10a22211 --- /dev/null +++ b/drivers/clk/sunxi-ng/ccu-sun60i-de352.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ + +#ifndef _CCU_SUN60I_DE352_H_ +#define _CCU_SUN60I_DE352_H_ + +#include +#include + +#define CLK_NUMBER (CLK_MIXER1 + 1) + +#endif /* _CCU_SUN60I_DE352_H_ */ From 3b1e3c1dff6207b8b2657038c08dd3d3e477d422 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:19:17 +0800 Subject: [PATCH 08/16] drm/sun4i: add optional engine enable and quiesce hooks Allow an engine to prepare scanout before TCON enable and stop DMA before TCON disable. Invoke the optional hooks from the existing atomic CRTC enable and disable paths. Existing engines keep their current behavior without implementing the hooks. DE352 uses them to keep clock references and scanout state aligned across modesets. --- drivers/gpu/drm/sun4i/sun4i_crtc.c | 4 +++ drivers/gpu/drm/sun4i/sunxi_engine.h | 43 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/drivers/gpu/drm/sun4i/sun4i_crtc.c b/drivers/gpu/drm/sun4i/sun4i_crtc.c index 18e74047b0f566..8e6ff75a943af6 100644 --- a/drivers/gpu/drm/sun4i/sun4i_crtc.c +++ b/drivers/gpu/drm/sun4i/sun4i_crtc.c @@ -113,6 +113,8 @@ static void sun4i_crtc_atomic_disable(struct drm_crtc *crtc, DRM_DEBUG_DRIVER("Disabling the CRTC\n"); + sunxi_engine_quiesce(scrtc->engine, crtc); + drm_crtc_vblank_off(crtc); sun4i_tcon_set_status(scrtc->tcon, encoder, false); @@ -134,6 +136,8 @@ static void sun4i_crtc_atomic_enable(struct drm_crtc *crtc, DRM_DEBUG_DRIVER("Enabling the CRTC\n"); + sunxi_engine_enable(scrtc->engine, &crtc->state->adjusted_mode); + sun4i_tcon_set_status(scrtc->tcon, encoder, true); drm_crtc_vblank_on(crtc); diff --git a/drivers/gpu/drm/sun4i/sunxi_engine.h b/drivers/gpu/drm/sun4i/sunxi_engine.h index ec0c4932f15cf3..35e6515ec88c4c 100644 --- a/drivers/gpu/drm/sun4i/sunxi_engine.h +++ b/drivers/gpu/drm/sun4i/sunxi_engine.h @@ -7,6 +7,7 @@ #define _SUNXI_ENGINE_H_ struct drm_plane; +struct drm_atomic_state; struct drm_crtc; struct drm_device; struct drm_crtc_state; @@ -123,6 +124,23 @@ struct sunxi_engine_ops { */ void (*mode_set)(struct sunxi_engine *engine, const struct drm_display_mode *mode); + + /** + * @quiesce: + * + * Stop the engine from scanning out memory before the CRTC + * is disabled. Optional. + */ + void (*quiesce)(struct sunxi_engine *engine, struct drm_crtc *crtc); + + /** + * @enable: + * + * (Re)initialize the engine before the TCON is started. + * Optional. + */ + void (*enable)(struct sunxi_engine *engine, + const struct drm_display_mode *mode); }; /** @@ -215,4 +233,29 @@ sunxi_engine_mode_set(struct sunxi_engine *engine, if (engine->ops && engine->ops->mode_set) engine->ops->mode_set(engine, mode); } + +/** + * sunxi_engine_quiesce() - stop scanout before disabling the timing controller + * @engine: engine attached to the CRTC + * @crtc: CRTC being disabled + */ +static inline void +sunxi_engine_quiesce(struct sunxi_engine *engine, struct drm_crtc *crtc) +{ + if (engine->ops && engine->ops->quiesce) + engine->ops->quiesce(engine, crtc); +} + +/** + * sunxi_engine_enable() - prepare scanout before starting the timing controller + * @engine: engine attached to the CRTC + * @mode: adjusted display mode + */ +static inline void +sunxi_engine_enable(struct sunxi_engine *engine, + const struct drm_display_mode *mode) +{ + if (engine->ops && engine->ops->enable) + engine->ops->enable(engine, mode); +} #endif /* _SUNXI_ENGINE_H_ */ From cf281c2f9418620827b4892509ae4a2b196a9881 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:19:17 +0800 Subject: [PATCH 09/16] drm/sun4i: add A733 DE352 display engine Add a DE352 component to the existing sun4i DRM pipeline with its own Kconfig option. Expose linear ARGB8888, XRGB8888 and RGB565 primary scanout and an ARGB8888 cursor through the standard atomic plane interface. Program the blender, UI overlay, bypass blocks and channel routing using RCQ shadow buffers. Preserve the A733 ordering between DETOP routing, clock enable and RCQ submission, and rebuild state after a modeset. Require DMA CMA for the DRM master's reserved memory pool used by the RCQ allocator. Keep the hardware programming separate from the DRM component and report failed state commits. Limit this implementation to one mixer output and unscaled planes. --- drivers/gpu/drm/sun4i/Kconfig | 13 +- drivers/gpu/drm/sun4i/Makefile | 3 + drivers/gpu/drm/sun4i/sun4i_drv.c | 1 + drivers/gpu/drm/sun4i/sun60i_de.c | 485 +++++++++++ drivers/gpu/drm/sun4i/sun60i_de.h | 81 ++ drivers/gpu/drm/sun4i/sun60i_de_hw.c | 1193 ++++++++++++++++++++++++++ drivers/gpu/drm/sun4i/sun60i_de_hw.h | 14 + 7 files changed, 1789 insertions(+), 1 deletion(-) create mode 100644 drivers/gpu/drm/sun4i/sun60i_de.c create mode 100644 drivers/gpu/drm/sun4i/sun60i_de.h create mode 100644 drivers/gpu/drm/sun4i/sun60i_de_hw.c create mode 100644 drivers/gpu/drm/sun4i/sun60i_de_hw.h diff --git a/drivers/gpu/drm/sun4i/Kconfig b/drivers/gpu/drm/sun4i/Kconfig index b56ba00aabcaf5..9ddcf9783b6f28 100644 --- a/drivers/gpu/drm/sun4i/Kconfig +++ b/drivers/gpu/drm/sun4i/Kconfig @@ -80,9 +80,20 @@ config DRM_SUN8I_MIXER config DRM_SUN8I_TCON_TOP tristate - default DRM_SUN4I if DRM_SUN8I_MIXER!=n + default DRM_SUN4I if DRM_SUN8I_MIXER!=n || DRM_SUN60I_DE!=n help TCON TOP is responsible for configuring display pipeline for HDMI, TVE and LCD. +config DRM_SUN60I_DE + tristate "Allwinner A733 Display Engine 3.5.2" + depends on ARM64 || COMPILE_TEST + depends on DMA_CMA + select RESET_CONTROLLER + help + Enable the DE352 display engine found in the Allwinner A733. + The driver provides a primary plane and a cursor plane for the + sun4i DRM pipeline. If M is selected, the module is sun60i-de. + DMA CMA is required for the display engine's reserved memory pool. + endif diff --git a/drivers/gpu/drm/sun4i/Makefile b/drivers/gpu/drm/sun4i/Makefile index bad7497a0d11ed..42b2374ea5f556 100644 --- a/drivers/gpu/drm/sun4i/Makefile +++ b/drivers/gpu/drm/sun4i/Makefile @@ -18,6 +18,8 @@ sun8i-mixer-y += sun8i_mixer.o sun8i_ui_layer.o \ sun8i_vi_layer.o sun8i_ui_scaler.o \ sun8i_vi_scaler.o sun8i_csc.o +sun60i-de-y += sun60i_de.o sun60i_de_hw.o + sun4i-tcon-y += sun4i_crtc.o sun4i-tcon-y += sun4i_tcon_dclk.o sun4i-tcon-y += sun4i_lvds.o @@ -37,4 +39,5 @@ obj-$(CONFIG_DRM_SUN4I_HDMI) += sun4i-drm-hdmi.o obj-$(CONFIG_DRM_SUN6I_DSI) += sun6i_mipi_dsi.o obj-$(CONFIG_DRM_SUN8I_DW_HDMI) += sun8i-drm-hdmi.o obj-$(CONFIG_DRM_SUN8I_MIXER) += sun8i-mixer.o +obj-$(CONFIG_DRM_SUN60I_DE) += sun60i-de.o obj-$(CONFIG_DRM_SUN8I_TCON_TOP) += sun8i_tcon_top.o diff --git a/drivers/gpu/drm/sun4i/sun4i_drv.c b/drivers/gpu/drm/sun4i/sun4i_drv.c index c11dfb2739fa98..b6ef8947b6d04c 100644 --- a/drivers/gpu/drm/sun4i/sun4i_drv.c +++ b/drivers/gpu/drm/sun4i/sun4i_drv.c @@ -436,6 +436,7 @@ static const struct of_device_id sun4i_drv_of_table[] = { { .compatible = "allwinner,sun20i-d1-display-engine" }, { .compatible = "allwinner,sun50i-a64-display-engine" }, { .compatible = "allwinner,sun50i-h6-display-engine" }, + { .compatible = "allwinner,sun60i-a733-display-engine" }, { } }; MODULE_DEVICE_TABLE(of, sun4i_drv_of_table); diff --git a/drivers/gpu/drm/sun4i/sun60i_de.c b/drivers/gpu/drm/sun4i/sun60i_de.c new file mode 100644 index 00000000000000..a41baa1cd1c195 --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_de.c @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * Allwinner sun60i / A733 DE352 display engine for sun4i-drm. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sun4i_drv.h" +#include "sun60i_de.h" +#include "sun60i_de_hw.h" +#include "sunxi_engine.h" + +static const u32 sun60i_de_formats[] = { + DRM_FORMAT_ARGB8888, + DRM_FORMAT_XRGB8888, + DRM_FORMAT_RGB565, +}; + +static const u32 sun60i_de_cursor_formats[] = { + DRM_FORMAT_ARGB8888, +}; + +static const u64 sun60i_de_modifiers[] = { + DRM_FORMAT_MOD_LINEAR, + DRM_FORMAT_MOD_INVALID, +}; + +static u8 sun60i_de_bld_port_from_mode(u32 chn_cfg_mode, u8 ui_channel) +{ + if (ui_channel != 0) + return 3 + ui_channel; + + switch (chn_cfg_mode) { + case 1: + case 2: + return 2; + default: + return 3; + } +} + +static void sun60i_de_init_topology(struct sun60i_de *de) +{ + u32 chn_cfg_mode = 0; + u32 tcon_id = 4; + + de->display_id = 0; + /* + * DE2TCON nibble is the TCON *device_index*, not the OF graph + * endpoint reg. Reference A733 HDMI steady state is 0x0000fff4 + * (disp0 → tcon 4). Looking up port@display_id also fails here: + * sun4i output is port@1. Prefer DT "allwinner,tcon-id". + */ + de->tcon_id = 4; + de->ui_channel = 0; + de->chn_cfg_mode = 0; + + if (!of_property_read_u32(de->dev->of_node, "chn_cfg_mode", &chn_cfg_mode)) + de->chn_cfg_mode = chn_cfg_mode; + de->bld_port = sun60i_de_bld_port_from_mode(de->chn_cfg_mode, de->ui_channel); + + if (!of_property_read_u32(de->dev->of_node, "allwinner,tcon-id", &tcon_id)) + de->tcon_id = tcon_id & 0xf; +} + +static int sun60i_de_hw_enable(struct sun60i_de *de) +{ + int ret; + + if (de->enabled) + return 0; + + /* + * Parent clocks/resets are owned by display_clocks. The mixer only + * controls its exported DETOP module/MBUS gates and reset. DE352 must + * see the module clock before its core reset is released; reversing + * these two operations can wedge the DE AHB port before any RTMX MMIO. + */ + ret = clk_prepare_enable(de->clk_mixer); + if (ret) + return ret; + ret = reset_control_deassert(de->rst_mixer); + if (ret) + goto err_disable_clk_mixer; + ret = clk_prepare_enable(de->clk_bus_mixer); + if (ret) + goto err_assert_mixer_reset; + ret = sun60i_de_hw_bus_init(de); + if (ret) + goto err_disable_clk_bus_mixer; + + ret = sun60i_de_hw_bus_verify(de); + if (ret) + goto err_disable_clk_bus_mixer; + + de->enabled = true; + return 0; + +err_disable_clk_bus_mixer: + clk_disable_unprepare(de->clk_bus_mixer); +err_assert_mixer_reset: + reset_control_assert(de->rst_mixer); +err_disable_clk_mixer: + clk_disable_unprepare(de->clk_mixer); + + return ret; +} + +static void sun60i_de_hw_disable_clocks(struct sun60i_de *de) +{ + if (!de->enabled) + return; + + sun60i_de_hw_disable(de); + clk_disable_unprepare(de->clk_bus_mixer); + clk_disable_unprepare(de->clk_mixer); + reset_control_assert(de->rst_mixer); + + de->enabled = false; +} + +static struct sun60i_de_scanout_state * +sun60i_de_plane_scanout(struct sun60i_de *de, struct drm_plane *plane) +{ + return plane->type == DRM_PLANE_TYPE_CURSOR ? &de->cursor_scanout : + &de->scanout; +} + +static void sun60i_de_update_plane_scanout(struct sun60i_de *de, + struct drm_plane *plane, + struct drm_atomic_state *state) +{ + struct sun60i_de_scanout_state *scanout = sun60i_de_plane_scanout(de, plane); + struct drm_plane_state *new_state; + struct drm_plane_state *old_state; + + new_state = drm_atomic_get_new_plane_state(state, plane); + if (!new_state) + return; + old_state = drm_atomic_get_old_plane_state(state, plane); + + if (!new_state->crtc || !new_state->visible || !new_state->fb) { + memset(scanout, 0, sizeof(*scanout)); + return; + } + + drm_fb_dma_sync_non_coherent(plane->dev, old_state, new_state); + + scanout->enabled = true; + scanout->addr = drm_fb_dma_get_gem_addr(new_state->fb, new_state, 0); + scanout->pitch = new_state->fb->pitches[0]; + scanout->format = new_state->fb->format->format; + scanout->src_x = new_state->src.x1 >> 16; + scanout->src_y = new_state->src.y1 >> 16; + scanout->src_w = drm_rect_width(&new_state->src) >> 16; + scanout->src_h = drm_rect_height(&new_state->src) >> 16; + scanout->crtc_x = new_state->dst.x1; + scanout->crtc_y = new_state->dst.y1; + scanout->crtc_w = drm_rect_width(&new_state->dst); + scanout->crtc_h = drm_rect_height(&new_state->dst); +} + +static void sun60i_de_commit(struct sunxi_engine *engine, + struct drm_crtc *crtc, + struct drm_atomic_state *state) +{ + struct sun60i_de *de = engine_to_sun60i_de(engine); + int ret; + + sun60i_de_update_plane_scanout(de, &de->primary, state); + sun60i_de_update_plane_scanout(de, &de->cursor, state); + + if (!de->enabled || !de->mode_valid) { + dev_err_once(de->dev, + "de: commit skipped (enabled=%d mode_valid=%d)\n", + de->enabled, de->mode_valid); + return; + } + + ret = sun60i_de_hw_commit(de); + if (ret) + dev_err_ratelimited(de->dev, "Failed to commit display state: %d\n", ret); +} + +static void sun60i_de_mode_set(struct sunxi_engine *engine, + const struct drm_display_mode *mode) +{ + struct sun60i_de *de = engine_to_sun60i_de(engine); + + drm_mode_copy(&de->mode, mode); + de->mode_valid = true; +} + +static void sun60i_de_quiesce(struct sunxi_engine *engine, + struct drm_crtc *crtc) +{ + struct sun60i_de *de = engine_to_sun60i_de(engine); + + memset(&de->scanout, 0, sizeof(de->scanout)); + memset(&de->cursor_scanout, 0, sizeof(de->cursor_scanout)); + /* + * A modeset quiesce is a real hardware disable. Keep the clock + * framework state in lockstep with DETOP: sun60i_de_hw_disable() used + * to close the module/MBUS gates behind the framework's back while + * leaving de->enabled set. The following enable then returned early, + * so RCQ completed against clock-gated BLD/UI blocks and the display + * stayed black after the first mode change. + */ + sun60i_de_hw_disable_clocks(de); +} + +static void sun60i_de_enable(struct sunxi_engine *engine, + const struct drm_display_mode *mode) +{ + struct sun60i_de *de = engine_to_sun60i_de(engine); + int ret; + + drm_mode_copy(&de->mode, mode); + de->mode_valid = true; + + ret = sun60i_de_hw_enable(de); + if (ret) { + dev_err(de->dev, "de: enable failed: %d\n", ret); + return; + } +} + +static int sun60i_de_plane_atomic_check(struct drm_plane *plane, + struct drm_atomic_state *state) +{ + struct drm_plane_state *new_state; + struct drm_crtc_state *crtc_state; + int ret; + + new_state = drm_atomic_get_new_plane_state(state, plane); + if (!new_state->crtc) + return 0; + + crtc_state = drm_atomic_get_new_crtc_state(state, new_state->crtc); + if (WARN_ON(!crtc_state)) + return -EINVAL; + + ret = drm_atomic_helper_check_plane_state(new_state, crtc_state, + DRM_PLANE_NO_SCALING, + DRM_PLANE_NO_SCALING, + plane->type == DRM_PLANE_TYPE_CURSOR, + true); + if (ret) + return ret; + + if (plane->type == DRM_PLANE_TYPE_CURSOR && new_state->visible && + (drm_rect_width(&new_state->src) > 256 << 16 || + drm_rect_height(&new_state->src) > 256 << 16)) + return -EINVAL; + + return 0; +} + +static void sun60i_de_plane_atomic_disable(struct drm_plane *plane, + struct drm_atomic_state *state) +{ + struct sun60i_de *de = plane_to_sun60i_de(plane); + struct sun60i_de_scanout_state *scanout = sun60i_de_plane_scanout(de, plane); + + memset(scanout, 0, sizeof(*scanout)); +} + +static void sun60i_de_plane_atomic_update(struct drm_plane *plane, + struct drm_atomic_state *state) +{ + /* + * Scanout is latched in sun60i_de_commit() via the CRTC flush path. + * Provide a non-NULL atomic_update so drm_atomic_helper_commit_planes() + * does not call into a NULL function pointer. + */ +} + +static const struct drm_plane_helper_funcs sun60i_de_plane_helper_funcs = { + .prepare_fb = drm_gem_plane_helper_prepare_fb, + .atomic_check = sun60i_de_plane_atomic_check, + .atomic_update = sun60i_de_plane_atomic_update, + .atomic_disable = sun60i_de_plane_atomic_disable, +}; + +static const struct drm_plane_funcs sun60i_de_plane_funcs = { + .update_plane = drm_atomic_helper_update_plane, + .disable_plane = drm_atomic_helper_disable_plane, + .destroy = drm_plane_cleanup, + .reset = drm_atomic_helper_plane_reset, + .atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_plane_destroy_state, +}; + +static struct drm_plane **sun60i_de_layers_init(struct drm_device *drm, + struct sunxi_engine *engine) +{ + struct sun60i_de *de = engine_to_sun60i_de(engine); + struct drm_plane **planes; + int ret; + + planes = devm_kcalloc(drm->dev, 3, sizeof(*planes), GFP_KERNEL); + if (!planes) + return ERR_PTR(-ENOMEM); + + ret = drm_universal_plane_init(drm, &de->primary, 0, + &sun60i_de_plane_funcs, + sun60i_de_formats, + ARRAY_SIZE(sun60i_de_formats), + sun60i_de_modifiers, + DRM_PLANE_TYPE_PRIMARY, + "sun60i-primary"); + if (ret) + return ERR_PTR(ret); + + drm_plane_helper_add(&de->primary, &sun60i_de_plane_helper_funcs); + + ret = drm_universal_plane_init(drm, &de->cursor, 0, + &sun60i_de_plane_funcs, + sun60i_de_cursor_formats, + ARRAY_SIZE(sun60i_de_cursor_formats), + sun60i_de_modifiers, + DRM_PLANE_TYPE_CURSOR, + "sun60i-cursor"); + if (ret) + return ERR_PTR(ret); + + drm_plane_helper_add(&de->cursor, &sun60i_de_plane_helper_funcs); + + planes[0] = &de->primary; + planes[1] = &de->cursor; + planes[2] = NULL; + + return planes; +} + +static const struct sunxi_engine_ops sun60i_de_engine_ops = { + .commit = sun60i_de_commit, + .layers_init = sun60i_de_layers_init, + .mode_set = sun60i_de_mode_set, + .quiesce = sun60i_de_quiesce, + .enable = sun60i_de_enable, +}; + +/* + * T527-style mixer@100000 lists engine/top/display/detop. All sun60i_de + * offsets are from DE base (0x05000000); detop is DE+0x8000. Map the full + * 4MiB window without exclusive claim so clock@8000 can own DETOP. + */ +static int sun60i_de_map_regs(struct platform_device *pdev, struct sun60i_de *de) +{ + struct resource *detop, *res; + resource_size_t base; + + detop = platform_get_resource_byname(pdev, IORESOURCE_MEM, "detop"); + if (detop) { + if (detop->start < 0x8000) + return -EINVAL; + base = detop->start - 0x8000; + de->regs = devm_ioremap(&pdev->dev, base, 0x400000); + if (!de->regs) + return -ENOMEM; + return 0; + } + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EINVAL; + de->regs = devm_ioremap_resource(&pdev->dev, res); + return PTR_ERR_OR_ZERO(de->regs); +} + +static int sun60i_de_bind(struct device *dev, struct device *master, void *data) +{ + struct platform_device *pdev = to_platform_device(dev); + struct drm_device *drm = data; + struct sun4i_drv *drv = drm->dev_private; + struct sun60i_de *de; + int ret; + + de = devm_kzalloc(dev, sizeof(*de), GFP_KERNEL); + if (!de) + return -ENOMEM; + + de->dev = dev; + de->engine.ops = &sun60i_de_engine_ops; + de->engine.node = dev->of_node; + de->engine.id = 0; + + sun60i_de_init_topology(de); + + ret = sun60i_de_map_regs(pdev, de); + if (ret) + return dev_err_probe(dev, ret, "failed to map DE regs\n"); + + de->clk_mixer = devm_clk_get(dev, "mod"); + if (IS_ERR(de->clk_mixer)) + return dev_err_probe(dev, PTR_ERR(de->clk_mixer), + "failed to get mixer module clock\n"); + + de->clk_bus_mixer = devm_clk_get(dev, "bus"); + if (IS_ERR(de->clk_bus_mixer)) + return dev_err_probe(dev, PTR_ERR(de->clk_bus_mixer), + "failed to get mixer bus clock\n"); + + de->rst_mixer = devm_reset_control_get_shared(dev, "mod"); + if (IS_ERR(de->rst_mixer)) + return dev_err_probe(dev, PTR_ERR(de->rst_mixer), + "failed to get mixer reset\n"); + + ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)); + if (ret) + return ret; + + list_add_tail(&de->engine.list, &drv->engine_list); + dev_set_drvdata(dev, de); + + return 0; +} + +static void sun60i_de_unbind(struct device *dev, struct device *master, void *data) +{ + struct sun60i_de *de = dev_get_drvdata(dev); + + if (!de) + return; + + sun60i_de_hw_disable_clocks(de); + + /* The DRM master owns plane cleanup through drm_mode_config_cleanup(). */ + list_del(&de->engine.list); + dev_set_drvdata(dev, NULL); +} + +static const struct component_ops sun60i_de_ops = { + .bind = sun60i_de_bind, + .unbind = sun60i_de_unbind, +}; + +static int sun60i_de_probe(struct platform_device *pdev) +{ + return component_add(&pdev->dev, &sun60i_de_ops); +} + +static void sun60i_de_remove(struct platform_device *pdev) +{ + component_del(&pdev->dev, &sun60i_de_ops); +} + +static const struct of_device_id sun60i_de_of_match[] = { + { .compatible = "allwinner,sun60i-a733-de352-mixer-0" }, + { .compatible = "allwinner,sun60i-a733-de" }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, sun60i_de_of_match); + +static struct platform_driver sun60i_de_platform_driver = { + .probe = sun60i_de_probe, + .remove = sun60i_de_remove, + .driver = { + .name = "sun60i-de", + .of_match_table = sun60i_de_of_match, + }, +}; +module_platform_driver(sun60i_de_platform_driver); + +MODULE_DESCRIPTION("Allwinner sun60i DE352 display engine"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpu/drm/sun4i/sun60i_de.h b/drivers/gpu/drm/sun4i/sun60i_de.h new file mode 100644 index 00000000000000..490be4eb6da95a --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_de.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ + +#ifndef _SUN60I_DE_H_ +#define _SUN60I_DE_H_ + +#include +#include + +#include +#include +#include + +#include "sunxi_engine.h" + +struct sun60i_de_scanout_state { + bool enabled; + dma_addr_t addr; + u32 pitch; + u32 format; + u32 src_x; + u32 src_y; + u32 src_w; + u32 src_h; + u32 crtc_x; + u32 crtc_y; + u32 crtc_w; + u32 crtc_h; +}; + +struct sun60i_de { + struct sunxi_engine engine; + struct device *dev; + struct drm_plane primary; + struct drm_plane cursor; + struct clk *clk_mixer; + struct clk *clk_bus_mixer; + struct reset_control *rst_mixer; + void __iomem *regs; + struct drm_display_mode mode; + struct sun60i_de_scanout_state scanout; + struct sun60i_de_scanout_state cursor_scanout; + u8 display_id; + u8 tcon_id; + u8 ui_channel; + u8 bld_port; + u8 chn_cfg_mode; + bool mode_valid; + bool enabled; + bool rtmx_running; + u32 programmed_hdisplay; + u32 programmed_vdisplay; + bool rcq_ready; + bool rcq_head_done; + + dma_addr_t rcq_dma; + size_t rcq_alloc_size; + unsigned int rcq_nblocks; + void *rcq_cpu; + void *rcq_bld_shadow; + void *rcq_fmt_shadow; + void *rcq_ovl_shadow; + void *rcq_vsu_shadow; + void *rcq_tfbd_shadow; + void *rcq_ccsc_shadow; +}; + +static inline struct sun60i_de *engine_to_sun60i_de(struct sunxi_engine *engine) +{ + return container_of(engine, struct sun60i_de, engine); +} + +static inline struct sun60i_de *plane_to_sun60i_de(struct drm_plane *plane) +{ + if (plane->type == DRM_PLANE_TYPE_CURSOR) + return container_of(plane, struct sun60i_de, cursor); + + return container_of(plane, struct sun60i_de, primary); +} + +#endif /* _SUN60I_DE_H_ */ diff --git a/drivers/gpu/drm/sun4i/sun60i_de_hw.c b/drivers/gpu/drm/sun4i/sun60i_de_hw.c new file mode 100644 index 00000000000000..60568ea8813be6 --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_de_hw.c @@ -0,0 +1,1193 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* Allwinner sun60i / A733 DE352 display engine programming. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "sun60i_de.h" +#include "sun60i_de_hw.h" + +#define SUN60I_DE_RESET_OFFSET 0x8000 +#define SUN60I_DE_CLK_OFFSET 0x8004 +#define SUN60I_DE_MBUS_CLK_OFFSET 0x8008 +/* BSP de_top DE_RESERVE_CTL: bit12 enables RCQ FIFO update mode (v3xx) */ +#define SUN60I_DE_RESERVE_CTL_OFFSET 0x800c +#define SUN60I_DE_RESERVE_CTL_RCQ_FIFO_EN BIT(12) +/* sun60iw2 (A733): BSP de_top_set_ahb_read_mode(1) before RCQ/RTMX access */ +#define SUN60I_DE_RESERVE_CTL_AHB_CONFLICT BIT(8) +#define SUN60I_DE_VER_CTL_OFFSET 0x8014 +#define SUN60I_DE_DE2TCON_MUX_OFFSET 0x8010 +#define SUN60I_DE_VCH2CORE_MUX_OFFSET 0x8028 +#define SUN60I_DE_UCH2CORE_MUX_OFFSET 0x802c +#define SUN60I_DE_PORT2CHN_MUX_OFFSET 0x8030 +#define SUN60I_DE_BUF_DEPTH_OFFSET 0x8050 +#define SUN60I_DE_URGENCY_CTL_OFFSET 0x8080 +#define SUN60I_DE_GATING_CTL_OFFSET 0x80e8 +#define SUN60I_DE_GLB_CTL_OFFSET 0x8100 +#define SUN60I_DE_OUT_SIZE_OFFSET 0x8108 +#define SUN60I_DE_AUTO_CLK_OFFSET 0x810c +#define SUN60I_DE_AUTO_CLK_RCQ_GATE BIT(16) +#define SUN60I_DE_RCQ_CTL_OFFSET 0x8110 +#define SUN60I_DE_RCQ_HEADER_LADDR_OFFSET 0x8114 +#define SUN60I_DE_GLB_STS_OFFSET 0x8104 +#define SUN60I_DE_GLB_STS_BUSY BIT(4) +#define SUN60I_DE_FRAME_END BIT(0) +#define SUN60I_DE_RCQ_FINISH BIT(2) +#define SUN60I_DE_RCQ_ACCEPT BIT(3) +#define SUN60I_DE_IRQ_STATE_MASK GENMASK(3, 0) + +#define SUN60I_DE_DISP_CLK_SHIFT 4 + +#define SUN60I_DE_GLB_CTL_RTMX_EN BIT(0) +#define SUN60I_DE_GLB_CTL_FIELD_REVERSE BIT(9) + +#define SUN60I_DE_DISP_STRIDE 0x40 +#define SUN60I_DE_ASYNC_BRIDGE_OFFSET 0x804c +#define SUN60I_DE_CHN_SIZE 0x20000 +#define SUN60I_DE_CHN_BASE(phy) (0x100000 + (phy) * SUN60I_DE_CHN_SIZE) +#define SUN60I_DE_CHN_OVL_OFFSET 0x1000 +#define SUN60I_DE_CHN_SCALER_OFFSET 0x4000 +#define SUN60I_DE_UI_PHY_CHN 6 +#define SUN60I_DE_UI_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_OVL_OFFSET) +#define SUN60I_DE_CHN_CCSC_OFFSET 0x0800 +#define SUN60I_DE_CHN_SNR_OFFSET 0x6400 +#define SUN60I_DE_CHN_SHARP_OFFSET 0x6000 +#define SUN60I_DE_UI_CCSC_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_CCSC_OFFSET) +#define SUN60I_DE_UI_SNR_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_SNR_OFFSET) +#define SUN60I_DE_UI_SHARP_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_SHARP_OFFSET) +#define SUN60I_DE_UI_SCALER_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_SCALER_OFFSET) +#define SUN60I_DE_DISP_BASE(disp) (0x280000 + (disp) * 0x20000) +#define SUN60I_DE_FMT_BASE(disp) (SUN60I_DE_DISP_BASE(disp) + 0x5000) +#define SUN60I_DE_DITHER_BASE(disp) (SUN60I_DE_DISP_BASE(disp) + 0x8000) +#define SUN60I_DE_GAMMA_BASE(disp) (SUN60I_DE_DISP_BASE(disp) + 0x9000) +#define SUN60I_DE_BLD_BASE(disp) (SUN60I_DE_DISP_BASE(disp) + 0x1000) + +#define SUN60I_DE_BLD_EN 0x0000 +#define SUN60I_DE_BLD_PIPE_FCOLOR(pipe) (0x0004 + (pipe) * 0x10) +#define SUN60I_DE_BLD_PIPE_IN_SIZE(pipe) (0x0008 + (pipe) * 0x10) +#define SUN60I_DE_BLD_PIPE_IN_COORD(pipe) (0x000c + (pipe) * 0x10) +#define SUN60I_DE_BLD_ROUT_CTL 0x0080 +#define SUN60I_DE_BLD_PREMUL_CTL 0x0084 +#define SUN60I_DE_BLD_BG_COLOR 0x0088 +#define SUN60I_DE_BLD_OUT_SIZE 0x008c +#define SUN60I_DE_BLD_BLEND_CTL(pipe) (0x0090 + (pipe) * 0x4) +#define SUN60I_DE_BLD_OUT_CTL 0x00fc + +#define SUN60I_DE_BLD_FCOLOR_EN(pipe) BIT(pipe) +#define SUN60I_DE_BLD_PIPE_EN(pipe) BIT(8 + (pipe)) +#define SUN60I_DE_BLD_MODE_SRCOVER 0x03010301 +#define SUN60I_DE_BLD_MODE_DST 0x01000100 +#define SUN60I_DE_BLD_OUT_CTL_INTERLACE BIT(1) +#define SUN60I_DE_BLD_OUT_CTL_FMT_RGB (0 << 8) + +#define SUN60I_DE_CCSC_CTL_OFF 0x00 +#define SUN60I_DE_CCSC_D0_OFF 0x04 +#define SUN60I_DE_CCSC_C0_OFF 0x10 +#define SUN60I_DE_CCSC_BYPASS_COEFF 0x00000400 +#define SUN60I_DE_SHARP_CTL_OFF 0x00 + +#define SUN60I_DE_RCQ_FMT_SIZE 0x2c +#define SUN60I_DE_FMT_SIZE_REG_OFF 0x04 +#define SUN60I_DE_FMT_LIMIT_Y_OFF 0x20 +#define SUN60I_DE_FMT_LIMIT_C0_OFF 0x24 +#define SUN60I_DE_FMT_LIMIT_C1_OFF 0x28 +#define SUN60I_DE_FMT_LIMIT_FULL 0x0fff0000 +#define SUN60I_DE_RCQ_OVL_DS_OFF 0xe0 +#define SUN60I_DE_RCQ_OVL_DS_SIZE 0x1c +#define SUN60I_DE_CHN_TFBD_OFFSET 0x5400 +#define SUN60I_DE_UI_TFBD_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_TFBD_OFFSET) + +#define SUN60I_DE_VSU_CTL_SIZE 0x14 +#define SUN60I_DE_VSU_ATTR_SIZE 0x08 +#define SUN60I_DE_VSU_PARA_SIZE 0x1c +#define SUN60I_DE_VSU_COEFF_SIZE 0x80 +#define SUN60I_DE_VSU8_CPARA_OFF 0x0c0 +#define SUN60I_DE_VSU8_COEFF0_OFF 0x200 +#define SUN60I_DE_VSU8_COEFF1_OFF 0x400 +#define SUN60I_DE_VSU8_COEFF2_OFF 0x600 +#define SUN60I_DE_VSU_SHADOW_SIZE 0x680 + +#define SUN60I_DE_RCQ_TFBD_SIZE 0x4 +#define SUN60I_DE_RCQ_CCSC_CTL_SIZE 0x4 +#define SUN60I_DE_RCQ_ALIGN 32 +#define SUN60I_DE_RCQ_BLOCK_COUNT 19 + +#define SUN60I_DE_UI_LAY_STRIDE 0x20 +#define SUN60I_DE_UI_LAY_CTL(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x00) +#define SUN60I_DE_UI_LAY_MBSIZE(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x04) +#define SUN60I_DE_UI_LAY_MBCOOR(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x08) +#define SUN60I_DE_UI_LAY_PITCH(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x0c) +#define SUN60I_DE_UI_LAY_TOP_LADDR(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x10) +#define SUN60I_DE_UI_LAY_BOT_LADDR(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x14) +#define SUN60I_DE_UI_LAY_FCOLOR(layer) ((layer) * SUN60I_DE_UI_LAY_STRIDE + 0x18) +#define SUN60I_DE_UI_TOP_HADDR 0x0080 +#define SUN60I_DE_UI_BOT_HADDR 0x0084 +#define SUN60I_DE_UI_WIN_SIZE 0x0088 +#define SUN60I_DE_UI_HORI_DS 0x00f0 +#define SUN60I_DE_UI_VERT_DS 0x00f8 + +#define SUN60I_DE_UI_CTL_EN BIT(0) +#define SUN60I_DE_UI_CTL_ALPHA_MODE_SHIFT 1 +#define SUN60I_DE_UI_CTL_FMT_SHIFT 8 +#define SUN60I_DE_UI_CTL_GLB_ALPHA_SHIFT 24 +#define SUN60I_DE_UI_CTL_COLOR_FILL BIT(4) + +#define SUN60I_DE_VSU8_CTL_EN BIT(0) +#define SUN60I_DE_VSU8_SCALE_MODE_OFF 0x10 +#define SUN60I_DE_VSU8_OUT_SIZE_OFF 0x40 +#define SUN60I_DE_VSU8_GLB_ALPHA_OFF 0x44 +#define SUN60I_DE_VSU8_Y_IN_SIZE_OFF 0x80 +#define SUN60I_DE_VSU8_Y_HSTEP_OFF 0x88 +#define SUN60I_DE_VSU8_Y_VSTEP_OFF 0x8c +#define SUN60I_DE_VSU8_Y_HPHASE_OFF 0x90 +#define SUN60I_DE_VSU8_Y_VPHASE_OFF 0x98 +/* VSU8 1:1 step: (1 << 19) << VSU8_STEP_VALID_START_BIT(1) */ +#define SUN60I_DE_VSU8_STEP_1TO1 0x100000 + +#define SUN60I_DE_MBUS_CLK_EN BIT(0) +#define SUN60I_DE_MBUS_RESET_DEASSERT BIT(4) +#define SUN60I_DE_MBUS_AUTO_GATE BIT(8) + +#define SUN60I_DE_RCQ_HEAD_SIZE 16 +#define SUN60I_DE_RCQ_HEAD_COUNT \ + ALIGN(SUN60I_DE_RCQ_BLOCK_COUNT, 2) +#define SUN60I_DE_RCQ_HEAD_LEN (SUN60I_DE_RCQ_HEAD_COUNT * \ + SUN60I_DE_RCQ_HEAD_SIZE) +#define SUN60I_DE_RCQ_HEAD_LEN_ALIGNED (ALIGN(SUN60I_DE_RCQ_HEAD_LEN, \ + SUN60I_DE_RCQ_HEAD_SIZE * 2)) +#define SUN60I_DE_RCQ_OVL_LAY0_SIZE 0x1c +#define SUN60I_DE_RCQ_OVL_PARA_SIZE 0x0c +#define SUN60I_DE_RCQ_OVL_PARA_OFF 0x80 +#define SUN60I_DE_RCQ_BLD_ATTR_SIZE 0x60 +#define SUN60I_DE_RCQ_BLD_CTL_SIZE 0x24 +#define SUN60I_DE_RCQ_BLD_CTL_OFF 0x80 +#define SUN60I_DE_RCQ_BLD_CK_SIZE 0x60 +#define SUN60I_DE_RCQ_BLD_CK_OFF 0xa0 +#define SUN60I_DE_RCQ_OVL_SHADOW_SIZE 0x100 +#define SUN60I_DE_RCQ_BLD_SHADOW_SIZE 0x100 + +#define SUN60I_DE_AHB_TIMEOUT_OFFSET 0x80e4 + +/* HDMI UI phys6: clk gating bit (phy + 2), see de_rtmx_set_chn_mux sun60iw2 */ +#define SUN60I_DE_HDMI_UI_GATING BIT(8) + +#define SUN60I_DE_GLB_CTL_PIXEL_MODE_SHIFT 16 + +#define SUN60I_DE_CHN_CDC_OFFSET 0x8000 +#define SUN60I_DE_UI_CDC_BASE (SUN60I_DE_CHN_BASE(SUN60I_DE_UI_PHY_CHN) + \ + SUN60I_DE_CHN_CDC_OFFSET) + +/* TCON DEBUG @0xfc — current scanline (DE352 RCQ wait window, see BSP rcq_wait_line) */ +#define SUN60I_TCON_DEBUG_OFF 0x00fc +#define SUN60I_TCON_DEBUG_TV_LINE_MASK GENMASK(11, 0) + +struct sun60i_de_rcq_head { + u32 low_addr; + u32 dw0; + u32 dirty; + u32 reg_offset; +}; + +static void sun60i_de_top_auto_clk_write(struct sun60i_de *de, bool enable, + bool rcq_gate); +static void sun60i_de_top_glb_rtmx_write(struct sun60i_de *de, bool enable); +static void sun60i_de_top_set_de2tcon_mux(struct sun60i_de *de, u32 tcon); +static void sun60i_de_top_setup_chn_mux(struct sun60i_de *de); +static void sun60i_de_rcq_program_head(struct sun60i_de *de); +static void sun60i_de_top_rtmx_buf_depth(struct sun60i_de *de); +static void sun60i_de_top_set_out_size(struct sun60i_de *de); +static void sun60i_de_top_set_urgency(struct sun60i_de *de); + +static inline void sun60i_de_writel(struct sun60i_de *de, u32 reg, u32 value) +{ + writel(value, de->regs + reg); +} + +static inline u32 sun60i_de_readl(struct sun60i_de *de, u32 reg) +{ + return readl(de->regs + reg); +} + +/* RMW one field — matches BSP SET_BITS / sun55i_de regmap_read pattern. */ +static void sun60i_de_rmw(struct sun60i_de *de, u32 reg, u32 mask, u32 val) +{ + u32 tmp = sun60i_de_readl(de, reg); + + tmp = (tmp & ~mask) | (val & mask); + sun60i_de_writel(de, reg, tmp); +} + +static void sun60i_de_rmw_bits(struct sun60i_de *de, u32 reg, u32 shift, + u32 width, u32 val) +{ + u32 mask = ((width >= 32) ? ~0U : ((1U << width) - 1U)) << shift; + + sun60i_de_rmw(de, reg, mask, (val << shift) & mask); +} + +static void sun60i_de_shadow_writel(void *shadow, u32 offset, u32 value) +{ + writel(value, (u8 __iomem *)shadow + offset); +} + +static u32 sun60i_de_rcq_pack_dw0(dma_addr_t phy, u32 len) +{ + return (len & 0xffffff) | ((phy >> 32) & 0xff) << 24; +} + +static void sun60i_de_vsu_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + sun60i_de_shadow_writel(de->rcq_vsu_shadow, offset, value); +} + +static void sun60i_de_ccsc_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + sun60i_de_shadow_writel(de->rcq_ccsc_shadow, offset, value); +} + +static void sun60i_de_tfbd_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + sun60i_de_shadow_writel(de->rcq_tfbd_shadow, offset, value); +} + +static void sun60i_de_fmt_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + sun60i_de_shadow_writel(de->rcq_fmt_shadow, offset, value); +} + +static void sun60i_de_ovl_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + sun60i_de_shadow_writel(de->rcq_ovl_shadow, offset, value); +} + +static void sun60i_de_bld_writel(struct sun60i_de *de, u32 offset, u32 value) +{ + if (de->rcq_bld_shadow) + sun60i_de_shadow_writel(de->rcq_bld_shadow, offset, value); +} + +static size_t sun60i_de_rcq_pool_data_size(void) +{ + size_t size = 0; + + size += ALIGN(SUN60I_DE_RCQ_BLD_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + size += ALIGN(SUN60I_DE_RCQ_FMT_SIZE, SUN60I_DE_RCQ_ALIGN); + size += ALIGN(SUN60I_DE_RCQ_OVL_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + size += ALIGN(SUN60I_DE_VSU_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + size += ALIGN(SUN60I_DE_RCQ_TFBD_SIZE, SUN60I_DE_RCQ_ALIGN); + size += ALIGN(SUN60I_DE_RCQ_CCSC_CTL_SIZE, SUN60I_DE_RCQ_ALIGN); + + return size; +} + +static struct device *sun60i_de_dma_dev(struct sun60i_de *de) +{ + /* + * memory-region = <&drm_cma> is on the sun4i_drv master node, not the + * DE component. Allocating via de->dev has no cma_area and dma_alloc_wc + * then wedges the SoC on A733 (hang right after the warn). + */ + if (de->primary.dev) + return de->primary.dev->dev; + + return de->dev; +} + +static size_t sun60i_de_rcq_total_size(void) +{ + size_t size = SUN60I_DE_RCQ_HEAD_LEN_ALIGNED + sun60i_de_rcq_pool_data_size(); + + size = PAGE_ALIGN(size); + if (size <= PAGE_SIZE) + size = PAGE_SIZE * 2; + + return size; +} + +static void sun60i_de_rcq_sync_for_device(struct sun60i_de *de) +{ + /* + * RCQ pool comes from dma_alloc_wc(), not dma_map_single(). Calling + * dma_sync_single_for_device() on a coherent/WC dma_addr is undefined + * and on A733 with the IOMMU can leave the mapping unusable. + * WC already needs only a write barrier so the DE DMA sees CPU stores. + */ + if (!de->rcq_cpu) + return; + + /* Publish WC descriptor and shadow writes before the RCQ doorbell. */ + wmb(); +} + +static u32 sun60i_de_pack_size(u32 width, u32 height) +{ + return (width ? width - 1 : 0) | + (height ? (height - 1) << 16 : 0); +} + +static void sun60i_de_hw_program_fmt(struct sun60i_de *de) +{ + u32 size; + + if (!de->rcq_fmt_shadow || !de->mode_valid) + return; + + size = sun60i_de_pack_size(de->mode.crtc_hdisplay, de->mode.crtc_vdisplay); + + memset(de->rcq_fmt_shadow, 0, SUN60I_DE_RCQ_FMT_SIZE); + sun60i_de_fmt_writel(de, SUN60I_DE_FMT_SIZE_REG_OFF, size); + sun60i_de_fmt_writel(de, SUN60I_DE_FMT_LIMIT_Y_OFF, SUN60I_DE_FMT_LIMIT_FULL); + sun60i_de_fmt_writel(de, SUN60I_DE_FMT_LIMIT_C0_OFF, SUN60I_DE_FMT_LIMIT_FULL); + sun60i_de_fmt_writel(de, SUN60I_DE_FMT_LIMIT_C1_OFF, SUN60I_DE_FMT_LIMIT_FULL); +} + +static void sun60i_de_hw_program_tfbd(struct sun60i_de *de) +{ + if (!de->rcq_tfbd_shadow) + return; + + memset(de->rcq_tfbd_shadow, 0, SUN60I_DE_RCQ_TFBD_SIZE); + sun60i_de_tfbd_writel(de, 0, 0); +} + +static void sun60i_de_rcq_free(struct sun60i_de *de) +{ + struct device *dma_dev = sun60i_de_dma_dev(de); + + if (!de->rcq_cpu) + return; + + dma_free_wc(dma_dev, de->rcq_alloc_size ? de->rcq_alloc_size : + sun60i_de_rcq_total_size(), de->rcq_cpu, de->rcq_dma); + de->rcq_cpu = NULL; + de->rcq_dma = 0; + de->rcq_alloc_size = 0; + de->rcq_nblocks = 0; + de->rcq_bld_shadow = NULL; + de->rcq_fmt_shadow = NULL; + de->rcq_ovl_shadow = NULL; + de->rcq_vsu_shadow = NULL; + de->rcq_tfbd_shadow = NULL; + de->rcq_ccsc_shadow = NULL; + de->rcq_ready = false; + de->rcq_head_done = false; +} + +static void sun60i_de_rcq_fill_head(struct sun60i_de_rcq_head *head, + dma_addr_t phy, u32 len, u32 reg_off) +{ + head->low_addr = lower_32_bits(phy); + head->dw0 = sun60i_de_rcq_pack_dw0(phy, len); + head->dirty = 0; + head->reg_offset = reg_off; +} + +static void sun60i_de_rcq_layout_shadows(u8 *base, dma_addr_t pool_dma, + struct sun60i_de *de, + dma_addr_t *bld_dma, dma_addr_t *fmt_dma, + dma_addr_t *ovl_dma, dma_addr_t *vsu_dma, + dma_addr_t *tfbd_dma, dma_addr_t *ccsc_dma) +{ + size_t off = SUN60I_DE_RCQ_HEAD_LEN_ALIGNED; + + de->rcq_bld_shadow = base + off; + *bld_dma = pool_dma + off; + off += ALIGN(SUN60I_DE_RCQ_BLD_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + + de->rcq_fmt_shadow = base + off; + *fmt_dma = pool_dma + off; + off += ALIGN(SUN60I_DE_RCQ_FMT_SIZE, SUN60I_DE_RCQ_ALIGN); + + de->rcq_ovl_shadow = base + off; + *ovl_dma = pool_dma + off; + off += ALIGN(SUN60I_DE_RCQ_OVL_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + + de->rcq_vsu_shadow = base + off; + *vsu_dma = pool_dma + off; + off += ALIGN(SUN60I_DE_VSU_SHADOW_SIZE, SUN60I_DE_RCQ_ALIGN); + + de->rcq_tfbd_shadow = base + off; + *tfbd_dma = pool_dma + off; + off += ALIGN(SUN60I_DE_RCQ_TFBD_SIZE, SUN60I_DE_RCQ_ALIGN); + + de->rcq_ccsc_shadow = base + off; + *ccsc_dma = pool_dma + off; +} + +static int sun60i_de_rcq_init(struct sun60i_de *de) +{ + struct sun60i_de_rcq_head *heads; + struct device *dma_dev = sun60i_de_dma_dev(de); + dma_addr_t bld_dma, fmt_dma, ovl_dma, vsu_dma, tfbd_dma, ccsc_dma; + dma_addr_t bld_base = SUN60I_DE_BLD_BASE(de->display_id); + dma_addr_t fmt_base = SUN60I_DE_FMT_BASE(de->display_id); + size_t size; + u8 *base; + unsigned int idx = 0; + + if (de->rcq_ready) + return 0; + + size = sun60i_de_rcq_total_size(); + + if (!dma_dev->cma_area) { + dev_err(de->dev, + "de: no cma_area on dma_dev %s (need master memory-region)\n", + dev_name(dma_dev)); + return -ENODEV; + } + + base = dma_alloc_wc(dma_dev, size, &de->rcq_dma, GFP_KERNEL); + if (!base) { + dev_err(de->dev, "de: rcq dma_alloc_wc(%zu) failed\n", size); + return -ENOMEM; + } + + de->rcq_cpu = base; + memset(base, 0, size); + heads = (struct sun60i_de_rcq_head *)base; + + sun60i_de_rcq_layout_shadows(base, de->rcq_dma, de, &bld_dma, &fmt_dma, + &ovl_dma, &vsu_dma, &tfbd_dma, &ccsc_dma); + + /* BLD ×3 */ + sun60i_de_rcq_fill_head(&heads[idx++], bld_dma, SUN60I_DE_RCQ_BLD_ATTR_SIZE, + bld_base); + sun60i_de_rcq_fill_head(&heads[idx++], + bld_dma + SUN60I_DE_RCQ_BLD_CTL_OFF, + SUN60I_DE_RCQ_BLD_CTL_SIZE, + bld_base + SUN60I_DE_RCQ_BLD_CTL_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + bld_dma + SUN60I_DE_RCQ_BLD_CK_OFF, + SUN60I_DE_RCQ_BLD_CK_SIZE, + bld_base + SUN60I_DE_RCQ_BLD_CK_OFF); + /* FMT */ + sun60i_de_rcq_fill_head(&heads[idx++], fmt_dma, SUN60I_DE_RCQ_FMT_SIZE, + fmt_base); + /* OVL ×6 (LAY0-3 + PARA + DS) */ + sun60i_de_rcq_fill_head(&heads[idx++], ovl_dma, SUN60I_DE_RCQ_OVL_LAY0_SIZE, + SUN60I_DE_UI_BASE); + sun60i_de_rcq_fill_head(&heads[idx++], + ovl_dma + SUN60I_DE_UI_LAY_STRIDE, + SUN60I_DE_RCQ_OVL_LAY0_SIZE, + SUN60I_DE_UI_BASE + SUN60I_DE_UI_LAY_STRIDE); + sun60i_de_rcq_fill_head(&heads[idx++], + ovl_dma + SUN60I_DE_UI_LAY_STRIDE * 2, + SUN60I_DE_RCQ_OVL_LAY0_SIZE, + SUN60I_DE_UI_BASE + SUN60I_DE_UI_LAY_STRIDE * 2); + sun60i_de_rcq_fill_head(&heads[idx++], + ovl_dma + SUN60I_DE_UI_LAY_STRIDE * 3, + SUN60I_DE_RCQ_OVL_LAY0_SIZE, + SUN60I_DE_UI_BASE + SUN60I_DE_UI_LAY_STRIDE * 3); + sun60i_de_rcq_fill_head(&heads[idx++], + ovl_dma + SUN60I_DE_RCQ_OVL_PARA_OFF, + SUN60I_DE_RCQ_OVL_PARA_SIZE, + SUN60I_DE_UI_BASE + SUN60I_DE_RCQ_OVL_PARA_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + ovl_dma + SUN60I_DE_RCQ_OVL_DS_OFF, + SUN60I_DE_RCQ_OVL_DS_SIZE, + SUN60I_DE_UI_BASE + SUN60I_DE_RCQ_OVL_DS_OFF); + /* VSU8 ×7 */ + sun60i_de_rcq_fill_head(&heads[idx++], vsu_dma, SUN60I_DE_VSU_CTL_SIZE, + SUN60I_DE_UI_SCALER_BASE); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_OUT_SIZE_OFF, + SUN60I_DE_VSU_ATTR_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_OUT_SIZE_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_Y_IN_SIZE_OFF, + SUN60I_DE_VSU_PARA_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_Y_IN_SIZE_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_CPARA_OFF, + SUN60I_DE_VSU_PARA_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_CPARA_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_COEFF0_OFF, + SUN60I_DE_VSU_COEFF_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_COEFF0_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_COEFF1_OFF, + SUN60I_DE_VSU_COEFF_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_COEFF1_OFF); + sun60i_de_rcq_fill_head(&heads[idx++], + vsu_dma + SUN60I_DE_VSU8_COEFF2_OFF, + SUN60I_DE_VSU_COEFF_SIZE, + SUN60I_DE_UI_SCALER_BASE + SUN60I_DE_VSU8_COEFF2_OFF); + /* TFBD + CCSC disable */ + sun60i_de_rcq_fill_head(&heads[idx++], tfbd_dma, SUN60I_DE_RCQ_TFBD_SIZE, + SUN60I_DE_UI_TFBD_BASE); + sun60i_de_rcq_fill_head(&heads[idx++], ccsc_dma, SUN60I_DE_RCQ_CCSC_CTL_SIZE, + SUN60I_DE_UI_CCSC_BASE); + + WARN_ON(idx != SUN60I_DE_RCQ_BLOCK_COUNT); + + de->rcq_ready = true; + de->rcq_nblocks = SUN60I_DE_RCQ_BLOCK_COUNT; + de->rcq_alloc_size = size; + + return 0; +} + +/* + * DE352 RCQ updates must land in the vertical blanking window (BSP + * rcq_wait_line=true). Pulsing RCQ mid-frame can leave stale/white pixels. + */ +static void sun60i_de_rcq_wait_safe_line(struct sun60i_de *de) +{ + u32 status; + u32 status_reg = SUN60I_DE_GLB_STS_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + + if (!de->rtmx_running) + return; + + /* + * BSP submits RCQ from the display sync tasklet. Use RTMX frame-end as + * the same synchronization point: clear the stale W1C flag, wait for a + * fresh frame boundary, then pulse RCQ. A timeout is non-fatal during + * the first enable because TCON may not have started scanning yet. + */ + sun60i_de_writel(de, status_reg, SUN60I_DE_FRAME_END); + if (readl_poll_timeout_atomic(de->regs + status_reg, status, + status & SUN60I_DE_FRAME_END, + 50, 50000)) + dev_dbg(de->dev, "de: no frame-end before RCQ update\n"); +} + +/* + * Initialise the RCQ head dirty flags only. The head pointer is pushed into + * the RCQ FIFO by sun60i_de_rcq_submit() on every update (BSP re-pushes each + * request), so we must not push it here as well or the FIFO would double-count. + */ +static void sun60i_de_rcq_program_head(struct sun60i_de *de) +{ + struct sun60i_de_rcq_head *heads = de->rcq_cpu; + u32 head_reg = SUN60I_DE_RCQ_HEADER_LADDR_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 head_len; + unsigned int i, nblocks; + + nblocks = de->rcq_nblocks ? de->rcq_nblocks : SUN60I_DE_RCQ_BLOCK_COUNT; + head_len = ALIGN(nblocks, 2) * SUN60I_DE_RCQ_HEAD_SIZE; + head_len = ALIGN(head_len, SUN60I_DE_RCQ_HEAD_SIZE * 2); + + for (i = 0; i < nblocks; i++) + heads[i].dirty = 0; + + sun60i_de_rcq_sync_for_device(de); + + /* The hardware requires the RCQ length to be written last. */ + sun60i_de_writel(de, head_reg, lower_32_bits(de->rcq_dma)); + sun60i_de_writel(de, head_reg + 0x4, upper_32_bits(de->rcq_dma)); + sun60i_de_writel(de, head_reg + 0x8, head_len); +} + +static void sun60i_de_rcq_submit(struct sun60i_de *de) +{ + struct sun60i_de_rcq_head *heads = de->rcq_cpu; + u32 ctl_reg = SUN60I_DE_RCQ_CTL_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 status_reg = SUN60I_DE_GLB_STS_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 status; + unsigned int i, nblocks; + + if (!de->rcq_head_done) + return; + + nblocks = de->rcq_nblocks ? de->rcq_nblocks : SUN60I_DE_RCQ_BLOCK_COUNT; + + for (i = 0; i < nblocks; i++) + heads[i].dirty = 1; + + /* + * dma_alloc_wc() memory is write-combining: flush the CPU write buffer + * to DRAM so the RCQ DMA engine reads the freshly filled head+shadow, + * not stale/zero data (a zeroed shadow disables BLD -> black screen). + */ + wmb(); + sun60i_de_rcq_sync_for_device(de); + + sun60i_de_rcq_wait_safe_line(de); + + /* Do not mistake a previous request's sticky W1C bits for completion. */ + sun60i_de_writel(de, status_reg, + SUN60I_DE_RCQ_ACCEPT | SUN60I_DE_RCQ_FINISH); + + /* + * DE352 (Android dump): RESERVE_CTL=0 (no RCQ FIFO), head programmed + * once @0x8114, updates via de_top_set_rcq_update() = RCQ_CTL bit0 + * only. RCQ FIFO + head re-push is de355-only. + */ + sun60i_de_writel(de, ctl_reg, BIT(0)); + + if (readl_poll_timeout_atomic(de->regs + status_reg, status, + status & SUN60I_DE_RCQ_FINISH, + 10, 50000)) { + dev_err_ratelimited(de->dev, + "de: RCQ update timed out (status=0x%08x)\n", + status); + return; + } + + /* Ack this request so the next submit must observe a fresh completion. */ + sun60i_de_writel(de, status_reg, + status & (SUN60I_DE_RCQ_ACCEPT | SUN60I_DE_RCQ_FINISH)); +} + +enum sun60i_de_rgb_format { + SUN60I_DE_FMT_ARGB8888 = 0x00, + SUN60I_DE_FMT_XRGB8888 = 0x04, + SUN60I_DE_FMT_RGB565 = 0x0a, +}; + +/* VSU8: BSP disables for RGB 1:1; partial enable → fixed green on A733 */ +static bool sun60i_de_needs_scaler(struct sun60i_de *de) +{ + /* + * BSP de_rtmx_chn_fix_size(): RGB/YUV444 with ovl_out == scn_out + * leaves scale_en=0 and de_scaler_disable() — VSU8 must not run with + * partial setup (missing C-plane + FIR coeffs → fixed green garbage). + */ + if (!de->scanout.enabled) + return false; + + return de->scanout.src_w != de->mode.crtc_hdisplay || + de->scanout.src_h != de->mode.crtc_vdisplay; +} + +static void sun60i_de_hw_program_vsu8(struct sun60i_de *de) +{ + u32 size; + + if (!de->rcq_vsu_shadow) + return; + + memset(de->rcq_vsu_shadow, 0, SUN60I_DE_VSU_SHADOW_SIZE); + + if (!sun60i_de_needs_scaler(de)) { + /* + * A523 patch 0009: 1:1 @1080p bypass VSU (en=0). Line buffer + * is 2560px; unity step is only needed above that width. + */ + sun60i_de_vsu_writel(de, 0, 0); + + return; + } + + size = sun60i_de_pack_size(de->mode.crtc_hdisplay, de->mode.crtc_vdisplay); + + sun60i_de_vsu_writel(de, 0, SUN60I_DE_VSU8_CTL_EN); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_SCALE_MODE_OFF, 0); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_OUT_SIZE_OFF, size); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_GLB_ALPHA_OFF, 0xff); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_Y_IN_SIZE_OFF, size); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_Y_HSTEP_OFF, SUN60I_DE_VSU8_STEP_1TO1); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_Y_VSTEP_OFF, SUN60I_DE_VSU8_STEP_1TO1); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_CPARA_OFF, size); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_CPARA_OFF + 0x08, SUN60I_DE_VSU8_STEP_1TO1); + sun60i_de_vsu_writel(de, SUN60I_DE_VSU8_CPARA_OFF + 0x0c, SUN60I_DE_VSU8_STEP_1TO1); +} + +/* UI channel: BSP leaves channel CSC off ("ui channel no csc") */ +static void sun60i_de_hw_program_ccsc(struct sun60i_de *de) +{ + if (!de->rcq_ccsc_shadow) + return; + + memset(de->rcq_ccsc_shadow, 0, SUN60I_DE_RCQ_CCSC_CTL_SIZE); + sun60i_de_ccsc_writel(de, SUN60I_DE_CCSC_CTL_OFF, 0); + +} + +static u32 sun60i_de_pack_coord(u32 x, u32 y) +{ + return (x & 0xffff) | ((y & 0xffff) << 16); +} + +int sun60i_de_hw_bus_init(struct sun60i_de *de) +{ + if (!de->regs) + return -EINVAL; + + /* Avoid reads until the DE module clock and reset are enabled. */ + if (!de->clk_mixer) { + u32 bit = BIT(de->display_id * SUN60I_DE_DISP_CLK_SHIFT); + + /* Keyed enable: BIT(16) | display gate — write-only. */ + sun60i_de_writel(de, SUN60I_DE_CLK_OFFSET, bit | BIT(16)); + sun60i_de_writel(de, SUN60I_DE_RESET_OFFSET, bit | BIT(16)); + udelay(50); + } + + if (!de->clk_bus_mixer) { + sun60i_de_writel(de, SUN60I_DE_MBUS_CLK_OFFSET, + SUN60I_DE_MBUS_RESET_DEASSERT); + udelay(10); + sun60i_de_writel(de, SUN60I_DE_MBUS_CLK_OFFSET, + SUN60I_DE_MBUS_RESET_DEASSERT | + SUN60I_DE_MBUS_CLK_EN); + } + + sun60i_de_writel(de, SUN60I_DE_RESERVE_CTL_OFFSET, + SUN60I_DE_RESERVE_CTL_AHB_CONFLICT); + + return 0; +} + +int sun60i_de_hw_bus_verify(struct sun60i_de *de) +{ + if (!de->regs) + return -EINVAL; + + return 0; +} + +/* + * DE352 UI path (DRM de_top_set_chn_mux_v2 + port2chn): + * uch2core @0x802c: RMW width-2 at (type_id<<2), value=disp + * port2chn @0x8030+disp*4: RMW width-4 at (port<<2), value=type_id+8 (UI) + * type_id 0 = first UI (= phys chn 6 on DE352 map). + */ +static void sun60i_de_top_setup_chn_mux(struct sun60i_de *de) +{ + const u32 type_id = de->ui_channel; /* 0 for primary UI */ + const u32 port = de->bld_port; + const u32 hw_disp = de->display_id; + u32 port_reg = SUN60I_DE_PORT2CHN_MUX_OFFSET + de->display_id * 0x4; + + /* Match DRM v2: UI adds +8 in the port→chn table. */ + sun60i_de_rmw_bits(de, SUN60I_DE_UCH2CORE_MUX_OFFSET, + type_id << 2, 2, hw_disp); + sun60i_de_rmw_bits(de, port_reg, port << 2, 4, type_id + 8); +} + +/* + * Reference steady state: PORT2CHN0=0x0000a810, UCH2CORE=0. + * Write-only — RMW of this block historically AHB-stalled on A733. + */ +static void sun60i_de_top_enable_chn_gating(struct sun60i_de *de) +{ + /* + * DRM BSP comments this out for de352; disp2 SUN60IW2 + * de_rtmx_set_chn_mux() sets GATING@0x80e8 per channel before RCQ + * head. HDMI UI phys6 uses gating id phy+2 = 8. + */ + sun60i_de_writel(de, SUN60I_DE_GATING_CTL_OFFSET, + SUN60I_DE_HDMI_UI_GATING); +} + +static void sun60i_de_top_set_de2tcon_mux(struct sun60i_de *de, u32 tcon) +{ + u32 shift = de->display_id * 4; + u32 val; + + /* + * Write-only (RMW read hung A733). Match reference dump width: + * DE2TCON=0x0000fff4 — unused display nibbles = 0xf in the low 16 bits. + * Values outside the low 16 bits can stall the interconnect. + */ + val = 0xffff; + val &= ~(0xfu << shift); + val |= (tcon & 0xf) << shift; + sun60i_de_writel(de, SUN60I_DE_DE2TCON_MUX_OFFSET, val); +} + +static void sun60i_de_top_set_urgency(struct sun60i_de *de) +{ + /* + * Preserve the low-half enable bits while setting the urgency depth. + */ + sun60i_de_writel(de, SUN60I_DE_URGENCY_CTL_OFFSET, 0x0800ffff); +} + +/* + * DE352 display_config_v2 (REF vmlinux disasm) minus RCQ head: + * urgency → BUF → GLB → AUTO(bit0) → out_size → [de2tcon] + * (clk_enable already done in bus_init; support_rcq_gate=0) + */ +static void sun60i_de_top_set_out_size(struct sun60i_de *de) +{ + u32 out_reg = SUN60I_DE_OUT_SIZE_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 value = sun60i_de_pack_size(de->mode.crtc_hdisplay, + de->mode.crtc_vdisplay); + + sun60i_de_writel(de, out_reg, value); +} + +static void sun60i_de_top_auto_clk_write(struct sun60i_de *de, bool enable, + bool rcq_gate) +{ + u32 auto_reg = SUN60I_DE_AUTO_CLK_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 val = 0; + + if (enable) + val |= BIT(0); + if (rcq_gate) + val |= SUN60I_DE_AUTO_CLK_RCQ_GATE; + + /* Write-only: avoid DETOP read in RTMX window. */ + sun60i_de_writel(de, auto_reg, val); +} + +static void sun60i_de_top_glb_rtmx_write(struct sun60i_de *de, bool enable) +{ + u32 glb_reg = SUN60I_DE_GLB_CTL_OFFSET + + de->display_id * SUN60I_DE_DISP_STRIDE; + u32 glb_ctl = 0; + + if (enable) { + glb_ctl = SUN60I_DE_GLB_CTL_RTMX_EN | + (0 << SUN60I_DE_GLB_CTL_PIXEL_MODE_SHIFT); + if (de->mode.flags & DRM_MODE_FLAG_INTERLACE) + glb_ctl |= SUN60I_DE_GLB_CTL_FIELD_REVERSE; + } + + sun60i_de_writel(de, glb_reg, glb_ctl); +} + +static void sun60i_de_top_rtmx_buf_depth(struct sun60i_de *de) +{ + sun60i_de_writel(de, SUN60I_DE_BUF_DEPTH_OFFSET, 0x2000); +} + +/* + * DE352 BSP de_top_display_config_v2() order: + * urgency → BUF → GLB → AUTO_CLK → OUT_SIZE → + * DE2TCON → RCQ_HEAD (caller) → channel mux (caller) + * + * This intentionally differs from sun55i. On A733, arming RCQ_HEAD before + * DE2TCON causes the following DETOP write to stall the interconnect during + * the first cold-boot fbdev commit. + * + */ +static void sun60i_de_top_rtmx_enable_atomic(struct sun60i_de *de) +{ + sun60i_de_top_glb_rtmx_write(de, true); + sun60i_de_top_auto_clk_write(de, true, false); + sun60i_de_top_set_out_size(de); + sun60i_de_top_set_de2tcon_mux(de, de->tcon_id); + /* Make all DETOP routing writes visible before RCQ is armed. */ + wmb(); +} + +static void sun60i_de_top_rtmx_enable(struct sun60i_de *de, bool enable) +{ + if (!enable) { + sun60i_de_top_glb_rtmx_write(de, false); + sun60i_de_top_auto_clk_write(de, false, false); + return; + } + + sun60i_de_top_rtmx_enable_atomic(de); +} + +/* + * DE352 top (0x8100 block) — BSP references: + * + * A733 programming order: + * bus_init: mod_clk/mbus/RESERVE_CTL only + * commit: RCQ pool+head (19 blocks) → shadow → hdmi chn_mux → gating + * → async/buf → GLB+AUTO → de2tcon → pulse + */ +static int sun60i_de_map_format(u32 drm_format, u32 *de_format, + u32 *alpha_mode) +{ + switch (drm_format) { + case DRM_FORMAT_ARGB8888: + *de_format = SUN60I_DE_FMT_ARGB8888; + *alpha_mode = 2; + return 0; + case DRM_FORMAT_XRGB8888: + *de_format = SUN60I_DE_FMT_XRGB8888; + *alpha_mode = 1; + return 0; + case DRM_FORMAT_RGB565: + *de_format = SUN60I_DE_FMT_RGB565; + *alpha_mode = 1; + return 0; + default: + return -EINVAL; + } +} + +static void sun60i_de_program_blender(struct sun60i_de *de, bool enabled) +{ + bool interlace = !!(de->mode.flags & DRM_MODE_FLAG_INTERLACE); + u32 route = 0x00ffffff; + u32 out_ctl = SUN60I_DE_BLD_OUT_CTL_FMT_RGB; + u32 blend = SUN60I_DE_BLD_MODE_SRCOVER; + u32 bld_en; + u32 pipe_fcolor; + + if (interlace) + out_ctl |= SUN60I_DE_BLD_OUT_CTL_INTERLACE; + + if (!de->rcq_bld_shadow) + return; + + if (enabled) { + route &= ~GENMASK(3, 0); + route |= de->bld_port; + } + + if (!enabled) { + bld_en = 0; + pipe_fcolor = 0; + } else { + /* BSP de_bld_output_set_attr(): RGB pipes use FCOLOR black bg + SRCOVER */ + bld_en = SUN60I_DE_BLD_PIPE_EN(0) | SUN60I_DE_BLD_FCOLOR_EN(0); + pipe_fcolor = 0xff000000; + blend = SUN60I_DE_BLD_MODE_SRCOVER; + } + + sun60i_de_bld_writel(de, SUN60I_DE_BLD_EN, bld_en); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_PIPE_FCOLOR(0), pipe_fcolor); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_PIPE_IN_SIZE(0), + enabled ? sun60i_de_pack_size(de->mode.crtc_hdisplay, + de->mode.crtc_vdisplay) : 0); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_PIPE_IN_COORD(0), + 0); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_ROUT_CTL, route); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_PREMUL_CTL, 0); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_BG_COLOR, 0); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_OUT_SIZE, + sun60i_de_pack_size(de->mode.crtc_hdisplay, + de->mode.crtc_vdisplay)); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_BLEND_CTL(0), blend); + sun60i_de_bld_writel(de, SUN60I_DE_BLD_OUT_CTL, out_ctl); +} + +static void sun60i_de_disable_overlay(struct sun60i_de *de) +{ + unsigned int i; + + if (!de->rcq_ovl_shadow) + return; + + for (i = 0; i < 4; i++) { + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_CTL(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_MBSIZE(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_MBCOOR(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_PITCH(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_TOP_LADDR(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_BOT_LADDR(i), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_FCOLOR(i), 0); + } + + sun60i_de_ovl_writel(de, SUN60I_DE_UI_TOP_HADDR, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_BOT_HADDR, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_WIN_SIZE, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_HORI_DS, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_VERT_DS, 0); + /* DS RCQ block @0xE0 includes hori/vert_ds; clear whole region */ + memset((u8 *)de->rcq_ovl_shadow + SUN60I_DE_RCQ_OVL_DS_OFF, 0, + SUN60I_DE_RCQ_OVL_DS_SIZE); +} + +static int sun60i_de_program_overlay_layer(struct sun60i_de *de, + const struct sun60i_de_scanout_state *scanout, + unsigned int layer, u32 *top_haddr) +{ + u32 de_format; + u32 alpha_mode; + u32 ctl; + int ret; + + if (!scanout->enabled) + return 0; + + if (scanout->src_w != scanout->crtc_w || + scanout->src_h != scanout->crtc_h) + return -EINVAL; + + ret = sun60i_de_map_format(scanout->format, &de_format, &alpha_mode); + if (ret) + return ret; + + ctl = SUN60I_DE_UI_CTL_EN | + (alpha_mode << SUN60I_DE_UI_CTL_ALPHA_MODE_SHIFT) | + (de_format << SUN60I_DE_UI_CTL_FMT_SHIFT) | + (0xff << SUN60I_DE_UI_CTL_GLB_ALPHA_SHIFT); + + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_CTL(layer), ctl); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_MBSIZE(layer), + sun60i_de_pack_size(scanout->src_w, scanout->src_h)); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_MBCOOR(layer), + sun60i_de_pack_coord(scanout->crtc_x, scanout->crtc_y)); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_PITCH(layer), scanout->pitch); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_TOP_LADDR(layer), + lower_32_bits(scanout->addr)); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_BOT_LADDR(layer), 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_LAY_FCOLOR(layer), 0); + + *top_haddr |= (upper_32_bits(scanout->addr) & 0xff) << (layer * 8); + + return 0; +} + +static int sun60i_de_program_overlay(struct sun60i_de *de) +{ + u32 top_haddr = 0; + int ret; + + if (!de->rcq_ovl_shadow) + return -EINVAL; + + memset(de->rcq_ovl_shadow, 0, SUN60I_DE_RCQ_OVL_SHADOW_SIZE); + sun60i_de_disable_overlay(de); + + ret = sun60i_de_program_overlay_layer(de, &de->scanout, 0, &top_haddr); + if (ret) + return ret; + + ret = sun60i_de_program_overlay_layer(de, &de->cursor_scanout, 1, + &top_haddr); + if (ret) + return ret; + + sun60i_de_ovl_writel(de, SUN60I_DE_UI_TOP_HADDR, top_haddr); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_BOT_HADDR, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_WIN_SIZE, + sun60i_de_pack_size(de->mode.crtc_hdisplay, + de->mode.crtc_vdisplay)); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_HORI_DS, 0); + sun60i_de_ovl_writel(de, SUN60I_DE_UI_VERT_DS, 0); + + return 0; +} + +static void sun60i_de_sync_one_scanout(struct sun60i_de *de, + const struct sun60i_de_scanout_state *scanout) +{ + struct device *dma_dev = sun60i_de_dma_dev(de); + size_t size; + + if (!scanout->enabled || !scanout->addr || !scanout->pitch) + return; + + size = (size_t)scanout->pitch * scanout->src_h; + dma_sync_single_for_device(dma_dev, scanout->addr, size, + DMA_TO_DEVICE); +} + +static void sun60i_de_sync_scanout(struct sun60i_de *de) +{ + sun60i_de_sync_one_scanout(de, &de->scanout); + sun60i_de_sync_one_scanout(de, &de->cursor_scanout); +} + +int sun60i_de_hw_commit(struct sun60i_de *de) +{ + bool full_setup; + bool first_rtmx; + int ret; + + if (!de->mode_valid) + return 0; + + /* + * sun55i_de_enable() order: + * urgency → RCQ head → de2tcon → buf → chn_mux → size → AUTO → GLB + * Then shadows + RCQ submit. All DETOP updates are RMW. + */ + full_setup = !de->rtmx_running || + de->programmed_hdisplay != de->mode.crtc_hdisplay || + de->programmed_vdisplay != de->mode.crtc_vdisplay; + first_rtmx = !de->rtmx_running; + + sun60i_de_top_set_urgency(de); + /* + * Complete the DE352 static display transaction before RCQ_HEAD. The + * BSP enables GLB/AUTO and routes DE2TCON first; doing DE2TCON after the + * head is armed hard-stalls A733 on the first fbdev modeset. + */ + if (first_rtmx) { + sun60i_de_top_rtmx_buf_depth(de); + sun60i_de_top_rtmx_enable_atomic(de); + de->rtmx_running = true; + de->programmed_hdisplay = de->mode.crtc_hdisplay; + de->programmed_vdisplay = de->mode.crtc_vdisplay; + } + + ret = sun60i_de_rcq_init(de); + if (ret) + return ret; + + sun60i_de_sync_scanout(de); + + if (full_setup) { + sun60i_de_hw_program_fmt(de); + sun60i_de_hw_program_vsu8(de); + sun60i_de_hw_program_tfbd(de); + sun60i_de_hw_program_ccsc(de); + de->programmed_hdisplay = de->mode.crtc_hdisplay; + de->programmed_vdisplay = de->mode.crtc_vdisplay; + } + + /* Rebuild the complete layer state because the shadow starts cleared. */ + ret = sun60i_de_program_overlay(de); + if (ret) + return ret; + + sun60i_de_program_blender(de, de->scanout.enabled || + de->cursor_scanout.enabled); + if (!de->rcq_head_done) { + sun60i_de_rcq_program_head(de); + de->rcq_head_done = true; + } + + /* BSP configures channel routing after the RCQ head is installed. */ + if (full_setup) { + sun60i_de_top_setup_chn_mux(de); + sun60i_de_top_enable_chn_gating(de); + } + + /* First setup already programmed OUT_SIZE before RCQ_HEAD. */ + if (!first_rtmx) + sun60i_de_top_set_out_size(de); + + sun60i_de_rcq_submit(de); + + return 0; +} + +void sun60i_de_hw_disable(struct sun60i_de *de) +{ + sun60i_de_disable_overlay(de); + sun60i_de_program_blender(de, false); + /* Latch the disabled plane state before stopping RTMX/freeing its RCQ. */ + sun60i_de_rcq_submit(de); + sun60i_de_top_rtmx_enable(de, false); + /* + * Module, MBUS and reset lifetime belongs to display_clocks and the + * clk/reset framework. Do not modify those gates directly here: the + * caller drops the matching clock/reset references after RTMX is idle. + */ + + de->rtmx_running = false; + de->rcq_head_done = false; + de->programmed_hdisplay = 0; + de->programmed_vdisplay = 0; + sun60i_de_rcq_free(de); +} diff --git a/drivers/gpu/drm/sun4i/sun60i_de_hw.h b/drivers/gpu/drm/sun4i/sun60i_de_hw.h new file mode 100644 index 00000000000000..cb5d382f3011dc --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_de_hw.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ + +#ifndef _SUN60I_DE_HW_H_ +#define _SUN60I_DE_HW_H_ + +#include "sun60i_de.h" + +int sun60i_de_hw_commit(struct sun60i_de *de); +void sun60i_de_hw_disable(struct sun60i_de *de); +int sun60i_de_hw_bus_init(struct sun60i_de *de); +int sun60i_de_hw_bus_verify(struct sun60i_de *de); + +#endif /* _SUN60I_DE_HW_H_ */ From b93266af0d20bd0ef0d9ad24de0f094e3f583442 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:19:50 +0800 Subject: [PATCH 10/16] drm/sun4i: add A733 TV timing-controller support Program the A733-specific TCON-TV timing layout and HDMI pixel path. Extend TCON top with the additional fabric clock and reset while keeping legacy mux handling confined to the existing variants. Provide the HDMI gate helper and preserve reset error propagation and resource unwinding. Panel routing and external LVDS PHY support are added separately. --- drivers/gpu/drm/sun4i/sun4i_tcon.c | 96 +++++++++++++++- drivers/gpu/drm/sun4i/sun4i_tcon.h | 29 ++++- drivers/gpu/drm/sun4i/sun8i_tcon_top.c | 148 +++++++++++++++++++++---- drivers/gpu/drm/sun4i/sun8i_tcon_top.h | 9 ++ 4 files changed, 261 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/sun4i/sun4i_tcon.c b/drivers/gpu/drm/sun4i/sun4i_tcon.c index 960e83c8291da3..6f2a72795d8654 100644 --- a/drivers/gpu/drm/sun4i/sun4i_tcon.c +++ b/drivers/gpu/drm/sun4i/sun4i_tcon.c @@ -6,9 +6,12 @@ * Maxime Ripard */ +#include #include +#include #include #include +#include #include #include #include @@ -40,6 +43,81 @@ #include "sun8i_tcon_top.h" #include "sunxi_engine.h" +static void sun60i_tcon1_mode_set(struct sun4i_tcon *tcon, + const struct drm_display_mode *mode) +{ + unsigned int hdisplay = mode->crtc_hdisplay; + unsigned int hsync = mode->crtc_hsync_end - mode->crtc_hsync_start; + unsigned int hback = mode->crtc_htotal - mode->crtc_hsync_end; + unsigned int vdisplay = mode->crtc_vdisplay; + unsigned int vtotal = mode->crtc_vtotal; + bool interlace = !!(mode->flags & DRM_MODE_FLAG_INTERLACE); + unsigned int field_div = interlace ? 2 : 1; + unsigned int vsync = (mode->crtc_vsync_end - mode->crtc_vsync_start) / + field_div; + unsigned int vback = (mode->crtc_vtotal - mode->crtc_vsync_end) / + field_div; + unsigned int visible_lines = vdisplay / field_div; + int start_delay; + u32 val; + + WARN_ON(!tcon->quirks->has_channel_1); + + /* Pixel clock lives on HDMI CLK_HDMI_TV; keep sclk1 as bus helper. */ + clk_set_rate(tcon->sclk1, mode->crtc_clock * 1000); + + regmap_write(tcon->regs, SUN60I_TCON_TV_DATA_IO_POL0_REG, 0); + regmap_write(tcon->regs, SUN60I_TCON_TV_DATA_IO_POL1_REG, 0); + regmap_write(tcon->regs, SUN60I_TCON_TV_DATA_IO_TRI0_REG, 0xffffffff); + regmap_write(tcon->regs, SUN60I_TCON_TV_DATA_IO_TRI1_REG, 0xffff0000); + regmap_write(tcon->regs, SUN60I_TCON_PIXEL_DEPTH_MODE_REG, + SUN60I_TCON_PIXELDEPTH_8BIT); + + regmap_write(tcon->regs, SUN60I_TCON_TV_IO_TRI_REG, 0x0fffffff); + regmap_write(tcon->regs, SUN4I_TCON0_IO_POL_REG, 0x07000000); + + /* BASIC1 = VT (A733); skip legacy BASIC0 upscale layout. */ + val = SUN60I_TCON_TV_BASIC1_VT(interlace ? vtotal : vtotal * 2); + regmap_write(tcon->regs, SUN4I_TCON1_BASIC1_REG, val); + + regmap_write(tcon->regs, SUN4I_TCON1_BASIC2_REG, + SUN4I_TCON1_BASIC2_X(hdisplay) | + SUN4I_TCON1_BASIC2_Y(visible_lines)); + + regmap_write(tcon->regs, SUN4I_TCON1_BASIC3_REG, + SUN4I_TCON1_BASIC3_H_TOTAL(mode->crtc_htotal) | + SUN4I_TCON1_BASIC3_H_BACKPORCH(hsync + hback)); + + /* 15.2.10.10: BASIC4 is VBP only on A733. */ + regmap_write(tcon->regs, SUN4I_TCON1_BASIC4_REG, + SUN60I_TCON_TV_BASIC4_VBP(vsync + vback)); + + regmap_write(tcon->regs, SUN4I_TCON1_BASIC5_REG, + SUN4I_TCON1_BASIC5_H_SYNC(hsync) | + SUN4I_TCON1_BASIC5_V_SYNC(vsync)); + + /* 2D: fill bypass (15.2.7.4); leave begin/end alone. */ + regmap_write(tcon->regs, SUN4I_TCON1_FILL_CTL_REG, 0); + + /* Disable CEU (identity / off). */ + regmap_write(tcon->regs, SUN4I_TCON_CEU_CTL_REG, 0); + + start_delay = (vtotal - vdisplay) / field_div - 5; + start_delay = clamp(start_delay, 0, 31); + + val = SUN4I_TCON1_CTL_CLK_DELAY(start_delay); + /* Keep EN clear here; channel_set_status() enables the channel. */ + regmap_write(tcon->regs, SUN4I_TCON1_CTL_REG, val); + + /* PIXEL_MODE=1ppc (must match DE GLB); bit31 matches REF GCTL. */ + regmap_write(tcon->regs, SUN4I_TCON_GCTL_REG, + SUN4I_TCON_GCTL_TCON_ENABLE | + FIELD_PREP(SUN60I_TCON_TV_GCTL_PIXEL_MODE, + SUN60I_TCON_TV_GCTL_PIXEL_1)); + + regmap_write(tcon->regs, SUN60I_TCON_TV_SRC_CTL_REG, 0); +} + static struct drm_connector *sun4i_tcon_get_connector(const struct drm_encoder *encoder) { struct drm_connector *connector; @@ -727,7 +805,10 @@ void sun4i_tcon_mode_set(struct sun4i_tcon *tcon, break; case DRM_MODE_ENCODER_TVDAC: case DRM_MODE_ENCODER_TMDS: - sun4i_tcon1_mode_set(tcon, mode); + if (tcon->quirks->sun60i_tv) + sun60i_tcon1_mode_set(tcon, mode); + else + sun4i_tcon1_mode_set(tcon, mode); sun4i_tcon_set_mux(tcon, 1, encoder); break; default: @@ -1542,6 +1623,18 @@ static const struct sun4i_tcon_quirks sun20i_d1_lcd_quirks = { .set_mux = sun8i_r40_tcon_tv_set_mux, }; +static const struct sun4i_tcon_quirks sun60i_a733_tv_quirks = { + .has_channel_1 = true, + .polarity_in_ch0 = true, + .sun60i_tv = true, + /* + * Call R40-style TCON-TOP mux (hdmi gate + PORT_SEL). On A733 VO1, + * PORT_SEL@0x1c is RAZ/WI (devmem write of 0x2 does not stick; GATE@0x20 + * is writable). DE→TCON routing is DE2TCON@0x8010, not PORT_SEL. + */ + .set_mux = sun8i_r40_tcon_tv_set_mux, +}; + /* sun4i_drv uses this list to check if a device node is a TCON */ const struct of_device_id sun4i_tcon_of_table[] = { { .compatible = "allwinner,sun4i-a10-tcon", .data = &sun4i_a10_quirks }, @@ -1561,6 +1654,7 @@ const struct of_device_id sun4i_tcon_of_table[] = { { .compatible = "allwinner,sun9i-a80-tcon-tv", .data = &sun9i_a80_tcon_tv_quirks }, { .compatible = "allwinner,sun20i-d1-tcon-lcd", .data = &sun20i_d1_lcd_quirks }, { .compatible = "allwinner,sun20i-d1-tcon-tv", .data = &sun8i_r40_tv_quirks }, + { .compatible = "allwinner,sun60i-a733-tcon-tv", .data = &sun60i_a733_tv_quirks }, { } }; MODULE_DEVICE_TABLE(of, sun4i_tcon_of_table); diff --git a/drivers/gpu/drm/sun4i/sun4i_tcon.h b/drivers/gpu/drm/sun4i/sun4i_tcon.h index fa23aa23fe4a45..5e59e1f238d9a2 100644 --- a/drivers/gpu/drm/sun4i/sun4i_tcon.h +++ b/drivers/gpu/drm/sun4i/sun4i_tcon.h @@ -130,6 +130,33 @@ #define SUN4I_TCON1_CTL_CLK_DELAY(delay) ((delay << 4) & SUN4I_TCON1_CTL_CLK_DELAY_MASK) #define SUN4I_TCON1_CTL_SRC_SEL_MASK GENMASK(1, 0) +/* A733 TCON-TV: 15.2.10.5 TV_SRC_CTL — bit0 VDBIST, [2:1] pattern. */ +#define SUN60I_TCON_TV_SRC_CTL_REG 0x40 +#define SUN60I_TCON_TV_SRC_CTL_VDBIST BIT(0) +#define SUN60I_TCON_TV_SRC_CTL_PATTERN GENMASK(2, 1) + +/* A733 TCON-TV IO at common-module offsets (not 0xf0/0xf4). */ +#define SUN60I_TCON_TV_IO_TRI_REG 0x8c +#define SUN60I_TCON_TV_IO_TRI_DATA_MASK GENMASK(23, 0) +#define SUN60I_TCON_TV_IO_TRI_IO2 BIT(26) +#define SUN60I_TCON_TV_IO_TRI_IO3 BIT(27) +#define SUN60I_TCON_TV_DATA_IO_POL0_REG 0x330 +#define SUN60I_TCON_TV_DATA_IO_POL1_REG 0x334 +#define SUN60I_TCON_TV_DATA_IO_TRI0_REG 0x338 +#define SUN60I_TCON_TV_DATA_IO_TRI1_REG 0x33c +#define SUN60I_TCON_PIXEL_DEPTH_MODE_REG 0x340 +/* 15.2.10.27: PIXELDEPTH 001=8-bit, IPI_FORMAT 000=RGB */ +#define SUN60I_TCON_PIXELDEPTH_8BIT 0x1 + +/* A733 TV_GCTL @0x00: PIXEL_MODE[5:4], UNSTRICT[0]; bit31 used like REF. */ +#define SUN60I_TCON_TV_GCTL_PIXEL_MODE GENMASK(5, 4) +#define SUN60I_TCON_TV_GCTL_PIXEL_1 0 + +/* A733 BASIC1 @0x98 is VT, not the legacy upscale X/Y. */ +#define SUN60I_TCON_TV_BASIC1_VT(vt) ((vt) & 0x1ffff) +/* 15.2.10.10 BASIC4 is VBP only (VT lives in BASIC1). */ +#define SUN60I_TCON_TV_BASIC4_VBP(bp) (((bp) - 1) & 0x7fff) + #define SUN4I_TCON1_BASIC0_REG 0x94 #define SUN4I_TCON1_BASIC0_X(width) ((((width) - 1) & 0xfff) << 16) #define SUN4I_TCON1_BASIC0_Y(height) (((height) - 1) & 0xfff) @@ -243,6 +270,7 @@ struct sun4i_tcon_quirks { bool needs_edp_reset; /* a80 edp reset needed for tcon0 access */ bool supports_lvds; /* Does the TCON support an LVDS output? */ bool polarity_in_ch0; /* some tcon1 channels have polarity bits in tcon0 pol register */ + bool sun60i_tv; /* A733 TCON-TV register map + builtin pattern */ u8 dclk_min_div; /* minimum divider for TCON0 DCLK */ /* callback to handle tcon muxing options */ @@ -299,5 +327,4 @@ void sun4i_tcon_set_status(struct sun4i_tcon *crtc, const struct drm_encoder *encoder, bool enable); extern const struct of_device_id sun4i_tcon_of_table[]; - #endif /* __SUN4I_TCON_H__ */ diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c index 8adda578c51ba4..be9f531b4a127a 100644 --- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.c +++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.c @@ -16,8 +16,10 @@ #include "sun8i_tcon_top.h" struct sun8i_tcon_top_quirks { + bool has_tcon_tv0; bool has_tcon_tv1; bool has_dsi; + bool has_legacy_mux; }; static bool sun8i_tcon_top_node_is_tcon_top(struct device_node *node) @@ -36,6 +38,11 @@ int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon) return -EINVAL; } + /* A733 uses bit 28 as the TV0 HDMI gate, not as a source mux. */ + if (of_device_is_compatible(dev->of_node, + "allwinner,sun60i-a733-tcon-top")) + return sun8i_tcon_top_hdmi_gate_enable(dev, true); + if (tcon < 2 || tcon > 3) { dev_err(dev, "TCON index must be 2 or 3!\n"); return -EINVAL; @@ -54,9 +61,48 @@ int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon) } EXPORT_SYMBOL(sun8i_tcon_top_set_hdmi_src); +int sun8i_tcon_top_hdmi_gate_enable(struct device *dev, bool enable) +{ + struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev); + unsigned long flags; + u32 val, setup; + + if (!tcon_top || !sun8i_tcon_top_node_is_tcon_top(dev->of_node)) + return -EINVAL; + + spin_lock_irqsave(&tcon_top->reg_lock, flags); + + /* Match BSP tcon_top_hdmi_set_gate() for sun60iw2. */ + val = readl(tcon_top->regs + TCON_TOP_GATE_SRC_REG); + if (enable) { + val |= BIT(TCON_TOP_TCON_TV0_GATE); + val |= BIT(TCON_TOP_TV0_HDMI_GATE); + } else { + val &= ~BIT(TCON_TOP_TV0_HDMI_GATE); + } + writel(val, tcon_top->regs + TCON_TOP_GATE_SRC_REG); + + /* + * HDMI path (15.2.9.1): TV0 from HDMI (bit1=0), pixel clk from + * HDMI PHY (bit3=0), CCMU as TV0_CLK_SRC (bit0=0). Matches REF SETUP=0. + */ + setup = readl(tcon_top->regs + TCON_TOP_TCON_TV_SETUP_REG); + if (enable) + setup &= ~(TCON_TOP_TV0_CLK_SRC_CVBS | + TCON_TOP_TV0_EDP_HDMI_CK_SEL | + TCON_TOP_TV0_HDMIPHY_CCU_CK_SEL); + writel(setup, tcon_top->regs + TCON_TOP_TCON_TV_SETUP_REG); + + spin_unlock_irqrestore(&tcon_top->reg_lock, flags); + + return 0; +} +EXPORT_SYMBOL(sun8i_tcon_top_hdmi_gate_enable); + int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon) { struct sun8i_tcon_top *tcon_top = dev_get_drvdata(dev); + const struct sun8i_tcon_top_quirks *quirks; unsigned long flags; u32 reg; @@ -65,6 +111,10 @@ int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon) return -EINVAL; } + quirks = of_device_get_match_data(dev); + if (!quirks || !quirks->has_legacy_mux) + return 0; + if (mixer > 1) { dev_err(dev, "Mixer index is too high!\n"); return -EINVAL; @@ -146,41 +196,76 @@ static int sun8i_tcon_top_bind(struct device *dev, struct device *master, spin_lock_init(&tcon_top->reg_lock); - tcon_top->rst = devm_reset_control_get(dev, NULL); - if (IS_ERR(tcon_top->rst)) { - dev_err(dev, "Couldn't get our reset line\n"); - return PTR_ERR(tcon_top->rst); + /* + * H6/R40 use a single unnamed reset. A733 VO1 also needs + * RST_BUS_VIDEO_OUT1 ("rst_bus_reg") for the HDMI APB fabric. + */ + tcon_top->rst = devm_reset_control_get_optional(dev, "bus"); + if (IS_ERR(tcon_top->rst)) + return dev_err_probe(dev, PTR_ERR(tcon_top->rst), + "Couldn't get bus reset\n"); + if (!tcon_top->rst) { + tcon_top->rst = devm_reset_control_get(dev, NULL); + if (IS_ERR(tcon_top->rst)) { + dev_err(dev, "Couldn't get our reset line\n"); + return PTR_ERR(tcon_top->rst); + } } + tcon_top->rst_reg = devm_reset_control_get_optional(dev, "rst_bus_reg"); + if (IS_ERR(tcon_top->rst_reg)) + return PTR_ERR(tcon_top->rst_reg); + tcon_top->bus = devm_clk_get(dev, "bus"); if (IS_ERR(tcon_top->bus)) { dev_err(dev, "Couldn't get the bus clock\n"); return PTR_ERR(tcon_top->bus); } + tcon_top->ahb_gate = devm_clk_get_optional(dev, "ahb-gate"); + if (IS_ERR(tcon_top->ahb_gate)) + return PTR_ERR(tcon_top->ahb_gate); + regs = devm_platform_ioremap_resource(pdev, 0); tcon_top->regs = regs; if (IS_ERR(regs)) return PTR_ERR(regs); + if (tcon_top->rst_reg) { + ret = reset_control_deassert(tcon_top->rst_reg); + if (ret) { + dev_err(dev, "Could not deassert rst_bus_reg\n"); + return ret; + } + } + ret = reset_control_deassert(tcon_top->rst); if (ret) { dev_err(dev, "Could not deassert ctrl reset control\n"); - return ret; + goto err_assert_rst_reg; + } + + ret = clk_prepare_enable(tcon_top->ahb_gate); + if (ret) { + dev_err(dev, "Could not enable ahb-gate clock\n"); + goto err_assert_reset; } ret = clk_prepare_enable(tcon_top->bus); if (ret) { dev_err(dev, "Could not enable bus clock\n"); - goto err_assert_reset; + goto err_disable_ahb_gate; } /* * At least on H6, some registers have some bits set by default - * which may cause issues. Clear them here. + * which may cause issues. Clear them here. A733 VO0/VO1 uses a + * different register layout, so preserve the firmware state there. */ - writel(0, regs + TCON_TOP_PORT_SEL_REG); - writel(0, regs + TCON_TOP_GATE_SRC_REG); + if (quirks->has_legacy_mux) { + writel(0, regs + TCON_TOP_PORT_SEL_REG); + writel(0, regs + TCON_TOP_GATE_SRC_REG); + } /* * TCON TOP has two muxes, which select parent clock for each TCON TV @@ -191,10 +276,11 @@ static int sun8i_tcon_top_bind(struct device *dev, struct device *master, * to TVE clock parent. */ i = 0; - clk_data->hws[CLK_TCON_TOP_TV0] = - sun8i_tcon_top_register_gate(dev, "tcon-tv0", regs, - &tcon_top->reg_lock, - TCON_TOP_TCON_TV0_GATE, i++); + if (quirks->has_tcon_tv0) + clk_data->hws[CLK_TCON_TOP_TV0] = + sun8i_tcon_top_register_gate(dev, "tcon-tv0", regs, + &tcon_top->reg_lock, + TCON_TOP_TCON_TV0_GATE, i++); if (quirks->has_tcon_tv1) clk_data->hws[CLK_TCON_TOP_TV1] = @@ -214,10 +300,12 @@ static int sun8i_tcon_top_bind(struct device *dev, struct device *master, goto err_unregister_gates; } - ret = of_clk_add_hw_provider(dev->of_node, of_clk_hw_onecell_get, - clk_data); - if (ret) - goto err_unregister_gates; + if (of_property_present(dev->of_node, "#clock-cells")) { + ret = of_clk_add_hw_provider(dev->of_node, of_clk_hw_onecell_get, + clk_data); + if (ret) + goto err_unregister_gates; + } dev_set_drvdata(dev, tcon_top); @@ -228,8 +316,13 @@ static int sun8i_tcon_top_bind(struct device *dev, struct device *master, if (!IS_ERR_OR_NULL(clk_data->hws[i])) clk_hw_unregister_gate(clk_data->hws[i]); clk_disable_unprepare(tcon_top->bus); +err_disable_ahb_gate: + clk_disable_unprepare(tcon_top->ahb_gate); err_assert_reset: reset_control_assert(tcon_top->rst); +err_assert_rst_reg: + if (tcon_top->rst_reg) + reset_control_assert(tcon_top->rst_reg); return ret; } @@ -241,13 +334,17 @@ static void sun8i_tcon_top_unbind(struct device *dev, struct device *master, struct clk_hw_onecell_data *clk_data = tcon_top->clk_data; int i; - of_clk_del_provider(dev->of_node); + if (of_property_present(dev->of_node, "#clock-cells")) + of_clk_del_provider(dev->of_node); for (i = 0; i < CLK_NUM; i++) if (clk_data->hws[i]) clk_hw_unregister_gate(clk_data->hws[i]); clk_disable_unprepare(tcon_top->bus); + clk_disable_unprepare(tcon_top->ahb_gate); reset_control_assert(tcon_top->rst); + if (tcon_top->rst_reg) + reset_control_assert(tcon_top->rst_reg); } static const struct component_ops sun8i_tcon_top_ops = { @@ -266,16 +363,25 @@ static void sun8i_tcon_top_remove(struct platform_device *pdev) } static const struct sun8i_tcon_top_quirks sun8i_r40_tcon_top_quirks = { + .has_tcon_tv0 = true, .has_tcon_tv1 = true, .has_dsi = true, + .has_legacy_mux = true, }; static const struct sun8i_tcon_top_quirks sun20i_d1_tcon_top_quirks = { + .has_tcon_tv0 = true, .has_dsi = true, + .has_legacy_mux = true, }; static const struct sun8i_tcon_top_quirks sun50i_h6_tcon_top_quirks = { - /* Nothing special */ + .has_tcon_tv0 = true, + .has_legacy_mux = true, +}; + +static const struct sun8i_tcon_top_quirks sun60i_a733_tv_top_quirks = { + .has_tcon_tv0 = true, }; /* sun4i_drv uses this list to check if a device node is a TCON TOP */ @@ -292,6 +398,10 @@ const struct of_device_id sun8i_tcon_top_of_table[] = { .compatible = "allwinner,sun50i-h6-tcon-top", .data = &sun50i_h6_tcon_top_quirks }, + { + .compatible = "allwinner,sun60i-a733-tcon-top", + .data = &sun60i_a733_tv_top_quirks + }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, sun8i_tcon_top_of_table); diff --git a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h index 0390584a330e50..02b04c1e5f49ad 100644 --- a/drivers/gpu/drm/sun4i/sun8i_tcon_top.h +++ b/drivers/gpu/drm/sun4i/sun8i_tcon_top.h @@ -9,7 +9,11 @@ #include #include +/* VIDEO_OUT1_SYS @0x00 — A733 User Manual 15.2.9.1 */ #define TCON_TOP_TCON_TV_SETUP_REG 0x00 +#define TCON_TOP_TV0_CLK_SRC_CVBS BIT(0) /* 0=CCMU, 1=CVBS */ +#define TCON_TOP_TV0_EDP_HDMI_CK_SEL BIT(1) /* 0=HDMI, 1=EDP */ +#define TCON_TOP_TV0_HDMIPHY_CCU_CK_SEL BIT(3) /* 0=HDMI PHY, 1=CCU */ #define TCON_TOP_PORT_SEL_REG 0x1C #define TCON_TOP_PORT_DE0_MSK GENMASK(1, 0) @@ -20,14 +24,18 @@ #define TCON_TOP_TCON_TV1_GATE 24 #define TCON_TOP_TCON_TV0_GATE 20 #define TCON_TOP_TCON_DSI_GATE 16 +/* A733/sun60iw2: bit28 is HDMI pixel-path gate, not H6 hdmi_src mux. */ +#define TCON_TOP_TV0_HDMI_GATE 28 #define CLK_NUM 3 struct sun8i_tcon_top { struct clk *bus; + struct clk *ahb_gate; struct clk_hw_onecell_data *clk_data; void __iomem *regs; struct reset_control *rst; + struct reset_control *rst_reg; /* * spinlock is used to synchronize access to same @@ -40,5 +48,6 @@ extern const struct of_device_id sun8i_tcon_top_of_table[]; int sun8i_tcon_top_set_hdmi_src(struct device *dev, int tcon); int sun8i_tcon_top_de_config(struct device *dev, int mixer, int tcon); +int sun8i_tcon_top_hdmi_gate_enable(struct device *dev, bool enable); #endif /* _SUN8I_TCON_TOP_H_ */ From 1ce9b6ad1a809e295e13665aff11f277c9facb65 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:20:10 +0800 Subject: [PATCH 11/16] drm/bridge: dw-hdmi: allow sink settling and EDID retries Allow platform glue to delay a connected hotplug notification and retry EDID reads when a sink raises HPD before its DDC interface is ready. Check connection state before retrying and after the hotplug delay. Keep the new delay and retry fields optional. Zero-initialized platform data preserves the behavior of existing dw-hdmi users. --- drivers/gpu/drm/bridge/synopsys/dw-hdmi.c | 31 ++++++++++++++++++++++- include/drm/bridge/dw_hdmi.h | 5 ++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi.c index 206b099a35e9a5..71d7ed422df632 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi.c @@ -2457,11 +2457,26 @@ static const struct drm_edid *dw_hdmi_edid_read(struct dw_hdmi *hdmi, { const struct drm_edid *drm_edid; const struct edid *edid; + unsigned int retry = 0; if (!hdmi->ddc) return NULL; - drm_edid = drm_edid_read_ddc(connector, hdmi->ddc); + do { + drm_edid = drm_edid_read_ddc(connector, hdmi->ddc); + if (drm_edid) + break; + + if (retry >= hdmi->plat_data->edid_retry_count || + hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data) != + connector_status_connected) + break; + + dev_dbg(hdmi->dev, "EDID read failed, retrying (%u/%u)\n", + retry + 1, hdmi->plat_data->edid_retry_count); + msleep(hdmi->plat_data->edid_retry_delay_ms); + } while (++retry); + if (!drm_edid) { dev_dbg(hdmi->dev, "failed to get edid\n"); return NULL; @@ -3131,6 +3146,20 @@ static irqreturn_t dw_hdmi_irq(int irq, void *dev_id) } if (status != connector_status_unknown) { + /* + * Some sinks raise HPD before their DDC interface is ready. Give + * platform glue a way to defer the connected notification so the + * first EDID probe does not fall back to a generic mode. Recheck + * HPD afterwards in case the cable was removed during the delay. + */ + if (status == connector_status_connected && + hdmi->plat_data->hpd_debounce_ms) { + msleep(hdmi->plat_data->hpd_debounce_ms); + phy_stat = hdmi_readb(hdmi, HDMI_PHY_STAT0); + if (!(phy_stat & HDMI_PHY_HPD)) + status = connector_status_disconnected; + } + dev_dbg(hdmi->dev, "EVENT=%s\n", status == connector_status_connected ? "plugin" : "plugout"); diff --git a/include/drm/bridge/dw_hdmi.h b/include/drm/bridge/dw_hdmi.h index 6a46baa0737cd0..597cbd0159859c 100644 --- a/include/drm/bridge/dw_hdmi.h +++ b/include/drm/bridge/dw_hdmi.h @@ -127,6 +127,11 @@ struct dw_hdmi_plat_data { struct regmap *regm; unsigned int output_port; + /* Optional sink settling delays; zero preserves the default behavior. */ + unsigned int hpd_debounce_ms; + /* Number of additional DDC attempts after the initial EDID read. */ + unsigned int edid_retry_count; + unsigned int edid_retry_delay_ms; unsigned long input_bus_encoding; bool use_drm_infoframe; From 70d5ad6a1e31019e11780a922d9ddd4867068c90 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:21:10 +0800 Subject: [PATCH 12/16] drm/sun4i: add A733 DesignWare HDMI support Add A733 platform glue and separate top-PHY and Synopsys PHY helpers to the existing sun8i DesignWare HDMI driver. Configure the reference clock, resistor calibration, PLL and internal PHY transactions for the selected TMDS rate. Sequence the TCON gate, fabric clocks and resets before dw-hdmi accesses the controller. Use the optional sink-settling settings and retain the PHY transaction timeout diagnostics and A733 video-output fixups. --- drivers/gpu/drm/sun4i/Makefile | 3 + drivers/gpu/drm/sun4i/sun60i_hdmi.h | 63 ++ drivers/gpu/drm/sun4i/sun60i_hdmi_phy.c | 98 +++ drivers/gpu/drm/sun4i/sun60i_hdmi_snps_phy.c | 617 +++++++++++++++++++ drivers/gpu/drm/sun4i/sun60i_hdmi_top_phy.c | 483 +++++++++++++++ drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c | 288 ++++++++- drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h | 4 + 7 files changed, 1552 insertions(+), 4 deletions(-) create mode 100644 drivers/gpu/drm/sun4i/sun60i_hdmi.h create mode 100644 drivers/gpu/drm/sun4i/sun60i_hdmi_phy.c create mode 100644 drivers/gpu/drm/sun4i/sun60i_hdmi_snps_phy.c create mode 100644 drivers/gpu/drm/sun4i/sun60i_hdmi_top_phy.c diff --git a/drivers/gpu/drm/sun4i/Makefile b/drivers/gpu/drm/sun4i/Makefile index 42b2374ea5f556..371cbcf568924f 100644 --- a/drivers/gpu/drm/sun4i/Makefile +++ b/drivers/gpu/drm/sun4i/Makefile @@ -13,6 +13,9 @@ sun4i-drm-hdmi-y += sun4i_hdmi_tmds_clk.o sun8i-drm-hdmi-y += sun8i_dw_hdmi.o sun8i-drm-hdmi-y += sun8i_hdmi_phy.o sun8i-drm-hdmi-y += sun8i_hdmi_phy_clk.o +sun8i-drm-hdmi-y += sun60i_hdmi_phy.o +sun8i-drm-hdmi-y += sun60i_hdmi_top_phy.o +sun8i-drm-hdmi-y += sun60i_hdmi_snps_phy.o sun8i-mixer-y += sun8i_mixer.o sun8i_ui_layer.o \ sun8i_vi_layer.o sun8i_ui_scaler.o \ diff --git a/drivers/gpu/drm/sun4i/sun60i_hdmi.h b/drivers/gpu/drm/sun4i/sun60i_hdmi.h new file mode 100644 index 00000000000000..2825cfe7e04bbd --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_hdmi.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ + +#ifndef _SUN60I_HDMI_H_ +#define _SUN60I_HDMI_H_ + +#include +#include + +#include +#include + +#define SUN60I_HDMI_TOP_PHY_OFFSET 0x000e0000 +#define SUN60I_HDMI_RESCAL_PHYS 0x03000160 +#define SUN60I_HDMI_RESCAL_SIZE 0x8 + +struct sun60i_hdmi { + struct device *dev; + void __iomem *regs; + void __iomem *rescal_regs; + struct clk *clk_cec; + struct clk *clk_tcon_tv; + struct clk *clk_hdmi; + struct clk *clk_hdmi_24m; + struct clk *clk_hdcp; + struct clk *clk_dcxo; + /* Glue-layer handles used to clock register accesses before dw-hdmi binds. */ + struct clk *clk_isfr; + struct clk *clk_iahb; + struct reset_control *rst_bus_sub; + struct reset_control *rst_bus_main; + struct reset_control *rst_bus_hdcp; + bool top_phy_inited; + bool resistor_src_onboard; + u8 clock_src; + u8 pixel_repeat; + int irq; + bool irq_snps_masked; +}; + +int sun60i_hdmi_rescal_board_init(struct sun60i_hdmi *hdmi); +int sun60i_hdmi_top_phy_init(struct sun60i_hdmi *hdmi); +int sun60i_hdmi_top_phy_config(struct sun60i_hdmi *hdmi, + const struct drm_display_mode *mode); +void sun60i_hdmi_top_phy_disable(struct sun60i_hdmi *hdmi); +bool sun60i_hdmi_top_phy_wait_txready(struct sun60i_hdmi *hdmi, + unsigned int ms); + +int sun60i_hdmi_snps_phy_configure(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi, + const struct drm_display_mode *mode); +void sun60i_hdmi_snps_phy_disable(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi); +void sun60i_hdmi_snps_phy_enable_tmds(struct sun60i_hdmi *hdmi); +void sun60i_hdmi_snps_phy_probe_init(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi); + +void sun60i_hdmi_apply_video_fixup(struct sun60i_hdmi *hdmi); +void sun60i_hdmi_clear_avmute(struct sun60i_hdmi *hdmi); + +const struct dw_hdmi_phy_ops *sun60i_hdmi_phy_ops_get(void); + +#endif /* _SUN60I_HDMI_H_ */ diff --git a/drivers/gpu/drm/sun4i/sun60i_hdmi_phy.c b/drivers/gpu/drm/sun4i/sun60i_hdmi_phy.c new file mode 100644 index 00000000000000..32cceff8183e3c --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_hdmi_phy.c @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * sun60i / A733 DW HDMI PHY glue (Top-PHY V2 + SNPS internal). + */ + +#include +#include +#include + +#include +#include + +#include "sun60i_hdmi.h" + +#define SUN60I_DW_HDMI_FC_GCP 0x1018 +#define SUN60I_DW_HDMI_FC_GCP_CLEAR_AVMUTE BIT(0) +#define SUN60I_DW_HDMI_PHY_CONF0 0x3000 +#define SUN60I_DW_HDMI_PHY_CONF0_ENTMDS BIT(6) + +void sun60i_hdmi_clear_avmute(struct sun60i_hdmi *hdmi) +{ + u8 gcp; + + if (!hdmi->regs) + return; + + gcp = readb(hdmi->regs + SUN60I_DW_HDMI_FC_GCP); + gcp &= ~BIT(1); + gcp |= SUN60I_DW_HDMI_FC_GCP_CLEAR_AVMUTE; + writeb(gcp, hdmi->regs + SUN60I_DW_HDMI_FC_GCP); +} + +static int sun60i_hdmi_phy_init(struct dw_hdmi *dw_hdmi, void *data, + const struct drm_display_info *display, + const struct drm_display_mode *mode) +{ + struct sun60i_hdmi *hdmi = data; + int ret; + + ret = sun60i_hdmi_top_phy_config(hdmi, mode); + if (ret) + return ret; + + ret = sun60i_hdmi_snps_phy_configure(dw_hdmi, hdmi, mode); + if (ret) + return ret; + + mdelay(50); + sun60i_hdmi_clear_avmute(hdmi); + sun60i_hdmi_snps_phy_enable_tmds(hdmi); + sun60i_hdmi_apply_video_fixup(hdmi); + + return 0; +} + +static void sun60i_hdmi_phy_disable(struct dw_hdmi *dw_hdmi, void *data) +{ + struct sun60i_hdmi *hdmi = data; + + sun60i_hdmi_snps_phy_disable(dw_hdmi, hdmi); + sun60i_hdmi_top_phy_disable(hdmi); +} + +static const struct dw_hdmi_phy_ops sun60i_hdmi_phy_ops = { + .init = sun60i_hdmi_phy_init, + .disable = sun60i_hdmi_phy_disable, + .setup_hpd = dw_hdmi_phy_setup_hpd, + .read_hpd = dw_hdmi_phy_read_hpd, + .update_hpd = dw_hdmi_phy_update_hpd, +}; + +const struct dw_hdmi_phy_ops *sun60i_hdmi_phy_ops_get(void) +{ + return &sun60i_hdmi_phy_ops; +} + +void sun60i_hdmi_apply_video_fixup(struct sun60i_hdmi *hdmi) +{ + u8 invid0, invconf, phy; + + if (!hdmi->regs) + return; + + invid0 = readb(hdmi->regs + 0x0200); + invid0 &= ~BIT(7); + writeb(invid0, hdmi->regs + 0x0200); + + invconf = readb(hdmi->regs + 0x1000); + invconf |= BIT(3); + writeb(invconf, hdmi->regs + 0x1000); + + phy = readb(hdmi->regs + SUN60I_DW_HDMI_PHY_CONF0); + phy |= SUN60I_DW_HDMI_PHY_CONF0_ENTMDS; + writeb(phy, hdmi->regs + SUN60I_DW_HDMI_PHY_CONF0); + + sun60i_hdmi_clear_avmute(hdmi); +} diff --git a/drivers/gpu/drm/sun4i/sun60i_hdmi_snps_phy.c b/drivers/gpu/drm/sun4i/sun60i_hdmi_snps_phy.c new file mode 100644 index 00000000000000..347196dd449f6d --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_hdmi_snps_phy.c @@ -0,0 +1,617 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * sun60i internal DWC HDMI PHY programming (from BSP phy_snps.c). + * + * A733 uses top-PHY for the pixel PLL. BSP sun60i skips snps init reset and + * still programs MPLL/drive over internal PHY I2CM, then GEN2 power + ENTMDS. + */ + +#include +#include +#include +#include +#include + +#include +#include + + +#include "sun60i_hdmi.h" + +/* DW-HDMI register offsets (from synopsys/dw-hdmi.h, not in UAPI header) */ +#define SUN60I_DW_HDMI_PHY_CONF0 0x3000 +#define SUN60I_DW_HDMI_PHY_STAT0 0x3004 +#define SUN60I_DW_HDMI_PHY_CONF0_ENTMDS BIT(6) +#define SUN60I_DW_HDMI_PHY_CONF0_SVSRET BIT(5) +#define SUN60I_DW_HDMI_PHY_CONF0_GEN2_PDDQ BIT(4) +#define SUN60I_DW_HDMI_PHY_CONF0_GEN2_TXPWRON BIT(3) +#define SUN60I_DW_HDMI_PHY_CONF0_SELDATAENPOL BIT(1) +#define SUN60I_DW_HDMI_PHY_CONF0_SELDIPIF BIT(0) +#define SUN60I_DW_HDMI_PHY_CONF0_ENHPDRXSENSE BIT(2) +#define SUN60I_DW_HDMI_PHY_TX_PHY_LOCK BIT(0) +#define SUN60I_DW_HDMI_PHY_I2CM_SLAVE 0x3020 +#define SUN60I_DW_HDMI_PHY_I2CM_SLAVE_GEN2 0x69 +#define SUN60I_DW_HDMI_JTAG_PHY_CONFIG 0x3034 +#define SUN60I_DW_HDMI_JTAG_I2C_MODE BIT(4) +#define SUN60I_DW_HDMI_IH_I2CMPHY_STAT0 0x0108 +#define SUN60I_DW_HDMI_IH_MUTE_I2CMPHY_STAT0 0x0188 +#define SUN60I_DW_HDMI_PHY_I2CM_ADDRESS 0x3021 +#define SUN60I_DW_HDMI_PHY_I2CM_DATAO_1 0x3022 +#define SUN60I_DW_HDMI_PHY_I2CM_DATAO_0 0x3023 +#define SUN60I_DW_HDMI_PHY_I2CM_DATAI_1 0x3024 +#define SUN60I_DW_HDMI_PHY_I2CM_DATAI_0 0x3025 +#define SUN60I_DW_HDMI_PHY_I2CM_OPERATION 0x3026 +#define SUN60I_DW_HDMI_PHY_I2CM_INT 0x3027 +#define SUN60I_DW_HDMI_PHY_I2CM_CTLINT 0x3028 +#define SUN60I_DW_HDMI_PHY_I2CM_DIV 0x3029 +#define SUN60I_DW_HDMI_PHY_I2CM_SOFTRSTZ 0x302a +#define SUN60I_PHY_I2CM_DONE_STATUS BIT(0) +#define SUN60I_PHY_I2CM_NACK_STATUS BIT(4) +#define SUN60I_PHY_I2CM_ARB_STATUS BIT(0) +#define SUN60I_I2CMPHY_DONE BIT(1) +#define SUN60I_I2CMPHY_ERROR BIT(0) +#define SUN60I_DW_HDMI_MC_CLKDIS 0x4001 +#define SUN60I_DW_HDMI_MC_PHYRSTZ 0x4005 +#define SUN60I_MC_PHYRSTZ_ASSERT 0x00 +#define SUN60I_MC_PHYRSTZ_DEASSERT 0x01 + +#define SUN60I_SNPS_MPLL_REG1 0x06 +#define SUN60I_SNPS_MPLL_REG2 0x10 +#define SUN60I_SNPS_MPLL_REG3 0x11 +#define SUN60I_SNPS_DRIVE_REG1 0x19 +#define SUN60I_SNPS_DRIVE_REG2 0x0e +#define SUN60I_SNPS_DRIVE_REG3 0x09 + +struct sun60i_snps_mpll_entry { + unsigned int pixel_khz; + u16 data[3]; +}; + +struct sun60i_snps_drive_entry { + unsigned int min_khz; + unsigned int max_khz; + u16 data[3]; +}; + +/* sun60i_mpll_rep0 @ 8bpp (C422-0), NOT sun50i values */ +static const struct sun60i_snps_mpll_entry sun60i_snps_mpll[] = { + { 148500, { 0x0001, 0x2080, 0x020a } }, + { 154000, { 0x0001, 0x2080, 0x020a } }, /* sun60i: same as 148500/165000 */ + { 165000, { 0x0001, 0x2080, 0x020a } }, + { 297000, { 0x0000, 0x3041, 0x0205 } }, +}; + +/* sun60i_drive[] from BSP phy_snps.c */ +static const struct sun60i_snps_drive_entry sun60i_snps_drive[] = { + { 25000, 165000, { 0x0007, 0x8160, 0x8188 } }, + { 165000, 340000, { 0x0004, 0x8040, 0x8e85 } }, + { 340000, 600000, { 0x0000, 0x80c0, 0x82f6 } }, +}; + +static void sun60i_snps_phy_set_bit(struct sun60i_hdmi *hdmi, u32 reg, + u32 mask, bool on) +{ + u8 tmp = readb(hdmi->regs + reg); + + if (on) + tmp |= mask; + else + tmp &= ~mask; + writeb(tmp, hdmi->regs + reg); +} + +static void sun60i_snps_phy_write_mask(struct sun60i_hdmi *hdmi, u32 reg, + u8 mask, u8 val) +{ + u8 tmp = readb(hdmi->regs + reg); + + tmp &= ~mask; + tmp |= mask & (val << (ffs(mask) - 1)); + writeb(tmp, hdmi->regs + reg); +} + +static void sun60i_snps_phy_set_power(struct sun60i_hdmi *hdmi, bool on) +{ + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_GEN2_PDDQ, !on); + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_GEN2_TXPWRON, on); +} + +static void sun60i_snps_phy_config_svsret(struct sun60i_hdmi *hdmi) +{ + /* + * BSP dw_phy_config_svsret() + dw_hdmi PHY configure path: leave + * SVSRET asserted before I2C/MPLL programming. Without it CONF0 stays + * ~0x4E and internal TX_PHY_LOCK (STAT0 bit0) never sets on A733. + */ + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_SVSRET, false); + udelay(5); + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_SVSRET, true); +} + +static void sun60i_snps_phy_mc_reset(struct sun60i_hdmi *hdmi, bool deassert) +{ + writeb(deassert ? SUN60I_MC_PHYRSTZ_DEASSERT : SUN60I_MC_PHYRSTZ_ASSERT, + hdmi->regs + SUN60I_DW_HDMI_MC_PHYRSTZ); +} + +static void sun60i_snps_phy_prepare_interface(struct sun60i_hdmi *hdmi) +{ + /* + * BSP dw_phy_config_interface(): select internal PHY I2C and program + * slave address directly. Do NOT use dw_hdmi_phy_i2c_set_addr() — its + * hdmi_phy_test_clear() pulse breaks I2CM on sun60i. + */ + writeb(SUN60I_DW_HDMI_JTAG_I2C_MODE, + hdmi->regs + SUN60I_DW_HDMI_JTAG_PHY_CONFIG); + writeb(SUN60I_DW_HDMI_PHY_I2CM_SLAVE_GEN2, + hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_SLAVE); + + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_SELDATAENPOL, true); + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_SELDIPIF, false); +} + +static void sun60i_snps_phy_mute_i2c_irq(struct sun60i_hdmi *hdmi) +{ + /* + * PHY-I2CM is polled locally while the Linux IRQ is disabled. Mask + * only that sub-source. Setting IH_MUTE.MUTE_ALL here also masks the + * external DDC master; its transfer reaches DONE, but dw-hdmi never + * receives the completion IRQ and consequently falls back to no-EDID + * modes such as 1024x768. + */ + writeb(0xff, hdmi->regs + SUN60I_DW_HDMI_IH_MUTE_I2CMPHY_STAT0); +} + +static void sun60i_snps_phy_irq_begin(struct sun60i_hdmi *hdmi) +{ + if (hdmi->irq <= 0 || hdmi->irq_snps_masked) + return; + + disable_irq_nosync(hdmi->irq); + hdmi->irq_snps_masked = true; +} + +static void sun60i_snps_phy_irq_end(struct sun60i_hdmi *hdmi) +{ + if (hdmi->irq <= 0 || !hdmi->irq_snps_masked) + return; + + enable_irq(hdmi->irq); + hdmi->irq_snps_masked = false; +} + +static void sun60i_snps_enable_mc_clocks(struct sun60i_hdmi *hdmi) +{ + writeb(0x00, hdmi->regs + SUN60I_DW_HDMI_MC_CLKDIS); +} + +static void sun60i_snps_phy_gen2_reset(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + /* + * sun8i A83T path before internal PHY I2C. BSP sun60i skips snps init + * reset but A733 may still need MC PHY reset to kick I2CM. + */ + if (dw_hdmi) + dw_hdmi_phy_gen2_reset(dw_hdmi); + else if (hdmi && hdmi->regs) + writeb(SUN60I_MC_PHYRSTZ_DEASSERT, hdmi->regs + SUN60I_DW_HDMI_MC_PHYRSTZ); + + udelay(10); +} + +static void sun60i_snps_phy_assert_power(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi, bool on) +{ + sun60i_snps_phy_set_power(hdmi, on); + if (!dw_hdmi) + return; + + dw_hdmi_phy_gen2_txpwron(dw_hdmi, on ? 1 : 0); + dw_hdmi_phy_gen2_pddq(dw_hdmi, on ? 0 : 1); +} + +static void sun60i_snps_phy_config_init(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + /* + * BSP _snps_phy_config_init(): MC PHY reset, power down, SVSRET pulse, + * hold PHY in reset, then arm I2C interface for MPLL/drive writes. + */ + sun60i_snps_phy_mc_reset(hdmi, true); + sun60i_snps_phy_assert_power(dw_hdmi, hdmi, false); + sun60i_snps_phy_config_svsret(hdmi); + sun60i_snps_phy_gen2_reset(dw_hdmi, hdmi); + sun60i_snps_phy_prepare_interface(hdmi); +} + +static void sun60i_snps_phy_init_i2cm_hw(struct sun60i_hdmi *hdmi) +{ + /* + * dw_hdmi leaves PHY_I2CM_INT/CTLINT at 0xff after initialize_hdmi_ih_mutes(). + * disp2 dw_phy_initialize() unmasks (mask=0), sets polarity, standard mode. + * Pulse SOFTRSTZ 0->1 to clear a stuck controller; leave deasserted (1). + */ + writeb(0x00, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_DIV); + writeb(0x00, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_SOFTRSTZ); + udelay(10); + writeb(0x01, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_SOFTRSTZ); + + writeb(BIT(3), hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_INT); + writeb(BIT(3) | BIT(7), hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_CTLINT); + sun60i_snps_phy_write_mask(hdmi, SUN60I_DW_HDMI_PHY_I2CM_INT, BIT(2), 0); + sun60i_snps_phy_write_mask(hdmi, SUN60I_DW_HDMI_PHY_I2CM_CTLINT, BIT(2), 0); + sun60i_snps_phy_write_mask(hdmi, SUN60I_DW_HDMI_PHY_I2CM_CTLINT, BIT(6), 0); + sun60i_snps_phy_mute_i2c_irq(hdmi); +} + +static void sun60i_snps_phy_prepare_i2cm(struct sun60i_hdmi *hdmi) +{ + /* + * Poll IH_I2CMPHY_STAT0 directly; keep IH muted so dw_hdmi hardirq + * does not fire during atomic commit (was: IRQ storm + disable #413). + */ + writeb(0xff, hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + sun60i_snps_phy_mute_i2c_irq(hdmi); +} + +static const struct sun60i_snps_mpll_entry * +sun60i_snps_find_mpll(unsigned int pixel_khz) +{ + const struct sun60i_snps_mpll_entry *best = &sun60i_snps_mpll[0]; + unsigned int best_diff = UINT_MAX; + size_t i; + + for (i = 0; i < ARRAY_SIZE(sun60i_snps_mpll); i++) { + unsigned int diff = abs((int)sun60i_snps_mpll[i].pixel_khz - + (int)pixel_khz); + + if (diff < best_diff) { + best = &sun60i_snps_mpll[i]; + best_diff = diff; + } + } + + return best; +} + +#define SUN60I_SNPS_I2C_PROBE_MS 50 +#define SUN60I_SNPS_I2C_XFER_MS 100 +#define SUN60I_SNPS_TXREADY_WAIT_MS 200 +#define SUN60I_SNPS_PIXEL_SETTLE_MS 20 +#define SUN60I_SNPS_I2C_POLL_US 10 + +static bool sun60i_snps_phy_locked(struct sun60i_hdmi *hdmi) +{ + return !!(readb(hdmi->regs + SUN60I_DW_HDMI_PHY_STAT0) & + SUN60I_DW_HDMI_PHY_TX_PHY_LOCK); +} + +static void sun60i_snps_phy_finish_tx(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + sun60i_snps_phy_assert_power(dw_hdmi, hdmi, true); + sun60i_hdmi_snps_phy_enable_tmds(hdmi); +} + +static bool sun60i_snps_wait_internal_lock(struct sun60i_hdmi *hdmi, + unsigned int ms) +{ + unsigned int i; + + for (i = 0; i < ms; i++) { + if (sun60i_snps_phy_locked(hdmi)) + return true; + udelay(1000); + } + + return false; +} + +static bool sun60i_snps_wait_txready(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + unsigned int i; + + mdelay(SUN60I_SNPS_PIXEL_SETTLE_MS); + sun60i_snps_phy_finish_tx(dw_hdmi, hdmi); + + for (i = 0; i < SUN60I_SNPS_TXREADY_WAIT_MS; i++) { + if (sun60i_hdmi_top_phy_wait_txready(hdmi, 1)) + return true; + + udelay(1000); + } + + return false; +} + +static bool sun60i_snps_phy_i2c_error(struct sun60i_hdmi *hdmi) +{ + u8 ctl = readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_CTLINT); + + return !!(ctl & (SUN60I_PHY_I2CM_NACK_STATUS | + SUN60I_PHY_I2CM_ARB_STATUS)); +} + +static bool sun60i_snps_phy_i2c_done_ms(struct sun60i_hdmi *hdmi, + unsigned int ms) +{ + u8 ih_stat = 0; + u8 phy_int = 0; + unsigned int tries = ms * (1000000 / SUN60I_SNPS_I2C_POLL_US); + unsigned int i; + + for (i = 0; i < tries; i++) { + ih_stat = readb(hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + phy_int = readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_INT); + + if (ih_stat & (SUN60I_I2CMPHY_DONE | SUN60I_I2CMPHY_ERROR)) + break; + if (phy_int & SUN60I_PHY_I2CM_DONE_STATUS) + break; + if (sun60i_snps_phy_i2c_error(hdmi)) + break; + + udelay(SUN60I_SNPS_I2C_POLL_US); + } + + if (sun60i_snps_phy_i2c_error(hdmi)) + return false; + + if (phy_int & SUN60I_PHY_I2CM_DONE_STATUS) { + if (ih_stat) + writeb(ih_stat, hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + return true; + } + + if (!(ih_stat & (SUN60I_I2CMPHY_DONE | SUN60I_I2CMPHY_ERROR))) + return false; + + writeb(ih_stat, hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + return (ih_stat & SUN60I_I2CMPHY_DONE) && !(ih_stat & SUN60I_I2CMPHY_ERROR); +} + +static void sun60i_snps_phy_log_i2cm(struct sun60i_hdmi *hdmi, const char *tag) +{ + dev_warn(hdmi->dev, + "snps-phy: %s slave=0x%02x jtag=0x%02x ih=0x%02x pint=0x%02x ctl=0x%02x op=0x%02x ih_mute=0x%02x conf0=0x%02x\n", + tag, + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_SLAVE), + readb(hdmi->regs + SUN60I_DW_HDMI_JTAG_PHY_CONFIG), + readb(hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0), + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_INT), + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_CTLINT), + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_OPERATION), + readb(hdmi->regs + SUN60I_DW_HDMI_IH_MUTE_I2CMPHY_STAT0), + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_CONF0)); +} + +static int sun60i_snps_phy_i2c_write_ms(struct sun60i_hdmi *hdmi, u16 data, + u8 addr, unsigned int wait_ms) +{ + sun60i_snps_phy_prepare_i2cm(hdmi); + writeb(0xff, hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + writeb(addr, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_ADDRESS); + writeb(data >> 8, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_DATAO_1); + writeb(data & 0xff, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_DATAO_0); + writeb(0x10, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_OPERATION); + + if (!sun60i_snps_phy_i2c_done_ms(hdmi, wait_ms)) { + sun60i_snps_phy_log_i2cm(hdmi, "I2C write timeout"); + dev_warn(hdmi->dev, "snps-phy: I2C write 0x%04x @0x%02x failed\n", + data, addr); + return -EIO; + } + + return 0; +} + +static bool sun60i_snps_phy_i2c_probe(struct sun60i_hdmi *hdmi, u16 data, + u8 addr) +{ + sun60i_snps_phy_prepare_i2cm(hdmi); + writeb(0xff, hdmi->regs + SUN60I_DW_HDMI_IH_I2CMPHY_STAT0); + writeb(addr, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_ADDRESS); + writeb(data >> 8, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_DATAO_1); + writeb(data & 0xff, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_DATAO_0); + writeb(0x10, hdmi->regs + SUN60I_DW_HDMI_PHY_I2CM_OPERATION); + + return sun60i_snps_phy_i2c_done_ms(hdmi, SUN60I_SNPS_I2C_PROBE_MS); +} + +static bool sun60i_snps_phy_i2c_probe_mpll(struct sun60i_hdmi *hdmi, + unsigned int pixel_khz) +{ + const struct sun60i_snps_mpll_entry *entry = sun60i_snps_find_mpll(pixel_khz); + + return sun60i_snps_phy_i2c_probe(hdmi, entry->data[0], + SUN60I_SNPS_MPLL_REG1); +} + +static bool sun60i_snps_phy_i2c_alive(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi, + unsigned int pixel_khz) +{ + bool ok; + + /* config_init() already left analog off and I2CM armed. */ + ok = sun60i_snps_phy_i2c_probe_mpll(hdmi, pixel_khz); + if (ok) + return true; + + sun60i_snps_phy_log_i2cm(hdmi, "I2CM probe failed"); + + sun60i_snps_phy_gen2_reset(dw_hdmi, hdmi); + sun60i_snps_phy_init_i2cm_hw(hdmi); + ok = sun60i_snps_phy_i2c_probe_mpll(hdmi, pixel_khz); + if (!ok) + sun60i_snps_phy_log_i2cm(hdmi, "I2CM probe failed retry"); + + return ok; +} + +static int sun60i_snps_program_mpll(struct sun60i_hdmi *hdmi, + unsigned int pixel_khz) +{ + const struct sun60i_snps_mpll_entry *entry = sun60i_snps_find_mpll(pixel_khz); + int ret; + + ret = sun60i_snps_phy_i2c_write_ms(hdmi, entry->data[0], + SUN60I_SNPS_MPLL_REG1, SUN60I_SNPS_I2C_XFER_MS); + if (ret) + return ret; + ret = sun60i_snps_phy_i2c_write_ms(hdmi, entry->data[1], + SUN60I_SNPS_MPLL_REG2, SUN60I_SNPS_I2C_XFER_MS); + if (ret) + return ret; + + return sun60i_snps_phy_i2c_write_ms(hdmi, entry->data[2], + SUN60I_SNPS_MPLL_REG3, SUN60I_SNPS_I2C_XFER_MS); +} + +static int sun60i_snps_program_drive(struct sun60i_hdmi *hdmi, + unsigned int tmds_khz) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(sun60i_snps_drive); i++) { + const struct sun60i_snps_drive_entry *e = &sun60i_snps_drive[i]; + int ret; + + if (tmds_khz < e->min_khz || tmds_khz >= e->max_khz) + continue; + + ret = sun60i_snps_phy_i2c_write_ms(hdmi, e->data[0], + SUN60I_SNPS_DRIVE_REG1, + SUN60I_SNPS_I2C_XFER_MS); + if (ret) + return ret; + ret = sun60i_snps_phy_i2c_write_ms(hdmi, e->data[1], + SUN60I_SNPS_DRIVE_REG2, + SUN60I_SNPS_I2C_XFER_MS); + if (ret) + return ret; + return sun60i_snps_phy_i2c_write_ms(hdmi, e->data[2], + SUN60I_SNPS_DRIVE_REG3, + SUN60I_SNPS_I2C_XFER_MS); + } + + return -EINVAL; +} + +void sun60i_hdmi_snps_phy_probe_init(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + if (!dw_hdmi || !hdmi || !hdmi->regs) + return; + + /* + * BSP dw_phy_init(): I2C access + HPD sense. sun60i snps init skips + * reset/power in configure; interface is armed once here at bind. + */ + sun60i_snps_phy_prepare_interface(hdmi); + sun60i_snps_phy_init_i2cm_hw(hdmi); + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_ENHPDRXSENSE, true); +} + +int sun60i_hdmi_snps_phy_configure(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi, + const struct drm_display_mode *mode) +{ + unsigned int pixel_khz = mode->clock; + unsigned int tmds_khz = mode->clock; + bool i2c_ok; + int ret; + + if (!dw_hdmi || !hdmi || !hdmi->regs || !mode->clock) + return -EINVAL; + + sun60i_snps_phy_irq_begin(hdmi); + + /* + * BSP sun60i: skip PHY reset in init; top-PHY already configured. + * Internal PHY I2CM is often dead on A733 — probe once, else MMIO-only. + */ + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_ENTMDS, false); + + sun60i_snps_phy_config_init(dw_hdmi, hdmi); + sun60i_snps_phy_init_i2cm_hw(hdmi); + sun60i_snps_enable_mc_clocks(hdmi); + + i2c_ok = sun60i_snps_phy_i2c_alive(dw_hdmi, hdmi, pixel_khz); + if (i2c_ok) { + /* MPLL/drive while analog off (BSP snps_phy_config order). */ + ret = sun60i_snps_program_mpll(hdmi, pixel_khz); + if (ret) { + dev_warn(hdmi->dev, + "snps-phy: MPLL I2C failed (%d), continuing MMIO-only\n", + ret); + i2c_ok = false; + } + if (i2c_ok) { + ret = sun60i_snps_program_drive(hdmi, tmds_khz); + if (ret) { + dev_warn(hdmi->dev, + "snps-phy: drive I2C failed (%d), continuing MMIO-only\n", + ret); + i2c_ok = false; + } + } + } + + sun60i_snps_phy_config_svsret(hdmi); + sun60i_snps_phy_assert_power(dw_hdmi, hdmi, true); + + if (i2c_ok && !sun60i_snps_wait_internal_lock(hdmi, 500)) + dev_warn(hdmi->dev, + "snps-phy: internal lock timeout (stat0=0x%02x)\n", + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_STAT0)); + + if (!sun60i_snps_wait_txready(dw_hdmi, hdmi)) + dev_warn(hdmi->dev, + "snps-phy: top-PHY txready timeout (conf0=0x%02x stat0=0x%02x)\n", + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_CONF0), + readb(hdmi->regs + SUN60I_DW_HDMI_PHY_STAT0)); + + /* Always leave ENTMDS on even if txready never asserted. */ + sun60i_snps_phy_finish_tx(dw_hdmi, hdmi); + + mdelay(10); + + sun60i_snps_phy_irq_end(hdmi); + + return 0; +} + +void sun60i_hdmi_snps_phy_enable_tmds(struct sun60i_hdmi *hdmi) +{ + if (!hdmi || !hdmi->regs) + return; + + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_ENTMDS, true); +} + +void sun60i_hdmi_snps_phy_disable(struct dw_hdmi *dw_hdmi, + struct sun60i_hdmi *hdmi) +{ + if (!hdmi || !hdmi->regs) + return; + + /* + * BSP sun60i snps_phy_disconfig(): skip dw_phy_standby() — powering + * down the internal PHY between modesets breaks the next top-PHY path. + */ + sun60i_snps_phy_set_bit(hdmi, SUN60I_DW_HDMI_PHY_CONF0, + SUN60I_DW_HDMI_PHY_CONF0_ENTMDS, false); +} diff --git a/drivers/gpu/drm/sun4i/sun60i_hdmi_top_phy.c b/drivers/gpu/drm/sun4i/sun60i_hdmi_top_phy.c new file mode 100644 index 00000000000000..62fc2975280595 --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun60i_hdmi_top_phy.c @@ -0,0 +1,483 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ +/* + * sun60i top-PHY helper aligned with BSP phy_top.c init/config sequence. + */ + +#include +#include +#include +#include +#include +#include + +#include "sun60i_hdmi.h" + +/* DW-HDMI main controller register (byte); not in public dw_hdmi.h UAPI */ +#define SUN60I_DW_HDMI_MC_CLKDIS 0x4001 +#define SUN60I_DW_HDMI_FC_INVIDCONF 0x1000 +#define SUN60I_DW_HDMI_FC_AVICONF0 0x1005 +#define SUN60I_DW_HDMI_FC_GCP 0x1018 +#define SUN60I_DW_HDMI_MC_FLOWCTRL 0x4004 + +#define SUN60I_TOP_PHY_REG0000 0x0000 +#define SUN60I_TOP_PHY_REG0004 0x0004 +#define SUN60I_TOP_PHY_REG0010 0x0010 +#define SUN60I_TOP_PHY_REG0020 0x0020 +#define SUN60I_TOP_PHY_REG0024 0x0024 +#define SUN60I_TOP_PHY_REG0028 0x0028 +#define SUN60I_TOP_PHY_REG002c 0x002c +#define SUN60I_TOP_PHY_REG0030 0x0030 +#define SUN60I_TOP_PHY_REG0040 0x0040 + +#define SUN60I_RESCAL_RES0_OFFSET 0x4 + +/* BSP top_phy_power(ON): reset, pddq, txpwron, enhpdrxsense */ +#define SUN60I_TOP_PHY_PWR_ON (BIT(0) | BIT(1) | BIT(2) | BIT(4)) +#define SUN60I_TOP_PHY_TXPWRON BIT(2) +#define SUN60I_TOP_PHY_TXREADY BIT(0) +#define SUN60I_TOP_PHY_PLL_OUT_GATE BIT(27) +#define SUN60I_TOP_PHY_LOCK_ENABLE BIT(29) +#define SUN60I_TOP_PHY_PLL_LDO_EN BIT(30) +#define SUN60I_TOP_PHY_PLL_EN BIT(31) +#define SUN60I_TOP_PHY_LEVEL_SHIFTER BIT(7) +#define SUN60I_TOP_PHY_HDMI_OUTCLK_SEL BIT(8) +#define SUN60I_TOP_PHY_PLL_LOCKED BIT(0) + +#define SUN60I_RESCAL_HDMI_RES_SEL BIT(12) +#define SUN60I_RESCAL_MODE BIT(2) +#define SUN60I_RESCAL_ANA_EN BIT(1) +#define SUN60I_RESCAL_EN BIT(0) +#define SUN60I_RESCAL_RES0_MASK GENMASK(31, 24) + +#define SUN60I_PLLVCO_MIN_KHZ 1260000 +#define SUN60I_PLLVCO_MAX_KHZ 2250000 +#define SUN60I_PLL_AUTO_MAX_RESULT 5 + +struct sun60i_top_phy_pll { + unsigned long pixel_clock; + u32 pll_value; + u32 ldo_value; + u32 pll_pattern0; + u32 pll_pattern1; +}; + +struct sun60i_top_phy_pll_cal { + u32 pll_n; + u32 pll_m; + u32 pll_p; + u64 diff; +}; + +static const struct sun60i_top_phy_pll sun60i_top_phy_24m[] = { + { 13500, 0xe85f3500, 0x00035000, 0x00000000, 0x30000000 }, + { 27000, 0xe8576200, 0x00035000, 0x80000000, 0x30000000 }, + { 54000, 0xe82b6200, 0x00035000, 0x80000000, 0x30000000 }, + { 65000, 0xe8246200, 0x00035000, 0x80000000, 0x30000000 }, + { 74250, 0xe81f6200, 0x00035000, 0x80000000, 0x30000000 }, + { 148500, 0xe80f6200, 0x00035000, 0x80000000, 0x30000000 }, + { 185625, 0xe80f6200, 0x00035000, 0x80000000, 0x30000000 }, + { 297000, 0xe8076200, 0x00035000, 0x00000000, 0x30000000 }, + { 371250, 0xe8076200, 0x00035000, 0x00000000, 0x30000000 }, + { 594000, 0xe8036200, 0x00035000, 0x00000000, 0x30000000 }, +}; + +static const struct sun60i_top_phy_pll sun60i_top_phy_26m[] = { + { 13500, 0xe8673500, 0x00035000, 0x00000000, 0x30000000 }, + { 27000, 0xe8595c00, 0x00035000, 0x80000000, 0x30000000 }, + { 54000, 0xe80c1a00, 0x00035000, 0x80000000, 0x30000000 }, + { 65000, 0xe8235a00, 0x00035000, 0x80000000, 0x30000000 }, + { 74250, 0xe81f5a00, 0x00035000, 0x80000000, 0x30000000 }, + { 108000, 0xe80c3500, 0x00035000, 0x00000000, 0x30000000 }, + { 148500, 0xe80f5a00, 0x00035000, 0x80000000, 0x30000000 }, + { 185625, 0xe80f5a00, 0x00035000, 0x80000000, 0x30000000 }, + { 297000, 0xe807b602, 0x00035000, 0x00000000, 0x30000000 }, + { 371250, 0xe807b602, 0x00035000, 0x00000000, 0x30000000 }, + { 594000, 0xe803b602, 0x00035000, 0x00000000, 0x30000000 }, +}; + +static inline void sun60i_top_phy_write(struct sun60i_hdmi *hdmi, + u32 reg, u32 value) +{ + writel(value, hdmi->regs + SUN60I_HDMI_TOP_PHY_OFFSET + reg); +} + +static inline u32 sun60i_top_phy_read(struct sun60i_hdmi *hdmi, u32 reg) +{ + return readl(hdmi->regs + SUN60I_HDMI_TOP_PHY_OFFSET + reg); +} + +static unsigned long sun60i_top_phy_diff(unsigned long a, unsigned long b) +{ + return a > b ? a - b : b - a; +} + +static void sun60i_top_phy_pll_set_output(struct sun60i_hdmi *hdmi, bool on) +{ + u32 value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + + if (on) + value |= SUN60I_TOP_PHY_PLL_OUT_GATE; + else + value &= ~SUN60I_TOP_PHY_PLL_OUT_GATE; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); +} + +static void sun60i_top_phy_set_clock_select(struct sun60i_hdmi *hdmi, u8 sel) +{ + u32 value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0024); + + if (sel) + value |= SUN60I_TOP_PHY_HDMI_OUTCLK_SEL; + else + value &= ~SUN60I_TOP_PHY_HDMI_OUTCLK_SEL; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0024, value); +} + +static void sun60i_top_phy_apply_pll_fields(struct sun60i_hdmi *hdmi, + u32 pll_value) +{ + u32 value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + + value &= ~(BIT(1) | GENMASK(7, 5) | GENMASK(15, 8) | GENMASK(22, 16)); + value |= (pll_value & 0x00000002); + value |= (pll_value & 0x00000020); + value |= (pll_value & 0x000000c0); + value |= (pll_value & 0x0000ff00); + value |= (pll_value & 0x007f0000); + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); +} + +static int sun60i_top_phy_pll_auto_cal(unsigned long dcxo_khz, + unsigned long pixel_clock, + struct sun60i_top_phy_pll *cfg) +{ + struct sun60i_top_phy_pll_cal temp[SUN60I_PLL_AUTO_MAX_RESULT]; + struct sun60i_top_phy_pll_cal result = {}; + unsigned long min_diff = ULONG_MAX; + unsigned long dcxo_hz = dcxo_khz; + unsigned long target_clk = pixel_clock; + unsigned int temp_count = 0; + bool has_m1 = false, has_result = false; + unsigned long temp_vco_max = 0; + u16 temp_m, temp_n, temp_p; + unsigned int i; + + for (temp_m = 1; temp_m <= 2; temp_m++) { + for (temp_n = 1; temp_n <= 255; temp_n++) { + unsigned long temp_vco = (dcxo_hz * temp_n) / temp_m; + + if (temp_vco < SUN60I_PLLVCO_MIN_KHZ || + temp_vco > SUN60I_PLLVCO_MAX_KHZ) + continue; + + for (temp_p = 1; temp_p <= 128; temp_p++) { + unsigned long temp_clk = + (dcxo_hz * temp_n) / (temp_m * temp_p); + unsigned long temp_diff = + sun60i_top_phy_diff(temp_clk, target_clk); + + if (temp_diff < min_diff) + min_diff = temp_diff; + } + } + } + + for (temp_m = 1; temp_m <= 2; temp_m++) { + for (temp_n = 1; temp_n <= 255; temp_n++) { + unsigned long temp_vco = (dcxo_hz * temp_n) / temp_m; + + if (temp_vco < SUN60I_PLLVCO_MIN_KHZ || + temp_vco > SUN60I_PLLVCO_MAX_KHZ) + continue; + + for (temp_p = 1; temp_p <= 128; temp_p++) { + unsigned long temp_clk = + (dcxo_hz * temp_n) / (temp_m * temp_p); + unsigned long temp_diff = + sun60i_top_phy_diff(temp_clk, target_clk); + + if (temp_diff == min_diff && + temp_count < SUN60I_PLL_AUTO_MAX_RESULT) { + temp[temp_count].pll_n = temp_n; + temp[temp_count].pll_m = temp_m; + temp[temp_count].pll_p = temp_p; + temp[temp_count].diff = temp_diff; + temp_count++; + } + } + } + } + + if (!temp_count) + return -EINVAL; + + for (i = 0; i < temp_count; i++) { + if (temp[i].pll_m == 1) { + has_m1 = true; + break; + } + } + + for (i = 0; i < temp_count; i++) { + unsigned long temp_vco; + + if (has_m1 && temp[i].pll_m != 1) + continue; + + temp_vco = dcxo_hz * temp[i].pll_n; + if (temp_vco > temp_vco_max) { + temp_vco_max = temp_vco; + result = temp[i]; + has_result = true; + } + } + + if (!has_result) + return -EINVAL; + + cfg->pixel_clock = pixel_clock; + cfg->pll_value = 0xe8000000; + cfg->pll_value |= (result.pll_m - 1) << 1; + cfg->pll_value |= (result.pll_n - 1) << 8; + cfg->pll_value |= (result.pll_p - 1) << 16; + cfg->ldo_value = 0x00035000; + cfg->pll_pattern0 = 0x00000000; + cfg->pll_pattern1 = 0x30000000; + return 0; +} + +static int sun60i_top_phy_lookup_pll(const struct sun60i_top_phy_pll *table, + size_t count, unsigned long dcxo_khz, + unsigned long pixel_clock, + struct sun60i_top_phy_pll *cfg) +{ + unsigned long tolerance; + size_t i; + + for (i = 0; i < count; i++) { + tolerance = pixel_clock / 1000; + if (!tolerance) + tolerance = 1; + if (sun60i_top_phy_diff(pixel_clock, table[i].pixel_clock) <= + tolerance) { + *cfg = table[i]; + return 0; + } + } + + if (!sun60i_top_phy_pll_auto_cal(dcxo_khz, pixel_clock, cfg)) + return 0; + + for (i = 0; i + 1 < count; i++) { + if (pixel_clock > table[i].pixel_clock && + pixel_clock < table[i + 1].pixel_clock) { + if ((table[i + 1].pixel_clock - pixel_clock) > + (pixel_clock - table[i].pixel_clock)) + *cfg = table[i]; + else + *cfg = table[i + 1]; + return 0; + } + } + + *cfg = table[0]; + for (i = 1; i < count; i++) { + if (sun60i_top_phy_diff(pixel_clock, table[i].pixel_clock) < + sun60i_top_phy_diff(pixel_clock, cfg->pixel_clock)) + *cfg = table[i]; + } + + return 0; +} + +bool sun60i_hdmi_top_phy_wait_txready(struct sun60i_hdmi *hdmi, + unsigned int ms) +{ + unsigned int i; + + if (!hdmi->regs) + return false; + + for (i = 0; i < ms; i++) { + u32 stat = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0010); + + if (stat & SUN60I_TOP_PHY_TXREADY) + return true; + udelay(1000); + } + + return false; +} + +int sun60i_hdmi_rescal_board_init(struct sun60i_hdmi *hdmi) +{ + u32 value; + + if (!hdmi->rescal_regs) + return 0; + + if (hdmi->resistor_src_onboard) { + value = readl(hdmi->rescal_regs); + value |= SUN60I_RESCAL_HDMI_RES_SEL; + writel(value, hdmi->rescal_regs); + + value = readl(hdmi->rescal_regs + SUN60I_RESCAL_RES0_OFFSET); + value &= ~SUN60I_RESCAL_RES0_MASK; + writel(value, hdmi->rescal_regs + SUN60I_RESCAL_RES0_OFFSET); + return 0; + } + + value = readl(hdmi->rescal_regs); + value &= ~SUN60I_RESCAL_EN; + writel(value, hdmi->rescal_regs); + + value &= ~SUN60I_RESCAL_HDMI_RES_SEL; + writel(value, hdmi->rescal_regs); + + value |= SUN60I_RESCAL_ANA_EN; + writel(value, hdmi->rescal_regs); + + value |= SUN60I_RESCAL_EN; + writel(value, hdmi->rescal_regs); + + udelay(200); + + value &= ~SUN60I_RESCAL_ANA_EN; + writel(value, hdmi->rescal_regs); + + value &= ~SUN60I_RESCAL_EN; + writel(value, hdmi->rescal_regs); + + return 0; +} + +int sun60i_hdmi_top_phy_init(struct sun60i_hdmi *hdmi) +{ + u32 value; + + if (!hdmi->regs) + return -ENODEV; + + if (hdmi->top_phy_inited) + return 0; + + /* BSP: top_phy_power(ON) */ + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0000); + value |= SUN60I_TOP_PHY_PWR_ON; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0000, value); + + /* Route TMDS to HDMI pads (not GPIO). */ + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0004, 0); + + /* BSP init: outgate off, then clock source select. */ + sun60i_top_phy_pll_set_output(hdmi, false); + sun60i_top_phy_set_clock_select(hdmi, hdmi->clock_src); + + hdmi->top_phy_inited = true; + return 0; +} + +int sun60i_hdmi_top_phy_config(struct sun60i_hdmi *hdmi, + const struct drm_display_mode *mode) +{ + const struct sun60i_top_phy_pll *table; + struct sun60i_top_phy_pll cfg = {}; + size_t table_count; + unsigned long dcxo_rate, dcxo_khz, pixel_clock; + u32 value; + int i, ret; + + if (!hdmi->clk_dcxo || !hdmi->regs) + return 0; + + /* BSP top_phy_config(): disable output gate first. */ + sun60i_top_phy_pll_set_output(hdmi, false); + + if (hdmi->clock_src) { + dev_dbg(hdmi->dev, "top-phy: CCMU clock source, skip PLL config\n"); + return 0; + } + + dcxo_rate = clk_get_rate(hdmi->clk_dcxo); + switch (dcxo_rate) { + case 24000000: + table = sun60i_top_phy_24m; + table_count = ARRAY_SIZE(sun60i_top_phy_24m); + break; + case 26000000: + table = sun60i_top_phy_26m; + table_count = ARRAY_SIZE(sun60i_top_phy_26m); + break; + default: + dev_warn(hdmi->dev, "unsupported dcxo clock %luHz for top-phy\n", + dcxo_rate); + return -EINVAL; + } + + dcxo_khz = dcxo_rate / 1000; + pixel_clock = mode->clock * (hdmi->pixel_repeat + 1); + + ret = sun60i_top_phy_lookup_pll(table, table_count, dcxo_khz, + pixel_clock, &cfg); + if (ret) + return ret; + + sun60i_top_phy_apply_pll_fields(hdmi, cfg.pll_value); + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0028, cfg.ldo_value); + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG002c, cfg.pll_pattern0); + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0030, cfg.pll_pattern1); + + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + if (!(value & SUN60I_TOP_PHY_PLL_EN)) { + value |= SUN60I_TOP_PHY_PLL_EN; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); + } + + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + if (!(value & SUN60I_TOP_PHY_PLL_LDO_EN)) { + value |= SUN60I_TOP_PHY_PLL_LDO_EN; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); + } + + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0024); + value |= SUN60I_TOP_PHY_LEVEL_SHIFTER; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0024, value); + + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + value |= SUN60I_TOP_PHY_LOCK_ENABLE; + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); + + for (i = 0; i < 200; i++) { + if (sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0040) & + SUN60I_TOP_PHY_PLL_LOCKED) + break; + udelay(100); + } + + if (i == 200) { + dev_warn(hdmi->dev, "top-phy lock timeout for %lu kHz mode\n", + pixel_clock); + return -ETIMEDOUT; + } + + udelay(20); + sun60i_top_phy_pll_set_output(hdmi, true); + + return 0; +} + +void sun60i_hdmi_top_phy_disable(struct sun60i_hdmi *hdmi) +{ + u32 value; + + if (!hdmi->regs) + return; + + sun60i_top_phy_pll_set_output(hdmi, false); + + value = sun60i_top_phy_read(hdmi, SUN60I_TOP_PHY_REG0020); + value &= ~(SUN60I_TOP_PHY_PLL_EN | SUN60I_TOP_PHY_PLL_LDO_EN | + SUN60I_TOP_PHY_LOCK_ENABLE); + sun60i_top_phy_write(hdmi, SUN60I_TOP_PHY_REG0020, value); +} diff --git a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c index 96532709c2a7ed..da6fecbc0836f6 100644 --- a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c +++ b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.c @@ -3,10 +3,16 @@ * Copyright (c) 2018 Jernej Skrabec */ +#include #include +#include +#include #include #include +#include +#include #include +#include #include #include @@ -14,6 +20,7 @@ #include "sun8i_dw_hdmi.h" #include "sun8i_tcon_top.h" +#include "sun60i_hdmi.h" static void sun8i_dw_hdmi_encoder_mode_set(struct drm_encoder *encoder, struct drm_display_mode *mode, @@ -21,7 +28,16 @@ static void sun8i_dw_hdmi_encoder_mode_set(struct drm_encoder *encoder, { struct sun8i_dw_hdmi *hdmi = encoder_to_sun8i_dw_hdmi(encoder); - clk_set_rate(hdmi->clk_tmds, mode->crtc_clock * 1000); + if (hdmi->quirks->integrated_phy) { + if (hdmi->sun60i.clk_tcon_tv) + clk_set_rate(hdmi->sun60i.clk_tcon_tv, + adj_mode->crtc_clock * 1000); + if (hdmi->sun60i.clk_hdmi) + clk_set_rate(hdmi->sun60i.clk_hdmi, + adj_mode->crtc_clock * 1000); + } else { + clk_set_rate(hdmi->clk_tmds, mode->crtc_clock * 1000); + } } static const struct drm_encoder_helper_funcs @@ -40,6 +56,237 @@ sun8i_dw_hdmi_mode_valid_a83t(struct dw_hdmi *hdmi, void *data, return MODE_OK; } +static enum drm_mode_status +sun8i_dw_hdmi_mode_valid_a733(struct dw_hdmi *hdmi, void *data, + const struct drm_display_info *info, + const struct drm_display_mode *mode) +{ + if (mode->clock > 594000) + return MODE_CLOCK_HIGH; + + return MODE_OK; +} + +static int sun8i_dw_hdmi_tcon_top_gate(struct device *dev, bool enable) +{ + struct device_node *remote; + struct platform_device *top_pdev; + int ret = -ENODEV; + + remote = of_graph_get_remote_node(dev->of_node, 0, -1); + if (!remote) + return ret; + + top_pdev = of_find_device_by_node(remote); + of_node_put(remote); + if (!top_pdev) + return ret; + + ret = sun8i_tcon_top_hdmi_gate_enable(&top_pdev->dev, enable); + put_device(&top_pdev->dev); + + return ret; +} + +static int sun8i_dw_hdmi_bind_sun60i(struct sun8i_dw_hdmi *hdmi, + struct platform_device *pdev, + struct drm_device *drm) +{ + struct device *dev = &pdev->dev; + struct sun60i_hdmi *s60 = &hdmi->sun60i; + struct resource *res; + struct dw_hdmi_plat_data *plat_data = &hdmi->plat_data; + int ret; + + s60->dev = dev; + + /* + * Share the HDMI MMIO window with dw_hdmi_bind(), which claims the + * platform resource via devm_ioremap_resource(). Use a non-exclusive + * map here so Top-PHY/SNPS helpers can reach +0xe0000 without -EBUSY. + */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EINVAL; + s60->regs = devm_ioremap(dev, res->start, resource_size(res)); + if (!s60->regs) + return -ENOMEM; + + s60->rescal_regs = devm_ioremap(dev, SUN60I_HDMI_RESCAL_PHYS, + SUN60I_HDMI_RESCAL_SIZE); + + s60->clk_cec = devm_clk_get_optional(dev, "clk_cec"); + if (IS_ERR(s60->clk_cec)) + return dev_err_probe(dev, PTR_ERR(s60->clk_cec), + "Couldn't get cec clock\n"); + + s60->clk_tcon_tv = devm_clk_get_optional(dev, "clk_tcon_tv"); + if (IS_ERR(s60->clk_tcon_tv)) + return dev_err_probe(dev, PTR_ERR(s60->clk_tcon_tv), + "Couldn't get tcon_tv clock\n"); + + s60->clk_hdmi = devm_clk_get(dev, "clk_hdmi"); + if (IS_ERR(s60->clk_hdmi)) + return dev_err_probe(dev, PTR_ERR(s60->clk_hdmi), + "Couldn't get hdmi clock\n"); + + s60->clk_hdmi_24m = devm_clk_get_optional(dev, "clk_hdmi_24M"); + if (IS_ERR(s60->clk_hdmi_24m)) + return dev_err_probe(dev, PTR_ERR(s60->clk_hdmi_24m), + "Couldn't get hdmi 24M clock\n"); + + s60->clk_hdcp = devm_clk_get_optional(dev, "clk_hdcp"); + if (IS_ERR(s60->clk_hdcp)) + return dev_err_probe(dev, PTR_ERR(s60->clk_hdcp), + "Couldn't get hdcp clock\n"); + + s60->clk_dcxo = devm_clk_get_optional(dev, "clk_dcxo"); + if (IS_ERR(s60->clk_dcxo)) + return dev_err_probe(dev, PTR_ERR(s60->clk_dcxo), + "Couldn't get dcxo clock\n"); + + s60->clk_isfr = devm_clk_get(dev, "isfr"); + if (IS_ERR(s60->clk_isfr)) + return dev_err_probe(dev, PTR_ERR(s60->clk_isfr), + "Couldn't get HDMI SFR clock\n"); + + s60->clk_iahb = devm_clk_get(dev, "iahb"); + if (IS_ERR(s60->clk_iahb)) + return dev_err_probe(dev, PTR_ERR(s60->clk_iahb), + "Couldn't get HDMI AHB clock\n"); + + s60->rst_bus_sub = devm_reset_control_get_optional(dev, "rst_bus_sub"); + if (IS_ERR(s60->rst_bus_sub)) + return dev_err_probe(dev, PTR_ERR(s60->rst_bus_sub), + "Couldn't get sub reset\n"); + + s60->rst_bus_main = devm_reset_control_get(dev, "rst_bus_main"); + if (IS_ERR(s60->rst_bus_main)) + return dev_err_probe(dev, PTR_ERR(s60->rst_bus_main), + "Couldn't get main reset\n"); + + s60->rst_bus_hdcp = devm_reset_control_get_optional(dev, "rst_bus_hdcp"); + if (IS_ERR(s60->rst_bus_hdcp)) + return dev_err_probe(dev, PTR_ERR(s60->rst_bus_hdcp), + "Couldn't get hdcp reset\n"); + + hdmi->rst_ctrl = s60->rst_bus_main; + + /* BSP opens the TCON HDMI path before the HDMI clocks and resets. */ + ret = sun8i_dw_hdmi_tcon_top_gate(dev, true); + if (ret) + return dev_err_probe(dev, ret, + "Couldn't enable TCON-top HDMI gate\n"); + + /* Match BSP/opentina: dcxo + clk_tcon_tv before MAIN/SUB reset. */ + ret = clk_prepare_enable(s60->clk_dcxo); + if (ret) + goto err_disable_tcon_gate; + + ret = clk_prepare_enable(s60->clk_tcon_tv); + if (ret) + goto err_disable_dcxo; + + ret = reset_control_deassert(s60->rst_bus_main); + if (ret) + goto err_disable_tcon_tv; + + ret = reset_control_deassert(s60->rst_bus_sub); + if (ret) + goto err_assert_main; + + ret = clk_prepare_enable(s60->clk_hdmi_24m); + if (ret) + goto err_assert_sub; + + ret = clk_prepare_enable(s60->clk_hdmi); + if (ret) + goto err_disable_hdmi24; + + ret = clk_prepare_enable(s60->clk_cec); + if (ret) + goto err_disable_hdmi; + + s60->clock_src = s60->clk_dcxo && + clk_get_rate(s60->clk_dcxo) > 25000000 ? 1 : 0; + + s60->irq = platform_get_irq(pdev, 0); + if (s60->irq < 0) { + ret = s60->irq; + goto err_disable_cec; + } + + /* Prefer the DT-provided DCXO as the HDMI SFR parent. */ + if (s60->clk_dcxo && clk_set_parent(s60->clk_isfr, s60->clk_dcxo)) + dev_warn(dev, "failed to set HDMI SFR clock parent\n"); + + ret = clk_prepare_enable(s60->clk_isfr); + if (ret) + goto err_disable_cec; + + ret = clk_prepare_enable(s60->clk_iahb); + if (ret) + goto err_disable_isfr; + + ret = sun60i_hdmi_rescal_board_init(s60); + if (ret) + goto err_disable_iahb; + + ret = sun60i_hdmi_top_phy_init(s60); + if (ret) + goto err_disable_iahb; + + plat_data->mode_valid = hdmi->quirks->mode_valid; + plat_data->use_drm_infoframe = hdmi->quirks->use_drm_infoframe; + plat_data->hpd_debounce_ms = 100; + plat_data->edid_retry_count = 5; + plat_data->edid_retry_delay_ms = 100; + plat_data->phy_ops = sun60i_hdmi_phy_ops_get(); + plat_data->phy_name = "sun60i_top_snps"; + plat_data->phy_data = s60; + /* CONFIG2_ID reads as vendor PHY on A733; force glue-layer ops. */ + plat_data->phy_force_vendor = true; + + platform_set_drvdata(pdev, hdmi); + + hdmi->hdmi = dw_hdmi_bind(pdev, &hdmi->encoder, plat_data); + if (IS_ERR(hdmi->hdmi)) { + ret = PTR_ERR(hdmi->hdmi); + goto err_disable_iahb; + } + + /* dw-hdmi now owns its runtime isfr/iahb clock references. */ + clk_disable_unprepare(s60->clk_iahb); + clk_disable_unprepare(s60->clk_isfr); + + sun60i_hdmi_snps_phy_probe_init(hdmi->hdmi, s60); + + return 0; + +err_disable_iahb: + clk_disable_unprepare(s60->clk_iahb); +err_disable_isfr: + clk_disable_unprepare(s60->clk_isfr); +err_disable_cec: + clk_disable_unprepare(s60->clk_cec); +err_disable_hdmi: + clk_disable_unprepare(s60->clk_hdmi); +err_disable_hdmi24: + clk_disable_unprepare(s60->clk_hdmi_24m); +err_assert_sub: + reset_control_assert(s60->rst_bus_sub); +err_assert_main: + reset_control_assert(s60->rst_bus_main); +err_disable_tcon_tv: + clk_disable_unprepare(s60->clk_tcon_tv); +err_disable_dcxo: + clk_disable_unprepare(s60->clk_dcxo); +err_disable_tcon_gate: + sun8i_dw_hdmi_tcon_top_gate(dev, false); + + return ret; +} + static enum drm_mode_status sun8i_dw_hdmi_mode_valid_h6(struct dw_hdmi *hdmi, void *data, const struct drm_display_info *info, @@ -128,6 +375,16 @@ static int sun8i_dw_hdmi_bind(struct device *dev, struct device *master, if (encoder->possible_crtcs == 0) return -EPROBE_DEFER; + drm_encoder_helper_add(encoder, &sun8i_dw_hdmi_encoder_helper_funcs); + drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS); + + if (hdmi->quirks->integrated_phy) { + ret = sun8i_dw_hdmi_bind_sun60i(hdmi, pdev, drm); + if (ret) + drm_encoder_cleanup(encoder); + return ret; + } + hdmi->rst_ctrl = devm_reset_control_get(dev, "ctrl"); if (IS_ERR(hdmi->rst_ctrl)) return dev_err_probe(dev, PTR_ERR(hdmi->rst_ctrl), @@ -179,9 +436,6 @@ static int sun8i_dw_hdmi_bind(struct device *dev, struct device *master, if (ret) goto err_disable_clk_tmds; - drm_encoder_helper_add(encoder, &sun8i_dw_hdmi_encoder_helper_funcs); - drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS); - plat_data->mode_valid = hdmi->quirks->mode_valid; plat_data->use_drm_infoframe = hdmi->quirks->use_drm_infoframe; sun8i_hdmi_phy_set_ops(hdmi->phy, plat_data); @@ -219,6 +473,22 @@ static void sun8i_dw_hdmi_unbind(struct device *dev, struct device *master, struct sun8i_dw_hdmi *hdmi = dev_get_drvdata(dev); dw_hdmi_unbind(hdmi->hdmi); + + if (hdmi->quirks->integrated_phy) { + struct sun60i_hdmi *s60 = &hdmi->sun60i; + + /* HDCP left off intentionally (see bind_sun60i). */ + clk_disable_unprepare(s60->clk_cec); + clk_disable_unprepare(s60->clk_hdmi); + clk_disable_unprepare(s60->clk_hdmi_24m); + reset_control_assert(s60->rst_bus_sub); + reset_control_assert(s60->rst_bus_main); + clk_disable_unprepare(s60->clk_tcon_tv); + clk_disable_unprepare(s60->clk_dcxo); + sun8i_dw_hdmi_tcon_top_gate(dev, false); + return; + } + sun8i_hdmi_phy_deinit(hdmi->phy); clk_disable_unprepare(hdmi->clk_tmds); reset_control_assert(hdmi->rst_ctrl); @@ -249,6 +519,12 @@ static const struct sun8i_dw_hdmi_quirks sun50i_h6_quirks = { .use_drm_infoframe = true, }; +static const struct sun8i_dw_hdmi_quirks sun60i_a733_quirks = { + .mode_valid = sun8i_dw_hdmi_mode_valid_a733, + .use_drm_infoframe = true, + .integrated_phy = true, +}; + static const struct of_device_id sun8i_dw_hdmi_dt_ids[] = { { .compatible = "allwinner,sun8i-a83t-dw-hdmi", @@ -258,6 +534,10 @@ static const struct of_device_id sun8i_dw_hdmi_dt_ids[] = { .compatible = "allwinner,sun50i-h6-dw-hdmi", .data = &sun50i_h6_quirks, }, + { + .compatible = "allwinner,sun60i-a733-dw-hdmi", + .data = &sun60i_a733_quirks, + }, { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, sun8i_dw_hdmi_dt_ids); diff --git a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h index ab80d52a70bbee..8c4d1d5d3f0524 100644 --- a/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h +++ b/drivers/gpu/drm/sun4i/sun8i_dw_hdmi.h @@ -13,6 +13,8 @@ #include #include +#include "sun60i_hdmi.h" + #define SUN8I_HDMI_PHY_DBG_CTRL_REG 0x0000 #define SUN8I_HDMI_PHY_DBG_CTRL_PX_LOCK BIT(0) #define SUN8I_HDMI_PHY_DBG_CTRL_POL_MASK GENMASK(15, 8) @@ -175,6 +177,7 @@ struct sun8i_dw_hdmi_quirks { const struct drm_display_info *info, const struct drm_display_mode *mode); unsigned int use_drm_infoframe : 1; + unsigned int integrated_phy : 1; }; struct sun8i_dw_hdmi { @@ -187,6 +190,7 @@ struct sun8i_dw_hdmi { struct regulator *regulator; const struct sun8i_dw_hdmi_quirks *quirks; struct reset_control *rst_ctrl; + struct sun60i_hdmi sun60i; }; extern struct platform_driver sun8i_hdmi_phy_driver; From ae47a5935b436a689ea952b64c523ec8707d346e Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:23:16 +0800 Subject: [PATCH 13/16] arm64: dts: allwinner: describe the A733 HDMI display pipeline Describe the DE352 clock controller and mixer, TCON-TV, Video Output 1 top controller and HDMI transmitter with their graph connections. Add the reserved CMA pool used by the DRM master. Keep the display consumers disabled until a board enables the pipeline. LCD, DSI and Combo-PHY nodes are introduced in the panel-output series. Describe only the DE_SYS child required by the existing IOMMU master power-domain discovery code. Keep this compatibility description with the display pipeline instead of enumerating unrelated power domains. --- .../dts/allwinner/sun60i-a733-display.dtsi | 219 ++++++++++++++++++ .../arm64/boot/dts/allwinner/sun60i-a733.dtsi | 15 ++ 2 files changed, 234 insertions(+) create mode 100644 arch/arm64/boot/dts/allwinner/sun60i-a733-display.dtsi diff --git a/arch/arm64/boot/dts/allwinner/sun60i-a733-display.dtsi b/arch/arm64/boot/dts/allwinner/sun60i-a733-display.dtsi new file mode 100644 index 00000000000000..785727e375ac60 --- /dev/null +++ b/arch/arm64/boot/dts/allwinner/sun60i-a733-display.dtsi @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: (GPL-2.0-or-later OR MIT) +/* Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/ { + de: de_drm: display-engine { + compatible = "allwinner,sun60i-a733-display-engine"; + allwinner,pipelines = <&mixer0>; + memory-region = <&drm_cma>; + iommus = <&mmu_aw 8 1>; + status = "disabled"; + }; +}; + +&soc { + bus@5000000 { + compatible = "allwinner,sun60i-a733-de352", "simple-bus"; + reg = <0x0 0x05000000 0x0 0x00400000>; + #address-cells = <1>; + #size-cells = <1>; + ranges = <0x0 0x0 0x05000000 0x400000>; + + display_clocks: clock@8000 { + compatible = "allwinner,sun60i-a733-de352-clk"; + reg = <0x8000 0x100>; + clocks = <&ccu CLK_DE_AHB_GATE>, + <&ccu CLK_MBUS_DESYS_GATE>, + <&ccu CLK_BUS_DE0>, + <&ccu CLK_DE0>; + clock-names = "ahb", "mbus", "bus", "mod"; + resets = <&ccu RST_BUS_DE_SY>, <&ccu RST_BUS_DE0>; + reset-names = "sys", "bus"; + power-domains = <&pck600 PD_DE_SYS>; + #clock-cells = <1>; + #reset-cells = <1>; + }; + + mixer0: mixer@100000 { + compatible = "allwinner,sun60i-a733-de352-mixer-0"; + reg = <0x100000 0x100000>, + <0x8100 0x40>, + <0x280000 0x20000>, + <0x8000 0x100>; + reg-names = "engine", "top", "display", "detop"; + chn_cfg_mode = <2>; + allwinner,tcon-id = <4>; + interrupts = , + ; + clocks = <&display_clocks CLK_BUS_MIXER0>, + <&display_clocks CLK_MIXER0>; + clock-names = "bus", "mod"; + resets = <&display_clocks RST_MIXER0>; + reset-names = "mod"; + iommus = <&mmu_aw 8 1>; + nsi = <&nsi0 2>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + mixer0_out: port@1 { + reg = <1>; + #address-cells = <1>; + #size-cells = <0>; + + mixer0_out_tcon_top: endpoint@0 { + reg = <0>; + remote-endpoint = <&tcon_top_mixer0_in_mixer0>; + }; + }; + }; + }; + }; + + tcon_top: tcon-top@5510000 { + compatible = "allwinner,sun60i-a733-tcon-top"; + reg = <0x0 0x05510000 0x0 0x1000>; + clocks = <&ccu CLK_DPSS_TOP1>, <&ccu CLK_HDMI_TV>, + <&ccu CLK_VID_OUT1_AHB_GATE>; + clock-names = "bus", "tcon-tv0", "ahb-gate"; + clock-output-names = "tcon-top-tv0"; + resets = <&ccu RST_BUS_DPSS_TOP1>, <&ccu RST_BUS_VIDEO_OUT1>; + reset-names = "bus", "rst_bus_reg"; + #clock-cells = <1>; + power-domains = <&pck600 PD_VO1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + tcon_top_mixer0_in: port@0 { + #address-cells = <1>; + #size-cells = <0>; + reg = <0>; + + tcon_top_mixer0_in_mixer0: endpoint@0 { + reg = <0>; + remote-endpoint = <&mixer0_out_tcon_top>; + }; + }; + + tcon_top_mixer0_out: port@1 { + #address-cells = <1>; + #size-cells = <0>; + reg = <1>; + + tcon_top_mixer0_out_tcon_tv: endpoint@2 { + reg = <2>; + remote-endpoint = <&tcon_tv_in_tcon_top>; + }; + }; + + tcon_top_hdmi_in: port@4 { + #address-cells = <1>; + #size-cells = <0>; + reg = <4>; + + tcon_top_hdmi_in_tcon_tv: endpoint@0 { + reg = <0>; + remote-endpoint = <&tcon_tv_out_tcon_top>; + }; + }; + + tcon_top_hdmi_out: port@5 { + reg = <5>; + + tcon_top_hdmi_out_hdmi: endpoint { + remote-endpoint = <&hdmi_in_tcon_top>; + }; + }; + }; + }; + + tcon_tv: tcon-tv@5730000 { + compatible = "allwinner,sun60i-a733-tcon-tv"; + reg = <0x0 0x05730000 0x0 0x1000>; + interrupts = ; + clocks = <&ccu CLK_TCONTV0>, <&tcon_top CLK_TCON_TOP_TV0>; + clock-names = "ahb", "tcon-ch1"; + resets = <&ccu RST_BUS_TCONTV0>; + reset-names = "lcd"; + power-domains = <&pck600 PD_VO1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + tcon_tv_in: port@0 { + reg = <0>; + + tcon_tv_in_tcon_top: endpoint { + remote-endpoint = <&tcon_top_mixer0_out_tcon_tv>; + }; + }; + + tcon_tv_out: port@1 { + #address-cells = <1>; + #size-cells = <0>; + reg = <1>; + + tcon_tv_out_tcon_top: endpoint@1 { + reg = <1>; + remote-endpoint = <&tcon_top_hdmi_in_tcon_tv>; + }; + }; + }; + }; + + hdmi: hdmi@5520000 { + compatible = "allwinner,sun60i-a733-dw-hdmi"; + #sound-dai-cells = <0>; + reg = <0x0 0x05520000 0x0 0x100000>; + reg-io-width = <1>; + interrupts = ; + clocks = <&ccu CLK_HDMI_SFR>, <&ccu CLK_HDMI>, <&ccu CLK_HDMI_REF>, + <&ccu CLK_HDMI_REF>, <&ccu CLK_HDMI_TV>, <&ccu CLK_HDMI>, + <&ccu CLK_HDMI_SFR>, <&ccu CLK_HDCP_ESM>, <&rtc_ccu CLK_DCXO>; + clock-names = "isfr", "iahb", "cec", + "clk_cec", "clk_tcon_tv", "clk_hdmi", "clk_hdmi_24M", + "clk_hdcp", "clk_dcxo"; + resets = <&ccu RST_BUS_HDMI_SUB>, <&ccu RST_BUS_HDMI_MAIN>, + <&ccu RST_BUS_HDMI_HDCP>; + reset-names = "rst_bus_sub", "rst_bus_main", "rst_bus_hdcp"; + assigned-clocks = <&ccu CLK_HDMI>, <&ccu CLK_HDMI_TV>, + <&ccu CLK_HDMI_REF>; + assigned-clock-parents = <0>, <&ccu CLK_PLL_VIDEO0_4X>, + <&ccu CLK_HDMI_CEC_32K>; + power-domains = <&pck600 PD_VO1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + hdmi_in: port@0 { + reg = <0>; + + hdmi_in_tcon_top: endpoint { + remote-endpoint = <&tcon_top_hdmi_out_hdmi>; + }; + }; + + hdmi_out: port@1 { + reg = <1>; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/allwinner/sun60i-a733.dtsi b/arch/arm64/boot/dts/allwinner/sun60i-a733.dtsi index 10bf64c730fdf9..d2d4e21de18592 100644 --- a/arch/arm64/boot/dts/allwinner/sun60i-a733.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun60i-a733.dtsi @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -305,6 +306,13 @@ reg = <0x0 0x42000000 0x0 0x00400000>; no-map; }; + + drm_cma: drm-cma@0 { + compatible = "shared-dma-pool"; + reusable; + size = <0x0 0x8000000>; + alignment = <0x0 0x400000>; + }; }; arm_pmu { @@ -1257,6 +1265,13 @@ reg = <0x0 0x07060000 0x0 0xb000>; clocks = <&r_ccu CLK_R_PPU>; #power-domain-cells = <1>; + #address-cells = <1>; + #size-cells = <0>; + + /* Required by the IOMMU driver's DE0 power-domain discovery. */ + pd_de_sys@1 { + reg = ; + }; }; }; #include "sun60i-a733-gpu.dtsi" From 7ea8e126e0d8f4c277e9ed496ef238c054ca8801 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:23:16 +0800 Subject: [PATCH 14/16] arm64: dts: allwinner: enable HDMI on A733 Demo AIOT Include and enable the DE352 to HDMI pipeline on the Demo AIOT board. Describe its display IOMMU master and connect I2S3 to the HDMI audio interface through a simple-audio-card. Do not enable panel paths without a board-specific panel description. --- .../dts/allwinner/sun60i-a733-demo-aiot.dts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/arch/arm64/boot/dts/allwinner/sun60i-a733-demo-aiot.dts b/arch/arm64/boot/dts/allwinner/sun60i-a733-demo-aiot.dts index 8147493d9fb043..67d36784b46a6d 100644 --- a/arch/arm64/boot/dts/allwinner/sun60i-a733-demo-aiot.dts +++ b/arch/arm64/boot/dts/allwinner/sun60i-a733-demo-aiot.dts @@ -1,4 +1,5 @@ #include "sun60i-a733.dtsi" +#include "sun60i-a733-display.dtsi" #include #include #include @@ -81,6 +82,23 @@ }; }; + hdmi_sound: sound-hdmi { + compatible = "simple-audio-card"; + simple-audio-card,name = "HDMI"; + simple-audio-card,format = "i2s"; + simple-audio-card,mclk-fs = <128>; + + simple-audio-card,cpu { + sound-dai = <&i2s3>; + dai-tdm-slot-num = <2>; + dai-tdm-slot-width = <32>; + }; + + simple-audio-card,codec { + sound-dai = <&hdmi>; + }; + }; + /* * AMP / remoteproc / rpmsg shared memory (layout aligned with Tina * sun60iw2 / a537 demo_aiot). Keep these regions below DRAM end. @@ -487,6 +505,13 @@ id = <5>; skip; }; + + /* DE0 is IOMMU master 8 and shares the DE_SYS power domain. */ + de0_iommu { + iommu-master; + id = <8>; + power-domains = <&pck600 PD_DE_SYS>; + }; }; &usbphy { @@ -553,3 +578,29 @@ &gpu { status = "okay"; }; + +/* I2S3 is internally routed to the DesignWare HDMI audio interface. */ +&i2s3 { + status = "okay"; +}; + +/* Enable the A733 DE352 to HDMI display pipeline. */ +&de_drm { + status = "okay"; +}; + +&mixer0 { + status = "okay"; +}; + +&tcon_top { + status = "okay"; +}; + +&tcon_tv { + status = "okay"; +}; + +&hdmi { + status = "okay"; +}; From d718e032bd546a3acdf08f1204c22054af4ec994 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:23:36 +0800 Subject: [PATCH 15/16] arm64: defconfig: enable the A733 HDMI display pipeline Enable DE352, its clock provider, TCON-TV and DesignWare HDMI in the A733 minimal configuration, together with console, CEC and HDMI audio support. Enable CMA and DMA CMA for the display memory pool. Select the dedicated DE352 driver rather than the legacy DE2 mixer. Keep DSI, Combo-PHY and panel selections in the panel-output series. --- arch/arm64/configs/a733_minimal_defconfig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/arch/arm64/configs/a733_minimal_defconfig b/arch/arm64/configs/a733_minimal_defconfig index 3b73dc812e9977..ef85d3addc0dd9 100644 --- a/arch/arm64/configs/a733_minimal_defconfig +++ b/arch/arm64/configs/a733_minimal_defconfig @@ -46,10 +46,13 @@ CONFIG_CPUFREQ_DT_PLATDEV=y CONFIG_ARM_ALLWINNER_SUN60I_CPUFREQ_NVMEM=y CONFIG_ARM_ALLWINNER_SUNXI_DSUFREQ=y CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y # CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS is not set # CONFIG_COMPAT_BRK is not set CONFIG_KSM=y CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_CMA=y +CONFIG_DMA_CMA=y CONFIG_NET=y CONFIG_PACKET=y CONFIG_UNIX=y @@ -222,6 +225,7 @@ CONFIG_PHY_CADENCE_DPHY=y CONFIG_PHY_CADENCE_DPHY_RX=y CONFIG_PHY_CADENCE_SIERRA=y CONFIG_PHY_CADENCE_SALVO=y +CONFIG_SUN60I_DE352_CCU=y CONFIG_ARM_CCI_PMU=y CONFIG_ARM_CCN=y CONFIG_ARM_CMN=y @@ -347,3 +351,16 @@ CONFIG_SND_SUN60I_A733_CODEC=y CONFIG_SND_SOC_DMIC=y CONFIG_SND_SIMPLE_CARD=y CONFIG_SND_SIMPLE_CARD_UTILS=y +CONFIG_DRM_FBDEV_EMULATION=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_FRAMEBUFFER_CONSOLE=y +CONFIG_DRM_SUN4I=y +# CONFIG_DRM_SUN4I_BACKEND is not set +# CONFIG_DRM_SUN4I_HDMI is not set +CONFIG_DRM_SUN8I_DW_HDMI=y +# CONFIG_DRM_SUN8I_MIXER is not set +CONFIG_DRM_SUN60I_DE=y +CONFIG_DRM_SUN8I_TCON_TOP=y +CONFIG_DRM_DW_HDMI=y +CONFIG_DRM_DW_HDMI_CEC=y +CONFIG_DRM_DW_HDMI_I2S_AUDIO=y From efdea5eb4d45b7c157fd0d471cec30603043b6b8 Mon Sep 17 00:00:00 2001 From: wangjunqiang Date: Sat, 26 Sep 2026 10:19:50 +0800 Subject: [PATCH 16/16] docs: gpu: describe the A733 HDMI display pipeline Document the DE352 to HDMI route, reserved memory requirement and board-level nodes needed to enable video and audio output. --- Documentation/gpu/index.rst | 1 + Documentation/gpu/sun60i-a733-display.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 Documentation/gpu/sun60i-a733-display.rst diff --git a/Documentation/gpu/index.rst b/Documentation/gpu/index.rst index 7dcb15850afdb2..9e99a3c9555ace 100644 --- a/Documentation/gpu/index.rst +++ b/Documentation/gpu/index.rst @@ -20,6 +20,7 @@ GPU Driver Developer's Guide vgaarbiter automated_testing implementation_guidelines + sun60i-a733-display todo rfc/index diff --git a/Documentation/gpu/sun60i-a733-display.rst b/Documentation/gpu/sun60i-a733-display.rst new file mode 100644 index 00000000000000..827b48f377ec68 --- /dev/null +++ b/Documentation/gpu/sun60i-a733-display.rst @@ -0,0 +1,22 @@ +.. SPDX-License-Identifier: GPL-2.0-only +.. Copyright(c) 2020 - 2023 Allwinner Technology Co.,Ltd. All rights reserved. + +============================= +Allwinner A733 display output +============================= + +The A733 DE352 mixer routes its output to TCON-TV and the DesignWare HDMI +controller through the Video Output 1 timing-controller top block. The +driver exposes one primary plane and an ARGB8888 cursor, with one mixer +output active at a time. + +HDMI board setup +================ + +Include ``sun60i-a733-display.dtsi`` and enable ``de_drm``, ``mixer0``, +``tcon_top``, ``tcon_tv`` and ``hdmi`` in the board description. The DRM +master uses the reserved display CMA pool for DMA allocations. HDMI audio +is connected to I2S3 through the board's simple-audio-card description. + +Panel paths are separate from the HDMI pipeline and require their own +controller, PHY and board-specific panel descriptions.