From 06db0bd44c03ed13a905f73fa71027d598ce549b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 4 Apr 2025 20:04:49 +0200 Subject: [PATCH 001/218] [DEBUG] usb: typec: tcpm: also log to dmesg Also log to normal dmesg to assist debugging hard reset issues. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index 7ef746a90a1774..d4e656f8cf34c1 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -826,6 +826,8 @@ static void _tcpm_log(struct tcpm_port *port, const char *fmt, va_list args) vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args); + dev_dbg(port->dev, "%s\n", tmpbuffer); + if (tcpm_log_full(port)) { port->logbuffer_head = max(port->logbuffer_head - 1, 0); strscpy(tmpbuffer, "overflow"); From fd51427c03fb959d160d75f025906ecd5765e717 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 1 Jul 2025 22:58:37 +0200 Subject: [PATCH 002/218] [DEBUG] usb: typec: fusb302: also log to dmesg Also log to normal dmesg to assist debugging hard reset issues. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index 9ab1277b7ed1ef..34354c47c49547 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -150,6 +150,8 @@ static void _fusb302_log(struct fusb302_chip *chip, const char *fmt, vsnprintf(tmpbuffer, sizeof(tmpbuffer), fmt, args); + dev_dbg(chip->dev, "%s\n", tmpbuffer); + mutex_lock(&chip->logbuffer_lock); if (fusb302_log_full(chip)) { From 858fa93255feee3734408ac6fb176b7f70f95888 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:09:35 +0200 Subject: [PATCH 003/218] math.h: add DIV_ROUND_UP_NO_OVERFLOW Add a new DIV_ROUND_UP helper, which cannot overflow when big numbers are being used. Signed-off-by: Sebastian Reichel --- include/linux/math.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/linux/math.h b/include/linux/math.h index 1e8fb3efbc8ce9..d7c384fd19d181 100644 --- a/include/linux/math.h +++ b/include/linux/math.h @@ -48,6 +48,17 @@ #define DIV_ROUND_UP __KERNEL_DIV_ROUND_UP +/** + * DIV_ROUND_UP_NO_OVERFLOW - divide two numbers and always round up + * @n: numerator / dividend + * @d: denominator / divisor + * + * This functions does the same as DIV_ROUND_UP, but internally uses a + * division and a modulo operation instead of math tricks. This way it + * avoids overflowing when handling big numbers. + */ +#define DIV_ROUND_UP_NO_OVERFLOW(n, d) (((n) / (d)) + !!((n) % (d))) + #define DIV_ROUND_DOWN_ULL(ll, d) \ ({ unsigned long long _tmp = (ll); do_div(_tmp, d); _tmp; }) From be8e9c203dd91a7f76929a158478a1a2a7bccc61 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:13:50 +0200 Subject: [PATCH 004/218] clk: divider: Fix divisor masking on 64 bit platforms The clock framework handles clock rates as "unsigned long", so u32 on 32-bit architectures and u64 on 64-bit architectures. The current code casts the dividend to u64 on 32-bit to avoid a potential overflow. For example DIV_ROUND_UP(3000000000, 1500000000) = (3.0G + 1.5G - 1) / 1.5G = = OVERFLOW / 1.5G, which has been introduced in commit 9556f9dad8f5 ("clk: divider: handle integer overflow when dividing large clock rates"). On 64 bit platforms this masks the divisor, so that only the lower 32 bit are used. Thus requesting a frequency >= 4.3GHz results in incorrect values. For example requesting 4300000000 (4.3 GHz) will effectively request ca. 5 MHz. Requesting clk_round_rate(clk, ULONG_MAX) is a bit of a special case, since that still returns correct values as long as the parent clock is below 8.5 GHz. Fix this by switching to DIV_ROUND_UP_NO_OVERFLOW, which cannot overflow. This avoids any requirements on the arguments (except that divisor should not be 0 obviously). Signed-off-by: Sebastian Reichel --- drivers/clk/clk-divider.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/clk/clk-divider.c b/drivers/clk/clk-divider.c index b3b485d23ea859..ef163c90d7f11a 100644 --- a/drivers/clk/clk-divider.c +++ b/drivers/clk/clk-divider.c @@ -226,7 +226,7 @@ static int _div_round_up(const struct clk_div_table *table, unsigned long parent_rate, unsigned long rate, unsigned long flags) { - int div = DIV_ROUND_UP_ULL((u64)parent_rate, rate); + int div = DIV_ROUND_UP_NO_OVERFLOW(parent_rate, rate); if (flags & CLK_DIVIDER_POWER_OF_TWO) div = __roundup_pow_of_two(div); @@ -243,7 +243,7 @@ static int _div_round_closest(const struct clk_div_table *table, int up, down; unsigned long up_rate, down_rate; - up = DIV_ROUND_UP_ULL((u64)parent_rate, rate); + up = DIV_ROUND_UP_NO_OVERFLOW(parent_rate, rate); down = parent_rate / rate; if (flags & CLK_DIVIDER_POWER_OF_TWO) { @@ -414,7 +414,7 @@ int divider_get_val(unsigned long rate, unsigned long parent_rate, { unsigned int div, value; - div = DIV_ROUND_UP_ULL((u64)parent_rate, rate); + div = DIV_ROUND_UP_NO_OVERFLOW(parent_rate, rate); if (!_is_valid_div(table, div, flags)) return -EINVAL; From 22cee5598f5edf1f413a497d6b1c258360835b09 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 18:09:57 +0200 Subject: [PATCH 005/218] clk: composite: replace open-coded abs_diff() Replace the open coded abs_diff() with the existing helper function. Suggested-by: Andy Shevchenko Signed-off-by: Sebastian Reichel --- drivers/clk/clk-composite.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/drivers/clk/clk-composite.c b/drivers/clk/clk-composite.c index 835b1e4e58697c..a4529172776e6b 100644 --- a/drivers/clk/clk-composite.c +++ b/drivers/clk/clk-composite.c @@ -6,6 +6,7 @@ #include #include #include +#include #include static u8 clk_composite_get_parent(struct clk_hw *hw) @@ -106,10 +107,7 @@ static int clk_composite_determine_rate(struct clk_hw *hw, if (ret) continue; - if (req->rate >= tmp_req.rate) - rate_diff = req->rate - tmp_req.rate; - else - rate_diff = tmp_req.rate - req->rate; + rate_diff = abs_diff(req->rate, tmp_req.rate); if (!rate_diff || !req->best_parent_hw || best_rate_diff > rate_diff) { From 5307cca63124fa07aff19dcee18cd9905cb91469 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:35:43 +0100 Subject: [PATCH 006/218] arm64: dts: rockchip: rk3588-evb1: add bluetooth rfkill Add rfkill support for bluetooth. Bluetooth support itself is still missing, but this ensures bluetooth can be powered off properly. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts index 09bc7b68dcc057..41249ff136598a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -121,6 +121,15 @@ pwms = <&pwm2 0 25000 0>; }; + bluetooth-rfkill { + compatible = "rfkill-gpio"; + label = "rfkill-bluetooth"; + radio-type = "bluetooth"; + shutdown-gpios = <&gpio3 RK_PA6 GPIO_ACTIVE_LOW>; + pinctrl-names = "default"; + pinctrl-0 = <&bluetooth_pwren>; + }; + hdmi0-con { compatible = "hdmi-connector"; type = "a"; @@ -599,6 +608,12 @@ }; }; + bluetooth { + bluetooth_pwren: bluetooth-pwren { + rockchip,pins = <3 RK_PA6 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + rtl8111 { rtl8111_isolate: rtl8111-isolate { rockchip,pins = <1 RK_PA4 RK_FUNC_GPIO &pcfg_pull_up>; From c1073b0208044200b5a35141eaec357e712be659 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:39:11 +0100 Subject: [PATCH 007/218] arm64: dts: rockchip: rk3588-evb1: improve PCIe ethernet pin muxing Also describe wake signal PCIe pinmux for the onboard LAN card. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts index 41249ff136598a..f2ebd099231037 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -555,7 +555,7 @@ &pcie2x1l1 { reset-gpios = <&gpio4 RK_PA2 GPIO_ACTIVE_HIGH>; pinctrl-names = "default"; - pinctrl-0 = <&pcie2_1_rst>, <&rtl8111_isolate>, <&pcie30x1m1_1_clkreqn>; + pinctrl-0 = <&pcie2_1_rst>, <&rtl8111_isolate>, <&pcie30x1m1_1_clkreqn>, <&pcie30x1m1_1_waken>; supports-clkreq; status = "okay"; }; From e16b9d7e9cb1f9af40e6cd41950582e3984d9a7b Mon Sep 17 00:00:00 2001 From: "Carsten Haitzler (Rasterman)" Date: Tue, 6 Feb 2024 10:12:54 +0000 Subject: [PATCH 008/218] arm64: dts: rockchip: Slow down EMMC a bit to keep IO stable This drops to hs200 mode and 150Mhz as this is actually stable across eMMC modules. There exist some that are incompatible at higher rates with the rk3588 and to avoid your filesystem corrupting due to IO errors, be more conservative and reduce the max. speed. Signed-off-by: Carsten Haitzler Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi index bf4a1d2e55ca30..f6dc2f6fd23f11 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi @@ -592,8 +592,8 @@ no-sdio; no-sd; non-removable; - mmc-hs400-1_8v; - mmc-hs400-enhanced-strobe; + max-frequency = <150000000>; + mmc-hs200-1_8v; status = "okay"; }; From c0673a58ba3772193afb547348d7d28c85d84523 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 25 Jul 2024 18:12:24 +0200 Subject: [PATCH 009/218] mfd: rk8xx: Fix shutdown handler When I converted rk808 to device managed resources I converted the rk808 specific pm_power_off handler to devm_register_sys_off_handler() using SYS_OFF_MODE_POWER_OFF_PREPARE, which is allowed to sleep. I did this because the driver's poweroff function makes use of regmap and the backend of that might sleep. But the PMIC poweroff function will kill off the board power and the kernel does some extra steps after the prepare handler. Thus the prepare handler should not be used for the PMIC's poweroff routine. Instead the normal SYS_OFF_MODE_POWER_OFF phase should be used. The old pm_power_off method is also being called from there, so this would have been a cleaner conversion anyways. But it still makes sense to investigate the sleep handling and check if there are any issues. Apparently the Rockchip and Meson I2C drivers (the only platforms using the PMICs handled by this driver) both have support for atomic transfers and thus may be called from the atomic poweroff context. Things are different on the SPI side. That is so far only used by rk806 and that one is only used by Rockchip RK3588. Unfortunately the Rockchip SPI driver does not support atomic transfers. That means this change will introduce an error splash directly before doing the final power off on all upstream supported RK3588 boards: [ 13.761353] ------------[ cut here ]------------ [ 13.761764] Voluntary context switch within RCU read-side critical section! [ 13.761776] WARNING: CPU: 0 PID: 1 at kernel/rcu/tree_plugin.h:330 rcu_note_context_switch+0x3ac/0x404 [ 13.763219] Modules linked in: [ 13.763498] CPU: 0 UID: 0 PID: 1 Comm: systemd-shutdow Not tainted 6.10.0-12284-g2818a9a19514 #1499 [ 13.764297] Hardware name: Rockchip RK3588 EVB1 V10 Board (DT) [ 13.764812] pstate: 604000c9 (nZCv daIF +PAN -UAO -TCO -DIT -SSBS BTYPE=--) [ 13.765427] pc : rcu_note_context_switch+0x3ac/0x404 [ 13.765871] lr : rcu_note_context_switch+0x3ac/0x404 [ 13.766314] sp : ffff800084f4b5b0 [ 13.766609] x29: ffff800084f4b5b0 x28: ffff00040139b800 x27: 00007dfb4439ae80 [ 13.767245] x26: ffff00040139bc80 x25: 0000000000000000 x24: ffff800082118470 [ 13.767880] x23: 0000000000000000 x22: ffff000400300000 x21: ffff000400300000 [ 13.768515] x20: ffff800083a9d600 x19: ffff0004fee48600 x18: fffffffffffed448 [ 13.769151] x17: 000000040044ffff x16: 005000f2b5503510 x15: 0000000000000048 [ 13.769787] x14: fffffffffffed490 x13: ffff80008473b3c0 x12: 0000000000000900 [ 13.770421] x11: 0000000000000300 x10: ffff800084797bc0 x9 : ffff80008473b3c0 [ 13.771057] x8 : 00000000ffffefff x7 : ffff8000847933c0 x6 : 0000000000000300 [ 13.771692] x5 : 0000000000000301 x4 : 40000000fffff300 x3 : 0000000000000000 [ 13.772328] x2 : 0000000000000000 x1 : 0000000000000000 x0 : ffff000400300000 [ 13.772964] Call trace: [ 13.773184] rcu_note_context_switch+0x3ac/0x404 [ 13.773598] __schedule+0x94/0xb0c [ 13.773907] schedule+0x34/0x104 [ 13.774198] schedule_timeout+0x84/0xfc [ 13.774544] wait_for_completion_timeout+0x78/0x14c [ 13.774980] spi_transfer_one_message+0x588/0x690 [ 13.775403] __spi_pump_transfer_message+0x19c/0x4ec [ 13.775846] __spi_sync+0x2a8/0x3c4 [ 13.776161] spi_write_then_read+0x120/0x208 [ 13.776543] rk806_spi_bus_read+0x54/0x88 [ 13.776905] _regmap_raw_read+0xec/0x16c [ 13.777257] _regmap_bus_read+0x44/0x7c [ 13.777601] _regmap_read+0x60/0xd8 [ 13.777915] _regmap_update_bits+0xf4/0x13c [ 13.778289] regmap_update_bits_base+0x64/0x98 [ 13.778686] rk808_power_off+0x70/0xfc [ 13.779024] sys_off_notify+0x40/0x6c [ 13.779356] atomic_notifier_call_chain+0x60/0x90 [ 13.779776] do_kernel_power_off+0x54/0x6c [ 13.780146] machine_power_off+0x18/0x24 [ 13.780499] kernel_power_off+0x70/0x7c [ 13.780845] __do_sys_reboot+0x210/0x270 [ 13.781198] __arm64_sys_reboot+0x24/0x30 [ 13.781558] invoke_syscall+0x48/0x10c [ 13.781897] el0_svc_common+0x3c/0xe8 [ 13.782228] do_el0_svc+0x20/0x2c [ 13.782528] el0_svc+0x34/0xd8 [ 13.782806] el0t_64_sync_handler+0x120/0x12c [ 13.783197] el0t_64_sync+0x190/0x194 [ 13.783527] ---[ end trace 0000000000000000 ]--- The board will shutdown nevertheless, since this also re-enables interrupts. A proper fix for this requires changes to the core SPI subsystem and will be done as a follow-up series. Note, that this patch also fixes a problem for the Asus C201. Without the function being registered as a proper shutdown handler the syscall for poweroff exits early and does not even call the shutdown prepare handler. This in turn means the system can no longer poweroff properly since my original change. Fixes: 4fec8a5a85c49 ("mfd: rk808: Convert to device managed resources") Cc: stable@vger.kernel.org Reported-by: Urja Signed-off-by: Sebastian Reichel --- drivers/mfd/rk8xx-core.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mfd/rk8xx-core.c b/drivers/mfd/rk8xx-core.c index 3dcf6abfda74ff..21182df45d1b6b 100644 --- a/drivers/mfd/rk8xx-core.c +++ b/drivers/mfd/rk8xx-core.c @@ -875,7 +875,7 @@ int rk8xx_probe(struct device *dev, int variant, unsigned int irq, struct regmap if (device_property_read_bool(dev, "system-power-controller") || device_property_read_bool(dev, "rockchip,system-power-controller")) { ret = devm_register_sys_off_handler(dev, - SYS_OFF_MODE_POWER_OFF_PREPARE, SYS_OFF_PRIO_HIGH, + SYS_OFF_MODE_POWER_OFF, SYS_OFF_PRIO_HIGH, &rk808_power_off, rk808); if (ret) return dev_err_probe(dev, ret, From ef6d60c43c4da22e5c7a09e756d7f809b5b7994f Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:40 -0500 Subject: [PATCH 010/218] dt-bindings: display: vop2: Add VP clock resets Add the documentation for VOP2 video ports reset clocks. One reset can be set per video port. Reviewed-by: Conor Dooley Signed-off-by: Detlev Casanova --- .../display/rockchip/rockchip-vop2.yaml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Documentation/devicetree/bindings/display/rockchip/rockchip-vop2.yaml b/Documentation/devicetree/bindings/display/rockchip/rockchip-vop2.yaml index 93da1fb9adc47b..8a8993fea896dc 100644 --- a/Documentation/devicetree/bindings/display/rockchip/rockchip-vop2.yaml +++ b/Documentation/devicetree/bindings/display/rockchip/rockchip-vop2.yaml @@ -82,6 +82,26 @@ properties: - {} - {} + resets: + minItems: 5 + items: + - description: AXI clock reset. + - description: AHB clock reset. + - description: Pixel clock reset for video port 0. + - description: Pixel clock reset for video port 1. + - description: Pixel clock reset for video port 2. + - description: Pixel clock reset for video port 3. + + reset-names: + minItems: 5 + items: + - const: aclk + - const: hclk + - const: dclk_vp0 + - const: dclk_vp1 + - const: dclk_vp2 + - const: dclk_vp3 + rockchip,grf: $ref: /schemas/types.yaml#/definitions/phandle description: @@ -153,6 +173,12 @@ allOf: interrupt-names: false + resets: + maxItems: 5 + + reset-names: + maxItems: 5 + ports: required: - port@0 @@ -200,6 +226,12 @@ allOf: interrupt-names: minItems: 4 + resets: + maxItems: 5 + + reset-names: + maxItems: 5 + ports: required: - port@0 @@ -251,6 +283,12 @@ allOf: interrupt-names: false + resets: + minItems: 6 + + reset-names: + minItems: 6 + ports: required: - port@0 @@ -289,6 +327,16 @@ examples: "dclk_vp0", "dclk_vp1", "dclk_vp2"; + resets = <&cru SRST_A_VOP>, + <&cru SRST_H_VOP>, + <&cru SRST_VOP0>, + <&cru SRST_VOP1>, + <&cru SRST_VOP2>; + reset-names = "aclk", + "hclk", + "dclk_vp0", + "dclk_vp1", + "dclk_vp2"; power-domains = <&power RK3568_PD_VO>; rockchip,grf = <&grf>; iommus = <&vop_mmu>; From e0fbf73b3dfe87b8e01f0c088217918c7828e92a Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:41 -0500 Subject: [PATCH 011/218] drm/rockchip: vop2: Add clock resets support At the end of initialization, each VP clock needs to be reset before they can be used. Failing to do so can put the VOP in an undefined state where the generated HDMI signal is either lost or not matching the selected mode. This issue can be reproduced by switching modes multiple times. Depending on the setup, after about 10 mode switches, the signal will be lost and the value in register 0x890 (VSYNCWIDTH + VFRONT) will take the value `0x0000018c`. That makes VSYNCWIDTH=0, which is wrong. Adding the clock resets after the VOP configuration fixes the issue. Signed-off-by: Detlev Casanova --- drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 28 ++++++++++++++++++++ drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 1 + 2 files changed, 29 insertions(+) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 843c7ef979b218..507d87010a6207 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -1621,6 +1622,26 @@ static int us_to_vertical_line(struct drm_display_mode *mode, int us) return us * mode->clock / mode->htotal / 1000; } +static int vop2_clk_reset(struct vop2_video_port *vp) +{ + struct reset_control *rstc = vp->dclk_rst; + struct vop2 *vop2 = vp->vop2; + int ret; + + if (!rstc) + return 0; + + ret = reset_control_assert(rstc); + if (ret < 0) + drm_warn(vop2->drm, "failed to assert reset\n"); + udelay(10); + ret = reset_control_deassert(rstc); + if (ret < 0) + drm_warn(vop2->drm, "failed to deassert reset\n"); + + return ret; +} + static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, struct drm_atomic_state *state) { @@ -1805,6 +1826,8 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, vop2_vp_write(vp, RK3568_VP_DSP_CTRL, dsp_ctrl); + vop2_clk_reset(vp); + vop2_crtc_atomic_try_set_gamma(vop2, vp, crtc, crtc_state); drm_crtc_vblank_on(crtc); @@ -2383,6 +2406,11 @@ static int vop2_create_crtcs(struct vop2 *vop2) vp->id = vp_data->id; vp->data = vp_data; + vp->dclk_rst = devm_reset_control_get_optional(vop2->dev, dclk_name); + if (IS_ERR(vp->dclk_rst)) + return dev_err_probe(drm->dev, PTR_ERR(vp->dclk_rst), + "failed to get %s reset\n", dclk_name); + snprintf(dclk_name, sizeof(dclk_name), "dclk_vp%d", vp->id); vp->dclk = devm_clk_get(vop2->dev, dclk_name); if (IS_ERR(vp->dclk)) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h index 37722652844a92..2cfe505f913c4c 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h @@ -238,6 +238,7 @@ struct vop2_video_port { struct vop2 *vop2; struct clk *dclk; struct clk *dclk_src; + struct reset_control *dclk_rst; unsigned int id; const struct vop2_video_port_data *data; From b278c7e70242fa8b91303728b444f533b7ab713f Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:42 -0500 Subject: [PATCH 012/218] arm64: dts: rockchip: Add VOP clock resets for rk3588s This adds the needed clock resets for all rk3588(s) based SOCs. Signed-off-by: Detlev Casanova --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index 4fb8888c281c8c..30fb74303fb1f1 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1453,6 +1453,18 @@ "pll_hdmiphy0"; iommus = <&vop_mmu>; power-domains = <&power RK3588_PD_VOP>; + resets = <&cru SRST_A_VOP>, + <&cru SRST_H_VOP>, + <&cru SRST_D_VOP0>, + <&cru SRST_D_VOP1>, + <&cru SRST_D_VOP2>, + <&cru SRST_D_VOP3>; + reset-names = "aclk", + "hclk", + "dclk_vp0", + "dclk_vp1", + "dclk_vp2", + "dclk_vp3"; rockchip,grf = <&sys_grf>; rockchip,vop-grf = <&vop_grf>; rockchip,vo1-grf = <&vo1_grf>; From 833543f9468f018803a566b59b6e3f39b1a0e9f7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 8 Nov 2024 17:35:44 +0100 Subject: [PATCH 013/218] arm64: dts: rockchip: rk3588-evb1: add DSI panel The RK3588 EVB1 comes with a W552793DBA-V10 Touchscreen/Display combination. It contains a Wanchanglong W552793BAA panel and a Goodix GT1158 touchscreen. This adds the DT description of it. Signed-off-by: Sebastian Reichel --- .../boot/dts/rockchip/rk3588-evb1-v10.dts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts index f2ebd099231037..76fd3c95c234f4 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -213,6 +213,15 @@ regulator-max-microvolt = <12000000>; }; + vcc3v3_lcd_mipi: regulator-vcc3v3-lcd-mipi { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_lcd_mipi"; + regulator-boot-on; + enable-active-high; + gpio = <&gpio1 RK_PC4 GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc_3v3_s0>; + }; + vcc3v3_pcie30: regulator-vcc3v3-pcie30 { compatible = "regulator-fixed"; regulator-name = "vcc3v3_pcie30"; @@ -347,6 +356,43 @@ cpu-supply = <&vdd_cpu_lit_s0>; }; +&dsi0 { + #address-cells = <1>; + #size-cells = <0>; + status = "okay"; + + panel@0 { + compatible = "wanchanglong,w552793baa", "raydium,rm67200"; + reg = <0>; + backlight = <&backlight>; + pinctrl-names = "default"; + pinctrl-0 = <&lcd_rst_gpio>; + vdd-supply = <&vcc3v3_lcd_mipi>; + iovcc-supply = <&vcc3v3_lcd_mipi>; + vsp-supply = <&vcc5v0_sys>; + vsn-supply = <&vcc5v0_sys>; + reset-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_LOW>; + + port { + mipi_panel_in: endpoint { + remote-endpoint = <&dsi0_out_panel>; + }; + }; + }; +}; + +&dsi0_in { + dsi0_in_vp3: endpoint { + remote-endpoint = <&vp3_out_dsi0>; + }; +}; + +&dsi0_out { + dsi0_out_panel: endpoint { + remote-endpoint = <&mipi_panel_in>; + }; +}; + &gmac0 { clock_in_out = "output"; phy-handle = <&rgmii_phy>; @@ -488,6 +534,22 @@ }; }; +&i2c6 { + status = "okay"; + + gt1x: touchscreen@14 { + compatible = "goodix,gt1158"; + reg = <0x14>; + interrupt-parent = <&gpio0>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&touchscreen_pins>; + reset-gpios = <&gpio0 RK_PD2 GPIO_ACTIVE_HIGH>; + AVDD28-supply = <&vcc3v3_lcd_mipi>; + VDDIO-supply = <&vcc3v3_lcd_mipi>; + }; +}; + &i2c7 { status = "okay"; @@ -527,6 +589,10 @@ }; }; +&mipidcphy0 { + status = "okay"; +}; + &pcie2x1l0 { pinctrl-names = "default"; pinctrl-0 = <&pcie2_0_rst>, <&pcie2_0_wake>, <&pcie2_0_clkreq>, <&wifi_host_wake_irq>; @@ -639,6 +705,12 @@ }; }; + mipi_dsi { + lcd_rst_gpio: lcd-rst { + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + pcie2 { pcie2_0_rst: pcie2-0-rst { rockchip,pins = <4 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>; @@ -667,6 +739,14 @@ }; }; + touchscreen { + touchscreen_pins: touchscreen-pins { + rockchip,pins = + <0 RK_PD2 RK_FUNC_GPIO &pcfg_pull_up>, + <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + usb { vcc5v0_host_en: vcc5v0-host-en { rockchip,pins = <4 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; @@ -1478,3 +1558,13 @@ remote-endpoint = <&hdmi1_in_vp1>; }; }; + +&vp3 { + #address-cells = <1>; + #size-cells = <0>; + + vp3_out_dsi0: endpoint@ROCKCHIP_VOP2_EP_MIPI0 { + reg = ; + remote-endpoint = <&dsi0_in_vp3>; + }; +}; From 7febd5d9ba959c6bd68ffdc1026269d230cd7aa1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 14 Nov 2024 17:36:18 +0100 Subject: [PATCH 014/218] drm/rockchip: vop2: Add core reset support A previous from Detlev Casanova adds reset handling for the video ports. This also resets the AHB and AXI interface when the system binds the VOP2 controller. This fixes issues when the bootloader (or a previously running kernel when using kexec) left the VOP2 initialized to some degree. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 15 +++++++++++++++ drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 507d87010a6207..2601446f9f0748 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -2692,6 +2692,21 @@ static int vop2_bind(struct device *dev, struct device *master, void *data) dev_set_drvdata(dev, vop2); + vop2->resets[RST_ACLK].id = "aclk"; + vop2->resets[RST_HCLK].id = "hclk"; + ret = devm_reset_control_bulk_get_optional_exclusive(vop2->dev, + RST_VOP2_MAX, vop2->resets); + if (ret) + return dev_err_probe(drm->dev, ret, "failed to get resets\n"); + + ret = reset_control_bulk_assert(RST_VOP2_MAX, vop2->resets); + if (ret < 0) + drm_warn(vop2->drm, "failed to assert resets\n"); + udelay(10); + ret = reset_control_bulk_deassert(RST_VOP2_MAX, vop2->resets); + if (ret < 0) + drm_warn(vop2->drm, "failed to deassert resets\n"); + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "vop"); if (!res) return dev_err_probe(drm->dev, -EINVAL, diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h index 2cfe505f913c4c..b8198714c1c912 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h @@ -8,6 +8,7 @@ #define _ROCKCHIP_DRM_VOP2_H #include +#include #include #include #include "rockchip_drm_drv.h" @@ -165,6 +166,12 @@ enum vop2_win_regs { VOP2_WIN_MAX_REG, }; +enum { + RST_ACLK, + RST_HCLK, + RST_VOP2_MAX +}; + struct vop2_regs_dump { const char *name; u32 base; @@ -330,6 +337,7 @@ struct vop2 { struct clk *pclk; struct clk *pll_hdmiphy0; struct clk *pll_hdmiphy1; + struct reset_control_bulk_data resets[RST_VOP2_MAX]; /* optional internal rgb encoder */ struct rockchip_rgb *rgb; From 56898f8f4a1c7e8e229e7e2bc1f7e633ed8653b6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 29 Jul 2025 18:21:03 +0200 Subject: [PATCH 015/218] arm64: dts: rockchip: Fix USB-C description for RK3588 EVB1 Fix the USB-C connector description, so that it follows the binding: port@0 is the high-speed lanes port@1 is the super-speed lanes port@2 is the SBU lanes Right now the high-speed and super-speed links are swapped and for the high-speed lanes the link points to the controller instead of the PHY. I'm still investigating if this should be changed. This also updates the port naming, so that it describes the hardware instead of how the drivers are using the information. These are effectively the same, but the DT should describe hardware and not software. Fixes: b37146b5a555 ("arm64: dts: rockchip: add USB3 to rk3588-evb1") Signed-off-by: Sebastian Reichel --- .../boot/dts/rockchip/rk3588-evb1-v10.dts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts index 76fd3c95c234f4..85adfadc6e4a9a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -497,24 +497,24 @@ port@0 { reg = <0>; - usbc0_orien_sw: endpoint { - remote-endpoint = <&usbdp_phy0_orientation_switch>; + usbc0_hs: endpoint { + remote-endpoint = <&usb_host0_xhci_to_usbc0>; }; }; port@1 { reg = <1>; - usbc0_role_sw: endpoint { - remote-endpoint = <&dwc3_0_role_switch>; + usbc0_ss: endpoint { + remote-endpoint = <&usbdp_phy0_ss>; }; }; port@2 { reg = <2>; - dp_altmode_mux: endpoint { - remote-endpoint = <&usbdp_phy0_dp_altmode_mux>; + usbc0_sbu: endpoint { + remote-endpoint = <&usbdp_phy0_sbu>; }; }; }; @@ -1493,14 +1493,14 @@ #address-cells = <1>; #size-cells = <0>; - usbdp_phy0_orientation_switch: endpoint@0 { + usbdp_phy0_ss: endpoint@0 { reg = <0>; - remote-endpoint = <&usbc0_orien_sw>; + remote-endpoint = <&usbc0_ss>; }; - usbdp_phy0_dp_altmode_mux: endpoint@1 { + usbdp_phy0_sbu: endpoint@1 { reg = <1>; - remote-endpoint = <&dp_altmode_mux>; + remote-endpoint = <&usbc0_sbu>; }; }; }; @@ -1525,9 +1525,9 @@ #address-cells = <1>; #size-cells = <0>; - dwc3_0_role_switch: endpoint@0 { + usb_host0_xhci_to_usbc0: endpoint@0 { reg = <0>; - remote-endpoint = <&usbc0_role_sw>; + remote-endpoint = <&usbc0_hs>; }; }; }; From c3034e74842ab5f400f0869bf108420b43ee501f Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 15 Mar 2025 09:34:02 +0100 Subject: [PATCH 016/218] arm64: dts: rockchip: enable camera I2C interfaces for ROCK 5B family Any camera related IP of the RK3588 is not yet supported and the cameras must be handled via overlays anyways, but it is sensible to expose the related I2C interfaces by default. This allows using i2cdetect to investigate anything connected to the CSI connectors right now. Since the Rockchip I2C driver implements proper power management there are no disadvantages, if nothing is connected to the port. Note, that the second CSI port's I2C in the Rock 5B+ and Rock 5T reuse I2C4, which is already used by fusb302 and thus already enabled. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi index f6dc2f6fd23f11..bb84286cb11c7f 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi @@ -304,6 +304,10 @@ }; }; +&i2c3 { + status = "okay"; +}; + &i2c4 { pinctrl-names = "default"; pinctrl-0 = <&i2c4m1_xfer>; From 5136c637e77c258adab75539965a6c278268b17d Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Tue, 10 Jun 2025 16:07:09 +0200 Subject: [PATCH 017/218] phy: rockchip: inno-usb2: add soft vbusvalid control With USB type C connectors, the vbus detect pin of the OTG controller attached to it is pulled high by a USB Type C controller chip such as the fusb302. This means USB enumeration on Type-C ports never works, as the vbus is always seen as high. Rockchip added some GRF register flags to deal with this situation. The RK3576 TRM calls these "soft_vbusvalid_bvalid" (con0 bit index 15) and "soft_vbusvalid_bvalid_sel" (con0 bit index 14). Downstream introduces a new vendor property which tells the USB 2 PHY that it's connected to a type C port, but we can do better. Since in such an arrangement, we'll have an OF graph connection from the USB controller to the USB connector anyway, we can walk said OF graph and check the connector's compatible to determine this without adding any further vendor properties. Do keep in mind that the usbdp PHY driver seemingly fiddles with these register fields as well, but what it does doesn't appear to be enough for us to get working USB enumeration, presumably because the whole vbus_attach logic needs to be adjusted as well either way. Signed-off-by: Nicolas Frattaroli Link: https://lore.kernel.org/r/20250610-rk3576-sige5-usb-v4-1-7e7f779619c1@collabora.com Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-inno-usb2.c | 113 +++++++++++++++++- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c index 8f4c08e599aa22..42116db38863e2 100644 --- a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c +++ b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,8 @@ struct rockchip_chg_det_reg { /** * struct rockchip_usb2phy_port_cfg - usb-phy port configuration. * @phy_sus: phy suspend register. + * @svbus_en: soft vbus bvalid enable register. + * @svbus_sel: soft vbus bvalid selection register. * @bvalid_det_en: vbus valid rise detection enable register. * @bvalid_det_st: vbus valid rise detection status register. * @bvalid_det_clr: vbus valid rise detection clear register. @@ -140,6 +143,8 @@ struct rockchip_chg_det_reg { */ struct rockchip_usb2phy_port_cfg { struct usb2phy_reg phy_sus; + struct usb2phy_reg svbus_en; + struct usb2phy_reg svbus_sel; struct usb2phy_reg bvalid_det_en; struct usb2phy_reg bvalid_det_st; struct usb2phy_reg bvalid_det_clr; @@ -203,6 +208,7 @@ struct rockchip_usb2phy_cfg { * @event_nb: hold event notification callback. * @state: define OTG enumeration states before device reset. * @mode: the dr_mode of the controller. + * @typec_vbus_det: whether to apply Type C logic to OTG vbus detection. */ struct rockchip_usb2phy_port { struct phy *phy; @@ -222,6 +228,7 @@ struct rockchip_usb2phy_port { struct notifier_block event_nb; enum usb_otg_state state; enum usb_dr_mode mode; + bool typec_vbus_det; }; /** @@ -495,6 +502,13 @@ static int rockchip_usb2phy_init(struct phy *phy) mutex_lock(&rport->mutex); if (rport->port_id == USB2PHY_PORT_OTG) { + if (rport->typec_vbus_det) { + if (rport->port_cfg->svbus_en.enable && + rport->port_cfg->svbus_sel.enable) { + property_enable(rphy->grf, &rport->port_cfg->svbus_en, true); + property_enable(rphy->grf, &rport->port_cfg->svbus_sel, true); + } + } if (rport->mode != USB_DR_MODE_HOST && rport->mode != USB_DR_MODE_UNKNOWN) { /* clear bvalid status and enable bvalid detect irq */ @@ -535,8 +549,7 @@ static int rockchip_usb2phy_init(struct phy *phy) if (ret) goto out; - schedule_delayed_work(&rport->otg_sm_work, - OTG_SCHEDULE_DELAY * 3); + schedule_delayed_work(&rport->otg_sm_work, 0); } else { /* If OTG works in host only mode, do nothing. */ dev_dbg(&rport->phy->dev, "mode %d\n", rport->mode); @@ -666,8 +679,17 @@ static void rockchip_usb2phy_otg_sm_work(struct work_struct *work) unsigned long delay; bool vbus_attach, sch_work, notify_charger; - vbus_attach = property_enabled(rphy->grf, - &rport->port_cfg->utmi_bvalid); + if (rport->port_cfg->svbus_en.enable && rport->typec_vbus_det) { + if (property_enabled(rphy->grf, &rport->port_cfg->svbus_en) && + property_enabled(rphy->grf, &rport->port_cfg->svbus_sel)) { + vbus_attach = true; + } else { + vbus_attach = false; + } + } else { + vbus_attach = property_enabled(rphy->grf, + &rport->port_cfg->utmi_bvalid); + } sch_work = false; notify_charger = false; @@ -1280,6 +1302,83 @@ static int rockchip_otg_event(struct notifier_block *nb, return NOTIFY_DONE; } +static const char *const rockchip_usb2phy_typec_cons[] = { + "usb-c-connector", + NULL, +}; + +static struct device_node *rockchip_usb2phy_to_controller(struct rockchip_usb2phy *rphy) +{ + struct device_node *np; + struct device_node *parent; + + for_each_node_with_property(np, "phys") { + struct of_phandle_iterator it; + int ret; + + of_for_each_phandle(&it, ret, np, "phys", NULL, 0) { + parent = of_get_parent(it.node); + if (it.node != rphy->dev->of_node && rphy->dev->of_node != parent) { + if (parent) + of_node_put(parent); + continue; + } + + /* + * Either the PHY phandle we're iterating or its parent + * matched, we don't care about which out of the two in + * particular as we just need to know it's the right + * USB controller for this PHY. + */ + of_node_put(it.node); + of_node_put(parent); + return np; + } + } + + return NULL; +} + +static bool rockchip_usb2phy_otg_is_type_c(struct rockchip_usb2phy *rphy) +{ + struct device_node *controller = rockchip_usb2phy_to_controller(rphy); + struct device_node *ports; + struct device_node *ep = NULL; + struct device_node *parent; + + if (!controller) + return false; + + ports = of_get_child_by_name(controller, "ports"); + if (ports) { + of_node_put(controller); + controller = ports; + } + + for_each_of_graph_port(controller, port) { + ep = of_get_child_by_name(port, "endpoint"); + if (!ep) + continue; + + parent = of_graph_get_remote_port_parent(ep); + of_node_put(ep); + if (!parent) + continue; + + if (of_device_compatible_match(parent, rockchip_usb2phy_typec_cons)) { + of_node_put(parent); + of_node_put(controller); + return true; + } + + of_node_put(parent); + } + + of_node_put(controller); + + return false; +} + static int rockchip_usb2phy_otg_port_init(struct rockchip_usb2phy *rphy, struct rockchip_usb2phy_port *rport, struct device_node *child_np) @@ -1301,6 +1400,8 @@ static int rockchip_usb2phy_otg_port_init(struct rockchip_usb2phy *rphy, mutex_init(&rport->mutex); + rport->typec_vbus_det = rockchip_usb2phy_otg_is_type_c(rphy); + rport->mode = of_usb_get_dr_mode_by_phy(child_np, -1); if (rport->mode == USB_DR_MODE_HOST || rport->mode == USB_DR_MODE_UNKNOWN) { @@ -2054,6 +2155,8 @@ static const struct rockchip_usb2phy_cfg rk3576_phy_cfgs[] = { .port_cfgs = { [USB2PHY_PORT_OTG] = { .phy_sus = { 0x0000, 8, 0, 0, 0x1d1 }, + .svbus_en = { 0x0000, 15, 15, 0, 1 }, + .svbus_sel = { 0x0000, 14, 14, 0, 1 }, .bvalid_det_en = { 0x00c0, 1, 1, 0, 1 }, .bvalid_det_st = { 0x00c4, 1, 1, 0, 1 }, .bvalid_det_clr = { 0x00c8, 1, 1, 0, 1 }, @@ -2091,6 +2194,8 @@ static const struct rockchip_usb2phy_cfg rk3576_phy_cfgs[] = { .port_cfgs = { [USB2PHY_PORT_OTG] = { .phy_sus = { 0x2000, 8, 0, 0, 0x1d1 }, + .svbus_en = { 0x2000, 15, 15, 0, 1 }, + .svbus_sel = { 0x2000, 14, 14, 0, 1 }, .bvalid_det_en = { 0x20c0, 1, 1, 0, 1 }, .bvalid_det_st = { 0x20c4, 1, 1, 0, 1 }, .bvalid_det_clr = { 0x20c8, 1, 1, 0, 1 }, From fe91af9ac233e674853725fb082e0d35fc1ac74a Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:24 +0200 Subject: [PATCH 018/218] dt-bindings: input: adc-keys: allow linux,input-type property adc-keys, unlike gpio-keys, does not allow linux,input-type as a valid property. This makes it impossible to model devices that have ADC inputs that should generate switch events. Add the property to the binding with the same default as gpio-keys. Signed-off-by: Nicolas Frattaroli Reviewed-by: Heiko Stuebner Link: https://lore.kernel.org/r/20250630-rock4d-audio-v1-1-0b3c8e8fda9c@collabora.com Signed-off-by: Sebastian Reichel --- Documentation/devicetree/bindings/input/adc-keys.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/devicetree/bindings/input/adc-keys.yaml b/Documentation/devicetree/bindings/input/adc-keys.yaml index 7aa078dead3781..e372ebc23d1651 100644 --- a/Documentation/devicetree/bindings/input/adc-keys.yaml +++ b/Documentation/devicetree/bindings/input/adc-keys.yaml @@ -42,6 +42,9 @@ patternProperties: linux,code: true + linux,input-type: + default: 1 # EV_KEY + press-threshold-microvolt: description: Voltage above or equal to which this key is considered pressed. No From fe1ee5599bff36ff02ed4687eac4781c903d28f8 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:25 +0200 Subject: [PATCH 019/218] Input: adc-keys - support types that aren't just keyboard keys Instead of doing something like what gpio-keys is doing, adc-keys hardcodes that all keycodes must be of type EV_KEY. This limits the usefulness of adc-keys, and overcomplicates the code with manual bit-setting logic. Instead, refactor the code to read the linux,input-type fwnode property, and get rid of the custom bit setting logic, replacing it with input_set_capability instead. input_report_key is replaced with input_event, which allows us to explicitly pass the type. Signed-off-by: Nicolas Frattaroli Reviewed-by: Heiko Stuebner Link: https://lore.kernel.org/r/20250630-rock4d-audio-v1-2-0b3c8e8fda9c@collabora.com Signed-off-by: Sebastian Reichel --- drivers/input/keyboard/adc-keys.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/drivers/input/keyboard/adc-keys.c b/drivers/input/keyboard/adc-keys.c index f1753207429db0..339dd4d4a08421 100644 --- a/drivers/input/keyboard/adc-keys.c +++ b/drivers/input/keyboard/adc-keys.c @@ -19,12 +19,14 @@ struct adc_keys_button { u32 voltage; u32 keycode; + u32 type; }; struct adc_keys_state { struct iio_channel *channel; u32 num_keys; u32 last_key; + u32 last_type; u32 keyup_voltage; const struct adc_keys_button *map; }; @@ -35,6 +37,7 @@ static void adc_keys_poll(struct input_dev *input) int i, value, ret; u32 diff, closest = 0xffffffff; int keycode = 0; + u32 type = EV_KEY; ret = iio_read_channel_processed(st->channel, &value); if (unlikely(ret < 0)) { @@ -46,6 +49,7 @@ static void adc_keys_poll(struct input_dev *input) if (diff < closest) { closest = diff; keycode = st->map[i].keycode; + type = st->map[i].type; } } } @@ -54,13 +58,14 @@ static void adc_keys_poll(struct input_dev *input) keycode = 0; if (st->last_key && st->last_key != keycode) - input_report_key(input, st->last_key, 0); + input_event(input, st->last_type, st->last_key, 0); if (keycode) - input_report_key(input, keycode, 1); + input_event(input, type, keycode, 1); input_sync(input); st->last_key = keycode; + st->last_type = type; } static int adc_keys_load_keymap(struct device *dev, struct adc_keys_state *st) @@ -93,6 +98,10 @@ static int adc_keys_load_keymap(struct device *dev, struct adc_keys_state *st) return -EINVAL; } + if (fwnode_property_read_u32(child, "linux,input-type", + &map[i].type)) + map[i].type = EV_KEY; + i++; } @@ -156,9 +165,8 @@ static int adc_keys_probe(struct platform_device *pdev) input->id.product = 0x0001; input->id.version = 0x0100; - __set_bit(EV_KEY, input->evbit); for (i = 0; i < st->num_keys; i++) - __set_bit(st->map[i].keycode, input->keybit); + input_set_capability(input, st->map[i].type, st->map[i].keycode); if (device_property_read_bool(dev, "autorepeat")) __set_bit(EV_REP, input->evbit); From 693ff422319e1f5cf360b13200228bc09eee656c Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:26 +0200 Subject: [PATCH 020/218] arm64: dts: rockchip: add analog audio to ROCK 4D The RADXA ROCK 4D, like many other Rockchip-based boards, uses an ES8388 analog audio codec. On the production version of the board, the codec's LOUT1 and ROUT1 pins are tied to the headphone jack, whereas pins LOUT2 and ROUT2 lead to a non-populated speaker amplifier that itself leads to a non-populated speaker jack. The schematic is still haunted by the ghosts of those symbols, but it clearly marks them as "NC". The 3.5mm TRRS jack has its microphone ring (and ground ring) wired to the codec's LINPUT1 and RINPUT1 pins for differential signalling. Furthermore, it uses the SoCs ADC to detect whether the inserted cable is of headphones (i.e., no microphone), or a headset (i.e., with microphone). The way this is done is that the ADC input taps the output of a 100K/100K resistor divider that divides the microphone ring pin that's pulled up to 3.3V. There is no ADC level difference between a completely empty jack and one with a set of headphones (i.e., ones that don't have a microphone) connected. Consequently headphone insertion detection isn't something that can be done. Add the necessary codec and audio card nodes. The non-populated parts, i.e. LOUT2 and ROUT2, are not modeled at all, as they are not present on the hardware. Also, add an adc-keys node for the headset detection, which uses an input type of EV_SW with the SW_MICROPHONE_INSERT keycode. Below the 220mV pressed voltage level of our SW_MICROPHONE_INSERT switch, we also define a button that emits a KEY_RESERVED code, which is there to model this part of the voltage range as not just being extra legroom for the button above it, but actually a state that is encountered in the real world, and should be recognised as a valid state for the ADC range to be in so that no "closer" ADC button is chosen. Signed-off-by: Nicolas Frattaroli Link: https://lore.kernel.org/r/20250630-rock4d-audio-v1-3-0b3c8e8fda9c@collabora.com Signed-off-by: Sebastian Reichel --- .../boot/dts/rockchip/rk3576-rock-4d.dts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts index 899a84b1fbf9e8..c005e9b041bea2 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -6,6 +6,7 @@ /dts-v1/; #include +#include #include #include #include @@ -45,6 +46,31 @@ shutdown-gpios = <&gpio2 RK_PD1 GPIO_ACTIVE_HIGH>; }; + es8388_sound: es8388-sound { + compatible = "simple-audio-card"; + simple-audio-card,format = "i2s"; + simple-audio-card,mclk-fs = <256>; + simple-audio-card,name = "On-board Analog ES8388"; + simple-audio-card,widgets = "Microphone", "Headphone Mic", + "Headphone", "Headphone"; + simple-audio-card,routing = "Headphone", "LOUT1", + "Headphone", "ROUT1", + "Left PGA Mux", "Differential Mux", + "Differential Mux", "LINPUT1", + "Differential Mux", "RINPUT1", + "LINPUT1", "Headphone Mic", + "RINPUT1", "Headphone Mic"; + + simple-audio-card,cpu { + sound-dai = <&sai1>; + }; + + simple-audio-card,codec { + sound-dai = <&es8388>; + system-clock-frequency = <12288000>; + }; + }; + leds: leds { compatible = "gpio-leds"; pinctrl-names = "default"; @@ -65,6 +91,37 @@ }; }; + saradc_keys: adc-keys { + compatible = "adc-keys"; + io-channels = <&saradc 3>; + io-channel-names = "buttons"; + keyup-threshold-microvolt = <3000000>; + poll-interval = <100>; + + /* + * During insertion and removal of a regular set of headphones, + * i.e. one without a microphone, the voltage level briefly + * dips below the 220mV of the headset connection switch. + * By having a button definition with a KEY_RESERVED signal + * between 0 to 220, we ensure no driver implementation thinks + * that the closest thing to 0V is 220mV so clearly there must + * be a headset connected. + */ + + button-headset-disconnected { + label = "Headset Microphone Disconnected"; + linux,code = ; + press-threshold-microvolt = <0>; + }; + + button-headset-connected { + label = "Headset Microphone Connected"; + linux,code = ; + linux,input-type = ; + press-threshold-microvolt = <220000>; + }; + }; + vcc_5v0_dcin: regulator-vcc-5v0-dcin { compatible = "regulator-fixed"; regulator-always-on; @@ -682,6 +739,25 @@ }; }; +&i2c3 { + status = "okay"; + + es8388: audio-codec@10 { + compatible = "everest,es8388", "everest,es8328"; + reg = <0x10>; + clocks = <&cru CLK_SAI1_MCLKOUT_TO_IO>; + AVDD-supply = <&vcca_3v3_s0>; + DVDD-supply = <&vcc_3v3_s0>; + HPVDD-supply = <&vcca_3v3_s0>; + PVDD-supply = <&vcc_3v3_s0>; + assigned-clocks = <&cru CLK_SAI1_MCLKOUT_TO_IO>; + assigned-clock-rates = <12288000>; + #sound-dai-cells = <0>; + pinctrl-names = "default"; + pinctrl-0 = <&sai1m0_mclk>; + }; +}; + &i2c6 { pinctrl-names = "default"; pinctrl-0 = <&i2c6m3_xfer>; @@ -770,10 +846,24 @@ }; }; +&sai1 { + pinctrl-names = "default"; + pinctrl-0 = <&sai1m0_lrck + &sai1m0_sclk + &sai1m0_sdi0 + &sai1m0_sdo0>; + status = "okay"; +}; + &sai6 { status = "okay"; }; +&saradc { + vref-supply = <&vcca1v8_pldo2_s0>; + status = "okay"; +}; + &sdmmc { bus-width = <4>; cap-mmc-highspeed; From 8c1607b4eaa17f5338b8c878e720e50b2904d476 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 3 Jul 2025 01:21:14 +0200 Subject: [PATCH 021/218] net: phy: realtek: Reset after clock enable On Radxa ROCK 4D boards we are seeing some issues with PHY detection and stability (e.g. link loss or not capable of transceiving packages) after new board revisions switched from a dedicated crystal to providing the 25 MHz PHY input clock from the SoC instead. This board is using a RTL8211F PHY, which is connected to an always-on regulator. Unfortunately the datasheet does not explicitly mention the power-up sequence regarding the clock, but it seems to assume that the clock is always-on (i.e. dedicated crystal). By doing an explicit reset after enabling the clock, the issue on the boards could no longer be observed. Note, that the RK3576 SoC used by the ROCK 4D board does not yet support system level PM, so the resume path has not been tested. Cc: stable@vger.kernel.org Fixes: 7300c9b574cc ("net: phy: realtek: Add optional external PHY clock") Signed-off-by: Sebastian Reichel --- drivers/net/phy/realtek/realtek_main.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/net/phy/realtek/realtek_main.c b/drivers/net/phy/realtek/realtek_main.c index 79c867ef64dad0..c35c89941d283b 100644 --- a/drivers/net/phy/realtek/realtek_main.c +++ b/drivers/net/phy/realtek/realtek_main.c @@ -291,6 +291,8 @@ static int rtl821x_probe(struct phy_device *phydev) if (IS_ERR(priv->clk)) return dev_err_probe(dev, PTR_ERR(priv->clk), "failed to get phy clock\n"); + if (priv->clk) + phy_reset_after_clk_enable(phydev); priv->enable_aldps = of_property_read_bool(dev->of_node, "realtek,aldps-enable"); @@ -936,8 +938,10 @@ static int rtl821x_resume(struct phy_device *phydev) struct rtl821x_priv *priv = phydev->priv; int ret; - if (!phydev->wol_enabled) + if (!phydev->wol_enabled && priv->clk) { clk_prepare_enable(priv->clk); + phy_reset_after_clk_enable(phydev); + } ret = genphy_resume(phydev); if (ret < 0) @@ -2456,7 +2460,7 @@ static struct phy_driver realtek_drvs[] = { .resume = rtl8211f_resume, .read_page = rtl821x_read_page, .write_page = rtl821x_write_page, - .flags = PHY_ALWAYS_CALL_SUSPEND, + .flags = PHY_ALWAYS_CALL_SUSPEND | PHY_RST_AFTER_CLK_EN, .led_hw_is_supported = rtl8211x_led_hw_is_supported, .led_hw_control_get = rtl8211f_led_hw_control_get, .led_hw_control_set = rtl8211f_led_hw_control_set, From d23db805e7443ae36b0a7bc5cd5317b50a2bcb11 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 8 Jul 2025 19:36:31 +0200 Subject: [PATCH 022/218] arm64: dts: rockchip: use MAC TX delay for ROCK 4D According to the Ethernet controller device tree binding "rgmii-id" means, that the PCB does not have extra long lines to add the required delays. This is indeed the case for the ROCK 4D. The problem is, that the Rockchip MAC Linux driver interprets the interface type differently and abuses the information to configure RX and TX delays in the MAC using (vendor) properties 'rx_delay' and 'tx_delay'. When Detlev Casanova upstreamed the ROCK 4D device tree, he used the correct description for the board ("rgmii-id"). This results in no delays being configured in the MAC. At the same time the PHY will provide some delays. This works to some degree, but is not a stable configuration. All five ROCK 4D production boards, which have recently been added to the Collabora LAVA lab for CI purposes have trouble with data not getting through after a connection has been established. Using the same delay setup as the vendor device tree fixes the functionality (at the cost of not properly following the DT binding). As we cannot fix the driver behavior for RK3576 (some other boards already depend on this), let's update the ROCK 4D DT instead. Cc: Andrew Lunn Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts index c005e9b041bea2..a0730c50c09653 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -329,7 +329,7 @@ &gmac0 { clock_in_out = "output"; phy-handle = <&rgmii_phy0>; - phy-mode = "rgmii-id"; + phy-mode = "rgmii-rxid"; pinctrl-names = "default"; pinctrl-0 = <ð0m0_miim ð0m0_tx_bus2 @@ -338,6 +338,8 @@ ð0m0_rgmii_bus ðm0_clk0_25m_out>; status = "okay"; + tx_delay = <0x20>; + rx_delay = <0x00>; }; &gpu { From bc5528e1a095abd4e06b0ef4641e2331dce33665 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 21 Nov 2025 19:27:00 +0100 Subject: [PATCH 023/218] PCI: dw-rockchip: Fix LTSSM set functions Before the Rockchip PCIe driver has been switched over to the FIELD_PREP_WM16 macro, PCIE_CLIENT_ENABLE_LTSSM and PCIE_CLIENT_DISABLE_LTSSM were setting bits with a mask of 0xc = 0b1100, which means BIT 2 and BIT 3. After the conversion it only sets bit 2, with bit 3 being handled by a separate define named PCIE_CLIENT_LD_RQ_RST_GRT. Apparently the conversion missed to make use of this new macros resulting in the third bit not being set. Fixes: 30e919570581 ("PCI: dw-rockchip: Switch to FIELD_PREP_WM16 macro") Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 731d93663ccae5..e86d7ee686fcbe 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -317,14 +317,14 @@ static void rockchip_pcie_ltssm_trace(struct rockchip_pcie *rockchip, static void rockchip_pcie_enable_ltssm(struct rockchip_pcie *rockchip) { - rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_ENABLE_LTSSM, - PCIE_CLIENT_GENERAL_CON); + u32 val = PCIE_CLIENT_ENABLE_LTSSM | PCIE_CLIENT_LD_RQ_RST_GRT; + rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_GENERAL_CON); } static void rockchip_pcie_disable_ltssm(struct rockchip_pcie *rockchip) { - rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_DISABLE_LTSSM, - PCIE_CLIENT_GENERAL_CON); + u32 val = PCIE_CLIENT_DISABLE_LTSSM | PCIE_CLIENT_LD_RQ_RST_GRT; + rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_GENERAL_CON); } static bool rockchip_pcie_link_up(struct dw_pcie *pci) From b268ec1df34019d06c8d00aaf167f34a8e0b49a3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 22 Dec 2025 20:25:56 +0100 Subject: [PATCH 024/218] PCI: dw-rockchip: Restore vpcie3v3 regulator handle This reverts c930b10f17c0 ("PCI: dw-rockchip: Simplify regulator setup with devm_regulator_get_enable_optional()"), which nicely cleaned up the code. The vpcie3v3 regulator handle is needed to disable the regulator during system suspend (to be added in its own patch). Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index e86d7ee686fcbe..04ffb16261de9c 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -111,6 +111,7 @@ struct rockchip_pcie { unsigned int clk_cnt; struct reset_control *rst; struct gpio_desc *rst_gpio; + struct regulator *vpcie3v3; struct irq_domain *irq_domain; const struct rockchip_pcie_of_data *data; bool supports_clkreq; @@ -784,15 +785,22 @@ static int rockchip_pcie_probe(struct platform_device *pdev) return ret; /* DON'T MOVE ME: must be enable before PHY init */ - ret = devm_regulator_get_enable_optional(dev, "vpcie3v3"); - if (ret < 0 && ret != -ENODEV) - return dev_err_probe(dev, ret, - "failed to enable vpcie3v3 regulator\n"); + rockchip->vpcie3v3 = devm_regulator_get_optional(dev, "vpcie3v3"); + if (IS_ERR(rockchip->vpcie3v3)) { + if (PTR_ERR(rockchip->vpcie3v3) != -ENODEV) + return dev_err_probe(dev, PTR_ERR(rockchip->vpcie3v3), + "failed to get vpcie3v3 regulator\n"); + rockchip->vpcie3v3 = NULL; + } else { + ret = regulator_enable(rockchip->vpcie3v3); + if (ret) + return dev_err_probe(dev, ret, + "failed to enable vpcie3v3 regulator\n"); + } ret = rockchip_pcie_phy_init(rockchip); if (ret) - return dev_err_probe(dev, ret, - "failed to initialize the phy\n"); + goto disable_regulator; ret = reset_control_deassert(rockchip->rst); if (ret) @@ -825,6 +833,9 @@ static int rockchip_pcie_probe(struct platform_device *pdev) clk_bulk_disable_unprepare(rockchip->clk_cnt, rockchip->clks); deinit_phy: rockchip_pcie_phy_deinit(rockchip); +disable_regulator: + if (rockchip->vpcie3v3) + regulator_disable(rockchip->vpcie3v3); return ret; } From aa680fe5f7a5746bb96f1b0c5fd0ffdc78da16eb Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:06:36 +0200 Subject: [PATCH 025/218] PCI: dw-rockchip: Move devm_phy_get out of phy_init By moving devm_phy_get() to the probe routine, rockchip_pcie_phy_init() can be used to re-initialize the PCIe PHY, which is for example needed after a system suspend/resume cycle. Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 04ffb16261de9c..42bc4ed4b22e2d 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -603,14 +603,8 @@ static int rockchip_pcie_resource_get(struct platform_device *pdev, static int rockchip_pcie_phy_init(struct rockchip_pcie *rockchip) { - struct device *dev = rockchip->pci.dev; int ret; - rockchip->phy = devm_phy_get(dev, "pcie-phy"); - if (IS_ERR(rockchip->phy)) - return dev_err_probe(dev, PTR_ERR(rockchip->phy), - "missing PHY\n"); - ret = phy_init(rockchip->phy); if (ret < 0) return ret; @@ -798,6 +792,13 @@ static int rockchip_pcie_probe(struct platform_device *pdev) "failed to enable vpcie3v3 regulator\n"); } + rockchip->phy = devm_phy_get(dev, "pcie-phy"); + if (IS_ERR(rockchip->phy)) { + ret = PTR_ERR(rockchip->phy); + dev_err_probe(dev, ret, "missing PHY\n"); + goto disable_regulator; + } + ret = rockchip_pcie_phy_init(rockchip); if (ret) goto disable_regulator; From 31f9c2054f7087d72a8dfe4f49e114413dd345a7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:21:02 +0200 Subject: [PATCH 026/218] PCI: dw-rockchip: Add helper function for enhanced LTSSM control mode Remove code duplication and improve readability by introducing a new function to setup the enhanced LTSSM mode. Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 42bc4ed4b22e2d..e0709e29e4584b 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -661,17 +661,25 @@ static irqreturn_t rockchip_pcie_ep_sys_irq_thread(int irq, void *arg) return IRQ_HANDLED; } +static void rockchip_pcie_enable_enhanced_ltssm_control_mode(struct rockchip_pcie *rockchip, u32 flags) +{ + u32 val; + + /* Enable the enhanced control mode of signal app_ltssm_enable */ + val = FIELD_PREP_WM16(PCIE_LTSSM_ENABLE_ENHANCE, 1); + if (flags) + val |= FIELD_PREP_WM16(flags, 1); + rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_HOT_RESET_CTRL); +} + static int rockchip_pcie_configure_rc(struct rockchip_pcie *rockchip) { struct dw_pcie_rp *pp; - u32 val; if (!IS_ENABLED(CONFIG_PCIE_ROCKCHIP_DW_HOST)) return -ENODEV; - /* LTSSM enable control mode */ - val = FIELD_PREP_WM16(PCIE_LTSSM_ENABLE_ENHANCE, 1); - rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_HOT_RESET_CTRL); + rockchip_pcie_enable_enhanced_ltssm_control_mode(rockchip, 0); rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_SET_MODE(PCIE_CLIENT_MODE_RC), @@ -705,13 +713,7 @@ static int rockchip_pcie_configure_ep(struct platform_device *pdev, return ret; } - /* - * LTSSM enable control mode, and automatically delay link training on - * hot reset/link-down reset. - */ - val = FIELD_PREP_WM16(PCIE_LTSSM_ENABLE_ENHANCE, 1) | - FIELD_PREP_WM16(PCIE_LTSSM_APP_DLY2_EN, 1); - rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_HOT_RESET_CTRL); + rockchip_pcie_enable_enhanced_ltssm_control_mode(rockchip, PCIE_LTSSM_APP_DLY2_EN); rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_SET_MODE(PCIE_CLIENT_MODE_EP), From 4d12b702e4b0e62648d0f9991b98bdf5ad9bcfe0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:27:04 +0200 Subject: [PATCH 027/218] PCI: dw-rockchip: Add helper function for controller mode Remove code duplication and improve readability by introducing a new function to setup the controller mode. Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index e0709e29e4584b..8d6f05d04d8327 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -672,6 +672,11 @@ static void rockchip_pcie_enable_enhanced_ltssm_control_mode(struct rockchip_pci rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_HOT_RESET_CTRL); } +static void rockchip_pcie_set_controller_mode(struct rockchip_pcie *rockchip, u32 mode) +{ + rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_SET_MODE(mode), PCIE_CLIENT_GENERAL_CON); +} + static int rockchip_pcie_configure_rc(struct rockchip_pcie *rockchip) { struct dw_pcie_rp *pp; @@ -680,10 +685,7 @@ static int rockchip_pcie_configure_rc(struct rockchip_pcie *rockchip) return -ENODEV; rockchip_pcie_enable_enhanced_ltssm_control_mode(rockchip, 0); - - rockchip_pcie_writel_apb(rockchip, - PCIE_CLIENT_SET_MODE(PCIE_CLIENT_MODE_RC), - PCIE_CLIENT_GENERAL_CON); + rockchip_pcie_set_controller_mode(rockchip, PCIE_CLIENT_MODE_RC); pp = &rockchip->pci.pp; pp->ops = &rockchip_pcie_host_ops; @@ -714,10 +716,7 @@ static int rockchip_pcie_configure_ep(struct platform_device *pdev, } rockchip_pcie_enable_enhanced_ltssm_control_mode(rockchip, PCIE_LTSSM_APP_DLY2_EN); - - rockchip_pcie_writel_apb(rockchip, - PCIE_CLIENT_SET_MODE(PCIE_CLIENT_MODE_EP), - PCIE_CLIENT_GENERAL_CON); + rockchip_pcie_set_controller_mode(rockchip, PCIE_CLIENT_MODE_EP); rockchip->pci.ep.ops = &rockchip_pcie_ep_ops; rockchip->pci.ep.page_size = SZ_64K; From 4e0d54a43bec84692b57aaf40776f6949c1c2784 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:30:11 +0200 Subject: [PATCH 028/218] PCI: dw-rockchip: Add helper function for DDL indicator Remove code duplication and improve readability by introducing a new function to setup the DLL indicator. Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 8d6f05d04d8327..4be536681e2bcf 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -677,6 +677,16 @@ static void rockchip_pcie_set_controller_mode(struct rockchip_pcie *rockchip, u3 rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_SET_MODE(mode), PCIE_CLIENT_GENERAL_CON); } +static void rockchip_pcie_unmask_dll_indicator(struct rockchip_pcie *rockchip) +{ + u32 val; + + /* unmask DLL up/down indicator and hot reset/link-down reset */ + val = FIELD_PREP_WM16(PCIE_RDLH_LINK_UP_CHGED, 0) | + FIELD_PREP_WM16(PCIE_LINK_REQ_RST_NOT_INT, 0); + rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_INTR_MASK_MISC); +} + static int rockchip_pcie_configure_rc(struct rockchip_pcie *rockchip) { struct dw_pcie_rp *pp; @@ -698,7 +708,6 @@ static int rockchip_pcie_configure_ep(struct platform_device *pdev, { struct device *dev = &pdev->dev; int irq, ret; - u32 val; if (!IS_ENABLED(CONFIG_PCIE_ROCKCHIP_DW_EP)) return -ENODEV; @@ -738,10 +747,7 @@ static int rockchip_pcie_configure_ep(struct platform_device *pdev, pci_epc_init_notify(rockchip->pci.ep.epc); - /* unmask DLL up/down indicator and hot reset/link-down reset */ - val = FIELD_PREP_WM16(PCIE_RDLH_LINK_UP_CHGED, 0) | - FIELD_PREP_WM16(PCIE_LINK_REQ_RST_NOT_INT, 0); - rockchip_pcie_writel_apb(rockchip, val, PCIE_CLIENT_INTR_MASK_MISC); + rockchip_pcie_unmask_dll_indicator(rockchip); return ret; } From 0c987a239aabff712762d6d47b361f6e134824dc Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:38:54 +0200 Subject: [PATCH 029/218] PCI: dw-rockchip: Add pme_turn_off support Prepare Rockchip PCIe controller for system suspend support by adding the PME turn off operation. Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 4be536681e2bcf..e1203d069b3a88 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -44,6 +44,7 @@ #define PCIE_CLIENT_LD_RQ_RST_GRT FIELD_PREP_WM16(BIT(3), 1) #define PCIE_CLIENT_ENABLE_LTSSM FIELD_PREP_WM16(BIT(2), 1) #define PCIE_CLIENT_DISABLE_LTSSM FIELD_PREP_WM16(BIT(2), 0) +#define PCIE_CLIENT_INTR_STATUS_MSG_RX 0x04 /* Interrupt Status Register Related to Legacy Interrupt */ #define PCIE_CLIENT_INTR_STATUS_LEGACY 0x8 @@ -63,6 +64,11 @@ /* Interrupt Mask Register Related to Miscellaneous Operation */ #define PCIE_CLIENT_INTR_MASK_MISC 0x24 +#define PCIE_CLIENT_POWER 0x2c +#define PCIE_CLIENT_MSG_GEN 0x34 +#define PME_READY_ENTER_L23 BIT(3) +#define PME_TURN_OFF FIELD_PREP_WM16(BIT(4), 1) +#define PME_TO_ACK FIELD_PREP_WM16(BIT(9), 1) /* Power Management Control Register */ #define PCIE_CLIENT_POWER_CON 0x2c @@ -445,8 +451,46 @@ static int rockchip_pcie_host_init(struct dw_pcie_rp *pp) return 0; } +static void rockchip_pcie_pme_turn_off(struct dw_pcie_rp *pp) +{ + struct dw_pcie *pci = to_dw_pcie_from_pp(pp); + struct rockchip_pcie *rockchip = to_rockchip_pcie(pci); + struct device *dev = rockchip->pci.dev; + u32 status; + int ret; + + /* 1. Broadcast PME_Turn_Off Message, bit 4 self-clear once done */ + rockchip_pcie_writel_apb(rockchip, PME_TURN_OFF, PCIE_CLIENT_MSG_GEN); + ret = readl_poll_timeout(rockchip->apb_base + PCIE_CLIENT_MSG_GEN, + status, !(status & BIT(4)), PCIE_PME_TO_L2_TIMEOUT_US / 10, + PCIE_PME_TO_L2_TIMEOUT_US); + if (ret) { + dev_warn(dev, "Failed to send PME_Turn_Off\n"); + return; + } + + /* 2. Wait for PME_TO_Ack, bit 9 will be set once received */ + ret = readl_poll_timeout(rockchip->apb_base + PCIE_CLIENT_INTR_STATUS_MSG_RX, + status, status & BIT(9), PCIE_PME_TO_L2_TIMEOUT_US / 10, + PCIE_PME_TO_L2_TIMEOUT_US); + if (ret) { + dev_warn(dev, "Failed to receive PME_TO_Ack\n"); + return; + } + + /* 3. Clear PME_TO_Ack and Wait for ready to enter L23 message */ + rockchip_pcie_writel_apb(rockchip, PME_TO_ACK, PCIE_CLIENT_INTR_STATUS_MSG_RX); + ret = readl_poll_timeout(rockchip->apb_base + PCIE_CLIENT_POWER, + status, status & PME_READY_ENTER_L23, + PCIE_PME_TO_L2_TIMEOUT_US / 10, + PCIE_PME_TO_L2_TIMEOUT_US); + if (ret) + dev_err(dev, "Failed to get ready to enter L23 message\n"); +} + static const struct dw_pcie_host_ops rockchip_pcie_host_ops = { .init = rockchip_pcie_host_init, + .pme_turn_off = rockchip_pcie_pme_turn_off, }; /* From 3541fe52e46e50046210224f3a678d1f5d4ced81 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:49:34 +0200 Subject: [PATCH 030/218] PCI: dw-rockchip: Add system PM support Add system PM support for Rockchip PCIe Designware Controllers. I've tested this on the Rockchip RK3576 EVB1, the Radxa ROCK 4D and the ArmSom Sige5 boards. While I haven't experienced any issues, most of my tests have been done without any devices attached (i.e. default board without any extras), so there _might_ still be some problems. As system suspend does not work at all right now, I think it makes sense to get at least the basic configurations working as soon as possible as it will allow us to catch regressions by enabling system suspend in CI systems like KernelCI. Co-developed-by: Shawn Lin Signed-off-by: Shawn Lin Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index e1203d069b3a88..9c6be61cf66ceb 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -119,6 +119,7 @@ struct rockchip_pcie { struct gpio_desc *rst_gpio; struct regulator *vpcie3v3; struct irq_domain *irq_domain; + u32 intx; const struct rockchip_pcie_of_data *data; bool supports_clkreq; struct delayed_work trace_work; @@ -892,6 +893,99 @@ static int rockchip_pcie_probe(struct platform_device *pdev) return ret; } +static int rockchip_pcie_suspend(struct device *dev) +{ + struct rockchip_pcie *rockchip = dev_get_drvdata(dev); + struct dw_pcie *pci = &rockchip->pci; + int ret; + + if (rockchip->data->mode == DW_PCIE_EP_TYPE) { + dev_err(dev, "suspend is not supported in EP mode\n"); + return -EOPNOTSUPP; + } + + rockchip->intx = rockchip_pcie_readl_apb(rockchip, PCIE_CLIENT_INTR_MASK_LEGACY); + + ret = dw_pcie_suspend_noirq(pci); + if (ret) + return ret; + + gpiod_set_value_cansleep(rockchip->rst_gpio, 0); + rockchip_pcie_phy_deinit(rockchip); + clk_bulk_disable_unprepare(rockchip->clk_cnt, rockchip->clks); + reset_control_assert(rockchip->rst); + if (rockchip->vpcie3v3) + regulator_disable(rockchip->vpcie3v3); + + return 0; +} + +static int rockchip_pcie_resume(struct device *dev) +{ + struct rockchip_pcie *rockchip = dev_get_drvdata(dev); + struct dw_pcie *pci = &rockchip->pci; + int ret; + + if (rockchip->data->mode == DW_PCIE_EP_TYPE) { + dev_err(dev, "resume is not supported in EP mode\n"); + return -EOPNOTSUPP; + } + + ret = clk_bulk_prepare_enable(rockchip->clk_cnt, rockchip->clks); + if (ret) { + dev_err(dev, "clock init failed: %d\n", ret); + return ret; + } + + if (rockchip->vpcie3v3) { + ret = regulator_enable(rockchip->vpcie3v3); + if (ret) + goto err_disable_clk; + } + + ret = rockchip_pcie_phy_init(rockchip); + if (ret) { + dev_err(dev, "phy init failed: %d\n", ret); + goto err_disable_regulator; + } + + reset_control_deassert(rockchip->rst); + + rockchip_pcie_writel_apb(rockchip, FIELD_PREP_WM16(0xffff, rockchip->intx), + PCIE_CLIENT_INTR_MASK_LEGACY); + + rockchip_pcie_enable_enhanced_ltssm_control_mode(rockchip, 0); + rockchip_pcie_set_controller_mode(rockchip, PCIE_CLIENT_MODE_RC); + rockchip_pcie_unmask_dll_indicator(rockchip); + + gpiod_set_value_cansleep(rockchip->rst_gpio, 1); + + ret = dw_pcie_resume_noirq(pci); + if (ret) { + dev_err(dev, "failed to resume: %d\n", ret); + /* + * During resume, dw_pcie_wait_for_link() is called and when + * there is no device connected at all it returns -EIO with + * the message "Device found, but not active". Ignore it for + * now. + */ + if (ret != -EIO) + goto err_deinit_phy; + } + + return 0; + +err_deinit_phy: + gpiod_set_value_cansleep(rockchip->rst_gpio, 0); + rockchip_pcie_phy_deinit(rockchip); +err_disable_regulator: + if (rockchip->vpcie3v3) + regulator_disable(rockchip->vpcie3v3); +err_disable_clk: + clk_bulk_disable_unprepare(rockchip->clk_cnt, rockchip->clks); + return ret; +} + static const struct rockchip_pcie_of_data rockchip_pcie_rc_of_data_rk3568 = { .mode = DW_PCIE_RC_TYPE, }; @@ -922,11 +1016,17 @@ static const struct of_device_id rockchip_pcie_of_match[] = { {}, }; +static const struct dev_pm_ops rockchip_pcie_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(rockchip_pcie_suspend, + rockchip_pcie_resume) +}; + static struct platform_driver rockchip_pcie_driver = { .driver = { .name = "rockchip-dw-pcie", .of_match_table = rockchip_pcie_of_match, .suppress_bind_attrs = true, + .pm = &rockchip_pcie_pm_ops, }, .probe = rockchip_pcie_probe, }; From c657ef94dd83f4428408b3a75244e6300cefcb07 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 24 Nov 2025 20:06:39 +0100 Subject: [PATCH 031/218] [RFC] PCI: dw-rockchip: port some suspend code from vendor kernel Rockchip's vendor kernel does these calls before starting the actual process of going into L2 state. I'm not sure about the rationale, hopefully Shawn can help out with that. Cc: Shawn Lin Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 9c6be61cf66ceb..65e85c7eab0587 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -76,6 +76,9 @@ #define PCIE_CLKREQ_NOT_READY FIELD_PREP_WM16(BIT(0), 0) #define PCIE_CLKREQ_PULL_DOWN FIELD_PREP_WM16(GENMASK(13, 12), 1) +/* General Debug Register */ +#define PCIE_CLIENT_GENERAL_DEBUG 0x104 + /* RASDES TBA information */ #define PCIE_CLIENT_CDM_RASDES_TBA_INFO_CMN 0x154 #define PCIE_CLIENT_CDM_RASDES_TBA_L1_1 BIT(4) @@ -893,6 +896,12 @@ static int rockchip_pcie_probe(struct platform_device *pdev) return ret; } + +static inline void rockchip_pcie_link_status_clear(struct rockchip_pcie *rockchip) +{ + rockchip_pcie_writel_apb(rockchip, PCIE_CLIENT_GENERAL_DEBUG, 0x0); +} + static int rockchip_pcie_suspend(struct device *dev) { struct rockchip_pcie *rockchip = dev_get_drvdata(dev); @@ -906,6 +915,11 @@ static int rockchip_pcie_suspend(struct device *dev) rockchip->intx = rockchip_pcie_readl_apb(rockchip, PCIE_CLIENT_INTR_MASK_LEGACY); + /* All sub-devices are in D3hot by PCIe stack */ + dw_pcie_dbi_ro_wr_dis(pci); + + rockchip_pcie_link_status_clear(rockchip); + ret = dw_pcie_suspend_noirq(pci); if (ret) return ret; From 3b285a40bb925a1aabb145a43b570ba27a09cc88 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 22 Aug 2025 18:49:35 +0200 Subject: [PATCH 032/218] dt-bindings: phy: rockchip-usbdp: add improved ports scheme Currently the Rockchip USBDP PHY is missing a documented port scheme. Meanwhile upstream RK3588 DTS files are a bit messy and use different port schemes. The upstream USBDP PHY Linux kernel driver does not yet parse the ports at all and thus does not create any implicit ABI either. But with the current mess it is not possible to properly support USB-C DP AltMode. Thus this introduces a proper port scheme following roughly the ports design of the Qualcomm QMP USB4-USB3-DP PHY controller binding with a slight difference that there is an additional port for the USB-C SBU port as the Rockchip USB-DP PHY also contains the SBU mux. Reviewed-by: Rob Herring (Arm) Signed-off-by: Sebastian Reichel --- .../bindings/phy/phy-rockchip-usbdp.yaml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml b/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml index 8b7059d5b1826f..f728acf057e404 100644 --- a/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml +++ b/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml @@ -114,6 +114,29 @@ properties: A port node to link the PHY to a TypeC controller for the purpose of handling orientation switching. + ports: + $ref: /schemas/graph.yaml#/properties/ports + properties: + port@0: + $ref: /schemas/graph.yaml#/properties/port + description: + Output endpoint of the PHY for USB (or DP when configured into 4 lane + mode), which should point to the superspeed port of a USB connector. + + port@1: + $ref: /schemas/graph.yaml#/properties/port + description: Incoming endpoint from the USB controller + + port@2: + $ref: /schemas/graph.yaml#/properties/port + description: Incoming endpoint from the DisplayPort controller + + port@3: + $ref: /schemas/graph.yaml#/properties/port + description: + Output endpoint of the PHY for DP, which should either point to the + SBU port of a USB-C connector or a DisplayPort connector input port. + required: - compatible - reg From d27ba1f100adc3ebc1b2546896d30eaa2f1c310e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 16:28:38 +0200 Subject: [PATCH 033/218] phy: rockchip: usbdp: Update mode_change after error handling If rk_udphy_init() or rk_udphy_setup() fails, the reinit will not be tried again. Fix this by only updating the variable after all potential errors have been handled. Note, that no errors have been seen on real hardware and failures would most likely be fatal and require at least a full reboot as the function already asserts the PHY reset lines. So this is more of a theoretical issue. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index fba35510d88ced..a5ffdf7a6e76b3 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1000,15 +1000,14 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) } if (udphy->status == UDPHY_MODE_NONE) { - udphy->mode_change = false; ret = rk_udphy_setup(udphy); if (ret) return ret; if (udphy->mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); - } else if (udphy->mode_change) { udphy->mode_change = false; + } else if (udphy->mode_change) { udphy->status = UDPHY_MODE_NONE; if (udphy->mode == UDPHY_MODE_DP) rk_udphy_u3_port_disable(udphy, true); @@ -1017,6 +1016,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) ret = rk_udphy_setup(udphy); if (ret) return ret; + udphy->mode_change = false; } udphy->status |= mode; From 97491809229c459937774697ffb4a7336d310c3b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 01:22:02 +0100 Subject: [PATCH 034/218] phy: rockchip: usbdp: Do not lose USB3 PHY status By default (i.e. without manually enabling runtime PM) DWC3 requests the USB3 PHY once and keeps it enabled all the time. When DisplayPort is being requested later on, a mode change is needed. This re-initializes the PHY. During re-initialization the status variable has incorrectly been cleared, which means the tracking information for USB3 is lost. This is not an immediate problem, since the DP side keeps the PHY enabled. But once DP is toggled off, the whole PHY will be disabled. This is a problem, because the USB side still needs it powered. Fix things by not clearing the status flags. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index a5ffdf7a6e76b3..12dfdd470a7826 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1008,7 +1008,6 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) rk_udphy_u3_port_disable(udphy, false); udphy->mode_change = false; } else if (udphy->mode_change) { - udphy->status = UDPHY_MODE_NONE; if (udphy->mode == UDPHY_MODE_DP) rk_udphy_u3_port_disable(udphy, true); From 57a1edc3b7ae6e1e77146ac35a41415b67faae57 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 13:45:06 +0200 Subject: [PATCH 035/218] phy: rockchip: usbdp: Fix devm_clk_bulk_get_all check If devm_clk_bulk_get_all() returns -EPROBE_DEFER, it is replaced with -ENODEV, permanently failing the driver probe instead of allowing it to defer. Avoid masking the error code to fix the issue. This effectively drops returning -ENODEV in case no clocks are being described in DT. This special case will now be handled by the follow-up check searching for "refclk" and exit with -EINVAL. None of this will be hit in practice, since the driver is only used by RK3588 and RK3576 - on these platforms the DT is validated to contain the clocks and the clock driver is force probed early. Thus there is no need to backport this. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 12dfdd470a7826..2845a012eafcca 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -427,8 +427,8 @@ static int rk_udphy_clk_init(struct rk_udphy *udphy, struct device *dev) int i; udphy->num_clks = devm_clk_bulk_get_all(dev, &udphy->clks); - if (udphy->num_clks < 1) - return -ENODEV; + if (udphy->num_clks < 0) + return udphy->num_clks; /* used for configure phy reference clock frequency */ for (i = 0; i < udphy->num_clks; i++) { From 1148bb5cba63c4bba44f0568284139efe6d17219 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 15:51:07 +0200 Subject: [PATCH 036/218] phy: rockchip: usbdp: Drop seamless DP takeover If the bootloader already enabled DP, keeping it enabled here means the DRM stack can do a seamless takeover. Right now the DRM does not support this and the code will leak clock resources on module removal, so just drop this feature. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 2845a012eafcca..ffe39c968c70c3 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -936,9 +936,9 @@ static int rk_udphy_get_initial_status(struct rk_udphy *udphy) regmap_read(udphy->pma_regmap, CMN_LANE_MUX_AND_EN_OFFSET, &value); if (FIELD_GET(CMN_DP_LANE_MUX_ALL, value) && FIELD_GET(CMN_DP_LANE_EN_ALL, value)) - udphy->status = UDPHY_MODE_DP; - else - rk_udphy_disable(udphy); + dev_dbg(udphy->dev, "Started with DP PHY pre-enabled; seamless takeover unsupported\n"); + + rk_udphy_disable(udphy); return 0; } From 190bddb38830f54fad825cece0382e0ae7cef5d1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 18:05:39 +0200 Subject: [PATCH 037/218] phy: rockchip: usbdp: Limit DP lane count to muxed lanes In theory the DP controller could request 4 lanes when the PHY is restricted to 2 lanes as the other half is used by USB3. With the current user (DW-DP) this cannot happen, but as the check is cheap and users might change in the future protect things accordingly. Not doing so would corrupt USB3 usage by the following code configuring the voltages. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index ffe39c968c70c3..6dff4dfc9363e5 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1126,6 +1126,9 @@ static int rk_udphy_dp_phy_verify_link_rate(struct rk_udphy *udphy, static int rk_udphy_dp_phy_verify_lanes(struct rk_udphy *udphy, struct phy_configure_opts_dp *dp) { + if (dp->lanes > udphy->dp_lanes) + return -EINVAL; + switch (dp->lanes) { case 1: case 2: From 64192dc9b0e1ff0556275e725f44d6a7c1d8947a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Feb 2026 19:26:12 +0100 Subject: [PATCH 038/218] phy: rockchip: usbdp: Keep clocks running on PHY re-init When a mode change is required rk_udphy_power_on() disables the clocks and then calls rk_udphy_setup(), which then enables all the clocks again before continuing with rk_udphy_init(). Considering that rk_udphy_init() does assert the reset lines, re-enabling the clocks is just delaying things. Avoid it by directly calling rk_udphy_init(). Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 6dff4dfc9363e5..c05ee65a9f73a2 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1011,8 +1011,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) if (udphy->mode == UDPHY_MODE_DP) rk_udphy_u3_port_disable(udphy, true); - rk_udphy_disable(udphy); - ret = rk_udphy_setup(udphy); + ret = rk_udphy_init(udphy); if (ret) return ret; udphy->mode_change = false; From 655778a9d4067866a4aa360d30fa08d630abfc32 Mon Sep 17 00:00:00 2001 From: Frank Wang Date: Fri, 16 Jan 2026 20:28:08 +0100 Subject: [PATCH 039/218] phy: rockchip: usbdp: Amend SSC modulation deviation Move SSC modulation deviation into private config of clock - 24M: 0x00d4[5:0] = 0x30 - 26M: 0x00d4[5:0] = 0x33 Signed-off-by: Frank Wang [Taken over from rockchip's kernel tree; register 0x00d4 is not described in the TRM] Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index c05ee65a9f73a2..a44babac3517fe 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -350,7 +350,8 @@ static const struct reg_sequence rk_udphy_24m_refclk_cfg[] = { {0x0a64, 0xa8}, {0x1a3c, 0xd0}, {0x1a44, 0xd0}, {0x1a48, 0x01}, {0x1a4c, 0x0d}, {0x1a54, 0xe0}, - {0x1a5c, 0xe0}, {0x1a64, 0xa8} + {0x1a5c, 0xe0}, {0x1a64, 0xa8}, + {0x00d4, 0x30} }; static const struct reg_sequence rk_udphy_26m_refclk_cfg[] = { @@ -377,7 +378,7 @@ static const struct reg_sequence rk_udphy_26m_refclk_cfg[] = { {0x0c30, 0x0e}, {0x0c48, 0x06}, {0x1c30, 0x0e}, {0x1c48, 0x06}, {0x028c, 0x18}, {0x0af0, 0x00}, - {0x1af0, 0x00} + {0x1af0, 0x00}, {0x00d4, 0x33} }; static const struct reg_sequence rk_udphy_init_sequence[] = { @@ -412,8 +413,7 @@ static const struct reg_sequence rk_udphy_init_sequence[] = { {0x0070, 0x7d}, {0x0074, 0x68}, {0x0af4, 0x1a}, {0x1af4, 0x1a}, {0x0440, 0x3f}, {0x10d4, 0x08}, - {0x20d4, 0x08}, {0x00d4, 0x30}, - {0x0024, 0x6e}, + {0x20d4, 0x08}, {0x0024, 0x6e} }; static inline int rk_udphy_grfreg_write(struct regmap *base, From f5484a3f615ef25529d68cc0f6dea642ba5eae46 Mon Sep 17 00:00:00 2001 From: William Wu Date: Fri, 16 Jan 2026 20:31:20 +0100 Subject: [PATCH 040/218] phy: rockchip: usbdp: Fix LFPS detect threshold control According to the LFPS Tx Low Power/LFPS Rx Detect Threshold [1], the device under test(DUT) must not respond if LFPS below the minimum LFPS Rx Detect Threshold 100mV. Test fail on Rockchip platforms, because the default LFPS detect threshold is set to 65mV. The USBDP PHY LFPS detect threshold voltage could be set to 30mV ~ 140mV, and since there could be 10-20% PVT variation, we set LFPS detect threshold voltage to 110mV. [1] https://compliance.usb.org/resources/LFPS_Rx_Tx_Low_Power_Compliance_Update_Rev5.pdf Signed-off-by: William Wu [Taken over from rockchip's kernel tree; the registers are not described in the TRM] Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index a44babac3517fe..7ed539d8c791b3 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -413,7 +413,8 @@ static const struct reg_sequence rk_udphy_init_sequence[] = { {0x0070, 0x7d}, {0x0074, 0x68}, {0x0af4, 0x1a}, {0x1af4, 0x1a}, {0x0440, 0x3f}, {0x10d4, 0x08}, - {0x20d4, 0x08}, {0x0024, 0x6e} + {0x20d4, 0x08}, {0x0024, 0x6e}, + {0x09c0, 0x0a}, {0x19c0, 0x0a} }; static inline int rk_udphy_grfreg_write(struct regmap *base, From 322fc65b71026eef23c08c5ded20d07e2470f846 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 21:32:59 +0100 Subject: [PATCH 041/218] phy: rockchip: usbdp: Add missing mode_change update rk_udphy_set_typec_default_mapping() updates the available modes, but does not set the mode_change as required. This results in missing re-initialization and thus non-working DisplayPort. Fix this issue by introducing a new helper to update the available modes. Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 7ed539d8c791b3..2ff05ef6eeacd1 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -619,6 +619,15 @@ static void rk_udphy_dp_hpd_event_trigger(struct rk_udphy *udphy, bool hpd) rk_udphy_grfreg_write(udphy->vogrf, &cfg->vogrfcfg[udphy->id].hpd_trigger, hpd); } +static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 mode) +{ + if (udphy->mode == mode) + return; + + udphy->mode_change = true; + udphy->mode = mode; +} + static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) { if (udphy->flip) { @@ -649,7 +658,7 @@ static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) gpiod_set_value_cansleep(udphy->sbu2_dc_gpio, 1); } - udphy->mode = UDPHY_MODE_DP_USB; + rk_udphy_mode_set(udphy, UDPHY_MODE_DP_USB); } static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, @@ -1388,10 +1397,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, usleep_range(750, 800); rk_udphy_dp_hpd_event_trigger(udphy, true); } else if (data->status & DP_STATUS_HPD_STATE) { - if (udphy->mode != mode) { - udphy->mode = mode; - udphy->mode_change = true; - } + rk_udphy_mode_set(udphy, mode); rk_udphy_dp_hpd_event_trigger(udphy, true); } else { rk_udphy_dp_hpd_event_trigger(udphy, false); From db6ec8b673f19ff3881b254d420058cac921eda6 Mon Sep 17 00:00:00 2001 From: Zhang Yubing Date: Thu, 29 Jan 2026 13:04:47 +0100 Subject: [PATCH 042/218] phy: rockchip: usbdp: Support single-lane DP Implement support for using just a single DisplayPort line. Signed-off-by: Zhang Yubing Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 64 ++++++++++------------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 2ff05ef6eeacd1..a87cb5c0596e70 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -193,6 +193,7 @@ struct rk_udphy { int id; bool dp_in_use; + int dp_lanes; /* PHY const config */ const struct rk_udphy_cfg *cfgs; @@ -537,6 +538,13 @@ static void rk_udphy_usb_bvalid_enable(struct rk_udphy *udphy, u8 enable) * <0 1> dpln0 dpln1 usbrx usbtx * <2 3> usbrx usbtx dpln0 dpln1 * --------------------------------------------------------------------------- + * if 1 lane for dp function, 2 lane for usb function, define rockchip,dp-lane-mux = ; + * sample as follow: + * --------------------------------------------------------------------------- + * B11-B10 A2-A3 A11-A10 B2-B3 + * rockchip,dp-lane-mux ln0(tx/rx) ln1(tx) ln2(tx/rx) ln3(tx) + * <0> dpln0 \ usbrx usbtx + * --------------------------------------------------------------------------- */ static void rk_udphy_dplane_select(struct rk_udphy *udphy) @@ -544,18 +552,18 @@ static void rk_udphy_dplane_select(struct rk_udphy *udphy) const struct rk_udphy_cfg *cfg = udphy->cfgs; u32 value = 0; - switch (udphy->mode) { - case UDPHY_MODE_DP: - value |= 2 << udphy->dp_lane_sel[2] * 2; + switch (udphy->dp_lanes) { + case 4: value |= 3 << udphy->dp_lane_sel[3] * 2; + value |= 2 << udphy->dp_lane_sel[2] * 2; fallthrough; - case UDPHY_MODE_DP_USB: - value |= 0 << udphy->dp_lane_sel[0] * 2; + case 2: value |= 1 << udphy->dp_lane_sel[1] * 2; - break; + fallthrough; - case UDPHY_MODE_USB: + case 1: + value |= 0 << udphy->dp_lane_sel[0] * 2; break; default: @@ -568,28 +576,6 @@ static void rk_udphy_dplane_select(struct rk_udphy *udphy) FIELD_PREP(DP_AUX_DOUT_SEL, udphy->dp_aux_dout_sel) | value); } -static int rk_udphy_dplane_get(struct rk_udphy *udphy) -{ - int dp_lanes; - - switch (udphy->mode) { - case UDPHY_MODE_DP: - dp_lanes = 4; - break; - - case UDPHY_MODE_DP_USB: - dp_lanes = 2; - break; - - case UDPHY_MODE_USB: - default: - dp_lanes = 0; - break; - } - - return dp_lanes; -} - static void rk_udphy_dplane_enable(struct rk_udphy *udphy, int dp_lanes) { u32 val = 0; @@ -659,6 +645,7 @@ static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) } rk_udphy_mode_set(udphy, UDPHY_MODE_DP_USB); + udphy->dp_lanes = 2; } static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, @@ -897,7 +884,7 @@ static int rk_udphy_parse_lane_mux_data(struct rk_udphy *udphy) return 0; } - if (num_lanes != 2 && num_lanes != 4) + if (num_lanes != 1 && num_lanes != 2 && num_lanes != 4) return dev_err_probe(udphy->dev, -EINVAL, "invalid number of lane mux\n"); @@ -923,9 +910,11 @@ static int rk_udphy_parse_lane_mux_data(struct rk_udphy *udphy) } udphy->mode = UDPHY_MODE_DP; - if (num_lanes == 2) { + udphy->dp_lanes = num_lanes; + if (num_lanes == 1 || num_lanes == 2) { udphy->mode |= UDPHY_MODE_USB; - udphy->flip = (udphy->lane_mux_sel[0] == PHY_LANE_MUX_DP); + udphy->flip = (udphy->lane_mux_sel[0] == PHY_LANE_MUX_DP) || + (udphy->lane_mux_sel[1] == PHY_LANE_MUX_DP); } return 0; @@ -1074,18 +1063,17 @@ static int rk_udphy_dp_phy_exit(struct phy *phy) static int rk_udphy_dp_phy_power_on(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); - int ret, dp_lanes; + int ret; mutex_lock(&udphy->mutex); - dp_lanes = rk_udphy_dplane_get(udphy); - phy_set_bus_width(phy, dp_lanes); + phy_set_bus_width(phy, udphy->dp_lanes); ret = rk_udphy_power_on(udphy, UDPHY_MODE_DP); if (ret) goto unlock; - rk_udphy_dplane_enable(udphy, dp_lanes); + rk_udphy_dplane_enable(udphy, udphy->dp_lanes); rk_udphy_dplane_select(udphy); @@ -1368,6 +1356,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; mode = UDPHY_MODE_DP; + udphy->dp_lanes = 4; break; case TYPEC_DP_STATE_D: @@ -1384,6 +1373,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; } mode = UDPHY_MODE_DP_USB; + udphy->dp_lanes = 2; break; } @@ -1532,7 +1522,7 @@ static int rk_udphy_probe(struct platform_device *pdev) ret = PTR_ERR(udphy->phy_dp); return dev_err_probe(dev, ret, "failed to create DP phy\n"); } - phy_set_bus_width(udphy->phy_dp, rk_udphy_dplane_get(udphy)); + phy_set_bus_width(udphy->phy_dp, udphy->dp_lanes); udphy->phy_dp->attrs.max_link_rate = 8100; phy_set_drvdata(udphy->phy_dp, udphy); From a7da4022c983334666152b6a99591897271993fc Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 22:45:18 +0100 Subject: [PATCH 043/218] phy: rockchip: usbdp: Rename DP lane functions The common prefix for DisplayPort related functions is rk_udphy_dp_ (with a final _), so update the two DP lane functions to follow that scheme. Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index a87cb5c0596e70..e8b2adecb08a44 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -547,7 +547,7 @@ static void rk_udphy_usb_bvalid_enable(struct rk_udphy *udphy, u8 enable) * --------------------------------------------------------------------------- */ -static void rk_udphy_dplane_select(struct rk_udphy *udphy) +static void rk_udphy_dp_lane_select(struct rk_udphy *udphy) { const struct rk_udphy_cfg *cfg = udphy->cfgs; u32 value = 0; @@ -576,7 +576,7 @@ static void rk_udphy_dplane_select(struct rk_udphy *udphy) FIELD_PREP(DP_AUX_DOUT_SEL, udphy->dp_aux_dout_sel) | value); } -static void rk_udphy_dplane_enable(struct rk_udphy *udphy, int dp_lanes) +static void rk_udphy_dp_lane_enable(struct rk_udphy *udphy, int dp_lanes) { u32 val = 0; int i; @@ -1073,9 +1073,9 @@ static int rk_udphy_dp_phy_power_on(struct phy *phy) if (ret) goto unlock; - rk_udphy_dplane_enable(udphy, udphy->dp_lanes); + rk_udphy_dp_lane_enable(udphy, udphy->dp_lanes); - rk_udphy_dplane_select(udphy); + rk_udphy_dp_lane_select(udphy); unlock: mutex_unlock(&udphy->mutex); @@ -1093,7 +1093,7 @@ static int rk_udphy_dp_phy_power_off(struct phy *phy) struct rk_udphy *udphy = phy_get_drvdata(phy); mutex_lock(&udphy->mutex); - rk_udphy_dplane_enable(udphy, 0); + rk_udphy_dp_lane_enable(udphy, 0); rk_udphy_power_off(udphy, UDPHY_MODE_DP); mutex_unlock(&udphy->mutex); From b6f59f46d0f1f91d3b21a6166953fd68795c7063 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:15:23 +0100 Subject: [PATCH 044/218] phy: rockchip: usbdp: Use FIELD_PREP_WM16_CONST Cleanup code by replacing open-coded version of FIELD_PREP_WM16_CONST with the existing helper macro. Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index e8b2adecb08a44..ceaf091b910112 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -75,7 +76,6 @@ #define TRSV_LN2_MON_RX_CDR_DONE_OFFSET 0x1b84 /* trsv_reg06E1 */ #define TRSV_LN2_MON_RX_CDR_LOCK_DONE BIT(0) -#define BIT_WRITEABLE_SHIFT 16 #define PHY_AUX_DP_DATA_POL_NORMAL 0 #define PHY_AUX_DP_DATA_POL_INVERT 1 #define PHY_LANE_MUX_USB 0 @@ -104,8 +104,8 @@ struct rk_udphy_grf_reg { #define _RK_UDPHY_GEN_GRF_REG(offset, mask, disable, enable) \ {\ offset, \ - FIELD_PREP_CONST(mask, disable) | (mask << BIT_WRITEABLE_SHIFT), \ - FIELD_PREP_CONST(mask, enable) | (mask << BIT_WRITEABLE_SHIFT), \ + FIELD_PREP_WM16_CONST(mask, disable), \ + FIELD_PREP_WM16_CONST(mask, enable), \ } #define RK_UDPHY_GEN_GRF_REG(offset, bitend, bitstart, disable, enable) \ From 6e8ff103f91a95349c28c9bf7389ba9bc34b1431 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:24:20 +0100 Subject: [PATCH 045/218] phy: rockchip: usbdp: Cleanup DP lane selection function Use FIELD_PREP_WM16() helpers to simplify the DP lane selection logic. Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 28 ++++++----------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index ceaf091b910112..da769790df982e 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -550,30 +550,16 @@ static void rk_udphy_usb_bvalid_enable(struct rk_udphy *udphy, u8 enable) static void rk_udphy_dp_lane_select(struct rk_udphy *udphy) { const struct rk_udphy_cfg *cfg = udphy->cfgs; - u32 value = 0; - - switch (udphy->dp_lanes) { - case 4: - value |= 3 << udphy->dp_lane_sel[3] * 2; - value |= 2 << udphy->dp_lane_sel[2] * 2; - fallthrough; - - case 2: - value |= 1 << udphy->dp_lane_sel[1] * 2; - fallthrough; + u32 value = FIELD_PREP_WM16(DP_LANE_SEL_ALL, 0); + int i; - case 1: - value |= 0 << udphy->dp_lane_sel[0] * 2; - break; + for (i = 0; i < udphy->dp_lanes; i++) + value |= field_prep(DP_LANE_SEL_N(udphy->dp_lane_sel[i]), i); - default: - break; - } + value |= FIELD_PREP_WM16(DP_AUX_DIN_SEL, udphy->dp_aux_din_sel); + value |= FIELD_PREP_WM16(DP_AUX_DOUT_SEL, udphy->dp_aux_dout_sel); - regmap_write(udphy->vogrf, cfg->vogrfcfg[udphy->id].dp_lane_reg, - ((DP_AUX_DIN_SEL | DP_AUX_DOUT_SEL | DP_LANE_SEL_ALL) << 16) | - FIELD_PREP(DP_AUX_DIN_SEL, udphy->dp_aux_din_sel) | - FIELD_PREP(DP_AUX_DOUT_SEL, udphy->dp_aux_dout_sel) | value); + regmap_write(udphy->vogrf, cfg->vogrfcfg[udphy->id].dp_lane_reg, value); } static void rk_udphy_dp_lane_enable(struct rk_udphy *udphy, int dp_lanes) From 3fa1a89ceb64dcf6d10e8dacb4d8e5ff5fa709b0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:06:48 +0100 Subject: [PATCH 046/218] phy: rockchip: usbdp: Register DP aux bridge Add support to use USB-C connectors with the DP altmode helper code on devicetree based platforms. To get this working there must be a DRM bridge chain from the DisplayPort controller to the USB-C connector. E.g. on Rockchip RK3576: root@rk3576 # cat /sys/kernel/debug/dri/0/encoder-0/bridges bridge[0]: dw_dp_bridge_funcs refcount: 7 type: [10] DP OF: /soc/dp@27e40000:rockchip,rk3576-dp ops: [0x47] detect edid hpd bridge[1]: drm_aux_bridge_funcs refcount: 4 type: [0] Unknown OF: /soc/phy@2b010000:rockchip,rk3576-usbdp-phy ops: [0x0] bridge[2]: drm_aux_hpd_bridge_funcs refcount: 5 type: [10] DP OF: /soc/i2c@2ac50000/typec-portc@22/connector:usb-c-connector ops: [0x4] hpd Reviewed-by: Neil Armstrong Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/Kconfig | 2 ++ drivers/phy/rockchip/phy-rockchip-usbdp.c | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/drivers/phy/rockchip/Kconfig b/drivers/phy/rockchip/Kconfig index 14698571b60759..39759bb2fa1d10 100644 --- a/drivers/phy/rockchip/Kconfig +++ b/drivers/phy/rockchip/Kconfig @@ -136,8 +136,10 @@ config PHY_ROCKCHIP_USBDP tristate "Rockchip USBDP COMBO PHY Driver" depends on ARCH_ROCKCHIP && OF depends on TYPEC + depends on DRM || DRM=n select GENERIC_PHY select USB_COMMON + select DRM_AUX_BRIDGE if DRM_BRIDGE help Enable this to support the Rockchip USB3.0/DP combo PHY with Samsung IP block. This is required for USB3 support on RK3588. diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index da769790df982e..b75c190a4311f9 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -6,6 +6,7 @@ * Copyright (C) 2024 Collabora Ltd */ +#include #include #include #include @@ -1438,6 +1439,7 @@ static int rk_udphy_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct phy_provider *phy_provider; + struct fwnode_handle *dp_aux_ep; struct resource *res; struct rk_udphy *udphy; void __iomem *base; @@ -1496,6 +1498,18 @@ static int rk_udphy_probe(struct platform_device *pdev) return ret; } + /* + * Only register the DRM bridge, if the DP aux channel is connected. + * Some boards use the USBDP PHY only for its USB3 capabilities. + */ + dp_aux_ep = fwnode_graph_get_endpoint_by_id(dev_fwnode(dev), 3, 0, 0); + if (dp_aux_ep) { + ret = drm_aux_bridge_register(dev); + fwnode_handle_put(dp_aux_ep); + if (ret) + return ret; + } + udphy->phy_u3 = devm_phy_create(dev, dev->of_node, &rk_udphy_usb3_phy_ops); if (IS_ERR(udphy->phy_u3)) { ret = PTR_ERR(udphy->phy_u3); From 4b03aeb99c9e8a03634a6321bc83edefdb8b0d76 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:53:21 +0100 Subject: [PATCH 047/218] phy: rockchip: usbdp: Drop DP HPD handling Drop the HPD handling logic from the USBDP PHY. The registers involved require the display controller power domain being enabled and thus the HPD signal should be handled by the displayport controller itself. Apart from that the HPD handling as it is done here is incorrect and misses hotplug events happening after the USB-C connector (e.g. when a USB-C to HDMI adapter is involved and the HDMI cable is replugged). Proper USB-C DP HPD support requires some restructuring of the DP controller driver, which will happen independent of this patch. The mainline kernel does not yet support USB-C DP AltMode on RK3588 and RK3576, so it is fine to drop this code without adding the counterpart in the DRM in an atomic change. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 85 +++-------------------- 1 file changed, 9 insertions(+), 76 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index b75c190a4311f9..383e68b2450601 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -128,7 +128,6 @@ struct rk_udphy_grf_cfg { struct rk_udphy_vogrf_cfg { /* vo-grf */ - struct rk_udphy_grf_reg hpd_trigger; u32 dp_lane_reg; }; @@ -186,14 +185,11 @@ struct rk_udphy { u32 dp_lane_sel[4]; u32 dp_aux_dout_sel; u32 dp_aux_din_sel; - bool dp_sink_hpd_sel; - bool dp_sink_hpd_cfg; unsigned int link_rate; unsigned int lanes; u8 bw; int id; - bool dp_in_use; int dp_lanes; /* PHY const config */ @@ -579,19 +575,6 @@ static void rk_udphy_dp_lane_enable(struct rk_udphy *udphy, int dp_lanes) CMN_DP_CMN_RSTN, FIELD_PREP(CMN_DP_CMN_RSTN, 0x0)); } -static void rk_udphy_dp_hpd_event_trigger(struct rk_udphy *udphy, bool hpd) -{ - const struct rk_udphy_cfg *cfg = udphy->cfgs; - - udphy->dp_sink_hpd_sel = true; - udphy->dp_sink_hpd_cfg = hpd; - - if (!udphy->dp_in_use) - return; - - rk_udphy_grfreg_write(udphy->vogrf, &cfg->vogrfcfg[udphy->id].hpd_trigger, hpd); -} - static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 mode) { if (udphy->mode == mode) @@ -1024,29 +1007,6 @@ static void rk_udphy_power_off(struct rk_udphy *udphy, u8 mode) rk_udphy_disable(udphy); } -static int rk_udphy_dp_phy_init(struct phy *phy) -{ - struct rk_udphy *udphy = phy_get_drvdata(phy); - - mutex_lock(&udphy->mutex); - - udphy->dp_in_use = true; - - mutex_unlock(&udphy->mutex); - - return 0; -} - -static int rk_udphy_dp_phy_exit(struct phy *phy) -{ - struct rk_udphy *udphy = phy_get_drvdata(phy); - - mutex_lock(&udphy->mutex); - udphy->dp_in_use = false; - mutex_unlock(&udphy->mutex); - return 0; -} - static int rk_udphy_dp_phy_power_on(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); @@ -1278,8 +1238,6 @@ static int rk_udphy_dp_phy_configure(struct phy *phy, } static const struct phy_ops rk_udphy_dp_phy_ops = { - .init = rk_udphy_dp_phy_init, - .exit = rk_udphy_dp_phy_exit, .power_on = rk_udphy_dp_phy_power_on, .power_off = rk_udphy_dp_phy_power_off, .configure = rk_udphy_dp_phy_configure, @@ -1333,6 +1291,14 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, struct rk_udphy *udphy = typec_mux_get_drvdata(mux); u8 mode; + /* + * Ignore mux events not involving DP AltMode, because + * the mode field is being reused, e.g. state->mode == 4 + * could be either TYPEC_MODE_USB4 or TYPEC_DP_STATE_C. + */ + if (!state->alt || state->alt->svid != USB_TYPEC_DP_SID) + return 0; + mutex_lock(&udphy->mutex); switch (state->mode) { @@ -1364,22 +1330,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, break; } - if (state->alt && state->alt->svid == USB_TYPEC_DP_SID) { - struct typec_displayport_data *data = state->data; - - if (!data) { - rk_udphy_dp_hpd_event_trigger(udphy, false); - } else if (data->status & DP_STATUS_IRQ_HPD) { - rk_udphy_dp_hpd_event_trigger(udphy, false); - usleep_range(750, 800); - rk_udphy_dp_hpd_event_trigger(udphy, true); - } else if (data->status & DP_STATUS_HPD_STATE) { - rk_udphy_mode_set(udphy, mode); - rk_udphy_dp_hpd_event_trigger(udphy, true); - } else { - rk_udphy_dp_hpd_event_trigger(udphy, false); - } - } + rk_udphy_mode_set(udphy, mode); mutex_unlock(&udphy->mutex); return 0; @@ -1535,20 +1486,6 @@ static int rk_udphy_probe(struct platform_device *pdev) return 0; } -static int __maybe_unused rk_udphy_resume(struct device *dev) -{ - struct rk_udphy *udphy = dev_get_drvdata(dev); - - if (udphy->dp_sink_hpd_sel) - rk_udphy_dp_hpd_event_trigger(udphy, udphy->dp_sink_hpd_cfg); - - return 0; -} - -static const struct dev_pm_ops rk_udphy_pm_ops = { - SET_LATE_SYSTEM_SLEEP_PM_OPS(NULL, rk_udphy_resume) -}; - static const char * const rk_udphy_rst_list[] = { "init", "cmn", "lane", "pcs_apb", "pma_apb" }; @@ -1572,7 +1509,6 @@ static const struct rk_udphy_cfg rk3576_udphy_cfgs = { }, .vogrfcfg = { { - .hpd_trigger = RK_UDPHY_GEN_GRF_REG(0x0000, 11, 10, 1, 3), .dp_lane_reg = 0x0000, }, }, @@ -1613,11 +1549,9 @@ static const struct rk_udphy_cfg rk3588_udphy_cfgs = { }, .vogrfcfg = { { - .hpd_trigger = RK_UDPHY_GEN_GRF_REG(0x0000, 11, 10, 1, 3), .dp_lane_reg = 0x0000, }, { - .hpd_trigger = RK_UDPHY_GEN_GRF_REG(0x0008, 11, 10, 1, 3), .dp_lane_reg = 0x0008, }, }, @@ -1653,7 +1587,6 @@ static struct platform_driver rk_udphy_driver = { .driver = { .name = "rockchip-usbdp-phy", .of_match_table = rk_udphy_dt_match, - .pm = &rk_udphy_pm_ops, }, }; module_platform_driver(rk_udphy_driver); From 72bd3f490eceeaf264c3d2be2f2848523d820145 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 11:25:21 +0200 Subject: [PATCH 048/218] phy: rockchip: usbdp: Rename mode_change to phy_needs_reinit Right now the mode_change property is set whenever the mode changes between USB-only, DP-only and USB-DP. It is needed, because on any mode change the PHY needs to be re-initialized. Apparently at least DP also requires a re-init when the cable orientation is changed, which is currently not being done (except when the orientation switch also involves a mode change). Prepare for this by renaming mode_change to phy_needs_reinit. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 383e68b2450601..c210aaeb283a5d 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -171,7 +171,7 @@ struct rk_udphy { /* PHY status management */ bool flip; - bool mode_change; + bool phy_needs_reinit; u8 mode; u8 status; @@ -580,7 +580,7 @@ static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 mode) if (udphy->mode == mode) return; - udphy->mode_change = true; + udphy->phy_needs_reinit = true; udphy->mode = mode; } @@ -975,15 +975,15 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) if (udphy->mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); - udphy->mode_change = false; - } else if (udphy->mode_change) { + udphy->phy_needs_reinit = false; + } else if (udphy->phy_needs_reinit) { if (udphy->mode == UDPHY_MODE_DP) rk_udphy_u3_port_disable(udphy, true); ret = rk_udphy_init(udphy); if (ret) return ret; - udphy->mode_change = false; + udphy->phy_needs_reinit = false; } udphy->status |= mode; From ef0932adfd9093982f81b3a3f452c8ae706863f8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:25:32 +0200 Subject: [PATCH 049/218] phy: rockchip: usbdp: Re-init the PHY on orientation change Changing the cable orientation reconfigures the lane muxing, which requires re-initializing the PHY. Without this DP functionality breaks, if the cable is re-plugged with swapped orientation. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index c210aaeb283a5d..402f3cafcf0ae1 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -622,6 +622,7 @@ static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, enum typec_orientation orien) { struct rk_udphy *udphy = typec_switch_get_drvdata(sw); + bool flipped = orien == TYPEC_ORIENTATION_REVERSE; mutex_lock(&udphy->mutex); @@ -633,7 +634,10 @@ static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, goto unlock_ret; } - udphy->flip = orien == TYPEC_ORIENTATION_REVERSE; + if (udphy->flip != flipped) + udphy->phy_needs_reinit = true; + + udphy->flip = flipped; rk_udphy_set_typec_default_mapping(udphy); rk_udphy_usb_bvalid_enable(udphy, true); From 968c1b49999a008e03eab506b616e1e7d008bede Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:45:50 +0200 Subject: [PATCH 050/218] phy: rockchip: usbdp: Factor out lane_mux_sel setup Avoid describing the USB+DP lane_mux_sel logic twice by introducing a helper function to reduce code duplication. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 81 +++++++++++------------ 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 402f3cafcf0ae1..46ab9fb45ca4fe 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -584,6 +584,42 @@ static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 mode) udphy->mode = mode; } +static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state) +{ + u8 mode; + + switch (state) { + case TYPEC_DP_STATE_C: + case TYPEC_DP_STATE_E: + udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; + mode = UDPHY_MODE_DP; + udphy->dp_lanes = 4; + break; + + case TYPEC_DP_STATE_D: + default: + if (udphy->flip) { + udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[2] = PHY_LANE_MUX_USB; + udphy->lane_mux_sel[3] = PHY_LANE_MUX_USB; + } else { + udphy->lane_mux_sel[0] = PHY_LANE_MUX_USB; + udphy->lane_mux_sel[1] = PHY_LANE_MUX_USB; + udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; + udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; + } + mode = UDPHY_MODE_DP_USB; + udphy->dp_lanes = 2; + break; + } + + rk_udphy_mode_set(udphy, mode); +} + static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) { if (udphy->flip) { @@ -591,10 +627,6 @@ static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) udphy->dp_lane_sel[1] = 1; udphy->dp_lane_sel[2] = 3; udphy->dp_lane_sel[3] = 2; - udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[2] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[3] = PHY_LANE_MUX_USB; udphy->dp_aux_dout_sel = PHY_AUX_DP_DATA_POL_INVERT; udphy->dp_aux_din_sel = PHY_AUX_DP_DATA_POL_INVERT; gpiod_set_value_cansleep(udphy->sbu1_dc_gpio, 1); @@ -604,18 +636,14 @@ static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) udphy->dp_lane_sel[1] = 3; udphy->dp_lane_sel[2] = 1; udphy->dp_lane_sel[3] = 0; - udphy->lane_mux_sel[0] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[1] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; udphy->dp_aux_dout_sel = PHY_AUX_DP_DATA_POL_NORMAL; udphy->dp_aux_din_sel = PHY_AUX_DP_DATA_POL_NORMAL; gpiod_set_value_cansleep(udphy->sbu1_dc_gpio, 0); gpiod_set_value_cansleep(udphy->sbu2_dc_gpio, 1); } - rk_udphy_mode_set(udphy, UDPHY_MODE_DP_USB); - udphy->dp_lanes = 2; + /* default to USB3 + DP as 4 lane USB is not supported */ + rk_udphy_set_typec_state(udphy, TYPEC_DP_STATE_D); } static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, @@ -1293,7 +1321,6 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) { struct rk_udphy *udphy = typec_mux_get_drvdata(mux); - u8 mode; /* * Ignore mux events not involving DP AltMode, because @@ -1305,38 +1332,10 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, mutex_lock(&udphy->mutex); - switch (state->mode) { - case TYPEC_DP_STATE_C: - case TYPEC_DP_STATE_E: - udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; - mode = UDPHY_MODE_DP; - udphy->dp_lanes = 4; - break; - - case TYPEC_DP_STATE_D: - default: - if (udphy->flip) { - udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[2] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[3] = PHY_LANE_MUX_USB; - } else { - udphy->lane_mux_sel[0] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[1] = PHY_LANE_MUX_USB; - udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; - udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; - } - mode = UDPHY_MODE_DP_USB; - udphy->dp_lanes = 2; - break; - } - - rk_udphy_mode_set(udphy, mode); + rk_udphy_set_typec_state(udphy, state->mode); mutex_unlock(&udphy->mutex); + return 0; } From f1e7515d2512dfe5618d77cec04dfa773a441177 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 13:43:09 +0200 Subject: [PATCH 051/218] phy: rockchip: usbdp: Use guard functions for mutex Convert the driver to use guard functions for mutex handling as a small cleanup. There is a small functional change in the DP PHY power up function, which no longer sleeps if the internal powerup code returns an error. This is not a problem as the sleep is only relevant for successful power-up. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 54 ++++++++++------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 46ab9fb45ca4fe..81aae3bc574761 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -652,14 +653,15 @@ static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, struct rk_udphy *udphy = typec_switch_get_drvdata(sw); bool flipped = orien == TYPEC_ORIENTATION_REVERSE; - mutex_lock(&udphy->mutex); + guard(mutex)(&udphy->mutex); if (orien == TYPEC_ORIENTATION_NONE) { gpiod_set_value_cansleep(udphy->sbu1_dc_gpio, 0); gpiod_set_value_cansleep(udphy->sbu2_dc_gpio, 0); /* unattached */ rk_udphy_usb_bvalid_enable(udphy, false); - goto unlock_ret; + + return 0; } if (udphy->flip != flipped) @@ -669,8 +671,6 @@ static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, rk_udphy_set_typec_default_mapping(udphy); rk_udphy_usb_bvalid_enable(udphy, true); -unlock_ret: - mutex_unlock(&udphy->mutex); return 0; } @@ -1044,26 +1044,25 @@ static int rk_udphy_dp_phy_power_on(struct phy *phy) struct rk_udphy *udphy = phy_get_drvdata(phy); int ret; - mutex_lock(&udphy->mutex); + scoped_guard(mutex, &udphy->mutex) { + phy_set_bus_width(phy, udphy->dp_lanes); - phy_set_bus_width(phy, udphy->dp_lanes); - - ret = rk_udphy_power_on(udphy, UDPHY_MODE_DP); - if (ret) - goto unlock; + ret = rk_udphy_power_on(udphy, UDPHY_MODE_DP); + if (ret) + return ret; - rk_udphy_dp_lane_enable(udphy, udphy->dp_lanes); + rk_udphy_dp_lane_enable(udphy, udphy->dp_lanes); - rk_udphy_dp_lane_select(udphy); + rk_udphy_dp_lane_select(udphy); + } -unlock: - mutex_unlock(&udphy->mutex); /* * If data send by aux channel too fast after phy power on, * the aux may be not ready which will cause aux error. Adding * delay to avoid this issue. */ usleep_range(10000, 11000); + return ret; } @@ -1071,10 +1070,10 @@ static int rk_udphy_dp_phy_power_off(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); - mutex_lock(&udphy->mutex); + guard(mutex)(&udphy->mutex); + rk_udphy_dp_lane_enable(udphy, 0); rk_udphy_power_off(udphy, UDPHY_MODE_DP); - mutex_unlock(&udphy->mutex); return 0; } @@ -1279,35 +1278,30 @@ static const struct phy_ops rk_udphy_dp_phy_ops = { static int rk_udphy_usb3_phy_init(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); - int ret = 0; - mutex_lock(&udphy->mutex); + guard(mutex)(&udphy->mutex); + /* DP only or high-speed, disable U3 port */ if (!(udphy->mode & UDPHY_MODE_USB) || udphy->hs) { rk_udphy_u3_port_disable(udphy, true); - goto unlock; + return 0; } - ret = rk_udphy_power_on(udphy, UDPHY_MODE_USB); - -unlock: - mutex_unlock(&udphy->mutex); - return ret; + return rk_udphy_power_on(udphy, UDPHY_MODE_USB); } static int rk_udphy_usb3_phy_exit(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); - mutex_lock(&udphy->mutex); + guard(mutex)(&udphy->mutex); + /* DP only or high-speed */ if (!(udphy->mode & UDPHY_MODE_USB) || udphy->hs) - goto unlock; + return 0; rk_udphy_power_off(udphy, UDPHY_MODE_USB); -unlock: - mutex_unlock(&udphy->mutex); return 0; } @@ -1330,12 +1324,10 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, if (!state->alt || state->alt->svid != USB_TYPEC_DP_SID) return 0; - mutex_lock(&udphy->mutex); + guard(mutex)(&udphy->mutex); rk_udphy_set_typec_state(udphy, state->mode); - mutex_unlock(&udphy->mutex); - return 0; } From 450f5c4f7af5dfc074cac73cf0948e16ca876187 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 10 Jun 2026 21:26:48 +0200 Subject: [PATCH 052/218] phy: rockchip: usbdp: Support going from DP-only mode to USB mode When a USB-C adapter, which maps all Superspeed lanes to DP is plugged in, the USB support is disabled in the PHY. When the adapter is unplugged and a different adapter with USB functionality is plugged in afterwards, USB functionality is not restored as the USB controller keeps the PHY enabled for the entire time. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 81aae3bc574761..7f26b74cb51516 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -178,6 +178,7 @@ struct rk_udphy { /* utilized for USB */ bool hs; /* flag for high-speed */ + bool usb_in_use; /* utilized for DP */ struct gpio_desc *sbu1_dc_gpio; @@ -1015,6 +1016,10 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) ret = rk_udphy_init(udphy); if (ret) return ret; + + if (udphy->mode & UDPHY_MODE_USB) + rk_udphy_u3_port_disable(udphy, false); + udphy->phy_needs_reinit = false; } @@ -1278,6 +1283,7 @@ static const struct phy_ops rk_udphy_dp_phy_ops = { static int rk_udphy_usb3_phy_init(struct phy *phy) { struct rk_udphy *udphy = phy_get_drvdata(phy); + int ret; guard(mutex)(&udphy->mutex); @@ -1287,7 +1293,13 @@ static int rk_udphy_usb3_phy_init(struct phy *phy) return 0; } - return rk_udphy_power_on(udphy, UDPHY_MODE_USB); + ret = rk_udphy_power_on(udphy, UDPHY_MODE_USB); + if (ret) + return ret; + + udphy->usb_in_use = true; + + return 0; } static int rk_udphy_usb3_phy_exit(struct phy *phy) @@ -1296,6 +1308,8 @@ static int rk_udphy_usb3_phy_exit(struct phy *phy) guard(mutex)(&udphy->mutex); + udphy->usb_in_use = false; + /* DP only or high-speed */ if (!(udphy->mode & UDPHY_MODE_USB) || udphy->hs) return 0; @@ -1315,6 +1329,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) { struct rk_udphy *udphy = typec_mux_get_drvdata(mux); + u8 old_mode; /* * Ignore mux events not involving DP AltMode, because @@ -1326,8 +1341,20 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, guard(mutex)(&udphy->mutex); + old_mode = udphy->mode; + rk_udphy_set_typec_state(udphy, state->mode); + /* + * If the new mode includes USB but the old one didn't (e.g. leaving + * DP-only), and the USB PHY was already initialized by the USB + * controller, we need to power on the USB side now since no + * subsequent phy_init call will come from the controller. + */ + if ((udphy->mode & UDPHY_MODE_USB) && !(old_mode & UDPHY_MODE_USB) && + udphy->usb_in_use && !udphy->hs) + return rk_udphy_power_on(udphy, UDPHY_MODE_USB); + return 0; } From d9167d412369b249f682da5d9d63e7173740084e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 18 Jun 2026 19:18:48 +0200 Subject: [PATCH 053/218] phy: rockchip: usbdp: Hold mutex in DP PHY configure rk_udphy_dp_phy_configure() accesses some variables from the struct rk_udphy, which are updated independently from the USB-C framework. The USB-C mux/orientation switch functions already hold a mutex to ensure mutual exclusive access to the struct rk_udphy states, so simply hold the same one in the DP PHY configuration function. Reproducing problems due to this on real hardware would be really hard, but could be possible when quickly re-connecting the USB-C connector. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 7f26b74cb51516..8c59c50f207e02 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1183,6 +1183,8 @@ static int rk_udphy_dp_phy_configure(struct phy *phy, u32 i, val, lane; int ret; + guard(mutex)(&udphy->mutex); + if (dp->set_rate) { ret = rk_udphy_dp_phy_verify_link_rate(udphy, dp); if (ret) From 3e80ea1c2f0b70db0469e8de7040d3e6ceee7708 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 11 Jun 2026 20:07:48 +0200 Subject: [PATCH 054/218] phy: rockchip: usbdp: Add some extra debug messages It's useful to log PHY reinit to ease debugging issues around USB-C hotplugging. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 8c59c50f207e02..6e80dfeee0e5e5 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -491,6 +492,8 @@ static void rk_udphy_u3_port_disable(struct rk_udphy *udphy, u8 disable) const struct rk_udphy_cfg *cfg = udphy->cfgs; const struct rk_udphy_grf_reg *preg; + dev_dbg(udphy->dev, "USB3 port %s\n", str_on_off(!disable)); + preg = udphy->id ? &cfg->grfcfg.usb3otg1_cfg : &cfg->grfcfg.usb3otg0_cfg; rk_udphy_grfreg_write(udphy->usbgrf, preg, disable); } @@ -784,6 +787,10 @@ static int rk_udphy_init(struct rk_udphy *udphy) const struct rk_udphy_cfg *cfg = udphy->cfgs; int ret; + dev_dbg(udphy->dev, "(re-)init PHY with USB=%s and DP=%s\n", + str_enabled_disabled(udphy->mode & UDPHY_MODE_USB), + str_enabled_disabled(udphy->mode & UDPHY_MODE_DP)); + rk_udphy_reset_assert_all(udphy); usleep_range(10000, 11000); @@ -854,6 +861,8 @@ static int rk_udphy_setup(struct rk_udphy *udphy) { int ret; + dev_dbg(udphy->dev, "enable PHY\n"); + ret = clk_bulk_prepare_enable(udphy->num_clks, udphy->clks); if (ret) { dev_err(udphy->dev, "failed to enable clk\n"); @@ -872,6 +881,7 @@ static int rk_udphy_setup(struct rk_udphy *udphy) static void rk_udphy_disable(struct rk_udphy *udphy) { + dev_dbg(udphy->dev, "disable PHY\n"); clk_bulk_disable_unprepare(udphy->num_clks, udphy->clks); rk_udphy_reset_assert_all(udphy); } From 7d55197a8cd430bf29e1bf2fbc4ad83ca49b4086 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 28 Jul 2025 16:58:21 +0200 Subject: [PATCH 055/218] arm64: dts: rockchip: add USB-C DP AltMode for ROCK 5B family Enable support for USB-C DP AltMode to the ROCK 5B/5B+/5T. Signed-off-by: Sebastian Reichel --- .../dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 83 ++++++++++++++++--- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi index bb84286cb11c7f..8625f35a64e125 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi @@ -177,6 +177,22 @@ cpu-supply = <&vdd_cpu_lit_s0>; }; +&dp0 { + status = "okay"; +}; + +&dp0_in { + dp0_in_vp2: endpoint { + remote-endpoint = <&vp2_out_dp0>; + }; +}; + +&dp0_out { + dp0_out_con: endpoint { + remote-endpoint = <&usbdp_phy0_dp_in>; + }; +}; + &gpu { mali-supply = <&vdd_gpu_s0>; status = "okay"; @@ -356,21 +372,21 @@ port@0 { reg = <0>; usbc0_hs: endpoint { - remote-endpoint = <&usb_host0_xhci_to_usbc0>; + remote-endpoint = <&usb_host0_xhci_hs>; }; }; port@1 { reg = <1>; usbc0_ss: endpoint { - remote-endpoint = <&usbdp_phy0_ss>; + remote-endpoint = <&usbdp_phy0_ss_out>; }; }; port@2 { reg = <2>; usbc0_sbu: endpoint { - remote-endpoint = <&usbdp_phy0_sbu>; + remote-endpoint = <&usbdp_phy0_dp_out>; }; }; }; @@ -1006,18 +1022,41 @@ orientation-switch; status = "okay"; - port { + ports { #address-cells = <1>; #size-cells = <0>; - usbdp_phy0_ss: endpoint@0 { + + port@0 { reg = <0>; - remote-endpoint = <&usbc0_ss>; + + usbdp_phy0_ss_out: endpoint { + remote-endpoint = <&usbc0_ss>; + }; }; - usbdp_phy0_sbu: endpoint@1 { + port@1 { reg = <1>; - remote-endpoint = <&usbc0_sbu>; + + usbdp_phy0_ss_in: endpoint { + remote-endpoint = <&usb_host0_xhci_ss>; + }; + }; + + port@2 { + reg = <2>; + + usbdp_phy0_dp_in: endpoint { + remote-endpoint = <&dp0_out_con>; + }; + }; + + port@3 { + reg = <3>; + + usbdp_phy0_dp_out: endpoint { + remote-endpoint = <&usbc0_sbu>; + }; }; }; }; @@ -1038,9 +1077,24 @@ usb-role-switch; status = "okay"; - port { - usb_host0_xhci_to_usbc0: endpoint { - remote-endpoint = <&usbc0_hs>; + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + + usb_host0_xhci_hs: endpoint { + remote-endpoint = <&usbc0_hs>; + }; + }; + + port@1 { + reg = <1>; + + usb_host0_xhci_ss: endpoint { + remote-endpoint = <&usbdp_phy0_ss_in>; + }; }; }; }; @@ -1079,3 +1133,10 @@ remote-endpoint = <&hdmi1_in_vp1>; }; }; + +&vp2 { + vp2_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp2>; + }; +}; From b976a5e50a0abc5c4b155c04ac09c7f12bc53424 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:13 +0200 Subject: [PATCH 056/218] media: dt-bindings: add rockchip rk3588 vicap Add documentation for the Rockchip RK3588 Video Capture (VICAP) unit. Signed-off-by: Michael Riesch --- .../bindings/media/rockchip,rk3588-vicap.yaml | 236 ++++++++++++++++++ MAINTAINERS | 1 + 2 files changed, 237 insertions(+) create mode 100644 Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml diff --git a/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml b/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml new file mode 100644 index 00000000000000..0911a6b07d1819 --- /dev/null +++ b/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml @@ -0,0 +1,236 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/media/rockchip,rk3588-vicap.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Rockchip RK3588 Video Capture (VICAP) + +maintainers: + - Michael Riesch + +description: + The Rockchip RK3588 Video Capture (VICAP) block features a digital video + port (DVP, a parallel video interface) and six MIPI CSI-2 ports. It receives + the data from camera sensors, video decoders, or other companion ICs and + transfers it into system main memory by AXI bus and/or passes it the image + signal processing (ISP) blocks. + +properties: + compatible: + enum: + - rockchip,rk3588-vicap + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + clocks: + items: + - description: ACLK + - description: HCLK + - description: DCLK + - description: ICLK0 + - description: ICLK1 + + clock-names: + items: + - const: aclk + - const: hclk + - const: dclk + - const: iclk_host0 + - const: iclk_host1 + + iommus: + maxItems: 1 + + resets: + maxItems: 9 + + rockchip,grf: + $ref: /schemas/types.yaml#/definitions/phandle + description: Phandle to general register file used for video input block control. + + power-domains: + maxItems: 1 + + ports: + $ref: /schemas/graph.yaml#/properties/ports + + properties: + port@0: + $ref: /schemas/graph.yaml#/$defs/port-base + unevaluatedProperties: false + description: The digital video port (DVP, a parallel video interface). + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + properties: + bus-type: + enum: [5, 6] + + required: + - bus-type + + port@1: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 0 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + port@2: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 1 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + port@3: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 2 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + port@4: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 3 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + port@5: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 4 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + + port@6: + $ref: /schemas/graph.yaml#/properties/port + description: Port connected to the MIPI CSI-2 receiver 5 output. + + properties: + endpoint: + $ref: video-interfaces.yaml# + unevaluatedProperties: false + +required: + - compatible + - reg + - interrupts + - clocks + - ports + +additionalProperties: false + +examples: + - | + #include + #include + #include + #include + #include + #include + + soc { + #address-cells = <2>; + #size-cells = <2>; + + vicap: video-capture@fdce0000 { + compatible = "rockchip,rk3588-vicap"; + reg = <0x0 0xfdce0000 0x0 0x800>; + interrupts = ; + clocks = <&cru ACLK_VICAP>, <&cru HCLK_VICAP>, + <&cru DCLK_VICAP>, <&cru ICLK_CSIHOST0>, + <&cru ICLK_CSIHOST1>; + clock-names = "aclk", "hclk", "dclk", "iclk_host0", "iclk_host1"; + iommus = <&vicap_mmu>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_A_VICAP>, <&cru SRST_H_VICAP>, + <&cru SRST_D_VICAP>, <&cru SRST_CSIHOST0_VICAP>, + <&cru SRST_CSIHOST1_VICAP>, <&cru SRST_CSIHOST2_VICAP>, + <&cru SRST_CSIHOST3_VICAP>, <&cru SRST_CSIHOST4_VICAP>, + <&cru SRST_CSIHOST5_VICAP>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + vicap_dvp: port@0 { + reg = <0>; + + vicap_dvp_input: endpoint { + bus-type = ; + bus-width = <16>; + pclk-sample = ; + remote-endpoint = <&it6801_output>; + }; + }; + + vicap_mipi0: port@1 { + reg = <1>; + + vicap_mipi0_input: endpoint { + remote-endpoint = <&csi0_output>; + }; + }; + + vicap_mipi1: port@2 { + reg = <2>; + + vicap_mipi1_input: endpoint { + remote-endpoint = <&csi1_output>; + }; + }; + + vicap_mipi2: port@3 { + reg = <3>; + + vicap_mipi2_input: endpoint { + remote-endpoint = <&csi2_output>; + }; + }; + + vicap_mipi3: port@4 { + reg = <4>; + + vicap_mipi3_input: endpoint { + remote-endpoint = <&csi3_output>; + }; + }; + + vicap_mipi4: port@5 { + reg = <5>; + + vicap_mipi4_input: endpoint { + remote-endpoint = <&csi4_output>; + }; + }; + + vicap_mipi5: port@6 { + reg = <6>; + + vicap_mipi5_input: endpoint { + remote-endpoint = <&csi5_output>; + }; + }; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index c8d4b913f26c12..f7f36be94da850 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23127,6 +23127,7 @@ S: Maintained F: Documentation/admin-guide/media/rkcif* F: Documentation/devicetree/bindings/media/rockchip,px30-vip.yaml F: Documentation/devicetree/bindings/media/rockchip,rk3568-vicap.yaml +F: Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml F: drivers/media/platform/rockchip/rkcif/ ROCKCHIP CRYPTO DRIVERS From 4668afc682df54b6a088d7457bd65360ed1135e2 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:14 +0200 Subject: [PATCH 057/218] media: rockchip: rkcif: add support for rk3588 vicap The RK3588 Video Capture (VICAP) unit features a Digital Video Port (DVP) and six MIPI CSI-2 capture interfaces. Add initial support for this variant to the rkcif driver and enable the MIPI CSI-2 capture interfaces. Signed-off-by: Michael Riesch --- .../rockchip/rkcif/rkcif-capture-mipi.c | 136 ++++++++++++++++++ .../rockchip/rkcif/rkcif-capture-mipi.h | 1 + .../platform/rockchip/rkcif/rkcif-common.h | 2 +- .../media/platform/rockchip/rkcif/rkcif-dev.c | 18 +++ 4 files changed, 156 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c index 9e67160a16e468..aa70d3e9db047f 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c +++ b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c @@ -30,10 +30,18 @@ #define RK3568_MIPI_CTRL0_CROP_EN BIT(5) #define RK3568_MIPI_CTRL0_WRDDR(type) ((type) << 1) +#define RK3588_MIPI_CTRL0_DMA_EN BIT(28) +#define RK3588_MIPI_CTRL0_HIGH_ALIGN BIT(27) +#define RK3588_MIPI_CTRL0_WRDDR(type) ((type) << 5) +#define RK3588_MIPI_CTRL0_CROP_EN BIT(4) +#define RK3588_MIPI_CTRL0_PARSE(type) ((type) << 1) + #define RKCIF_MIPI_CTRL0_DT_ID(id) ((id) << 10) #define RKCIF_MIPI_CTRL0_VC_ID(id) ((id) << 8) #define RKCIF_MIPI_CTRL0_CAP_EN BIT(0) +#define RKCIF_MIPI_CTRL_CAP_EN BIT(0) + #define RKCIF_MIPI_INT_FRAME0_END(id) BIT(8 + (id) * 2 + 0) #define RKCIF_MIPI_INT_FRAME1_END(id) BIT(8 + (id) * 2 + 1) @@ -481,6 +489,132 @@ const struct rkcif_mipi_match_data rkcif_rk3568_vicap_mipi_match_data = { }, }; +static u32 +rkcif_rk3588_mipi_ctrl0(struct rkcif_stream *stream, + const struct rkcif_output_fmt *active_out_fmt) +{ + u32 ctrl0 = 0; + + ctrl0 |= RK3588_MIPI_CTRL0_DMA_EN; + ctrl0 |= RKCIF_MIPI_CTRL0_DT_ID(active_out_fmt->mipi.dt); + ctrl0 |= RK3588_MIPI_CTRL0_CROP_EN; + ctrl0 |= RKCIF_MIPI_CTRL0_CAP_EN; + + switch (active_out_fmt->mipi.type) { + case RKCIF_MIPI_TYPE_RAW8: + break; + case RKCIF_MIPI_TYPE_RAW10: + ctrl0 |= RK3588_MIPI_CTRL0_PARSE(0x1); + if (!active_out_fmt->mipi.compact) + ctrl0 |= RK3588_MIPI_CTRL0_WRDDR(0x1); + break; + case RKCIF_MIPI_TYPE_RAW12: + ctrl0 |= RK3588_MIPI_CTRL0_PARSE(0x2); + if (!active_out_fmt->mipi.compact) + ctrl0 |= RK3588_MIPI_CTRL0_WRDDR(0x1); + break; + case RKCIF_MIPI_TYPE_RGB888: + break; + case RKCIF_MIPI_TYPE_YUV422SP: + ctrl0 |= RK3588_MIPI_CTRL0_WRDDR(0x4); + break; + case RKCIF_MIPI_TYPE_YUV420SP: + ctrl0 |= RK3588_MIPI_CTRL0_WRDDR(0x5); + break; + case RKCIF_MIPI_TYPE_YUV400: + ctrl0 |= RK3588_MIPI_CTRL0_WRDDR(0x3); + break; + default: + break; + } + + return ctrl0; +} + +const struct rkcif_mipi_match_data rkcif_rk3588_vicap_mipi_match_data = { + .mipi_num = 6, + .mipi_ctrl0 = rkcif_rk3588_mipi_ctrl0, + .regs = { + [RKCIF_MIPI_CTRL] = 0x20, + [RKCIF_MIPI_INTEN] = 0x74, + [RKCIF_MIPI_INTSTAT] = 0x78, + }, + .regs_id = { + [RKCIF_ID0] = { + [RKCIF_MIPI_CTRL0] = 0x00, + [RKCIF_MIPI_CTRL1] = 0x04, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x24, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x2c, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x34, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x28, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x30, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x8c, + }, + [RKCIF_ID1] = { + [RKCIF_MIPI_CTRL0] = 0x08, + [RKCIF_MIPI_CTRL1] = 0x0c, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x38, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x40, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x48, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x3c, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x44, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x90, + }, + [RKCIF_ID2] = { + [RKCIF_MIPI_CTRL0] = 0x10, + [RKCIF_MIPI_CTRL1] = 0x14, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x4c, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x54, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x5c, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x50, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x58, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x94, + }, + [RKCIF_ID3] = { + [RKCIF_MIPI_CTRL0] = 0x18, + [RKCIF_MIPI_CTRL1] = 0x1c, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x60, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x68, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x70, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x64, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x6c, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x98, + }, + }, + .blocks = { + { + .offset = 0x100, + }, + { + .offset = 0x200, + }, + { + .offset = 0x300, + }, + { + .offset = 0x400, + }, + { + .offset = 0x500, + }, + { + .offset = 0x600, + }, + }, +}; + static inline unsigned int rkcif_mipi_get_reg(struct rkcif_interface *interface, unsigned int index) { @@ -631,6 +765,8 @@ static int rkcif_mipi_start_streaming(struct rkcif_stream *stream) rkcif_mipi_stream_write(stream, RKCIF_MIPI_CTRL1, ctrl1); rkcif_mipi_stream_write(stream, RKCIF_MIPI_CTRL0, ctrl0); + rkcif_mipi_write(interface, RKCIF_MIPI_CTRL, RKCIF_MIPI_CTRL_CAP_EN); + ret = 0; out: diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h index 7f16eadc474c3b..7edaca44f653ca 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h +++ b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h @@ -13,6 +13,7 @@ #include "rkcif-common.h" extern const struct rkcif_mipi_match_data rkcif_rk3568_vicap_mipi_match_data; +extern const struct rkcif_mipi_match_data rkcif_rk3588_vicap_mipi_match_data; int rkcif_mipi_register(struct rkcif_device *rkcif); diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-common.h b/drivers/media/platform/rockchip/rkcif/rkcif-common.h index dd92cfbc879f01..4d9211ba9bda8d 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-common.h +++ b/drivers/media/platform/rockchip/rkcif/rkcif-common.h @@ -27,7 +27,7 @@ #include "rkcif-regs.h" #define RKCIF_DRIVER_NAME "rockchip-cif" -#define RKCIF_CLK_MAX 4 +#define RKCIF_CLK_MAX 5 enum rkcif_format_type { RKCIF_FMT_TYPE_INVALID, diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-dev.c b/drivers/media/platform/rockchip/rkcif/rkcif-dev.c index b4cf1146f13118..86575d398f807f 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-dev.c +++ b/drivers/media/platform/rockchip/rkcif/rkcif-dev.c @@ -53,6 +53,20 @@ static const struct rkcif_match_data rk3568_vicap_match_data = { .mipi = &rkcif_rk3568_vicap_mipi_match_data, }; +static const char *const rk3588_vicap_clks[] = { + "aclk", + "hclk", + "dclk", + "iclk_host0", + "iclk_host1", +}; + +static const struct rkcif_match_data rk3588_vicap_match_data = { + .clks = rk3588_vicap_clks, + .clks_num = ARRAY_SIZE(rk3588_vicap_clks), + .mipi = &rkcif_rk3588_vicap_mipi_match_data, +}; + static const struct of_device_id rkcif_plat_of_match[] = { { .compatible = "rockchip,px30-vip", @@ -62,6 +76,10 @@ static const struct of_device_id rkcif_plat_of_match[] = { .compatible = "rockchip,rk3568-vicap", .data = &rk3568_vicap_match_data, }, + { + .compatible = "rockchip,rk3588-vicap", + .data = &rk3588_vicap_match_data, + }, {} }; MODULE_DEVICE_TABLE(of, rkcif_plat_of_match); From f4c7b93c73857d14b68ee3c547479b379671aec4 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 16 Feb 2026 20:16:09 +0100 Subject: [PATCH 058/218] media: dt-bindings: rockchip csi-2: Add RK3588 support Add compatible for the RK3588 CSI-2 receiver, which is compatible with the one used by RK3568. Signed-off-by: Michael Riesch [Ported over from Michael's previous patch, rebased] Signed-off-by: Sebastian Reichel --- .../bindings/media/rockchip,rk3568-mipi-csi2.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml b/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml index 4ac4a3b6f40640..aa9a41927410bb 100644 --- a/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml +++ b/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml @@ -16,9 +16,13 @@ description: properties: compatible: - enum: - - fsl,imx93-mipi-csi2 - - rockchip,rk3568-mipi-csi2 + oneOf: + - const: fsl,imx93-mipi-csi2 + - items: + - enum: + - rockchip,rk3588-mipi-csi2 + - const: rockchip,rk3568-mipi-csi2 + - const: rockchip,rk3568-mipi-csi2 reg: maxItems: 1 From b1b7c05ed3402a6525448da4e01e092451f3605e Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:15 +0200 Subject: [PATCH 059/218] arm64: dts: rockchip: add mipi csi-2 receiver node to rk356x Add the device tree node for the RK356x MIPI CSI-2 Receiver. Signed-off-by: Michael Riesch Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/rk356x-base.dtsi | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk356x-base.dtsi b/arch/arm64/boot/dts/rockchip/rk356x-base.dtsi index 64bdd8b7754b5a..adb52e31c0c3fb 100644 --- a/arch/arm64/boot/dts/rockchip/rk356x-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk356x-base.dtsi @@ -638,6 +638,36 @@ #iommu-cells = <0>; }; + csi: csi@fdfb0000 { + compatible = "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdfb0000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI2HOST1>; + phys = <&csi_dphy>; + power-domains = <&power RK3568_PD_VI>; + resets = <&cru SRST_P_CSI2HOST1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi_in: port@0 { + reg = <0>; + }; + + csi_out: port@1 { + reg = <1>; + + csi_output: endpoint { + remote-endpoint = <&vicap_mipi_input>; + }; + }; + }; + }; + vicap: video-capture@fdfe0000 { compatible = "rockchip,rk3568-vicap"; reg = <0x0 0xfdfe0000 0x0 0x200>; @@ -666,6 +696,10 @@ vicap_mipi: port@1 { reg = <1>; + + vicap_mipi_input: endpoint { + remote-endpoint = <&csi_output>; + }; }; }; }; From 203259dc9368fc06e055bb249dea0f1dfc8febf4 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:15 +0200 Subject: [PATCH 060/218] arm64: dts: rockchip: add mipi csi-2 receiver nodes to rk3588 Add the device tree nodes for the six MIPI CSI-2 Receiver units of the RK3588. Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index 30fb74303fb1f1..c24fe445b0b4dd 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1430,6 +1430,162 @@ resets = <&cru SRST_A_AV1>, <&cru SRST_P_AV1>, <&cru SRST_A_AV1_BIU>, <&cru SRST_P_AV1_BIU>; }; + csi0: csi@fdd10000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd10000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_0>; + phys = <&mipidcphy0 PHY_TYPE_DPHY>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_0>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi0_in: port@0 { + reg = <0>; + }; + + csi0_out: port@1 { + reg = <1>; + }; + }; + }; + + csi1: csi@fdd20000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd20000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_1>; + phys = <&mipidcphy1 PHY_TYPE_DPHY>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi1_in: port@0 { + reg = <0>; + }; + + csi1_out: port@1 { + reg = <1>; + }; + }; + }; + + csi2: csi@fdd30000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd30000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_2>; + phys = <&csi_dphy0>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_2>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi2_in: port@0 { + reg = <0>; + }; + + csi2_out: port@1 { + reg = <1>; + }; + }; + }; + + csi3: csi@fdd40000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd40000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_3>; + phys = <&csi_dphy0>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_3>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi3_in: port@0 { + reg = <0>; + }; + + csi3_out: port@1 { + reg = <1>; + }; + }; + }; + + csi4: csi@fdd50000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd50000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_4>; + phys = <&csi_dphy1>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_4>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi4_in: port@0 { + reg = <0>; + }; + + csi4_out: port@1 { + reg = <1>; + }; + }; + }; + + csi5: csi@fdd60000 { + compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0xfdd60000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_5>; + phys = <&csi_dphy1>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_5>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi5_in: port@0 { + reg = <0>; + }; + + csi5_out: port@1 { + reg = <1>; + }; + }; + }; + vop: vop@fdd90000 { compatible = "rockchip,rk3588-vop"; reg = <0x0 0xfdd90000 0x0 0x4200>, <0x0 0xfdd95000 0x0 0x1000>; From 1aefdee61ab8a10da76d8671c1acf91a3f4ff6d3 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:15 +0200 Subject: [PATCH 061/218] arm64: dts: rockchip: add vicap node to rk3588 Add the device tree node for the RK3588 Video Capture (VICAP) unit. Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index c24fe445b0b4dd..c02e325a9366db 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1430,6 +1430,93 @@ resets = <&cru SRST_A_AV1>, <&cru SRST_P_AV1>, <&cru SRST_A_AV1_BIU>, <&cru SRST_P_AV1_BIU>; }; + vicap: video-capture@fdce0000 { + compatible = "rockchip,rk3588-vicap"; + reg = <0x0 0xfdce0000 0x0 0x800>; + interrupts = ; + clocks = <&cru ACLK_VICAP>, <&cru HCLK_VICAP>, + <&cru DCLK_VICAP>, <&cru ICLK_CSIHOST0>, + <&cru ICLK_CSIHOST1>; + clock-names = "aclk", "hclk", "dclk", "iclk_host0", "iclk_host1"; + iommus = <&vicap_mmu>; + power-domains = <&power RK3588_PD_VI>; + resets = <&cru SRST_A_VICAP>, <&cru SRST_H_VICAP>, + <&cru SRST_D_VICAP>, <&cru SRST_CSIHOST0_VICAP>, + <&cru SRST_CSIHOST1_VICAP>, <&cru SRST_CSIHOST2_VICAP>, + <&cru SRST_CSIHOST3_VICAP>, <&cru SRST_CSIHOST4_VICAP>, + <&cru SRST_CSIHOST5_VICAP>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + vicap_dvp: port@0 { + reg = <0>; + }; + + vicap_mipi0: port@1 { + reg = <1>; + + vicap_mipi0_input: endpoint { + remote-endpoint = <&csi0_output>; + }; + }; + + vicap_mipi1: port@2 { + reg = <2>; + + vicap_mipi1_input: endpoint { + remote-endpoint = <&csi1_output>; + }; + }; + + vicap_mipi2: port@3 { + reg = <3>; + + vicap_mipi2_input: endpoint { + remote-endpoint = <&csi2_output>; + }; + }; + + vicap_mipi3: port@4 { + reg = <4>; + + vicap_mipi3_input: endpoint { + remote-endpoint = <&csi3_output>; + }; + }; + + vicap_mipi4: port@5 { + reg = <5>; + + vicap_mipi4_input: endpoint { + remote-endpoint = <&csi4_output>; + }; + }; + + vicap_mipi5: port@6 { + reg = <6>; + + vicap_mipi5_input: endpoint { + remote-endpoint = <&csi5_output>; + }; + }; + }; + }; + + vicap_mmu: iommu@fdce0800 { + compatible = "rockchip,rk3588-iommu", "rockchip,rk3568-iommu"; + reg = <0x0 0xfdce0800 0x0 0x40>, <0x0 0xfdce0900 0x0 0x40>; + interrupts = ; + clock-names = "aclk", "iface"; + clocks = <&cru ACLK_VICAP>, <&cru HCLK_VICAP>; + #iommu-cells = <0>; + power-domains = <&power RK3588_PD_VI>; + rockchip,disable-mmu-reset; + status = "disabled"; + }; + csi0: csi@fdd10000 { compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; reg = <0x0 0xfdd10000 0x0 0x10000>; @@ -1452,6 +1539,10 @@ csi0_out: port@1 { reg = <1>; + + csi0_output: endpoint { + remote-endpoint = <&vicap_mipi0_input>; + }; }; }; }; @@ -1478,6 +1569,10 @@ csi1_out: port@1 { reg = <1>; + + csi1_output: endpoint { + remote-endpoint = <&vicap_mipi1_input>; + }; }; }; }; @@ -1504,6 +1599,10 @@ csi2_out: port@1 { reg = <1>; + + csi2_output: endpoint { + remote-endpoint = <&vicap_mipi2_input>; + }; }; }; }; @@ -1530,6 +1629,10 @@ csi3_out: port@1 { reg = <1>; + + csi3_output: endpoint { + remote-endpoint = <&vicap_mipi3_input>; + }; }; }; }; @@ -1556,6 +1659,10 @@ csi4_out: port@1 { reg = <1>; + + csi4_output: endpoint { + remote-endpoint = <&vicap_mipi4_input>; + }; }; }; }; @@ -1582,6 +1689,10 @@ csi5_out: port@1 { reg = <1>; + + csi5_output: endpoint { + remote-endpoint = <&vicap_mipi5_input>; + }; }; }; }; From dee6b290e005d99e8b3c5641bd37beb183a87ca5 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:15 +0200 Subject: [PATCH 062/218] arm64: dts: rockchip: use correct pinctrl for i2c3 on rock 5b family Use the correct pinctrl for the I2C4 bus on both the Radxa ROCK 5B family in their shared base dtsi. Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi index 8625f35a64e125..611a23b5fb87cb 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi @@ -321,6 +321,8 @@ }; &i2c3 { + pinctrl-names = "default"; + pinctrl-0 = <&i2c3m0_xfer>; status = "okay"; }; From 4ea1a6875baf8c345b61b7b99cafdcf9113a188e Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:15 +0200 Subject: [PATCH 063/218] arm64: dts: rockchip: add radxa camera 4k on rock 5b+ cam0 Add device tree overlay for the Radxa Camera 4K (featuring the Sony IMX415 image sensor) to applied on the Radxa ROCK 5B+ CAM0 port. Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/Makefile | 5 ++ .../rk3588-rock-5b-plus-radxa-cam4k-cam0.dtso | 90 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam0.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index cb55c6b70d0e56..d4ff476fb9814b 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -206,6 +206,7 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-pcie-ep.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-pcie-srns.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus-radxa-cam4k-cam0.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5t.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou-video-demo.dtbo @@ -321,6 +322,10 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-pcie-srns.dtb rk3588-rock-5b-pcie-srns-dtbs := rk3588-rock-5b.dtb \ rk3588-rock-5b-pcie-srns.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus-radxa-4k-cam.dtb +rk3588-rock-5b-plus-radxa-4k-cam-dtbs := rk3588-rock-5b-plus.dtb \ + rk3588-rock-5b-plus-radxa-cam4k-cam0.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou-haikou-video-demo.dtb rk3588-tiger-haikou-haikou-video-demo-dtbs := rk3588-tiger-haikou.dtb \ rk3588-tiger-haikou-video-demo.dtbo diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam0.dtso b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam0.dtso new file mode 100644 index 00000000000000..a6d52607fa93ce --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam0.dtso @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Device tree overlay for the Radxa Camera 4K attached to the CAM0 port of + * the Radxa ROCK 5B+. + */ + +/dts-v1/; +/plugin/; + +#include +#include +#include + +&{/} { + vcc_cam0: regulator-vcc-cam0 { + compatible = "regulator-fixed"; + enable-active-high; + gpio = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>; + pinctrl-names = "default"; + pinctrl-0 = <&cam0_power0_en>; + regulator-name = "vcc_cam0"; + vin-supply = <&vcc_3v3_s3>; + }; +}; + +&i2c3 { + #address-cells = <1>; + #size-cells = <0>; + status = "okay"; + + imx415: camera-sensor@1a { + compatible = "sony,imx415"; + reg = <0x1a>; + assigned-clocks = <&cru CLK_MIPI_CAMARAOUT_M3>; + assigned-clock-rates = <37125000>; + avdd-supply = <&vcc_cam0>; + clocks = <&cru CLK_MIPI_CAMARAOUT_M3>; + clock-names = "inck"; // TODO should not be necessary according to binding, apply driver fix + dvdd-supply = <&vcc_cam0>; + orientation = <2>; /* External */ + ovdd-supply = <&vcc_cam0>; + pinctrl-names = "default"; + pinctrl-0 = <&cam0_rstn &mipim0_camera3_clk>; + reset-gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_LOW>; + + port { + imx415_output: endpoint { + data-lanes = <1 2 3 4>; + link-frequencies = /bits/ 64 <445500000>; + remote-endpoint = <&csi2_input>; + }; + }; + }; +}; + +&pinctrl { + cam0 { + cam0_power0_en: cam0-power0-en-pinctrl { + rockchip,pins = <1 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + cam0_rstn: cam0-rstn-pinctrl { + rockchip,pins = <4 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; +}; + +&csi2 { + status = "okay"; +}; + +&csi2_in { + csi2_input: endpoint { + data-lanes = <1 2 3 4>; + link-frequencies = /bits/ 64 <445500000>; + remote-endpoint = <&imx415_output>; + }; +}; + +&csi_dphy0 { + status = "okay"; +}; + +&vicap { + status = "okay"; +}; + +&vicap_mmu { + status = "okay"; +}; From d182fa0f2d224bcfba33d1db28fc173ad89cc736 Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Mon, 1 Sep 2025 23:40:16 +0200 Subject: [PATCH 064/218] arm64: dts: rockchip: add radxa camera 4k on rock 5b+ cam1 Add device tree overlay for the Radxa Camera 4K (featuring the Sony IMX415 image sensor) to applied on the Radxa ROCK 5B+ CAM1 port. Signed-off-by: Michael Riesch --- arch/arm64/boot/dts/rockchip/Makefile | 4 +- .../rk3588-rock-5b-plus-radxa-cam4k-cam1.dtso | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam1.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index d4ff476fb9814b..761d82b4f4f2ac 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -207,6 +207,7 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-pcie-ep.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-pcie-srns.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus-radxa-cam4k-cam0.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus-radxa-cam4k-cam1.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5t.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou-video-demo.dtbo @@ -324,7 +325,8 @@ rk3588-rock-5b-pcie-srns-dtbs := rk3588-rock-5b.dtb \ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-rock-5b-plus-radxa-4k-cam.dtb rk3588-rock-5b-plus-radxa-4k-cam-dtbs := rk3588-rock-5b-plus.dtb \ - rk3588-rock-5b-plus-radxa-cam4k-cam0.dtbo + rk3588-rock-5b-plus-radxa-cam4k-cam0.dtbo \ + rk3588-rock-5b-plus-radxa-cam4k-cam1.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-tiger-haikou-haikou-video-demo.dtb rk3588-tiger-haikou-haikou-video-demo-dtbs := rk3588-tiger-haikou.dtb \ diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam1.dtso b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam1.dtso new file mode 100644 index 00000000000000..d7e9d873096540 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-plus-radxa-cam4k-cam1.dtso @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Device tree overlay for the Radxa Camera 4K attached to the CAM1 port of + * the Radxa ROCK 5B+. + */ + +/dts-v1/; +/plugin/; + +#include +#include +#include + +&{/} { + vcc_cam1: regulator-vcc-cam1 { + compatible = "regulator-fixed"; + enable-active-high; + gpio = <&gpio2 RK_PA6 GPIO_ACTIVE_HIGH>; + pinctrl-names = "default"; + pinctrl-0 = <&cam1_power0_en>; + regulator-name = "vcc_cam1"; + vin-supply = <&vcc_3v3_s3>; + }; +}; + +&i2c4 { + #address-cells = <1>; + #size-cells = <0>; + status = "okay"; + + cam1_imx415: camera-sensor@1a { + compatible = "sony,imx415"; + reg = <0x1a>; + assigned-clocks = <&cru CLK_MIPI_CAMARAOUT_M4>; + assigned-clock-rates = <37125000>; + avdd-supply = <&vcc_cam1>; + clocks = <&cru CLK_MIPI_CAMARAOUT_M4>; + clock-names = "inck"; // TODO should not be necessary according to binding, apply driver fix + dvdd-supply = <&vcc_cam1>; + orientation = <2>; /* External */ + ovdd-supply = <&vcc_cam1>; + pinctrl-names = "default"; + pinctrl-0 = <&cam1_rstn &mipim0_camera4_clk>; + reset-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + + port { + cam1_imx415_output: endpoint { + data-lanes = <1 2 3 4>; + link-frequencies = /bits/ 64 <445500000>; + remote-endpoint = <&csi4_input>; + }; + }; + }; +}; + +&pinctrl { + cam1 { + cam1_power0_en: cam1-power0-en-pinctrl { + rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + cam1_rstn: cam1-rstn-pinctrl { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; +}; + +&csi4 { + status = "okay"; +}; + +&csi4_in { + csi4_input: endpoint { + data-lanes = <1 2 3 4>; + link-frequencies = /bits/ 64 <445500000>; + remote-endpoint = <&cam1_imx415_output>; + }; +}; + +&csi_dphy1 { + status = "okay"; +}; + +&vicap { + status = "okay"; +}; + +&vicap_mmu { + status = "okay"; +}; From b43b314ce747a6f24bd206f72bba17822e6a996b Mon Sep 17 00:00:00 2001 From: Michael Riesch Date: Tue, 2 Sep 2025 01:24:33 +0200 Subject: [PATCH 065/218] [HACK] disable mipi hosts that are attached to csi dcphy While the DSI part of the MIPI Combo DCPHYs is supported, the CSI side should probably not be used right now. TODO: figure out the best approach to deal with this situation. --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index c02e325a9366db..7fd45ace52590d 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1458,17 +1458,21 @@ vicap_mipi0: port@1 { reg = <1>; +/* vicap_mipi0_input: endpoint { remote-endpoint = <&csi0_output>; }; +*/ }; vicap_mipi1: port@2 { reg = <2>; +/* vicap_mipi1_input: endpoint { remote-endpoint = <&csi1_output>; }; +*/ }; vicap_mipi2: port@3 { @@ -1517,6 +1521,7 @@ status = "disabled"; }; +/* csi0: csi@fdd10000 { compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; reg = <0x0 0xfdd10000 0x0 0x10000>; @@ -1576,6 +1581,7 @@ }; }; }; +*/ csi2: csi@fdd30000 { compatible = "rockchip,rk3588-mipi-csi2", "rockchip,rk3568-mipi-csi2"; From 3f883df802dee2a800b848c22fd15a451b1ffcbd Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 8 Oct 2025 18:15:24 +0200 Subject: [PATCH 066/218] arm64: dts: rockchip: add missing UFS regulators to ROCK 4D Add missing regulator information for the ROCK 4D UFS interface. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts index a0730c50c09653..90c4c251a9537e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -920,6 +920,9 @@ }; &ufshc { + vcc-supply = <&vcc_3v3_s0>; + vccq-supply = <&vcc_1v2_ufs_vccq_s0>; + vccq2-supply = <&vcc_1v8_ufs_vccq2_s0>; status = "okay"; }; From 7c258729a7becd08048fcb8ef9c1a85f0a6dc10e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 6 Nov 2025 16:21:05 +0100 Subject: [PATCH 067/218] net: phy: realtek: re-init after reset The reset in the resume function results in the custom configuration being lost, which effectively renders the 'realtek,aldps-enable' functionality useless. Fix this up by doing a re-init as necessary. Signed-off-by: Sebastian Reichel --- drivers/net/phy/realtek/realtek_main.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/net/phy/realtek/realtek_main.c b/drivers/net/phy/realtek/realtek_main.c index c35c89941d283b..5895d2e941abc9 100644 --- a/drivers/net/phy/realtek/realtek_main.c +++ b/drivers/net/phy/realtek/realtek_main.c @@ -936,11 +936,13 @@ static int rtl8211f_suspend(struct phy_device *phydev) static int rtl821x_resume(struct phy_device *phydev) { struct rtl821x_priv *priv = phydev->priv; + bool reinit = false; int ret; if (!phydev->wol_enabled && priv->clk) { clk_prepare_enable(priv->clk); phy_reset_after_clk_enable(phydev); + reinit = true; } ret = genphy_resume(phydev); @@ -949,6 +951,9 @@ static int rtl821x_resume(struct phy_device *phydev) msleep(20); + if (reinit) + phy_init_hw(phydev); + return 0; } From 44ff3e1e0c3b3b87d25e8b1dbed4966d52821e4e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 5 Nov 2025 18:44:09 +0100 Subject: [PATCH 068/218] arm64: dts: rockchip: Enable ALDPS on both network interfaces of RK3576 EVB1 ALDPS reduces the power consumption while the network interfaces are enabled, but no cable is plugged in. The datasheet specifies: > Whole system power consumption in ALDPS low power mode (with PLL > turned off) is 10.3mW for the RTL8211F(I), and 23.1 mW for the > RTL8211FD(I). For the 3.3V line supplying the network PHYs I see the following power numbers: * both interfaces down: 239mW * both interface up, 1 cable plugged: 1005mW * both interfaces up, 1 cable plugged after this series: 995mW Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts index fb0dd1bc51482c..7081c7cdd12641 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts @@ -810,6 +810,7 @@ assigned-clock-rates = <25000000>; pinctrl-names = "default"; pinctrl-0 = <&rgmii_phy0_rst>; + realtek,aldps-enable; reset-assert-us = <20000>; reset-deassert-us = <100000>; reset-gpios = <&gpio2 RK_PB5 GPIO_ACTIVE_LOW>; @@ -825,6 +826,7 @@ assigned-clock-rates = <25000000>; pinctrl-names = "default"; pinctrl-0 = <&rgmii_phy1_rst>; + realtek,aldps-enable; reset-assert-us = <20000>; reset-deassert-us = <100000>; reset-gpios = <&gpio3 RK_PA3 GPIO_ACTIVE_LOW>; From 9c40dc86384bdf1426f298bee7d5354ca538f8a1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 10 Jan 2026 04:29:23 +0100 Subject: [PATCH 069/218] arm64: defconfig: enable Rockchip VDEC Enable the VDPU381/VDPU383 video decoder driver used for H.264 and H.265 decoding on Rockchip RK3588 and RK3576. Signed-off-by: Sebastian Reichel --- arch/arm64/configs/defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig index 96ce783f24e722..807b40374bd45a 100644 --- a/arch/arm64/configs/defconfig +++ b/arch/arm64/configs/defconfig @@ -942,6 +942,7 @@ CONFIG_VIDEO_RENESAS_FDP1=m CONFIG_VIDEO_RENESAS_VSP1=m CONFIG_VIDEO_RCAR_DRIF=m CONFIG_VIDEO_ROCKCHIP_CIF=m +CONFIG_VIDEO_ROCKCHIP_VDEC=m CONFIG_VIDEO_ROCKCHIP_RGA=m CONFIG_VIDEO_SAMSUNG_EXYNOS_GSC=m CONFIG_VIDEO_SAMSUNG_S5P_JPEG=m From e7444b2879fae4418c3cf2820af941ff00f2133d Mon Sep 17 00:00:00 2001 From: Benjamin Gaignard Date: Wed, 15 Apr 2026 09:23:38 +0200 Subject: [PATCH 070/218] dt-bindings: iommu: verisilicon: Add binding for VSI IOMMU Add a device tree binding for the Verisilicon (VSI) IOMMU. This IOMMU sits in front of hardware encoder and decoder blocks on SoCs using Verisilicon IP, such as the Rockchip RK3588. Signed-off-by: Benjamin Gaignard Reviewed-by: Conor Dooley Link: https://lore.kernel.org/r/20260415072349.44237-3-benjamin.gaignard@collabora.com Signed-off-by: Sebastian Reichel --- .../bindings/iommu/verisilicon,iommu.yaml | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Documentation/devicetree/bindings/iommu/verisilicon,iommu.yaml diff --git a/Documentation/devicetree/bindings/iommu/verisilicon,iommu.yaml b/Documentation/devicetree/bindings/iommu/verisilicon,iommu.yaml new file mode 100644 index 00000000000000..d3ce9e603b61d7 --- /dev/null +++ b/Documentation/devicetree/bindings/iommu/verisilicon,iommu.yaml @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/iommu/verisilicon,iommu.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Verisilicon IOMMU + +maintainers: + - Benjamin Gaignard + +description: |+ + A Versilicon iommu translates io virtual addresses to physical addresses for + its associated video decoder. + +properties: + compatible: + items: + - const: rockchip,rk3588-av1-iommu + - const: verisilicon,iommu-1.2 + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + + clocks: + items: + - description: Core clock + - description: Interface clock + + clock-names: + items: + - const: core + - const: iface + + "#iommu-cells": + const: 0 + + power-domains: + maxItems: 1 + +required: + - compatible + - reg + - interrupts + - clocks + - clock-names + - "#iommu-cells" + +additionalProperties: false + +examples: + - | + #include + #include + + bus { + #address-cells = <2>; + #size-cells = <2>; + + iommu@fdca0000 { + compatible = "rockchip,rk3588-av1-iommu","verisilicon,iommu-1.2"; + reg = <0x0 0xfdca0000 0x0 0x600>; + interrupts = ; + clocks = <&cru ACLK_AV1>, <&cru PCLK_AV1>; + clock-names = "core", "iface"; + #iommu-cells = <0>; + }; + }; From f9ce51487f0edf5f0fa524515248e183b51347fa Mon Sep 17 00:00:00 2001 From: Benjamin Gaignard Date: Wed, 15 Apr 2026 09:23:39 +0200 Subject: [PATCH 071/218] iommu: Add verisilicon IOMMU driver The Verisilicon IOMMU hardware block can be found in combination with Verisilicon hardware video codecs (encoders or decoders) on different SoCs. Enable it will allow us to use non contiguous memory allocators for Verisilicon video codecs. Signed-off-by: Benjamin Gaignard Link: https://lore.kernel.org/r/20260415072349.44237-4-benjamin.gaignard@collabora.com Signed-off-by: Sebastian Reichel --- MAINTAINERS | 8 + drivers/iommu/Kconfig | 11 + drivers/iommu/Makefile | 1 + drivers/iommu/vsi-iommu.c | 796 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 816 insertions(+) create mode 100644 drivers/iommu/vsi-iommu.c diff --git a/MAINTAINERS b/MAINTAINERS index f7f36be94da850..4f5f21080fddea 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -27959,6 +27959,14 @@ F: drivers/media/v4l2-core/v4l2-isp.c F: include/media/v4l2-isp.h F: include/uapi/linux/media/v4l2-isp.h +VERISILICON IOMMU DRIVER +M: Benjamin Gaignard +L: iommu@lists.linux.dev +S: Maintained +F: Documentation/devicetree/bindings/iommu/verisilicon,iommu.yaml +F: drivers/iommu/vsi-iommu.c +F: include/linux/vsi-iommu.h + VF610 NAND DRIVER M: Stefan Agner L: linux-mtd@lists.infradead.org diff --git a/drivers/iommu/Kconfig b/drivers/iommu/Kconfig index f86262b11416d1..18d3d68af7cdfd 100644 --- a/drivers/iommu/Kconfig +++ b/drivers/iommu/Kconfig @@ -384,6 +384,17 @@ config SPRD_IOMMU Say Y here if you want to use the multimedia devices listed above. +config VSI_IOMMU + tristate "Verisilicon IOMMU Support" + depends on (ARCH_ROCKCHIP && ARM64) || COMPILE_TEST + select IOMMU_API + help + Support for IOMMUs used by Verisilicon sub-systems like video + decoders or encoder hardware blocks. + + Say Y here if you want to use this IOMMU in front of these + hardware blocks. + config IOMMU_DEBUG_PAGEALLOC bool "Debug IOMMU mappings against page allocations" depends on DEBUG_PAGEALLOC && IOMMU_API && PAGE_EXTENSION diff --git a/drivers/iommu/Makefile b/drivers/iommu/Makefile index 0275821f4ef985..887af357a7c963 100644 --- a/drivers/iommu/Makefile +++ b/drivers/iommu/Makefile @@ -36,4 +36,5 @@ obj-$(CONFIG_IOMMU_SVA) += iommu-sva.o obj-$(CONFIG_IOMMU_IOPF) += io-pgfault.o obj-$(CONFIG_SPRD_IOMMU) += sprd-iommu.o obj-$(CONFIG_APPLE_DART) += apple-dart.o +obj-$(CONFIG_VSI_IOMMU) += vsi-iommu.o obj-$(CONFIG_IOMMU_DEBUG_PAGEALLOC) += iommu-debug-pagealloc.o diff --git a/drivers/iommu/vsi-iommu.c b/drivers/iommu/vsi-iommu.c new file mode 100644 index 00000000000000..5d0721bd2c7af8 --- /dev/null +++ b/drivers/iommu/vsi-iommu.c @@ -0,0 +1,796 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (C) 2025 Collabora Ltd. + * + * IOMMU API for Verisilicon + * + * Module Authors: Yandong Lin + * Simon Xue + * Benjamin Gaignard + * + * This hardware block is using a 2 pages tables allocation structure. + * That make very similar to Rockhip iommu hardware blocks but it has + * it own driver because the registers offset and configuration bits + * are completely different. An additional reason is that this hardware + * has been developed by Verisilicon to be used by their hardware video + * decoders and not for a general purpose like Rockchip iommus. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "iommu-pages.h" + +struct vsi_iommu { + struct device *dev; + void __iomem *regs; + struct clk_bulk_data *clocks; + int num_clocks; + struct iommu_device iommu; + struct list_head node; /* entry in vsi_iommu_domain.iommus */ + struct iommu_domain *domain; /* domain to which iommu is attached */ + spinlock_t lock; /* lock to protect vsi_iommu fields */ + int irq; + bool enable; +}; + +struct vsi_iommu_domain { + struct list_head iommus; + struct device *dev; + u32 *dt; + dma_addr_t dt_dma; + struct iommu_domain domain; + u64 *pta; + dma_addr_t pta_dma; + spinlock_t lock; /* lock to protect vsi_iommu_domain fields */ +}; + +static struct iommu_domain vsi_identity_domain; + +#define NUM_DT_ENTRIES 1024 +#define NUM_PT_ENTRIES 1024 + +#define SPAGE_SIZE BIT(12) + +/* vsi iommu regs address */ +#define VSI_MMU_CONFIG1_BASE 0x1ac +#define VSI_MMU_AHB_EXCEPTION_BASE 0x380 +#define VSI_MMU_AHB_CONTROL_BASE 0x388 +#define VSI_MMU_AHB_TLB_ARRAY_BASE_L_BASE 0x38C + +/* MMU register offsets */ +#define VSI_MMU_FLUSH_BASE 0x184 +#define VSI_MMU_BIT_FLUSH BIT(4) + +#define VSI_MMU_PAGE_FAULT_ADDR 0x380 +#define VSI_MMU_STATUS_BASE 0x384 /* IRQ status */ + +#define VSI_MMU_BIT_ENABLE BIT(0) + +#define VSI_MMU_OUT_OF_BOUND BIT(28) +/* Irq mask */ +#define VSI_MMU_IRQ_MASK 0x7 + +#define VSI_DTE_PT_ADDRESS_MASK 0xffffffc0 +#define VSI_DTE_PT_VALID BIT(0) + +#define VSI_PAGE_DESC_LO_MASK 0xfffff000 +#define VSI_PAGE_DESC_HI_MASK GENMASK_ULL(39, 32) +#define VSI_PAGE_DESC_HI_SHIFT (32 - 4) + +static inline phys_addr_t vsi_dte_pt_address(u32 dte) +{ + return (phys_addr_t)dte & VSI_DTE_PT_ADDRESS_MASK; +} + +static inline u32 vsi_mk_dte(u32 dte) +{ + return (phys_addr_t)dte | VSI_DTE_PT_VALID; +} + +#define VSI_PTE_PAGE_WRITABLE BIT(2) +#define VSI_PTE_PAGE_VALID BIT(0) + +static inline phys_addr_t vsi_pte_page_address(u64 pte) +{ + return ((pte << VSI_PAGE_DESC_HI_SHIFT) & VSI_PAGE_DESC_HI_MASK) | + (pte & VSI_PAGE_DESC_LO_MASK); +} + +static u32 vsi_mk_pte(phys_addr_t page, int prot) +{ + u32 flags = 0; + + flags |= (prot & IOMMU_WRITE) ? VSI_PTE_PAGE_WRITABLE : 0; + + page = (page & VSI_PAGE_DESC_LO_MASK) | + ((page & VSI_PAGE_DESC_HI_MASK) >> VSI_PAGE_DESC_HI_SHIFT); + + return page | flags | VSI_PTE_PAGE_VALID; +} + +#define VSI_DTE_PT_VALID BIT(0) + +static inline bool vsi_dte_is_pt_valid(u32 dte) +{ + return dte & VSI_DTE_PT_VALID; +} + +static inline bool vsi_pte_is_page_valid(u32 pte) +{ + return pte & VSI_PTE_PAGE_VALID; +} + +static u32 vsi_mk_pte_invalid(u32 pte) +{ + return pte & ~VSI_PTE_PAGE_VALID; +} + +#define VSI_MASTER_TLB_MASK GENMASK_ULL(31, 10) +/* mode 0 : 4k */ +#define VSI_PTA_4K_MODE 0 + +static u64 vsi_mk_pta(dma_addr_t dt_dma) +{ + u64 val = (dt_dma & VSI_MASTER_TLB_MASK) | VSI_PTA_4K_MODE; + + return val; +} + +static struct vsi_iommu_domain *to_vsi_domain(struct iommu_domain *dom) +{ + return container_of(dom, struct vsi_iommu_domain, domain); +} + +static inline void vsi_table_flush(struct vsi_iommu_domain *vsi_domain, dma_addr_t dma, + unsigned int count) +{ + size_t size = count * sizeof(u32); /* count of u32 entry */ + + dma_sync_single_for_device(vsi_domain->dev, dma, size, DMA_TO_DEVICE); +} + +#define VSI_IOVA_DTE_MASK 0xffc00000 +#define VSI_IOVA_DTE_SHIFT 22 +#define VSI_IOVA_PTE_MASK 0x003ff000 +#define VSI_IOVA_PTE_SHIFT 12 +#define VSI_IOVA_PAGE_MASK 0x00000fff +#define VSI_IOVA_PAGE_SHIFT 0 + +static u32 vsi_iova_dte_index(u32 iova) +{ + return (iova & VSI_IOVA_DTE_MASK) >> VSI_IOVA_DTE_SHIFT; +} + +static u32 vsi_iova_pte_index(u32 iova) +{ + return (iova & VSI_IOVA_PTE_MASK) >> VSI_IOVA_PTE_SHIFT; +} + +static u32 vsi_iova_page_offset(u32 iova) +{ + return (iova & VSI_IOVA_PAGE_MASK) >> VSI_IOVA_PAGE_SHIFT; +} + +static irqreturn_t vsi_iommu_irq(int irq, void *dev_id) +{ + struct vsi_iommu *iommu = dev_id; + unsigned long flags; + dma_addr_t iova; + u32 status; + + if (pm_runtime_resume_and_get(iommu->dev) < 0) + return IRQ_NONE; + + spin_lock_irqsave(&iommu->lock, flags); + + status = readl(iommu->regs + VSI_MMU_STATUS_BASE); + if (status & VSI_MMU_IRQ_MASK) { + dev_err(iommu->dev, "unexpected int_status=%08x\n", status); + iova = readl(iommu->regs + VSI_MMU_PAGE_FAULT_ADDR); + report_iommu_fault(iommu->domain, iommu->dev, iova, status); + } + writel(0, iommu->regs + VSI_MMU_STATUS_BASE); + + spin_unlock_irqrestore(&iommu->lock, flags); + pm_runtime_put_autosuspend(iommu->dev); + + return IRQ_HANDLED; +} + +static struct vsi_iommu *vsi_iommu_get_from_dev(struct device *dev) +{ + struct iommu_fwspec *fwspec = dev_iommu_fwspec_get(dev); + struct device *iommu_dev = bus_find_device_by_fwnode(&platform_bus_type, + fwspec->iommu_fwnode); + + put_device(iommu_dev); + + return iommu_dev ? dev_get_drvdata(iommu_dev) : NULL; +} + +static struct iommu_domain *vsi_iommu_domain_alloc_paging(struct device *dev) +{ + struct vsi_iommu *iommu = dev_iommu_priv_get(dev); + struct vsi_iommu_domain *vsi_domain; + + vsi_domain = kzalloc(sizeof(*vsi_domain), GFP_KERNEL); + if (!vsi_domain) + return NULL; + + vsi_domain->dev = iommu->dev; + spin_lock_init(&vsi_domain->lock); + + /* + * iommu use a 2 level pagetable. + * Each level1 (dt) and level2 (pt) table has 1024 4-byte entries. + * Allocate one 4 KiB page for each table. + */ + vsi_domain->dt = iommu_alloc_pages_sz(GFP_KERNEL | GFP_DMA32, + SPAGE_SIZE); + if (!vsi_domain->dt) + goto err_free_domain; + + vsi_domain->dt_dma = dma_map_single(vsi_domain->dev, vsi_domain->dt, + SPAGE_SIZE, DMA_TO_DEVICE); + if (dma_mapping_error(vsi_domain->dev, vsi_domain->dt_dma)) { + dev_err(dev, "DMA map error for DT\n"); + goto err_free_dt; + } + + vsi_domain->pta = iommu_alloc_pages_sz(GFP_KERNEL | GFP_DMA32, + SPAGE_SIZE); + if (!vsi_domain->pta) + goto err_unmap_dt; + + vsi_domain->pta[0] = vsi_mk_pta(vsi_domain->dt_dma); + vsi_domain->pta_dma = dma_map_single(vsi_domain->dev, vsi_domain->pta, + SPAGE_SIZE, DMA_TO_DEVICE); + if (dma_mapping_error(vsi_domain->dev, vsi_domain->pta_dma)) { + dev_err(dev, "DMA map error for PTA\n"); + goto err_free_pta; + } + + INIT_LIST_HEAD(&vsi_domain->iommus); + + vsi_domain->domain.geometry.aperture_start = 0; + vsi_domain->domain.geometry.aperture_end = DMA_BIT_MASK(32); + vsi_domain->domain.geometry.force_aperture = true; + vsi_domain->domain.pgsize_bitmap = SZ_4K; + + return &vsi_domain->domain; + +err_free_pta: + iommu_free_pages(vsi_domain->pta); +err_unmap_dt: + dma_unmap_single(vsi_domain->dev, vsi_domain->dt_dma, + SPAGE_SIZE, DMA_TO_DEVICE); +err_free_dt: + iommu_free_pages(vsi_domain->dt); +err_free_domain: + kfree(vsi_domain); + + return NULL; +} + +static phys_addr_t vsi_iommu_iova_to_phys(struct iommu_domain *domain, + dma_addr_t iova) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + phys_addr_t pt_phys, phys = 0; + unsigned long flags; + u32 dte, pte; + u32 *page_table; + + spin_lock_irqsave(&vsi_domain->lock, flags); + dte = vsi_domain->dt[vsi_iova_dte_index(iova)]; + if (!vsi_dte_is_pt_valid(dte)) + goto unlock; + + pt_phys = vsi_dte_pt_address(dte); + page_table = (u32 *)phys_to_virt(pt_phys); + pte = page_table[vsi_iova_pte_index(iova)]; + if (!vsi_pte_is_page_valid(pte)) + goto unlock; + + phys = vsi_pte_page_address(pte) + vsi_iova_page_offset(iova); + +unlock: + spin_unlock_irqrestore(&vsi_domain->lock, flags); + return phys; +} + +static size_t vsi_iommu_unmap_iova(struct vsi_iommu_domain *vsi_domain, + u32 *pte_addr, dma_addr_t pte_dma, + size_t size) +{ + unsigned int pte_count; + unsigned int pte_total = size / SPAGE_SIZE; + + for (pte_count = 0; + pte_count < pte_total && pte_count < NUM_PT_ENTRIES; pte_count++) { + u32 pte = pte_addr[pte_count]; + + if (!vsi_pte_is_page_valid(pte)) + break; + + pte_addr[pte_count] = vsi_mk_pte_invalid(pte); + } + + vsi_table_flush(vsi_domain, pte_dma, pte_total); + + return pte_count * SPAGE_SIZE; +} + +static int vsi_iommu_map_iova(struct vsi_iommu_domain *vsi_domain, u32 *pte_addr, + dma_addr_t pte_dma, dma_addr_t iova, + phys_addr_t paddr, size_t size, int prot) +{ + unsigned int pte_count; + unsigned int pte_total = size / SPAGE_SIZE; + + for (pte_count = 0; + pte_count < pte_total && pte_count < NUM_PT_ENTRIES; pte_count++) { + u32 pte = pte_addr[pte_count]; + + if (vsi_pte_is_page_valid(pte)) + return (pte_count - 1) * SPAGE_SIZE; + + pte_addr[pte_count] = vsi_mk_pte(paddr, prot); + + paddr += SPAGE_SIZE; + } + + vsi_table_flush(vsi_domain, pte_dma, pte_total); + + return 0; +} + +static void vsi_iommu_flush_tlb(struct iommu_domain *domain) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + struct list_head *pos; + + list_for_each(pos, &vsi_domain->iommus) { + struct vsi_iommu *iommu; + + iommu = list_entry(pos, struct vsi_iommu, node); + if (!iommu) + continue; + + if (pm_runtime_get(iommu->dev) < 0) + continue; + + spin_lock(&iommu->lock); + + if (iommu->enable) { + writel(VSI_MMU_BIT_FLUSH, iommu->regs + VSI_MMU_FLUSH_BASE); + writel(0, iommu->regs + VSI_MMU_FLUSH_BASE); + } + + spin_unlock(&iommu->lock); + + pm_runtime_put_autosuspend(iommu->dev); + } +} + +static size_t vsi_iommu_unmap(struct iommu_domain *domain, unsigned long _iova, + size_t size, size_t count, struct iommu_iotlb_gather *gather) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + dma_addr_t pte_dma, iova = (dma_addr_t)_iova; + unsigned long flags; + phys_addr_t pt_phys; + u32 dte; + u32 *pte_addr; + size_t unmap_size = 0; + + spin_lock_irqsave(&vsi_domain->lock, flags); + + dte = vsi_domain->dt[vsi_iova_dte_index(iova)]; + /* Just return 0 if iova is unmapped */ + if (!vsi_dte_is_pt_valid(dte)) + goto unlock; + + pt_phys = vsi_dte_pt_address(dte); + pte_addr = (u32 *)phys_to_virt(pt_phys) + vsi_iova_pte_index(iova); + pte_dma = pt_phys + vsi_iova_pte_index(iova) * sizeof(u32); + unmap_size = vsi_iommu_unmap_iova(vsi_domain, pte_addr, pte_dma, size); + if (!unmap_size) + goto unlock; + + vsi_iommu_flush_tlb(domain); +unlock: + spin_unlock_irqrestore(&vsi_domain->lock, flags); + + return unmap_size; +} + +static u32 *vsi_dte_get_page_table(struct vsi_iommu_domain *vsi_domain, + dma_addr_t iova, gfp_t gfp) +{ + u32 *page_table, *dte_addr; + u32 dte_index, dte; + phys_addr_t pt_phys; + dma_addr_t pt_dma; + gfp_t flags; + + dte_index = vsi_iova_dte_index(iova); + dte_addr = &vsi_domain->dt[dte_index]; + dte = *dte_addr; + if (vsi_dte_is_pt_valid(dte)) + goto done; + + /* Do not allow to sleep while allocating the buffer */ + flags = (gfp & ~GFP_KERNEL) | GFP_ATOMIC | GFP_DMA32; + page_table = iommu_alloc_pages_sz(flags, PAGE_SIZE); + if (!page_table) + return ERR_PTR(-ENOMEM); + + pt_dma = dma_map_single(vsi_domain->dev, page_table, PAGE_SIZE, DMA_TO_DEVICE); + if (dma_mapping_error(vsi_domain->dev, pt_dma)) { + dev_err(vsi_domain->dev, "DMA mapping error while allocating page table\n"); + iommu_free_pages(page_table); + return ERR_PTR(-ENOMEM); + } + + dte = vsi_mk_dte(pt_dma); + *dte_addr = dte; + + vsi_table_flush(vsi_domain, + vsi_domain->dt_dma + dte_index * sizeof(u32), 1); +done: + pt_phys = vsi_dte_pt_address(dte); + return (u32 *)phys_to_virt(pt_phys); +} + +static int vsi_iommu_map(struct iommu_domain *domain, unsigned long _iova, + phys_addr_t paddr, size_t size, size_t count, + int prot, gfp_t gfp, size_t *mapped) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + dma_addr_t pte_dma, iova = (dma_addr_t)_iova; + u32 *page_table, *pte_addr; + u32 dte, pte_index; + unsigned long flags; + int ret; + + spin_lock_irqsave(&vsi_domain->lock, flags); + + page_table = vsi_dte_get_page_table(vsi_domain, iova, gfp); + if (IS_ERR(page_table)) { + spin_unlock_irqrestore(&vsi_domain->lock, flags); + return PTR_ERR(page_table); + } + + dte = vsi_domain->dt[vsi_iova_dte_index(iova)]; + pte_index = vsi_iova_pte_index(iova); + pte_addr = &page_table[pte_index]; + pte_dma = vsi_dte_pt_address(dte) + pte_index * sizeof(u32); + ret = vsi_iommu_map_iova(vsi_domain, pte_addr, pte_dma, iova, + paddr, size, prot); + if (!ret) + *mapped = size; + + vsi_iommu_flush_tlb(domain); + + spin_unlock_irqrestore(&vsi_domain->lock, flags); + + return ret; +} + +static void vsi_iommu_disable(struct vsi_iommu *iommu) +{ + writel(0, iommu->regs + VSI_MMU_AHB_CONTROL_BASE); + iommu->enable = false; +} + +static int vsi_iommu_identity_attach(struct iommu_domain *domain, + struct device *dev, struct iommu_domain *old) +{ + struct vsi_iommu *iommu = dev_iommu_priv_get(dev); + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + unsigned long flags; + int ret; + + ret = pm_runtime_resume_and_get(iommu->dev); + if (ret < 0) + return ret; + + spin_lock_irqsave(&vsi_domain->lock, flags); + spin_lock(&iommu->lock); + if (iommu->domain == domain) + goto unlock; + + vsi_iommu_disable(iommu); + list_del_init(&iommu->node); + + iommu->domain = domain; + +unlock: + spin_unlock(&iommu->lock); + spin_unlock_irqrestore(&vsi_domain->lock, flags); + pm_runtime_put_autosuspend(iommu->dev); + return 0; +} + +static const struct iommu_domain_ops vsi_identity_ops = { + .attach_dev = vsi_iommu_identity_attach, +}; + +static struct iommu_domain vsi_identity_domain = { + .type = IOMMU_DOMAIN_IDENTITY, + .ops = &vsi_identity_ops, +}; + +static void vsi_iommu_enable(struct vsi_iommu *iommu, struct iommu_domain *domain) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + + if (domain == &vsi_identity_domain) + return; + + writel(vsi_domain->pta_dma, iommu->regs + VSI_MMU_AHB_TLB_ARRAY_BASE_L_BASE); + writel(VSI_MMU_OUT_OF_BOUND, iommu->regs + VSI_MMU_CONFIG1_BASE); + writel(VSI_MMU_BIT_ENABLE, iommu->regs + VSI_MMU_AHB_EXCEPTION_BASE); + writel(VSI_MMU_BIT_ENABLE, iommu->regs + VSI_MMU_AHB_CONTROL_BASE); + iommu->enable = true; +} + +static int vsi_iommu_attach_device(struct iommu_domain *domain, + struct device *dev, struct iommu_domain *old) +{ + struct vsi_iommu *iommu = dev_iommu_priv_get(dev); + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + unsigned long flags; + int ret = 0; + + ret = pm_runtime_resume_and_get(iommu->dev); + if (ret < 0) + return ret; + + spin_lock_irqsave(&vsi_domain->lock, flags); + spin_lock(&iommu->lock); + + vsi_iommu_enable(iommu, domain); + writel(VSI_MMU_BIT_FLUSH, iommu->regs + VSI_MMU_FLUSH_BASE); + writel(0, iommu->regs + VSI_MMU_FLUSH_BASE); + + list_del_init(&iommu->node); + list_add_tail(&iommu->node, &vsi_domain->iommus); + + iommu->domain = domain; + + spin_unlock(&iommu->lock); + spin_unlock_irqrestore(&vsi_domain->lock, flags); + pm_runtime_put_autosuspend(iommu->dev); + return ret; +} + +static void vsi_iommu_domain_free(struct iommu_domain *domain) +{ + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(domain); + unsigned long flags; + int i; + + spin_lock_irqsave(&vsi_domain->lock, flags); + + WARN_ON(!list_empty(&vsi_domain->iommus)); + + for (i = 0; i < NUM_DT_ENTRIES; i++) { + u32 dte = vsi_domain->dt[i]; + + if (vsi_dte_is_pt_valid(dte)) { + phys_addr_t pt_phys = vsi_dte_pt_address(dte); + u32 *page_table = phys_to_virt(pt_phys); + + dma_unmap_single(vsi_domain->dev, pt_phys, + SPAGE_SIZE, DMA_TO_DEVICE); + iommu_free_pages(page_table); + } + } + + dma_unmap_single(vsi_domain->dev, vsi_domain->dt_dma, + SPAGE_SIZE, DMA_TO_DEVICE); + iommu_free_pages(vsi_domain->dt); + + dma_unmap_single(vsi_domain->dev, vsi_domain->pta_dma, + SPAGE_SIZE, DMA_TO_DEVICE); + iommu_free_pages(vsi_domain->pta); + + spin_unlock_irqrestore(&vsi_domain->lock, flags); + + kfree(vsi_domain); +} + +static struct iommu_device *vsi_iommu_probe_device(struct device *dev) +{ + struct vsi_iommu *iommu = vsi_iommu_get_from_dev(dev); + struct device_link *link; + + link = device_link_add(dev, iommu->dev, + DL_FLAG_STATELESS | DL_FLAG_PM_RUNTIME); + if (!link) + dev_err(dev, "Unable to link %s\n", dev_name(iommu->dev)); + + dev_iommu_priv_set(dev, iommu); + return &iommu->iommu; +} + +static void vsi_iommu_release_device(struct device *dev) +{ + struct vsi_iommu *iommu = dev_iommu_priv_get(dev); + + device_link_remove(dev, iommu->dev); +} + +static int vsi_iommu_of_xlate(struct device *dev, const struct of_phandle_args *args) +{ + return iommu_fwspec_add_ids(dev, args->args, 1); +} + +static const struct iommu_ops vsi_iommu_ops = { + .identity_domain = &vsi_identity_domain, + .release_domain = &vsi_identity_domain, + .domain_alloc_paging = vsi_iommu_domain_alloc_paging, + .of_xlate = vsi_iommu_of_xlate, + .probe_device = vsi_iommu_probe_device, + .release_device = vsi_iommu_release_device, + .device_group = generic_single_device_group, + .owner = THIS_MODULE, + .default_domain_ops = &(const struct iommu_domain_ops) { + .attach_dev = vsi_iommu_attach_device, + .map_pages = vsi_iommu_map, + .unmap_pages = vsi_iommu_unmap, + .iova_to_phys = vsi_iommu_iova_to_phys, + .free = vsi_iommu_domain_free, + } +}; + +static const struct of_device_id vsi_iommu_dt_ids[] = { + { + .compatible = "verisilicon,iommu-1.2", + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, vsi_iommu_dt_ids); + +static int vsi_iommu_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct vsi_iommu *iommu; + int err; + + iommu = devm_kzalloc(dev, sizeof(*iommu), GFP_KERNEL); + if (!iommu) + return -ENOMEM; + + iommu->dev = dev; + spin_lock_init(&iommu->lock); + INIT_LIST_HEAD(&iommu->node); + + iommu->regs = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(iommu->regs)) + return -ENOMEM; + + iommu->num_clocks = devm_clk_bulk_get_all(dev, &iommu->clocks); + if (iommu->num_clocks < 0) + return iommu->num_clocks; + + err = clk_bulk_prepare(iommu->num_clocks, iommu->clocks); + if (err) + return err; + + iommu->irq = platform_get_irq(pdev, 0); + if (iommu->irq < 0) + return iommu->irq; + + err = devm_request_irq(iommu->dev, iommu->irq, vsi_iommu_irq, + IRQF_SHARED, dev_name(dev), iommu); + if (err) + goto err_unprepare_clocks; + + dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)); + platform_set_drvdata(pdev, iommu); + + pm_runtime_set_autosuspend_delay(dev, 100); + pm_runtime_use_autosuspend(dev); + pm_runtime_enable(dev); + + err = iommu_device_sysfs_add(&iommu->iommu, dev, NULL, dev_name(dev)); + if (err) + goto err_runtime_disable; + + err = iommu_device_register(&iommu->iommu, &vsi_iommu_ops, dev); + if (err) + goto err_remove_sysfs; + + return 0; + +err_remove_sysfs: + iommu_device_sysfs_remove(&iommu->iommu); +err_runtime_disable: + pm_runtime_disable(dev); +err_unprepare_clocks: + clk_bulk_unprepare(iommu->num_clocks, iommu->clocks); + return err; +} + +static void vsi_iommu_shutdown(struct platform_device *pdev) +{ + struct vsi_iommu *iommu = platform_get_drvdata(pdev); + + disable_irq(iommu->irq); + pm_runtime_force_suspend(&pdev->dev); +} + +static int __maybe_unused vsi_iommu_suspend(struct device *dev) +{ + struct vsi_iommu *iommu = dev_get_drvdata(dev); + + vsi_iommu_disable(iommu); + + clk_bulk_disable(iommu->num_clocks, iommu->clocks); + + return 0; +} + +static int __maybe_unused vsi_iommu_resume(struct device *dev) +{ + struct vsi_iommu *iommu = dev_get_drvdata(dev); + unsigned long flags; + int ret; + + ret = clk_bulk_enable(iommu->num_clocks, iommu->clocks); + if (ret) + return ret; + + if (iommu->domain) { + struct vsi_iommu_domain *vsi_domain = to_vsi_domain(iommu->domain); + + spin_lock_irqsave(&vsi_domain->lock, flags); + spin_lock(&iommu->lock); + vsi_iommu_enable(iommu, iommu->domain); + spin_unlock(&iommu->lock); + spin_unlock_irqrestore(&vsi_domain->lock, flags); + } + + return 0; +} + +static DEFINE_RUNTIME_DEV_PM_OPS(vsi_iommu_pm_ops, + vsi_iommu_suspend, vsi_iommu_resume, + NULL); + +static struct platform_driver rockchip_vsi_iommu_driver = { + .probe = vsi_iommu_probe, + .shutdown = vsi_iommu_shutdown, + .driver = { + .name = "vsi_iommu", + .of_match_table = vsi_iommu_dt_ids, + .pm = pm_sleep_ptr(&vsi_iommu_pm_ops), + .suppress_bind_attrs = true, + }, +}; +module_platform_driver(rockchip_vsi_iommu_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Benjamin Gaignard "); +MODULE_DESCRIPTION("Verisilicon IOMMU driver"); From c097e0fe7b902da0d955cebedb7ab952ce9fef0e Mon Sep 17 00:00:00 2001 From: Benjamin Gaignard Date: Wed, 15 Apr 2026 09:23:40 +0200 Subject: [PATCH 072/218] arm64: dts: rockchip: Add verisilicon IOMMU node on RK3588 Add the device tree node for the Verisilicon IOMMU present in the RK3588 SoC. This IOMMU handles address translation for the VPU hardware blocks. Signed-off-by: Benjamin Gaignard Link: https://lore.kernel.org/r/20260415072349.44237-5-benjamin.gaignard@collabora.com Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index 7fd45ace52590d..e8855b26267d88 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1428,6 +1428,17 @@ clock-names = "aclk", "hclk"; power-domains = <&power RK3588_PD_AV1>; resets = <&cru SRST_A_AV1>, <&cru SRST_P_AV1>, <&cru SRST_A_AV1_BIU>, <&cru SRST_P_AV1_BIU>; + iommus = <&av1d_mmu>; + }; + + av1d_mmu: iommu@fdca0000 { + compatible = "rockchip,rk3588-av1-iommu", "verisilicon,iommu-1.2"; + reg = <0x0 0xfdca0000 0x0 0x600>; + interrupts = ; + clocks = <&cru ACLK_AV1>, <&cru PCLK_AV1>; + clock-names = "core", "iface"; + #iommu-cells = <0>; + power-domains = <&power RK3588_PD_AV1>; }; vicap: video-capture@fdce0000 { From cfa45f907e6e910ee4d95c7f537f72a08af5f437 Mon Sep 17 00:00:00 2001 From: Benjamin Gaignard Date: Wed, 15 Apr 2026 09:23:41 +0200 Subject: [PATCH 073/218] arm64: defconfig: enable Verisilicon IOMMU for Rockchip RK3588 Enable Verisilicon IOMMU used by Rockchip RK3588 AV1 hardware codec. This hardware block could be found in Radxa ROCK 5B board. Signed-off-by: Benjamin Gaignard Reviewed-by: Krzysztof Kozlowski Link: https://lore.kernel.org/r/20260415072349.44237-6-benjamin.gaignard@collabora.com Signed-off-by: Sebastian Reichel --- arch/arm64/configs/defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig index 807b40374bd45a..ba49c6abec9fa2 100644 --- a/arch/arm64/configs/defconfig +++ b/arch/arm64/configs/defconfig @@ -1626,6 +1626,7 @@ CONFIG_ARM_SMMU_V3=y CONFIG_MTK_IOMMU=y CONFIG_QCOM_IOMMU=y CONFIG_APPLE_DART=m +CONFIG_VSI_IOMMU=m CONFIG_REMOTEPROC=y CONFIG_IMX_REMOTEPROC=y CONFIG_MTK_SCP=m From 55cb3a4eafce43e184f313ed3b7617752cacf126 Mon Sep 17 00:00:00 2001 From: Andy Yan Date: Fri, 9 Jan 2026 18:01:19 +0800 Subject: [PATCH 074/218] [WIP] arm64: dts: rockchip: add USB-C DP AltMode for ArmSom Sige5 Enable USB-C DP AltMode for the ArmSom Sige5. Signed-off-by: Andy Yan This has two issues: 1. The USB-C hotplug detection does not yet work properly. I.e. even after plugging in a device with DP AltMode, /sys/class/drm/card0-DP-1 stays at 'disconnected'. This is a problem I've already seen on the Rock 5B, which needs further investigation. 2. The DT binding does not yet look good for upstream. On the plus side there are no regressions and this results in probing the DP driver, which means it is already good for regression testing. Thus I am already adding this to our development branch. Signed-off-by: Sebastian Reichel --- .../boot/dts/rockchip/rk3576-armsom-sige5.dts | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index 1c100ffd151869..9aca9eadf18008 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -272,6 +272,22 @@ cpu-supply = <&vdd_cpu_lit_s0>; }; +&dp { + status = "okay"; +}; + +&dp0_in { + dp0_in_vp1: endpoint { + remote-endpoint = <&vp1_out_dp0>; + }; +}; + +&dp0_out { + dp0_out_con: endpoint { + remote-endpoint = <&usbdp_phy_dp_in>; + }; +}; + &gmac0 { phy-mode = "rgmii-id"; clock_in_out = "output"; @@ -719,20 +735,22 @@ port@0 { reg = <0>; - usbc0_hs_ep: endpoint { + usbc0_hs: endpoint { remote-endpoint = <&usb_drd0_hs_ep>; }; }; + port@1 { reg = <1>; - usbc0_ss_ep: endpoint { - remote-endpoint = <&usb_drd0_ss_ep>; + usbc0_ss: endpoint { + remote-endpoint = <&usbdp_phy_ss_out>; }; }; + port@2 { reg = <2>; - usbc0_dp_ep: endpoint { - remote-endpoint = <&usbdp_phy_ep>; + usbc0_sbu: endpoint { + remote-endpoint = <&usbdp_phy0_dp_out>; }; }; }; @@ -988,14 +1006,14 @@ port@0 { reg = <0>; usb_drd0_hs_ep: endpoint { - remote-endpoint = <&usbc0_hs_ep>; + remote-endpoint = <&usbc0_hs>; }; }; port@1 { reg = <1>; usb_drd0_ss_ep: endpoint { - remote-endpoint = <&usbc0_ss_ep>; + remote-endpoint = <&usbdp_phy_ss_in>; }; }; }; @@ -1015,9 +1033,41 @@ sbu2-dc-gpios = <&gpio2 RK_PA7 GPIO_ACTIVE_HIGH>; status = "okay"; - port { - usbdp_phy_ep: endpoint { - remote-endpoint = <&usbc0_dp_ep>; + ports { + #address-cells = <1>; + #size-cells = <0>; + + + port@0 { + reg = <0>; + + usbdp_phy_ss_out: endpoint { + remote-endpoint = <&usbc0_ss>; + }; + }; + + port@1 { + reg = <1>; + + usbdp_phy_ss_in: endpoint { + remote-endpoint = <&usb_drd0_ss_ep>; + }; + }; + + port@2 { + reg = <2>; + + usbdp_phy_dp_in: endpoint { + remote-endpoint = <&dp0_out_con>; + }; + }; + + port@3 { + reg = <3>; + + usbdp_phy0_dp_out: endpoint { + remote-endpoint = <&usbc0_sbu>; + }; }; }; }; @@ -1036,3 +1086,10 @@ remote-endpoint = <&hdmi_in_vp0>; }; }; + +&vp1 { + vp1_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp1>; + }; +}; From 3a5c4dcbd2384008a9308a3f9bba13f34707d541 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:06 +0800 Subject: [PATCH 075/218] PCI: dw-rockchip: Add phy_calibrate() to check PHY lock status Current we keep controller in reset state when initializing PHY which is the right thing to do. But this case, the PHY is also reset because it refers to a signal from controller. Now we check PHY lock status inside .phy_init() callback which may be bogus for certain type of PHY, because of the fact above. Add phy_calibrate() to better check PHY lock status if provided. Signed-off-by: Shawn Lin Link: https://patch.msgid.link/1766560210-100883-2-git-send-email-shawn.lin@rock-chips.com Signed-off-by: Sebastian Reichel --- drivers/pci/controller/dwc/pcie-dw-rockchip.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/pci/controller/dwc/pcie-dw-rockchip.c b/drivers/pci/controller/dwc/pcie-dw-rockchip.c index 65e85c7eab0587..74598da98a7343 100644 --- a/drivers/pci/controller/dwc/pcie-dw-rockchip.c +++ b/drivers/pci/controller/dwc/pcie-dw-rockchip.c @@ -862,6 +862,12 @@ static int rockchip_pcie_probe(struct platform_device *pdev) if (ret) goto deinit_phy; + ret = phy_calibrate(rockchip->phy); + if (ret) { + dev_err(dev, "phy lock failed\n"); + goto assert_controller; + } + ret = rockchip_pcie_clk_init(rockchip); if (ret) goto deinit_phy; @@ -884,7 +890,8 @@ static int rockchip_pcie_probe(struct platform_device *pdev) } return 0; - +assert_controller: + reset_control_assert(rockchip->rst); deinit_clk: clk_bulk_disable_unprepare(rockchip->clk_cnt, rockchip->clks); deinit_phy: From 86ecbcf0b4dd4ebb0d04ec8fdba87465e6a5525c Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:07 +0800 Subject: [PATCH 076/218] phy: rockchip-snps-pcie3: Add phy_calibrate() support Move calibration from phy_init() to phy_calibrate(). Signed-off-by: Shawn Lin Link: https://patch.msgid.link/1766560210-100883-3-git-send-email-shawn.lin@rock-chips.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-snps-pcie3.c | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c index 4e8ffd173096a4..9933cda0b08ef8 100644 --- a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c +++ b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c @@ -71,6 +71,7 @@ struct rockchip_p3phy_priv { struct rockchip_p3phy_ops { int (*phy_init)(struct rockchip_p3phy_priv *priv); + int (*phy_calibrate)(struct rockchip_p3phy_priv *priv); }; static int rockchip_p3phy_set_mode(struct phy *phy, enum phy_mode mode, int submode) @@ -97,8 +98,6 @@ static int rockchip_p3phy_rk3568_init(struct rockchip_p3phy_priv *priv) { struct phy *phy = priv->phy; bool bifurcation = false; - int ret; - u32 reg; /* Deassert PCIe PMA output clamp mode */ regmap_write(priv->phy_grf, GRF_PCIE30PHY_CON9, GRF_PCIE30PHY_DA_OCM); @@ -124,25 +123,34 @@ static int rockchip_p3phy_rk3568_init(struct rockchip_p3phy_priv *priv) reset_control_deassert(priv->p30phy); + return 0; +} + +static int rockchip_p3phy_rk3568_calibrate(struct rockchip_p3phy_priv *priv) +{ + int ret; + u32 reg; + ret = regmap_read_poll_timeout(priv->phy_grf, GRF_PCIE30PHY_STATUS0, reg, SRAM_INIT_DONE(reg), 0, 500); if (ret) - dev_err(&priv->phy->dev, "%s: lock failed 0x%x, check input refclk and power supply\n", - __func__, reg); + dev_err(&priv->phy->dev, "lock failed 0x%x, check input refclk and power supply\n", + reg); + return ret; } static const struct rockchip_p3phy_ops rk3568_ops = { .phy_init = rockchip_p3phy_rk3568_init, + .phy_calibrate = rockchip_p3phy_rk3568_calibrate, }; static int rockchip_p3phy_rk3588_init(struct rockchip_p3phy_priv *priv) { u32 reg = 0; u8 mode = RK3588_LANE_AGGREGATION; /* default */ - int ret; regmap_write(priv->phy_grf, RK3588_PCIE3PHY_GRF_PHY0_LN0_CON1, priv->rx_cmn_refclk_mode[0] ? RK3588_RX_CMN_REFCLK_MODE_EN : @@ -184,6 +192,14 @@ static int rockchip_p3phy_rk3588_init(struct rockchip_p3phy_priv *priv) reset_control_deassert(priv->p30phy); + return 0; +} + +static int rockchip_p3phy_rk3588_calibrate(struct rockchip_p3phy_priv *priv) +{ + int ret; + u32 reg; + ret = regmap_read_poll_timeout(priv->phy_grf, RK3588_PCIE3PHY_GRF_PHY0_STATUS1, reg, RK3588_SRAM_INIT_DONE(reg), @@ -200,6 +216,7 @@ static int rockchip_p3phy_rk3588_init(struct rockchip_p3phy_priv *priv) static const struct rockchip_p3phy_ops rk3588_ops = { .phy_init = rockchip_p3phy_rk3588_init, + .phy_calibrate = rockchip_p3phy_rk3588_calibrate, }; static int rockchip_p3phy_init(struct phy *phy) @@ -234,10 +251,22 @@ static int rockchip_p3phy_exit(struct phy *phy) return 0; } +static int rockchip_p3phy_calibrate(struct phy *phy) +{ + struct rockchip_p3phy_priv *priv = phy_get_drvdata(phy); + int ret = 0; + + if (priv->ops->phy_calibrate) + ret = priv->ops->phy_calibrate(priv); + + return ret; +} + static const struct phy_ops rockchip_p3phy_ops = { .init = rockchip_p3phy_init, .exit = rockchip_p3phy_exit, .set_mode = rockchip_p3phy_set_mode, + .calibrate = rockchip_p3phy_calibrate, .owner = THIS_MODULE, }; From 4c92ba95b261f0253fed0195e26e44c83e1088ab Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:08 +0800 Subject: [PATCH 077/218] phy: rockchip-snps-pcie3: Increase sram init timeout Per massive test, 500us is not enough for all chips, increase it to 20000us for worse case recommended. Signed-off-by: Shawn Lin Link: https://patch.msgid.link/1766560210-100883-4-git-send-email-shawn.lin@rock-chips.com Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-snps-pcie3.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c index 9933cda0b08ef8..f5a5d0af3f0cbb 100644 --- a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c +++ b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c @@ -19,6 +19,9 @@ #include #include +/* Common definition */ +#define RK_SRAM_INIT_TIMEOUT_US 20000 + /* Register for RK3568 */ #define GRF_PCIE30PHY_CON1 0x4 #define GRF_PCIE30PHY_CON6 0x18 @@ -28,6 +31,7 @@ #define GRF_PCIE30PHY_WR_EN (0xf << 16) #define SRAM_INIT_DONE(reg) (reg & BIT(14)) + #define RK3568_BIFURCATION_LANE_0_1 BIT(0) /* Register for RK3588 */ @@ -134,7 +138,7 @@ static int rockchip_p3phy_rk3568_calibrate(struct rockchip_p3phy_priv *priv) ret = regmap_read_poll_timeout(priv->phy_grf, GRF_PCIE30PHY_STATUS0, reg, SRAM_INIT_DONE(reg), - 0, 500); + 0, RK_SRAM_INIT_TIMEOUT_US); if (ret) dev_err(&priv->phy->dev, "lock failed 0x%x, check input refclk and power supply\n", reg); @@ -203,11 +207,11 @@ static int rockchip_p3phy_rk3588_calibrate(struct rockchip_p3phy_priv *priv) ret = regmap_read_poll_timeout(priv->phy_grf, RK3588_PCIE3PHY_GRF_PHY0_STATUS1, reg, RK3588_SRAM_INIT_DONE(reg), - 0, 500); + 0, RK_SRAM_INIT_TIMEOUT_US); ret |= regmap_read_poll_timeout(priv->phy_grf, RK3588_PCIE3PHY_GRF_PHY1_STATUS1, reg, RK3588_SRAM_INIT_DONE(reg), - 0, 500); + 0, RK_SRAM_INIT_TIMEOUT_US); if (ret) dev_err(&priv->phy->dev, "lock failed 0x%x, check input refclk and power supply\n", reg); From 5352dc2ae1a05b9cadd13a542a28fdf232db0418 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:09 +0800 Subject: [PATCH 078/218] phy: rockchip-snps-pcie3: Check more sram init status for RK3588 All the lower 4 bits should be checked which shows the mpllx_state. Signed-off-by: Shawn Lin Link: https://patch.msgid.link/1766560210-100883-5-git-send-email-shawn.lin@rock-chips.com Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-snps-pcie3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c index f5a5d0af3f0cbb..6cc38e36a90612 100644 --- a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c +++ b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c @@ -43,7 +43,7 @@ #define RK3588_PCIE3PHY_GRF_PHY0_LN1_CON1 0x1104 #define RK3588_PCIE3PHY_GRF_PHY1_LN0_CON1 0x2004 #define RK3588_PCIE3PHY_GRF_PHY1_LN1_CON1 0x2104 -#define RK3588_SRAM_INIT_DONE(reg) (reg & BIT(0)) +#define RK3588_SRAM_INIT_DONE(reg) ((reg & 0xf) == 0xf) #define RK3588_BIFURCATION_LANE_0_1 BIT(0) #define RK3588_BIFURCATION_LANE_2_3 BIT(1) From 1a8ea425e35045d0bdad1179f08fe70c53ae3ef7 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:10 +0800 Subject: [PATCH 079/218] phy: rockchip-snps-pcie3: Only check PHY1 status when using it RK3588_LANE_AGGREGATION and RK3588_BIFURCATION_LANE_2_3 should be used to check if it need to check PHY1 status. Because in other cases, only PHY0 could show locked status. Signed-off-by: Shawn Lin Link: https://patch.msgid.link/1766560210-100883-6-git-send-email-shawn.lin@rock-chips.com Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-snps-pcie3.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c index 6cc38e36a90612..36b2142ec913ac 100644 --- a/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c +++ b/drivers/phy/rockchip/phy-rockchip-snps-pcie3.c @@ -183,6 +183,7 @@ static int rockchip_p3phy_rk3588_init(struct rockchip_p3phy_priv *priv) } reg = mode; + priv->pcie30_phymode = mode; regmap_write(priv->phy_grf, RK3588_PCIE3PHY_GRF_CMN_CON0, RK3588_PCIE30_PHY_MODE_EN | reg); @@ -208,10 +209,13 @@ static int rockchip_p3phy_rk3588_calibrate(struct rockchip_p3phy_priv *priv) RK3588_PCIE3PHY_GRF_PHY0_STATUS1, reg, RK3588_SRAM_INIT_DONE(reg), 0, RK_SRAM_INIT_TIMEOUT_US); - ret |= regmap_read_poll_timeout(priv->phy_grf, - RK3588_PCIE3PHY_GRF_PHY1_STATUS1, - reg, RK3588_SRAM_INIT_DONE(reg), - 0, RK_SRAM_INIT_TIMEOUT_US); + if (priv->pcie30_phymode & (RK3588_LANE_AGGREGATION | RK3588_BIFURCATION_LANE_2_3)) { + ret |= regmap_read_poll_timeout(priv->phy_grf, + RK3588_PCIE3PHY_GRF_PHY1_STATUS1, + reg, RK3588_SRAM_INIT_DONE(reg), + 0, RK_SRAM_INIT_TIMEOUT_US); + } + if (ret) dev_err(&priv->phy->dev, "lock failed 0x%x, check input refclk and power supply\n", reg); From 646bee96a8477bfa537ae983a0a9b0437200902b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 19 Jan 2026 13:11:40 +0100 Subject: [PATCH 080/218] mmc: sdhci-of-dwcmshc: Reduce clock reduction print level The system gracefully handles not being able to reduce the clock frequencies, so there is nothing to be fixed. Reduce the error print to debug level to keep the error log nice and short with important messages. Signed-off-by: Sebastian Reichel --- drivers/mmc/host/sdhci-of-dwcmshc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/mmc/host/sdhci-of-dwcmshc.c b/drivers/mmc/host/sdhci-of-dwcmshc.c index b9ecd91f44ad4a..0f9576b790602a 100644 --- a/drivers/mmc/host/sdhci-of-dwcmshc.c +++ b/drivers/mmc/host/sdhci-of-dwcmshc.c @@ -790,7 +790,7 @@ static void dwcmshc_rk3568_set_clock(struct sdhci_host *host, unsigned int clock if (clock <= 52000000) { if (host->mmc->ios.timing == MMC_TIMING_MMC_HS200 || host->mmc->ios.timing == MMC_TIMING_MMC_HS400) { - dev_err(mmc_dev(host->mmc), + dev_dbg(mmc_dev(host->mmc), "Can't reduce the clock below 52MHz in HS200/HS400 mode"); goto enable_clk; } From c957730d41d076de6ee6194c58f2ab792381f1f1 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 21 Jan 2026 12:29:32 +0200 Subject: [PATCH 081/218] drm/rockchip: dw_dp: Switch to drmm_kzalloc() Driver makes use of drmm_encoder_init() to initialize the encoder and automatically handle the cleanup by registering drm_encoder_cleanup() with drmm_add_action(). However, the internal structure containing the encoder part gets allocated with devm_kzalloc(), which happens while component_bind_all() is being called from Rockchip DRM driver. The component framework further ensures it is deallocated as part of releasing all the resources claimed during bind, which is triggered from component_unbind_all(). When the reference to the DRM device gets eventually dropped via drm_dev_put() in rockchip_drm_unbind(), drmm_encoder_alloc_release() attempts to access the now released encoder structure, leading to use-after-free. Ensure driver's internal structure is still reachable on encoder cleanup by switching from a device-managed allocation to a drm-managed one. Fixes: d68ba7bac955 ("drm/rockchip: Add RK3588 DPTX output support") Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index dac3d202971eda..532af476d250a5 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -82,7 +83,7 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * struct drm_connector *connector; int ret; - dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL); + dp = drmm_kzalloc(drm_dev, sizeof(*dp), GFP_KERNEL); if (!dp) return -ENOMEM; From b66197b0c452dc5e336a621a7597cc81c11936d8 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 21 Jan 2026 14:02:15 +0200 Subject: [PATCH 082/218] drm/rockchip: dw_dp: Fix null-ptr-deref in dw_dp_remove() Attempting to access driver data in the platform driver ->remove() callback may lead to a null pointer dereference since there is no guaranty that the component ->bind() callback invoking platform_set_drvdata() was executed. A common scenario is when Rockchip DRM driver didn't manage to run component_bind_all() because of an (unrelated) error causing early return from rockchip_drm_bind(). Drop the unnecessary call to platform_get_drvdata() and, instead, reference the target device structure via platform_device. Fixes: d68ba7bac955 ("drm/rockchip: Add RK3588 DPTX output support") Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 532af476d250a5..8945a245398ca4 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -133,9 +133,7 @@ static int dw_dp_probe(struct platform_device *pdev) static void dw_dp_remove(struct platform_device *pdev) { - struct rockchip_dw_dp *dp = platform_get_drvdata(pdev); - - component_del(dp->dev, &dw_dp_rockchip_component_ops); + component_del(&pdev->dev, &dw_dp_rockchip_component_ops); } static const struct dw_dp_plat_data rk3588_dp_plat_data = { From b3f25a9fb4c45b9c08263fd4a003534333c82d0f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 20 Jan 2026 21:44:26 +0200 Subject: [PATCH 083/218] drm/rockchip: dw_hdmi_qp: Switch to drmm_encoder_init() Simplify encoder initialization and cleanup by making use of drmm_encoder_init(), which takes care of registering drm_encoder_cleanup() with drmm_add_action(). Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index 1a09bcc96c3ece..c78db7f8ab6c76 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -477,7 +478,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, if (!pdev->dev.of_node) return -ENODEV; - hdmi = devm_kzalloc(&pdev->dev, sizeof(*hdmi), GFP_KERNEL); + hdmi = drmm_kzalloc(drm, sizeof(*hdmi), GFP_KERNEL); if (!hdmi) return -ENOMEM; @@ -586,16 +587,16 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, return ret; drm_encoder_helper_add(encoder, &dw_hdmi_qp_rockchip_encoder_helper_funcs); - drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS); + ret = drmm_encoder_init(drm, encoder, NULL, DRM_MODE_ENCODER_TMDS, NULL); + if (ret) + return dev_err_probe(hdmi->dev, ret, "Failed to init encoder"); platform_set_drvdata(pdev, hdmi); hdmi->hdmi = dw_hdmi_qp_bind(pdev, encoder, &plat_data); - if (IS_ERR(hdmi->hdmi)) { - drm_encoder_cleanup(encoder); + if (IS_ERR(hdmi->hdmi)) return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->hdmi), "Failed to bind dw-hdmi-qp"); - } connector = drm_bridge_connector_init(drm, encoder); if (IS_ERR(connector)) @@ -612,8 +613,6 @@ static void dw_hdmi_qp_rockchip_unbind(struct device *dev, struct rockchip_hdmi_qp *hdmi = dev_get_drvdata(dev); cancel_delayed_work_sync(&hdmi->hpd_work); - - drm_encoder_cleanup(&hdmi->encoder.encoder); } static const struct component_ops dw_hdmi_qp_rockchip_ops = { From 350b0ebfb5cc95b3e634f900d99cf9c520594b4c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 22 Jan 2026 14:57:54 +0100 Subject: [PATCH 084/218] thermal/drivers/rockchip: Shut up eFuse prober defer warning Shut up the following message printed at error level on ArmSom Sige5: rockchip-thermal 2ae70000.tsadc: failed reading trim of sensor 0: -EPROBE_DEFER Fixes: ae332ec0009d ("thermal/drivers/rockchip: Support reading trim values from OTP") Signed-off-by: Sebastian Reichel --- drivers/thermal/rockchip_thermal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/thermal/rockchip_thermal.c b/drivers/thermal/rockchip_thermal.c index c49ddf70f86e7b..2a40339de0d0ef 100644 --- a/drivers/thermal/rockchip_thermal.c +++ b/drivers/thermal/rockchip_thermal.c @@ -1638,8 +1638,7 @@ rockchip_thermal_register_sensor(struct platform_device *pdev, if (tsadc->get_trim_code && sensor->of_node) { error = rockchip_get_efuse_value(sensor->of_node, "trim", &trim); if (error < 0 && error != -ENOENT) { - dev_err(dev, "failed reading trim of sensor %d: %pe\n", - id, ERR_PTR(error)); + dev_err_probe(dev, error, "failed reading trim of sensor %d\n", id); return error; } if (trim) { From 9ae0d3cfddbbf99f4d72943bc7f41bc0b601c5ed Mon Sep 17 00:00:00 2001 From: Dmitry Osipenko Date: Wed, 18 Mar 2026 22:26:19 +0300 Subject: [PATCH 085/218] media: synopsys: hdmirx: Fix HPD hold time Increase time of holding HPD pin low by 50ms. This fixes EDID change not detected by sink/display side. Fixes: 7b59b132ad43 ("media: platform: synopsys: Add support for HDMI input driver") Reported-by: Ross Cawston Closes: https://lore.kernel.org/linux-rockchip/20260209061654.54757-1-ross@r-sc.ca/ Signed-off-by: Dmitry Osipenko Link: https://lore.kernel.org/r/20260318192619.3910060-1-dmitry.osipenko@collabora.com Signed-off-by: Sebastian Reichel --- drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c index 61ad20b18b8d65..4c8957505a50dc 100644 --- a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c +++ b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c @@ -506,9 +506,9 @@ static void hdmirx_hpd_ctrl(struct snps_hdmirx_dev *hdmirx_dev, bool en) hdmirx_writel(hdmirx_dev, CORE_CONFIG, hdmirx_dev->hpd_trigger_level_high ? en : !en); - /* 100ms delay as per HDMI spec */ + /* 100ms delay as per HDMI spec + extra 50ms to cover internal delay */ if (!en) - msleep(100); + msleep(100 + 50); } static void hdmirx_write_edid_data(struct snps_hdmirx_dev *hdmirx_dev, From 597f29c32968edaba2215cba6d9a7e869c61b058 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 23:57:28 +0100 Subject: [PATCH 086/218] drm/rockchip: vop2: Add clock rate mode check The display might offer modes, which exceed the maximum clock rate of a video output. This usually happens for displays that offer refresh rates above 60 Hz. This results in no picture (or a broken one) being displayed without manual intervention. Fix this by teaching the driver about the maximum achievable clock rates for each video port. The information about the maximum clock rates for each video channel and the tip about multiple pixels being processed per clock were provided by Andy Yan and roughly checked against the information available in the datasheet (which specifies limits like "2560x1600@60Hz with 10-bit" instead of a specific pixel rate). For the video ports supporting a 600 MHz input clock, there is some logic to handle up to 4 pixels in parallel when needed resulting in the extra multiplier. Suggested-by: Andy Yan Link: https://lore.kernel.org/linux-rockchip/1528d788.186b.19d08ed974c.Coremail.andyshrk@163.com/ Tested-by: Alexey Charkov # RK3576 Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 3 +++ drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 1 + drivers/gpu/drm/rockchip/rockchip_vop2_reg.c | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 2601446f9f0748..1ad6369336d2fb 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -1442,6 +1442,9 @@ static enum drm_mode_status vop2_crtc_mode_valid(struct drm_crtc *crtc, if (mode->hdisplay > vp->data->max_output.width) return MODE_BAD_HVALUE; + if (mode->clock > vp->data->max_pixel_clock_rate / 1000) + return MODE_CLOCK_HIGH; + return MODE_OK; } diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h index b8198714c1c912..0296e35579a921 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h @@ -232,6 +232,7 @@ struct vop2_video_port_data { u16 gamma_lut_len; u16 cubic_lut_len; struct vop_rect max_output; + u32 max_pixel_clock_rate; const u8 pre_scan_max_dly[4]; unsigned int offset; /** diff --git a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c index 02a788a4dfddaa..e584c34353b25c 100644 --- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c +++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c @@ -559,18 +559,21 @@ static const struct vop2_video_port_data rk3568_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 9 * 9 * 9, .max_output = { 4096, 2304 }, + .max_pixel_clock_rate = 600000000U, .pre_scan_max_dly = { 69, 53, 53, 42 }, .offset = 0xc00, }, { .id = 1, .gamma_lut_len = 1024, .max_output = { 2048, 1536 }, + .max_pixel_clock_rate = 200000000U, .pre_scan_max_dly = { 40, 40, 40, 40 }, .offset = 0xd00, }, { .id = 2, .gamma_lut_len = 1024, .max_output = { 1920, 1080 }, + .max_pixel_clock_rate = 150000000U, .pre_scan_max_dly = { 40, 40, 40, 40 }, .offset = 0xe00, }, @@ -775,6 +778,7 @@ static const struct vop2_video_port_data rk3576_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 9 * 9 * 9, /* 9x9x9 */ .max_output = { 4096, 2304 }, + .max_pixel_clock_rate = 600000000U * 2, /* win layer_mix hdr */ .pre_scan_max_dly = { 10, 8, 2, 0 }, .offset = 0xc00, @@ -785,6 +789,7 @@ static const struct vop2_video_port_data rk3576_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 729, /* 9x9x9 */ .max_output = { 2560, 1600 }, + .max_pixel_clock_rate = 300000000U, /* win layer_mix hdr */ .pre_scan_max_dly = { 10, 6, 0, 0 }, .offset = 0xd00, @@ -793,6 +798,7 @@ static const struct vop2_video_port_data rk3576_vop_video_ports[] = { .id = 2, .gamma_lut_len = 1024, .max_output = { 1920, 1080 }, + .max_pixel_clock_rate = 150000000U, /* win layer_mix hdr */ .pre_scan_max_dly = { 10, 6, 0, 0 }, .offset = 0xe00, @@ -1061,6 +1067,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 9 * 9 * 9, /* 9x9x9 */ .max_output = { 4096, 2304 }, + .max_pixel_clock_rate = 600000000U * 4, /* hdr2sdr sdr2hdr hdr2hdr sdr2sdr */ .pre_scan_max_dly = { 76, 65, 65, 54 }, .offset = 0xc00, @@ -1070,6 +1077,7 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 729, /* 9x9x9 */ .max_output = { 4096, 2304 }, + .max_pixel_clock_rate = 600000000U * 4, .pre_scan_max_dly = { 76, 65, 65, 54 }, .offset = 0xd00, }, { @@ -1078,12 +1086,14 @@ static const struct vop2_video_port_data rk3588_vop_video_ports[] = { .gamma_lut_len = 1024, .cubic_lut_len = 17 * 17 * 17, /* 17x17x17 */ .max_output = { 4096, 2304 }, + .max_pixel_clock_rate = 600000000U * 4, .pre_scan_max_dly = { 52, 52, 52, 52 }, .offset = 0xe00, }, { .id = 3, .gamma_lut_len = 1024, .max_output = { 2048, 1536 }, + .max_pixel_clock_rate = 150000000U, .pre_scan_max_dly = { 52, 52, 52, 52 }, .offset = 0xf00, }, From afb19a6a5cf29bd22fb8a02a8b2dd3b5ef410b13 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:44:57 +0100 Subject: [PATCH 087/218] [WIP] arm64: dts: rockchip: disable command queue engine for Sige5 The Sige5 board often runs into CQE errors, which make the boot unreliable; disable support for it to get consistent results until somebody found time to analyze the root cause. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index 9aca9eadf18008..411253b6eff038 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -931,6 +931,7 @@ no-sdio; no-sd; non-removable; + /delete-property/ supports-cqe; status = "okay"; }; From 8d658bbd8468b8d57bbc4d874267a0e8011da043 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 2 Mar 2026 22:37:40 +0100 Subject: [PATCH 088/218] usb: typec: tcpm: Also use SNK_WAIT_CAPABILITIES_TIMEOUT for vbus_never_low Most USB-C ports on Linux devices are self-powered (i.e. because the device offering a port has a battery or is powered from Mains using a dedicated power-supply). If a port instead is the only power source for a device things become fragile. SNK_WAIT_CAPABILITIES_TIMEOUT has been introduced to better handle these devices as it tries to avoid sending a hard reset. This further improves things by also trying to avoid the soft reset as that is simply escalated into a hard reset with some USB PD sources. This (hopefully, testing is still ongoing) fixes the following issue, which sometimes happens with the ROCK 5B from the Collabora LAVA lab (used for the KernelCI project): [ 16.330582] typec_fusb302 4-0022: pending state change SNK_WAIT_CAPABILITIES -> SNK_SOFT_RESET @ 310 ms [rev2 NONE_AMS] [ 16.641651] typec_fusb302 4-0022: state change SNK_WAIT_CAPABILITIES -> SNK_SOFT_RESET [delayed 310 ms] [ 16.642488] typec_fusb302 4-0022: AMS SOFT_RESET_AMS start [ 16.642968] typec_fusb302 4-0022: state change SNK_SOFT_RESET -> AMS_START [rev2 SOFT_RESET_AMS] [ 16.643741] typec_fusb302 4-0022: state change AMS_START -> SOFT_RESET_SEND [rev2 SOFT_RESET_AMS] [ 16.644525] typec_fusb302 4-0022: PD TX, header: 0x4d [ 16.647659] typec_fusb302 4-0022: sending PD message header: 4d [ 16.648190] typec_fusb302 4-0022: sending PD message len: 0 [ 16.650521] typec_fusb302 4-0022: IRQ: 0x41, a: 0x00, b: 0x00, status0: 0x83 [ 16.651154] typec_fusb302 4-0022: IRQ: BC_LVL, handler pending [ 16.653494] typec_fusb302 4-0022: IRQ: 0x41, a: 0x00, b: 0x00, status0: 0x83 [ 16.654120] typec_fusb302 4-0022: IRQ: BC_LVL, handler pending [ 16.656454] typec_fusb302 4-0022: IRQ: 0x41, a: 0x10, b: 0x00, status0: 0x83 [ 16.657078] typec_fusb302 4-0022: IRQ: BC_LVL, handler pending [ 16.657612] typec_fusb302 4-0022: IRQ: PD retry failed [ 16.658073] typec_fusb302 4-0022: PD TX complete, status: 2 [ 16.658583] typec_fusb302 4-0022: state change SOFT_RESET_SEND -> HARD_RESET_SEND [rev2 SOFT_RESET_AMS] [ 16.659411] typec_fusb302 4-0022: AMS SOFT_RESET_AMS finished [ 16.659920] typec_fusb302 4-0022: Initiating hard-reset, which might result in machine power-loss. [ 16.660705] typec_fusb302 4-0022: AMS HARD_RESET start [ 16.661162] typec_fusb302 4-0022: PD TX, type: 0x5 [ 16.664334] typec_fusb302 4-0022: IRQ: 0x41, a: 0x08, b: 0x00, status0: 0x83 [ 16.664965] typec_fusb302 4-0022: IRQ: BC_LVL, handler pending [ 16.665514] typec_fusb302 4-0022: IRQ: PD hardreset sent [ 16.666783] typec_fusb302 4-0022: PD TX complete, status: 0 [ 16.667305] typec_fusb302 4-0022: state change HARD_RESET_SEND -> HARD_RESET_START [rev2 HARD_RESET] [ 16.672730] typec_fusb302 4-0022: pd := off [ 16.673103] typec_fusb302 4-0022: state change HARD_RESET_START -> SNK_HARD_RESET_SINK_OFF [rev2 HARD_RESET] [ 16.673972] typec_fusb302 4-0022: vconn:=0 [ 16.674331] typec_fusb302 4-0022: vconn is already off [ 16.674787] typec_fusb302 4-0022: Requesting mux state 1, usb-role 2, orientation 1 --- board reset --- Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index d4e656f8cf34c1..008e903fdc0c28 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -5624,20 +5624,25 @@ static void run_state_machine(struct tcpm_port *port) tcpm_set_state(port, SNK_READY, 0); break; } + /* + * For non self-powered devices, first of all try explicitly + * requesting the source capabilities for better support of + * of non-compliant PD sources (a comment with more details is + * in the SNK_WAIT_CAPABILITIES_TIMEOUT state). + * * If VBUS has never been low, and we time out waiting * for source cap, try a soft reset first, in case we * were already in a stable contract before this boot. * Do this only once. */ - if (port->vbus_never_low) { + if (!port->self_powered) { + upcoming_state = SNK_WAIT_CAPABILITIES_TIMEOUT; + } else if (port->vbus_never_low) { port->vbus_never_low = false; upcoming_state = SNK_SOFT_RESET; } else { - if (!port->self_powered) - upcoming_state = SNK_WAIT_CAPABILITIES_TIMEOUT; - else - upcoming_state = hard_reset_state(port); + upcoming_state = hard_reset_state(port); } tcpm_set_state(port, upcoming_state, @@ -5659,10 +5664,17 @@ static void run_state_machine(struct tcpm_port *port) * and handled by all USB PD source and dual role devices * according to the specification. */ + if (port->vbus_never_low) { + port->vbus_never_low = false; + upcoming_state = SNK_SOFT_RESET; + } else { + upcoming_state = hard_reset_state(port); + } + if (tcpm_pd_send_control(port, PD_CTRL_GET_SOURCE_CAP, TCPC_TX_SOP)) - tcpm_set_state_cond(port, hard_reset_state(port), 0); + tcpm_set_state_cond(port, upcoming_state, 0); else - tcpm_set_state(port, hard_reset_state(port), + tcpm_set_state(port, upcoming_state, port->timings.sink_wait_cap_time); break; case SNK_NEGOTIATE_CAPABILITIES: From 63e981831fce623ac78d1ba25018681b475fe7a0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Mar 2026 18:16:31 +0100 Subject: [PATCH 089/218] usb: typec: tcpm: log when VDM AMS state machine is cancelled Since commit 0bc3ee92880d ("usb: typec: tcpm: Properly interrupt VDM AMS") the VDM AMS state machine is cancelled when a non-VDM message arrives. Apparently Dell U2725QE displays potentially interrupt the discover identity AMS to try a power role swap: [ 4.341899] AMS POWER_NEGOTIATION finished [ 4.341906] cc:=4 [ 4.353165] AMS DISCOVER_IDENTITY start [ 4.353187] PD TX, header: 0x176f [ 4.365820] PD TX complete, status: 0 [ 4.365885] PD RX, header: 0x24a [1] [ 4.365899] AMS DISCOVER_IDENTITY finished [ 4.365903] cc:=4 [ 4.376612] PD TX, header: 0x964 [ 4.481320] PD RX, header: 0x444f [1] [ 4.481353] PD RX, header: 0x64a [1] [ 4.481374] PD TX, header: 0x964 [ 4.492423] PD TX complete, status: 0 At this point the log stops without the discover identity being completed successfully. Apparently what happens is: 1. TCPM negotiated power as PD source 2. TCPM continues with identity discovery 3. Display asks for a power swap 4. TCPM stops state machine for identity discovery 5. TCPM rejects the power swap 6. Display sends identity discovery response 7. Display sends the power swap request once more Note, that this does not always happen. There is a race condition and sometimes the power swap request arrives before TCPM switches into the DISCOVER_IDENTITY phase, which avoids the AMS interruption and then things work as expected. I tried adding logic to restart the discover identity state machine in this case, which should be fine according to the specification as far as I can tell. Unfortunately it does not help with the Dell U2725QE: Once it runs into the above issue, the display seems to have messed up its internal message buffer resulting in the display sending answers to the previous message instead of the current one. Obviously no sensible PD communication is sensible at this point. Improve the situation a bit by at least giving a decent log entry. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index 008e903fdc0c28..b8c36f9a880527 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -1673,6 +1673,15 @@ static bool tcpm_ams_interruptible(struct tcpm_port *port) return true; } +static void tcpm_vdm_handle_ams_interruption(struct tcpm_port *port) +{ + tcpm_log(port, "VDM AMS state machine got interrupted"); + + port->vdm_state = VDM_STATE_ERR_BUSY; + tcpm_ams_finish(port); + mod_vdm_delayed_work(port, 0); +} + static int tcpm_ams_start(struct tcpm_port *port, enum tcpm_ams ams) { int ret = 0; @@ -3367,11 +3376,8 @@ static void tcpm_pd_data_request(struct tcpm_port *port, bool frs_enable; int ret; - if (tcpm_vdm_ams(port) && type != PD_DATA_VENDOR_DEF) { - port->vdm_state = VDM_STATE_ERR_BUSY; - tcpm_ams_finish(port); - mod_vdm_delayed_work(port, 0); - } + if (tcpm_vdm_ams(port) && type != PD_DATA_VENDOR_DEF) + tcpm_vdm_handle_ams_interruption(port); switch (type) { case PD_DATA_SOURCE_CAP: @@ -3568,11 +3574,8 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port, * Stop VDM state machine if interrupted by other Messages while NOT_SUPP is allowed in * VDM AMS if waiting for VDM responses and will be handled later. */ - if (tcpm_vdm_ams(port) && type != PD_CTRL_NOT_SUPP && type != PD_CTRL_GOOD_CRC) { - port->vdm_state = VDM_STATE_ERR_BUSY; - tcpm_ams_finish(port); - mod_vdm_delayed_work(port, 0); - } + if (tcpm_vdm_ams(port) && type != PD_CTRL_NOT_SUPP && type != PD_CTRL_GOOD_CRC) + tcpm_vdm_handle_ams_interruption(port); switch (type) { case PD_CTRL_GOOD_CRC: @@ -3890,11 +3893,8 @@ static void tcpm_pd_ext_msg_request(struct tcpm_port *port, unsigned int data_size = pd_ext_header_data_size_le(msg->ext_msg.header); /* stopping VDM state machine if interrupted by other Messages */ - if (tcpm_vdm_ams(port)) { - port->vdm_state = VDM_STATE_ERR_BUSY; - tcpm_ams_finish(port); - mod_vdm_delayed_work(port, 0); - } + if (tcpm_vdm_ams(port)) + tcpm_vdm_handle_ams_interruption(port); if (!(le16_to_cpu(msg->ext_msg.header) & PD_EXT_HDR_CHUNKED)) { tcpm_pd_handle_msg(port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); From 306de2fd39df088b6450b8d2534f8c46bec86517 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:45 +0200 Subject: [PATCH 090/218] phy: rockchip: samsung-hdptx: Fix rate recalculation for high bpc The PHY PLL can been programmed by an external component, e.g. the bootloader, just before the recalc_rate() callback is invoked during devm_clk_hw_register() in the probe path. Therefore rk_hdptx_phy_clk_recalc_rate() finds the PLL enabled and attempts to compute the clock rate, while making use of the bpc value from the HDMI PHY configuration, which always defaults to 8 because phy_configure() was not run at that point. As a consequence, the (re)calculated rate is incorrect when the actual bpc was higher than 8. Do not rely on any of the hdmi_cfg members when computing the clock rate and, instead, read the required input data (i.e. bpc), directly from the hardware registers. Fixes: 3481fc04d969 ("phy: rockchip: samsung-hdptx: Compute clk rate from PLL config") Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-1-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 2d973bc37f076f..7fb1c22318bbf6 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -2168,7 +2168,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx) struct lcpll_config lcpll_hw; struct ropll_config ropll_hw; u64 fout, sdm; - u32 mode, val; + u32 mode, bpc, val; int ret, i; ret = regmap_read(hdptx->regmap, CMN_REG(0008), &mode); @@ -2266,6 +2266,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx) if (ret) return 0; ropll_hw.pms_sdiv = ((val & PLL_PCG_POSTDIV_SEL_MASK) >> 4) + 1; + bpc = (FIELD_GET(PLL_PCG_CLK_SEL_MASK, val) << 1) + 8; fout = PLL_REF_CLK * ropll_hw.pms_mdiv; if (ropll_hw.sdm_en) { @@ -2280,7 +2281,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx) fout = fout + sdm; } - return div_u64(fout * 2, ropll_hw.pms_sdiv * 10); + return div_u64(fout * 2 * 8, ropll_hw.pms_sdiv * 10 * bpc); } static unsigned long rk_hdptx_phy_clk_recalc_rate(struct clk_hw *hw, @@ -2288,19 +2289,13 @@ static unsigned long rk_hdptx_phy_clk_recalc_rate(struct clk_hw *hw, { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); u32 status; - u64 rate; int ret; ret = regmap_read(hdptx->grf, GRF_HDPTX_CON0, &status); if (ret || !(status & HDPTX_I_PLL_EN)) return 0; - rate = rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx); - - if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) - return rate; - - return DIV_ROUND_CLOSEST_ULL(rate * 8, hdptx->hdmi_cfg.bpc); + return rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx); } static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, From 4d66898be29d0414fcb520bff873a054491abb95 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:46 +0200 Subject: [PATCH 091/218] phy: rockchip: samsung-hdptx: Handle uncommitted PHY config changes Any changes to the PHY link rate and/or color depth done via the HDMI PHY configuration API are not immediately programmed into the hardware, but are delayed until the PHY usage count gets incremented from 0 to 1, that is when it is powered on or when the PLL clock exposed through the CCF API is prepared, whichever comes first. Since the clock might remain in prepared state after subsequent PHY config changes, the programming can also be triggered via clk_ops.set_rate(). However, from the clock consumer perspective (i.e. VOP2 display controller), the (pixel) clock rate doesn't vary with bpc, as that is handled internally by the PHY and reflected in the TDMS character rate only. As a consequence, changing the bpc while preserving the modeline may lead to out-of-sync issues between CCF and HDMI PHY config state, because the .set_rate() callback is not invoked when clock rate remains constant. This may also happen when the PHY PLL has been pre-programmed by an external entity, e.g. the bootloader, which is actually a regression introduced by the recent FRL patches. Introduce a pll_config_dirty flag to keep track of uncommitted PHY config changes and use it in clk_ops.determine_rate() to invalidate the current clock rate (as known by CCF) and, consequently, ensure those changes are programmed into hardware via clk_ops.set_rate(). Moreover, proceed with a similar fix in phy_ops.power_on() callback, to handle the scenario where the CCF API is not used due to operating in FRL mode, while the clock is still in a prepared state and thus preventing rk_hdptx_phy_consumer_get() to apply the updated PHY configuration. Fixes: de5dba833118 ("phy: rockchip: samsung-hdptx: Add HDMI 2.1 FRL support") Fixes: 9d0ec51d7c22 ("phy: rockchip: samsung-hdptx: Add high color depth management") Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-2-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 86 ++++++++++--------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 7fb1c22318bbf6..14d266c8df5c82 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -413,6 +413,7 @@ struct rk_hdptx_phy { /* clk provider */ struct clk_hw hw; + bool pll_config_dirty; bool restrict_rate_change; atomic_t usage_count; @@ -1260,13 +1261,19 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx) static int rk_hdptx_pll_cmn_config(struct rk_hdptx_phy *hdptx) { + int ret; + if (hdptx->hdmi_cfg.rate <= HDMI20_MAX_RATE) - return rk_hdptx_tmds_ropll_cmn_config(hdptx); + ret = rk_hdptx_tmds_ropll_cmn_config(hdptx); + else if (hdptx->hdmi_cfg.rate == FRL_8G4L_RATE) + ret = rk_hdptx_frl_lcpll_ropll_cmn_config(hdptx); + else + ret = rk_hdptx_frl_lcpll_cmn_config(hdptx); - if (hdptx->hdmi_cfg.rate == FRL_8G4L_RATE) - return rk_hdptx_frl_lcpll_ropll_cmn_config(hdptx); + if (!ret) + hdptx->pll_config_dirty = false; - return rk_hdptx_frl_lcpll_cmn_config(hdptx); + return ret; } static int rk_hdptx_frl_lcpll_mode_config(struct rk_hdptx_phy *hdptx) @@ -1347,25 +1354,17 @@ static int rk_hdptx_phy_consumer_get(struct rk_hdptx_phy *hdptx) return 0; ret = regmap_read(hdptx->grf, GRF_HDPTX_STATUS, &status); - if (ret) - goto dec_usage; - - if (status & HDPTX_O_PLL_LOCK_DONE) - dev_warn(hdptx->dev, "PLL locked by unknown consumer!\n"); + if (ret) { + atomic_dec(&hdptx->usage_count); + return ret; + } - if (mode == PHY_MODE_DP) { + if (mode == PHY_MODE_DP) rk_hdptx_dp_reset(hdptx); - } else { - ret = rk_hdptx_pll_cmn_config(hdptx); - if (ret) - goto dec_usage; - } + else + rk_hdptx_pll_cmn_config(hdptx); return 0; - -dec_usage: - atomic_dec(&hdptx->usage_count); - return ret; } static int rk_hdptx_phy_consumer_put(struct rk_hdptx_phy *hdptx, bool force) @@ -1700,16 +1699,20 @@ static int rk_hdptx_phy_power_on(struct phy *phy) if (ret) rk_hdptx_phy_consumer_put(hdptx, true); } else { - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0)); + if (hdptx->pll_config_dirty) + ret = rk_hdptx_pll_cmn_config(hdptx); - if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) - ret = rk_hdptx_frl_lcpll_mode_config(hdptx); - else - ret = rk_hdptx_tmds_ropll_mode_config(hdptx); + if (!ret) { + regmap_write(hdptx->grf, GRF_HDPTX_CON0, + HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0)); - if (ret) + if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) + ret = rk_hdptx_frl_lcpll_mode_config(hdptx); + else + ret = rk_hdptx_tmds_ropll_mode_config(hdptx); + } else { rk_hdptx_phy_consumer_put(hdptx, true); + } } return ret; @@ -2081,7 +2084,10 @@ static int rk_hdptx_phy_configure(struct phy *phy, union phy_configure_opts *opt dev_err(hdptx->dev, "invalid hdmi params for phy configure\n"); } else { hdptx->restrict_rate_change = true; - dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__, + hdptx->pll_config_dirty = true; + + dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__, + hdptx->hdmi_cfg.mode ? "FRL" : "TMDS", hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc); } @@ -2303,8 +2309,19 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); - if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) - return hdptx->hdmi_cfg.rate; + /* + * Invalidate current clock rate to ensure rk_hdptx_phy_clk_set_rate() + * will be invoked to commit PLL configuration. + */ + if (hdptx->pll_config_dirty) { + req->rate = 0; + return 0; + } + + if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) { + req->rate = hdptx->hdmi_cfg.rate; + return 0; + } /* * FIXME: Temporarily allow altering TMDS char rate via CCF. @@ -2336,17 +2353,6 @@ static int rk_hdptx_phy_clk_set_rate(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate) { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); - unsigned long long link_rate = rate; - - if (hdptx->hdmi_cfg.mode != PHY_HDMI_MODE_FRL) - link_rate = DIV_ROUND_CLOSEST_ULL(rate * hdptx->hdmi_cfg.bpc, 8); - - /* Revert any unlikely link rate change since determine_rate() */ - if (hdptx->hdmi_cfg.rate != link_rate) { - dev_warn(hdptx->dev, "Reverting unexpected rate change from %llu to %llu\n", - link_rate, hdptx->hdmi_cfg.rate); - hdptx->hdmi_cfg.rate = link_rate; - } /* * The link rate would be normally programmed in HW during From adf434ab9ff688f9f49c96f1ab75c2ce77ea631f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:47 +0200 Subject: [PATCH 092/218] phy: rockchip: samsung-hdptx: Drop TMDS rate setup workaround Since commit ba9c2fe18c17 ("drm/rockchip: dw_hdmi_qp: Switch to phy_configure()") the TMDS rate setup doesn't rely anymore on the unconventional usage of the bus width, instead it is managed exclusively through the HDMI PHY configuration API. Drop the now obsolete workaround to retrieve the TMDS character rate via phy_get_bus_width() during power_on(). While at it, get rid of the extra call to rk_hdptx_phy_consumer_put() by moving the statement at the end of the function. Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-3-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 14d266c8df5c82..b56360b5df78a3 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -1655,22 +1655,6 @@ static int rk_hdptx_phy_power_on(struct phy *phy) enum phy_mode mode = phy_get_mode(phy); int ret, lane; - if (mode != PHY_MODE_DP) { - if (!hdptx->hdmi_cfg.rate && hdptx->hdmi_cfg.mode != PHY_HDMI_MODE_FRL) { - /* - * FIXME: Temporary workaround to setup TMDS char rate - * from the RK DW HDMI QP bridge driver. - * Will be removed as soon the switch to the HDMI PHY - * configuration API has been completed on both ends. - */ - hdptx->hdmi_cfg.rate = phy_get_bus_width(hdptx->phy) & 0xfffffff; - hdptx->hdmi_cfg.rate *= 100; - } - - dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__, - hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc); - } - ret = rk_hdptx_phy_consumer_get(hdptx); if (ret) return ret; @@ -1696,9 +1680,10 @@ static int rk_hdptx_phy_power_on(struct phy *phy) rk_hdptx_dp_pll_init(hdptx); ret = rk_hdptx_dp_aux_init(hdptx); - if (ret) - rk_hdptx_phy_consumer_put(hdptx, true); } else { + dev_dbg(hdptx->dev, "%s rate=%llu bpc=%u\n", __func__, + hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc); + if (hdptx->pll_config_dirty) ret = rk_hdptx_pll_cmn_config(hdptx); @@ -1710,11 +1695,12 @@ static int rk_hdptx_phy_power_on(struct phy *phy) ret = rk_hdptx_frl_lcpll_mode_config(hdptx); else ret = rk_hdptx_tmds_ropll_mode_config(hdptx); - } else { - rk_hdptx_phy_consumer_put(hdptx, true); } } + if (ret) + rk_hdptx_phy_consumer_put(hdptx, true); + return ret; } From 4c54c974d5ecf4a611a919887d4206e873fc1e33 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:48 +0200 Subject: [PATCH 093/218] phy: rockchip: samsung-hdptx: Drop restrict_rate_change handling Since commit 6efbd0f46dd8 ("phy: rockchip: samsung-hdptx: Restrict altering TMDS char rate via CCF"), adjusting the rate via the Common Clock Framework API has been disallowed. To avoid breaking existing users until switching to the PHY config API, it introduced a temporary exception to the rule, controlled via the 'restrict_rate_change' flag. As the API transition completed, remove the now deprecated exception logic. Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-4-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 42 ++++--------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index b56360b5df78a3..e5c8bc95a416ec 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -414,7 +414,6 @@ struct rk_hdptx_phy { /* clk provider */ struct clk_hw hw; bool pll_config_dirty; - bool restrict_rate_change; atomic_t usage_count; @@ -2069,7 +2068,6 @@ static int rk_hdptx_phy_configure(struct phy *phy, union phy_configure_opts *opt if (ret) { dev_err(hdptx->dev, "invalid hdmi params for phy configure\n"); } else { - hdptx->restrict_rate_change = true; hdptx->pll_config_dirty = true; dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__, @@ -2296,41 +2294,17 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); /* - * Invalidate current clock rate to ensure rk_hdptx_phy_clk_set_rate() - * will be invoked to commit PLL configuration. + * For uncommitted PLL configuration, invalidate the current clock rate + * to ensure rk_hdptx_phy_clk_set_rate() will be always invoked. + * Otherwise, restrict the rate according to the PHY link setup. */ - if (hdptx->pll_config_dirty) { + if (hdptx->pll_config_dirty) req->rate = 0; - return 0; - } - - if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) { + else if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) req->rate = hdptx->hdmi_cfg.rate; - return 0; - } - - /* - * FIXME: Temporarily allow altering TMDS char rate via CCF. - * To be dropped as soon as the RK DW HDMI QP bridge driver - * switches to make use of phy_configure(). - */ - if (!hdptx->restrict_rate_change && req->rate != hdptx->hdmi_cfg.rate) { - struct phy_configure_opts_hdmi hdmi = { - .tmds_char_rate = req->rate, - }; - - int ret = rk_hdptx_phy_verify_hdmi_config(hdptx, &hdmi, &hdptx->hdmi_cfg); - - if (ret) - return ret; - } - - /* - * The TMDS char rate shall be adjusted via phy_configure() only, - * hence ensure rk_hdptx_phy_clk_set_rate() won't be invoked with - * a different rate argument. - */ - req->rate = DIV_ROUND_CLOSEST_ULL(hdptx->hdmi_cfg.rate * 8, hdptx->hdmi_cfg.bpc); + else + req->rate = DIV_ROUND_CLOSEST_ULL(hdptx->hdmi_cfg.rate * 8, + hdptx->hdmi_cfg.bpc); return 0; } From 18d10f018723cea4792f4ded8898c9cfbbfb29e6 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:49 +0200 Subject: [PATCH 094/218] phy: rockchip: samsung-hdptx: Simplify GRF access with FIELD_PREP_WM16() The 16 most significant bits of the general-purpose register (GRF) are used as a write-enable mask for the remaining 16 bits. Make use of the recently introduced FIELD_PREP_WM16() macro to avoid open-coding the bit shift operations and improve code readability. Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-5-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index e5c8bc95a416ec..baa6916331bed0 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0+ /* * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd. - * Copyright (c) 2024 Collabora Ltd. + * Copyright (c) 2024-2026 Collabora Ltd. * * Author: Algea Cao * Author: Cristian Ciocaltea @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -949,7 +950,9 @@ static void rk_hdptx_pre_power_up(struct rk_hdptx_phy *hdptx) reset_control_assert(hdptx->rsts[RST_CMN].rstc); reset_control_assert(hdptx->rsts[RST_INIT].rstc); - val = (HDPTX_I_PLL_EN | HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16; + val = (FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, val); } @@ -960,8 +963,8 @@ static int rk_hdptx_post_enable_lane(struct rk_hdptx_phy *hdptx) reset_control_deassert(hdptx->rsts[RST_LANE].rstc); - val = (HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16 | - HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN; + val = (FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1) | + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, val); /* 3 lanes FRL mode */ @@ -990,16 +993,15 @@ static int rk_hdptx_post_enable_pll(struct rk_hdptx_phy *hdptx) u32 val; int ret; - val = (HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16 | - HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN; + val = (FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1) | + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, val); usleep_range(10, 15); reset_control_deassert(hdptx->rsts[RST_INIT].rstc); usleep_range(10, 15); - val = HDPTX_I_PLL_EN << 16 | HDPTX_I_PLL_EN; - regmap_write(hdptx->grf, GRF_HDPTX_CON0, val); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 1)); usleep_range(10, 15); reset_control_deassert(hdptx->rsts[RST_CMN].rstc); @@ -1037,7 +1039,9 @@ static void rk_hdptx_phy_disable(struct rk_hdptx_phy *hdptx) reset_control_assert(hdptx->rsts[RST_CMN].rstc); reset_control_assert(hdptx->rsts[RST_INIT].rstc); - val = (HDPTX_I_PLL_EN | HDPTX_I_BIAS_EN | HDPTX_I_BGR_EN) << 16; + val = (FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, val); } @@ -1135,7 +1139,7 @@ static int rk_hdptx_frl_lcpll_cmn_config(struct rk_hdptx_phy *hdptx) rk_hdptx_pre_power_up(hdptx); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, LC_REF_CLK_SEL << 16); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 0)); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_frl_lcpll_cmn_init_seq); @@ -1178,8 +1182,7 @@ static int rk_hdptx_frl_lcpll_ropll_cmn_config(struct rk_hdptx_phy *hdptx) rk_hdptx_pre_power_up(hdptx); /* ROPLL input reference clock from LCPLL (cascade mode) */ - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - (LC_REF_CLK_SEL << 16) | LC_REF_CLK_SEL); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 1)); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_frl_lcpll_ropll_cmn_init_seq); @@ -1218,7 +1221,7 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx) rk_hdptx_pre_power_up(hdptx); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, LC_REF_CLK_SEL << 16); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(LC_REF_CLK_SEL, 0)); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_common_cmn_init_seq); rk_hdptx_multi_reg_write(hdptx, rk_hdptx_tmds_cmn_init_seq); @@ -1336,11 +1339,9 @@ static void rk_hdptx_dp_reset(struct rk_hdptx_phy *hdptx) FIELD_PREP(LN_TX_DRV_EI_EN_MASK, 0)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x0)); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_BIAS_EN << 16 | FIELD_PREP(HDPTX_I_BIAS_EN, 0x0)); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_BGR_EN << 16 | FIELD_PREP(HDPTX_I_BGR_EN, 0x0)); + FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 0) | + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 0)); } static int rk_hdptx_phy_consumer_get(struct rk_hdptx_phy *hdptx) @@ -1611,9 +1612,8 @@ static int rk_hdptx_dp_aux_init(struct rk_hdptx_phy *hdptx) FIELD_PREP(OVRD_SB_VREG_EN_MASK, 0x1)); regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_BGR_EN << 16 | FIELD_PREP(HDPTX_I_BGR_EN, 0x1)); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_BIAS_EN << 16 | FIELD_PREP(HDPTX_I_BIAS_EN, 0x1)); + FIELD_PREP_WM16(HDPTX_I_BGR_EN, 1) | + FIELD_PREP_WM16(HDPTX_I_BIAS_EN, 1)); usleep_range(20, 25); reset_control_deassert(hdptx->rsts[RST_INIT].rstc); @@ -1660,7 +1660,7 @@ static int rk_hdptx_phy_power_on(struct phy *phy) if (mode == PHY_MODE_DP) { regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x1)); + FIELD_PREP_WM16(HDPTX_MODE_SEL, 1)); for (lane = 0; lane < 4; lane++) { regmap_update_bits(hdptx->regmap, LANE_REG(031e) + 0x400 * lane, @@ -1688,7 +1688,7 @@ static int rk_hdptx_phy_power_on(struct phy *phy) if (!ret) { regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_MODE_SEL << 16 | FIELD_PREP(HDPTX_MODE_SEL, 0x0)); + FIELD_PREP_WM16(HDPTX_MODE_SEL, 0)); if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) ret = rk_hdptx_frl_lcpll_mode_config(hdptx); @@ -1823,8 +1823,7 @@ static int rk_hdptx_phy_set_rate(struct rk_hdptx_phy *hdptx, u32 bw, status; int ret; - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x0)); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 0)); switch (dp->link_rate) { case 1620: @@ -1880,8 +1879,7 @@ static int rk_hdptx_phy_set_rate(struct rk_hdptx_phy *hdptx, regmap_update_bits(hdptx->regmap, CMN_REG(0095), DP_TX_LINK_BW_MASK, FIELD_PREP(DP_TX_LINK_BW_MASK, bw)); - regmap_write(hdptx->grf, GRF_HDPTX_CON0, - HDPTX_I_PLL_EN << 16 | FIELD_PREP(HDPTX_I_PLL_EN, 0x1)); + regmap_write(hdptx->grf, GRF_HDPTX_CON0, FIELD_PREP_WM16(HDPTX_I_PLL_EN, 1)); ret = regmap_read_poll_timeout(hdptx->grf, GRF_HDPTX_STATUS, status, FIELD_GET(HDPTX_O_PLL_LOCK_DONE, status), From 91ed7f58447c5409a77c5bea7085b8e4c9fb9250 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:50 +0200 Subject: [PATCH 095/218] phy: rockchip: samsung-hdptx: Consistently use bitfield macros Make the code more robust and improve readability by using the available bitfield macros (e.g. FIELD_PREP, FIELD_GET) whenever possible, instead of open coding the related bit operations. Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260227-hdptx-clk-fixes-v1-6-f998f2762d0f@collabora.com Signed-off-by: Sebastian Reichel --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index baa6916331bed0..3bde7fbb34b1cf 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -53,6 +53,12 @@ /* CMN_REG(001e) */ #define LCPLL_PI_EN_MASK BIT(5) #define LCPLL_100M_CLK_EN_MASK BIT(0) +/* CMN_REG(0022) */ +#define ANA_LCPLL_PMS_PDIV_MASK GENMASK(7, 4) +#define ANA_LCPLL_PMS_REFDIV_MASK GENMASK(3, 0) +/* CMN_REG(0023) */ +#define LCPLL_PMS_SDIV_RBR_MASK GENMASK(7, 4) +#define LCPLL_PMS_SDIV_HBR_MASK GENMASK(3, 0) /* CMN_REG(0025) */ #define LCPLL_PMS_IQDIV_RSTN_MASK BIT(4) /* CMN_REG(0028) */ @@ -1157,9 +1163,11 @@ static int rk_hdptx_frl_lcpll_cmn_config(struct rk_hdptx_phy *hdptx) regmap_write(hdptx->regmap, CMN_REG(0020), cfg->pms_mdiv); regmap_write(hdptx->regmap, CMN_REG(0021), cfg->pms_mdiv_afc); regmap_write(hdptx->regmap, CMN_REG(0022), - (cfg->pms_pdiv << 4) | cfg->pms_refdiv); + FIELD_PREP(ANA_LCPLL_PMS_PDIV_MASK, cfg->pms_pdiv) | + FIELD_PREP(ANA_LCPLL_PMS_REFDIV_MASK, cfg->pms_refdiv)); regmap_write(hdptx->regmap, CMN_REG(0023), - (cfg->pms_sdiv << 4) | cfg->pms_sdiv); + FIELD_PREP(LCPLL_PMS_SDIV_RBR_MASK, cfg->pms_sdiv) | + FIELD_PREP(LCPLL_PMS_SDIV_HBR_MASK, cfg->pms_sdiv)); regmap_write(hdptx->regmap, CMN_REG(002a), cfg->sdm_deno); regmap_write(hdptx->regmap, CMN_REG(002b), cfg->sdm_num_sign); regmap_write(hdptx->regmap, CMN_REG(002c), cfg->sdm_num); @@ -1229,8 +1237,10 @@ static int rk_hdptx_tmds_ropll_cmn_config(struct rk_hdptx_phy *hdptx) regmap_write(hdptx->regmap, CMN_REG(0051), cfg->pms_mdiv); regmap_write(hdptx->regmap, CMN_REG(0055), cfg->pms_mdiv_afc); regmap_write(hdptx->regmap, CMN_REG(0059), - (cfg->pms_pdiv << 4) | cfg->pms_refdiv); - regmap_write(hdptx->regmap, CMN_REG(005a), cfg->pms_sdiv << 4); + FIELD_PREP(ANA_ROPLL_PMS_PDIV_MASK, cfg->pms_pdiv) | + FIELD_PREP(ANA_ROPLL_PMS_REFDIV_MASK, cfg->pms_refdiv)); + regmap_write(hdptx->regmap, CMN_REG(005a), + FIELD_PREP(ROPLL_PMS_SDIV_RBR_MASK, cfg->pms_sdiv)); regmap_update_bits(hdptx->regmap, CMN_REG(005e), ROPLL_SDM_EN_MASK, FIELD_PREP(ROPLL_SDM_EN_MASK, cfg->sdm_en)); @@ -2192,7 +2202,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx) ret = regmap_read(hdptx->regmap, CMN_REG(002D), &val); if (ret) return 0; - lcpll_hw.sdc_n = (val & LCPLL_SDC_N_MASK) >> 1; + lcpll_hw.sdc_n = FIELD_GET(LCPLL_SDC_N_MASK, val); for (i = 0; i < ARRAY_SIZE(rk_hdptx_frl_lcpll_cfg); i++) { const struct lcpll_config *cfg = &rk_hdptx_frl_lcpll_cfg[i]; @@ -2253,7 +2263,7 @@ static u64 rk_hdptx_phy_clk_calc_rate_from_pll_cfg(struct rk_hdptx_phy *hdptx) ret = regmap_read(hdptx->regmap, CMN_REG(0086), &val); if (ret) return 0; - ropll_hw.pms_sdiv = ((val & PLL_PCG_POSTDIV_SEL_MASK) >> 4) + 1; + ropll_hw.pms_sdiv = FIELD_GET(PLL_PCG_POSTDIV_SEL_MASK, val) + 1; bpc = (FIELD_GET(PLL_PCG_CLK_SEL_MASK, val) << 1) + 8; fout = PLL_REF_CLK * ropll_hw.pms_mdiv; From b8549d608040b454d2fd191ad083388027823e6e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Feb 2026 20:36:26 +0200 Subject: [PATCH 096/218] [DEBUG] phy: rockchip: samsung-hdptx: Add verbose logging --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 3bde7fbb34b1cf..0684e1a0d1a17d 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -1028,6 +1028,8 @@ static void rk_hdptx_phy_disable(struct rk_hdptx_phy *hdptx) { u32 val; + dev_dbg(hdptx->dev, "PHY disable\n"); + reset_control_assert(hdptx->rsts[RST_APB].rstc); usleep_range(20, 30); reset_control_deassert(hdptx->rsts[RST_APB].rstc); @@ -1717,6 +1719,8 @@ static int rk_hdptx_phy_power_off(struct phy *phy) { struct rk_hdptx_phy *hdptx = phy_get_drvdata(phy); + dev_dbg(hdptx->dev, "power_off\n"); + return rk_hdptx_phy_consumer_put(hdptx, false); } @@ -2149,6 +2153,8 @@ static int rk_hdptx_phy_clk_prepare(struct clk_hw *hw) { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); + dev_dbg(hdptx->dev, "clk_prepare\n"); + return rk_hdptx_phy_consumer_get(hdptx); } @@ -2156,6 +2162,8 @@ static void rk_hdptx_phy_clk_unprepare(struct clk_hw *hw) { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); + dev_dbg(hdptx->dev, "clk_unprepare\n"); + rk_hdptx_phy_consumer_put(hdptx, true); } @@ -2287,13 +2295,20 @@ static unsigned long rk_hdptx_phy_clk_recalc_rate(struct clk_hw *hw, { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); u32 status; + u64 rate; int ret; ret = regmap_read(hdptx->grf, GRF_HDPTX_CON0, &status); - if (ret || !(status & HDPTX_I_PLL_EN)) + if (ret || !(status & HDPTX_I_PLL_EN)) { + dev_dbg(hdptx->dev, "%s ret=%d status=%x\n", __func__, ret, status); return 0; + } - return rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx); + rate = rk_hdptx_phy_clk_calc_rate_from_pll_cfg(hdptx); + + dev_dbg(hdptx->dev, "%s from_pll=%llu\n", __func__, rate); + + return rate; } static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, @@ -2301,6 +2316,8 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); + dev_dbg(hdptx->dev, "%s req=%lu par=%lu cfg=%llu dirty=%d\n", __func__, + req->rate, req->best_parent_rate, hdptx->hdmi_cfg.rate, hdptx->pll_config_dirty); /* * For uncommitted PLL configuration, invalidate the current clock rate * to ensure rk_hdptx_phy_clk_set_rate() will be always invoked. @@ -2322,6 +2339,8 @@ static int rk_hdptx_phy_clk_set_rate(struct clk_hw *hw, unsigned long rate, { struct rk_hdptx_phy *hdptx = to_rk_hdptx_phy(hw); + dev_dbg(hdptx->dev, "%s req=%lu par=%lu cfg=%llu dirty=%d\n", __func__, + rate, parent_rate, hdptx->hdmi_cfg.rate, hdptx->pll_config_dirty); /* * The link rate would be normally programmed in HW during * phy_ops.power_on() or clk_ops.prepare() callbacks, but it might @@ -2373,6 +2392,8 @@ static int rk_hdptx_phy_runtime_suspend(struct device *dev) { struct rk_hdptx_phy *hdptx = dev_get_drvdata(dev); + dev_dbg(hdptx->dev, "suspend\n"); + clk_bulk_disable_unprepare(hdptx->nr_clks, hdptx->clks); return 0; @@ -2383,6 +2404,8 @@ static int rk_hdptx_phy_runtime_resume(struct device *dev) struct rk_hdptx_phy *hdptx = dev_get_drvdata(dev); int ret; + dev_dbg(hdptx->dev, "resume\n"); + ret = clk_bulk_prepare_enable(hdptx->nr_clks, hdptx->clks); if (ret) dev_err(hdptx->dev, "Failed to enable clocks: %d\n", ret); From 568c4eb0514e5bf2ece3ba0091d47c453b574d12 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 21 Jan 2026 13:50:42 +0200 Subject: [PATCH 097/218] [MERGED] drm/rockchip: dw_dp: Simplify error handling Make the code a bit more compact by getting rid of the superfluous assignments around PTR_ERR(). While at it, also drop dev assignment in dw_dp_probe(). Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 8945a245398ca4..fafefee8370d6d 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -104,20 +104,15 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * drm_encoder_helper_add(encoder, &dw_dp_encoder_helper_funcs); dp->base = dw_dp_bind(dev, encoder, plat_data); - if (IS_ERR(dp->base)) { - ret = PTR_ERR(dp->base); - return ret; - } + if (IS_ERR(dp->base)) + return PTR_ERR(dp->base); connector = drm_bridge_connector_init(drm_dev, encoder); - if (IS_ERR(connector)) { - ret = PTR_ERR(connector); - return dev_err_probe(dev, ret, "Failed to init bridge connector"); - } + if (IS_ERR(connector)) + return dev_err_probe(dev, PTR_ERR(connector), + "Failed to init bridge connector"); - drm_connector_attach_encoder(connector, encoder); - - return 0; + return drm_connector_attach_encoder(connector, encoder); } static const struct component_ops dw_dp_rockchip_component_ops = { @@ -126,9 +121,7 @@ static const struct component_ops dw_dp_rockchip_component_ops = { static int dw_dp_probe(struct platform_device *pdev) { - struct device *dev = &pdev->dev; - - return component_add(dev, &dw_dp_rockchip_component_ops); + return component_add(&pdev->dev, &dw_dp_rockchip_component_ops); } static void dw_dp_remove(struct platform_device *pdev) From fc003d035f473e6701b32d4168c8f31e626cdc0c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Mar 2026 02:55:07 +0200 Subject: [PATCH 098/218] [MERGED] drm/bridge: synopsys: dw-dp: Support unregistering the AUX channel The DisplayPort AUX channel gets initialized and registered during dw_dp_bind(), but it is never unregistered, which may lead to resource leaks and/or use-after-free. Add the missing dw_dp_unbind() function to allow the users of the library to handle the required cleanup, i.e. unregister the AUX adapter. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260327-drm-rk-fixes-v3-1-fd2e6900c08c@collabora.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 6 ++++++ include/drm/bridge/dw_dp.h | 1 + 2 files changed, 7 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 45b37885d719dc..164da0dfee9a4e 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2097,6 +2097,12 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, } EXPORT_SYMBOL_GPL(dw_dp_bind); +void dw_dp_unbind(struct dw_dp *dp) +{ + drm_dp_aux_unregister(&dp->aux); +} +EXPORT_SYMBOL_GPL(dw_dp_unbind); + MODULE_AUTHOR("Andy Yan "); MODULE_DESCRIPTION("DW DP Core Library"); MODULE_LICENSE("GPL"); diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index 25363541e69d51..22105c3e8e4d66 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -24,4 +24,5 @@ struct dw_dp_plat_data { struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, const struct dw_dp_plat_data *plat_data); +void dw_dp_unbind(struct dw_dp *dp); #endif /* __DW_DP__ */ From 8d0616b3ff757f0610cd3b501463788c1ab31b57 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Mar 2026 02:55:08 +0200 Subject: [PATCH 099/218] [MERGED] drm/rockchip: dw_dp: Release core resources Core resources such as the DisplayPort AUX channel get initialized and registered during dw_dp_bind(), but are never unregistered, which may lead to memory leaks and/or use-after-free: [ 224.661371] BUG: KASAN: slab-use-after-free in device_is_dependent+0xe0/0x2b0 [ 224.662015] Read of size 8 at addr ffff00011aee8550 by task modprobe/658 [ 224.662612] [ 224.662752] CPU: 7 UID: 0 PID: 658 Comm: modprobe Not tainted 7.0.0-rc2-next-20260305 #14 PREEMPT [ 224.662759] Hardware name: Radxa ROCK 5B (DT) [ 224.662762] Call trace: [ 224.662764] show_stack+0x20/0x38 (C) [ 224.662772] dump_stack_lvl+0x6c/0x98 [ 224.662777] print_report+0x160/0x4b8 [ 224.662783] kasan_report+0xb4/0xe0 [ 224.662790] __asan_report_load8_noabort+0x20/0x30 [ 224.662796] device_is_dependent+0xe0/0x2b0 [ 224.662802] device_is_dependent+0x108/0x2b0 [ 224.662808] device_link_add+0x1f8/0x10b0 [ 224.662813] devm_of_phy_get_by_index+0x120/0x200 [ 224.662819] dw_dp_bind+0x34c/0xb10 [dw_dp] [ 224.662830] dw_dp_rockchip_bind+0x194/0x250 [rockchipdrm] [ 224.662864] component_bind_all+0x3a8/0x720 [ 224.662869] rockchip_drm_bind+0x120/0x390 [rockchipdrm] [ 224.662899] try_to_bring_up_aggregate_device+0x76c/0x838 [ 224.662904] component_master_add_with_match+0x1f4/0x230 [ 224.662909] rockchip_drm_platform_probe+0x420/0x538 [rockchipdrm] [ 224.662939] platform_probe+0xe8/0x168 [ 224.662945] really_probe+0x340/0x828 [ 224.662950] __driver_probe_device+0x2e0/0x350 [ 224.662954] driver_probe_device+0x80/0x140 [ 224.662959] __driver_attach+0x398/0x460 [ 224.662964] bus_for_each_dev+0xe0/0x198 [ 224.662968] driver_attach+0x50/0x68 [ 224.662972] bus_add_driver+0x2a0/0x4c0 [ 224.662977] driver_register+0x294/0x360 [ 224.662982] __platform_driver_register+0x7c/0x98 [ 224.662987] rockchip_drm_init+0xc4/0xff8 [rockchipdrm] Since a previous commit exported dw_dp_unbind() function in DW DP core library to take care of the necessary cleanup, use this in the component's unbind() callback, as well as in its bind() error path. Fixes: d68ba7bac955 ("drm/rockchip: Add RK3588 DPTX output support") Signed-off-by: Cristian Ciocaltea Link: https://lore.kernel.org/r/20260327-drm-rk-fixes-v3-2-fd2e6900c08c@collabora.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index fafefee8370d6d..94c1036d9d9f44 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -109,14 +109,28 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * connector = drm_bridge_connector_init(drm_dev, encoder); if (IS_ERR(connector)) - return dev_err_probe(dev, PTR_ERR(connector), - "Failed to init bridge connector"); + ret = dev_err_probe(dev, PTR_ERR(connector), + "Failed to init bridge connector"); + else + ret = drm_connector_attach_encoder(connector, encoder); - return drm_connector_attach_encoder(connector, encoder); + if (ret) + dw_dp_unbind(dp->base); + + return ret; +} + +static void dw_dp_rockchip_unbind(struct device *dev, struct device *master, + void *data) +{ + struct rockchip_dw_dp *dp = dev_get_drvdata(dev); + + dw_dp_unbind(dp->base); } static const struct component_ops dw_dp_rockchip_component_ops = { .bind = dw_dp_rockchip_bind, + .unbind = dw_dp_rockchip_unbind, }; static int dw_dp_probe(struct platform_device *pdev) From eaa655b1fa4051bf4f1f36e302ebef88458ba200 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 9 Mar 2026 21:12:33 +0200 Subject: [PATCH 100/218] [MERGED] drm/bridge: synopsys: dw-dp: Drop useless memory allocation The bridge gets allocated and initialized implicitly via the devm_drm_bridge_alloc() helper in dw_dp_bind(). However, this is preceded by an explicit allocation for the same dw_dp struct, which is never used anywhere as the return from devm_kzalloc() gets immediately overwritten by the aforementioned helper. Get rid of the unnecessary and confusing memory allocation. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 164da0dfee9a4e..6211ba6ba7bd6b 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1970,10 +1970,6 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, void __iomem *res; int ret; - dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL); - if (!dp) - return ERR_PTR(-ENOMEM); - dp = devm_drm_bridge_alloc(dev, struct dw_dp, bridge, &dw_dp_bridge_funcs); if (IS_ERR(dp)) return ERR_CAST(dp); From cc8968a83378e0a685834a6981c1eb2eddf7ca95 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:43:25 +0100 Subject: [PATCH 101/218] [DEBUG] usb: typec: altmode/displayport: print message on probe error Print an error message when the DisplayPort AltMode driver probe failed due to incorrect pin assignment or wrong data-role to ease debugging issues. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/altmodes/displayport.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/usb/typec/altmodes/displayport.c b/drivers/usb/typec/altmodes/displayport.c index 263a89c5f32433..3ff0882f3d97c1 100644 --- a/drivers/usb/typec/altmodes/displayport.c +++ b/drivers/usb/typec/altmodes/displayport.c @@ -766,8 +766,10 @@ int dp_altmode_probe(struct typec_altmode *alt) struct dp_altmode *dp; /* Port can only be DFP_U. */ - if (typec_altmode_get_data_role(alt) != TYPEC_HOST) + if (typec_altmode_get_data_role(alt) != TYPEC_HOST) { + dev_err(alt->dev.parent->parent, "Cannot probe DP AltMode as data-role is not HOST\n"); return -EPROTO; + } /* Make sure we have compatible pin configurations */ if (!(DP_CAP_PIN_ASSIGN_DFP_D(port->vdo) & @@ -775,6 +777,7 @@ int dp_altmode_probe(struct typec_altmode *alt) !(DP_CAP_PIN_ASSIGN_UFP_D(port->vdo) & DP_CAP_PIN_ASSIGN_DFP_D(alt->vdo))) { typec_altmode_put_plug(plug); + dev_err(alt->dev.parent->parent, "Cannot probe DP AltMode due to incorrect pin assignment\n"); return -ENODEV; } From 4eab616471edab98a088f8e6fc8e86867e0eeaed Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Feb 2026 15:55:04 +0100 Subject: [PATCH 102/218] drm/bridge: synopsys: dw-dp: Simplify driver data setting There is no need to get the platform device just for setting up the driver data. Simplify the logic. Reviewed-by: Andy Yan Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 94c1036d9d9f44..56658a45b77327 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -75,7 +75,6 @@ static const struct drm_encoder_helper_funcs dw_dp_encoder_helper_funcs = { static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void *data) { - struct platform_device *pdev = to_platform_device(dev); const struct dw_dp_plat_data *plat_data; struct drm_device *drm_dev = data; struct rockchip_dw_dp *dp; @@ -88,7 +87,7 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * return -ENOMEM; dp->dev = dev; - platform_set_drvdata(pdev, dp); + dev_set_drvdata(dev, dp); plat_data = of_device_get_match_data(dev); if (!plat_data) From bc9ed49cebcfeb9dc2b92a356691da606e16c1cc Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 6 Mar 2026 14:26:19 +0100 Subject: [PATCH 103/218] drm/bridge: synopsys: dw-dp: Support MEDIA_BUS_FMT_FIXED Add support for MEDIA_BUS_FMT_FIXED, which is e.g. requested for USB-C DP chains as the last bridge in the chain (aux-hpd-bridge) does not implement atomic_get_output_bus_fmts(), which results in the generic drm_atomic_bridge_chain_select_bus_fmts() code using MEDIA_BUS_FMT_FIXED instead. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 6211ba6ba7bd6b..fc4ddb7792d70f 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1528,6 +1528,7 @@ static int dw_dp_bridge_atomic_check(struct drm_bridge *bridge, struct drm_connector_state *conn_state) { struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode; + unsigned int out_bus_format = bridge_state->output_bus_cfg.format; struct dw_dp *dp = bridge_to_dp(bridge); struct dw_dp_bridge_state *state; const struct dw_dp_output_format *fmt; @@ -1538,7 +1539,10 @@ static int dw_dp_bridge_atomic_check(struct drm_bridge *bridge, state = to_dw_dp_bridge_state(bridge_state); mode = &state->mode; - fmt = dw_dp_get_output_format(bridge_state->output_bus_cfg.format); + if (out_bus_format == MEDIA_BUS_FMT_FIXED) + out_bus_format = MEDIA_BUS_FMT_RGB888_1X24; + + fmt = dw_dp_get_output_format(out_bus_format); if (!fmt) return -EINVAL; From af77ce1a4b64e8d5173626e1f4f26dbcdb270429 Mon Sep 17 00:00:00 2001 From: Luca Ceresoli Date: Mon, 11 May 2026 18:40:06 +0200 Subject: [PATCH 104/218] drm/bridge: add of_drm_get_bridge_by_endpoint() drm_of_find_panel_or_bridge() is widely used, but many callers pass NULL into the @panel or the @bridge arguments, thus making a very partial usage of this rather complex function. Besides, the bridge returned in @bridge is not refcounted, thus making this API unsafe when DRM bridge hotplug will be introduced. Solve both issues for the cases of calls to drm_of_find_panel_or_bridge() with a NULL @panel pointer by adding a new function that only looks for bridges (and is thus much simpler) and increments the refcount of the returned bridge. The new function is identical to drm_of_find_panel_or_bridge() except it: - handles bridge refcounting: uses of_drm_find_and_get_bridge() instead of of_drm_find_bridge() internally to return a refcounted bridge - is simpler to use: just takes no @panel parameter, returns the pointer in the return value instead of a double pointer argument - has a simpler implementation: it is equal to drm_of_find_panel_or_bridge() after removing the code that becomes dead when @panel == NULL Also add this function to drm_bridge.c and not drm_of.c because it returns bridges only. Reviewed-by: Dmitry Baryshkov Reviewed-by: Laurent Pinchart Link: https://patch.msgid.link/20260511-drm-bridge-alloc-getput-panel_or_bridge-v6-2-f61c9e498b3f@bootlin.com Signed-off-by: Luca Ceresoli [taken from upstream 03d1078112fddd706b2c1e4a7d98cf18700eb5df] Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/drm_bridge.c | 41 ++++++++++++++++++++++++++++++++++++ include/drm/drm_bridge.h | 7 ++++++ 2 files changed, 48 insertions(+) diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index d6f512b733896d..38d52004356dc9 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -1581,6 +1581,47 @@ struct drm_bridge *of_drm_find_bridge(struct device_node *np) return bridge; } EXPORT_SYMBOL(of_drm_find_bridge); + +/** + * of_drm_get_bridge_by_endpoint - return DRM bridge connected to a port/endpoint + * @np: device tree node containing output ports + * @port: port in the device tree node, or -1 for the first port found + * @endpoint: endpoint in the device tree node, or -1 for the first endpoint found + * + * Given a DT node's port and endpoint number, find the connected node and + * return the associated drm_bridge device. + * + * The refcount of the returned bridge is incremented. Use drm_bridge_put() + * when done with it. + * + * Returns a pointer to the connected drm_bridge, or a negative error on failure + */ +struct drm_bridge *of_drm_get_bridge_by_endpoint(const struct device_node *np, + int port, int endpoint) +{ + struct drm_bridge *bridge; + + /* + * of_graph_get_remote_node() produces a noisy error message if port + * node isn't found and the absence of the port is a legit case here, + * so at first we silently check whether graph is present in the + * device-tree node. + */ + if (!of_graph_is_present(np)) + return ERR_PTR(-ENODEV); + + struct device_node *remote __free(device_node) = + of_graph_get_remote_node(np, port, endpoint); + if (!remote) + return ERR_PTR(-ENODEV); + + bridge = of_drm_find_and_get_bridge(remote); + if (!bridge) + return ERR_PTR(-EPROBE_DEFER); + + return bridge; +} +EXPORT_SYMBOL_GPL(of_drm_get_bridge_by_endpoint); #endif /** diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index a8d67bd9ee505f..925a6f52d7cee2 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -1327,6 +1327,8 @@ int drm_bridge_attach(struct drm_encoder *encoder, struct drm_bridge *bridge, #ifdef CONFIG_OF struct drm_bridge *of_drm_find_and_get_bridge(struct device_node *np); struct drm_bridge *of_drm_find_bridge(struct device_node *np); +struct drm_bridge *of_drm_get_bridge_by_endpoint(const struct device_node *np, + int port, int endpoint); #else static inline struct drm_bridge *of_drm_find_and_get_bridge(struct device_node *np) { @@ -1336,6 +1338,11 @@ static inline struct drm_bridge *of_drm_find_bridge(struct device_node *np) { return NULL; } +static inline struct drm_bridge *of_drm_get_bridge_by_endpoint(const struct device_node *np, + int port, int endpoint) +{ + return ERR_PTR(-ENODEV); +} #endif static inline bool drm_bridge_is_last(struct drm_bridge *bridge) From f784f81958c4174160249a3ad4a03d0275b5c708 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:12:53 +0100 Subject: [PATCH 105/218] drm/bridge: synopsys: dw-dp: Add follow-up bridge support Add support to use USB-C connectors with the DP altmode helper code on devicetree based platforms. To get this working there must be a DRM bridge chain from the DisplayPort controller to the USB-C connector. E.g. on Rockchip RK3576: root@rk3576 # cat /sys/kernel/debug/dri/0/encoder-0/bridges bridge[0]: dw_dp_bridge_funcs refcount: 7 type: [10] DP OF: /soc/dp@27e40000:rockchip,rk3576-dp ops: [0x47] detect edid hpd bridge[1]: drm_aux_bridge_funcs refcount: 4 type: [0] Unknown OF: /soc/phy@2b010000:rockchip,rk3576-usbdp-phy ops: [0x0] bridge[2]: drm_aux_hpd_bridge_funcs refcount: 5 type: [10] DP OF: /soc/i2c@2ac50000/typec-portc@22/connector:usb-c-connector ops: [0x4] hpd Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index fc4ddb7792d70f..6150d7786bf777 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1970,7 +1970,7 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, { struct platform_device *pdev = to_platform_device(dev); struct dw_dp *dp; - struct drm_bridge *bridge; + struct drm_bridge *bridge, *next_bridge; void __iomem *res; int ret; @@ -2064,6 +2064,20 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, goto unregister_aux; } + next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 1, 0); + if (IS_ERR(next_bridge)) { + ret = PTR_ERR(next_bridge); + dev_err_probe(dev, ret, "failed to get follow-up bridge.\n"); + goto unregister_aux; + } + + ret = drm_bridge_attach(encoder, next_bridge, bridge, + DRM_BRIDGE_ATTACH_NO_CONNECTOR); + if (ret) { + dev_err_probe(dev, ret, "Failed to attach next bridge\n"); + goto unregister_aux; + } + dw_dp_init_hw(dp); ret = phy_init(dp->phy); From d4eaf9a4181a15a7f7e8dd23cb7719197bfbffea Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 26 Feb 2026 16:42:44 +0100 Subject: [PATCH 106/218] drm/bridge: Add out-of-band HPD notify handler For DP bridges, that can be used for DP AltMode, it might be necessary to enforce HPD status. There is an existing ->oob_hotplug_event() on the DRM connector, but it currently just calls into hpd_notify(). As DP bridge drivers usually also implement .detect and that also generates calls into hpd_notify, this is a bad place to force the HPD status as the follow-up detect call might force it off again resulting in all follow-up calls to the detection routine also failing. Avoid this by having a dedicated function for OOB events. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/display/drm_bridge_connector.c | 6 ++++++ include/drm/drm_bridge.h | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c index 39cc18f78eda11..4333cc66073ecf 100644 --- a/drivers/gpu/drm/display/drm_bridge_connector.c +++ b/drivers/gpu/drm/display/drm_bridge_connector.c @@ -180,6 +180,12 @@ static void drm_bridge_connector_oob_hotplug_event(struct drm_connector *connect struct drm_bridge_connector *bridge_connector = to_drm_bridge_connector(connector); + /* Notify all bridges in the pipeline of hotplug events. */ + drm_for_each_bridge_in_chain_scoped(bridge_connector->encoder, bridge) { + if (bridge->funcs->oob_notify) + bridge->funcs->oob_notify(bridge, connector, status); + } + drm_bridge_connector_handle_hpd(bridge_connector, status); } diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 925a6f52d7cee2..2946caf00bb9de 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -646,6 +646,20 @@ struct drm_bridge_funcs { */ void (*hpd_disable)(struct drm_bridge *bridge); + /** + * @oob_notify: + * + * Notify the bridge of out of band hot plug detection. + * + * This callback is optional, it may be implemented by bridges that + * need to be notified of display connection or disconnection for + * internal reasons. One use case is to force the DP controllers HPD + * signal for USB-C DP AltMode. + */ + void (*oob_notify)(struct drm_bridge *bridge, + struct drm_connector *connector, + enum drm_connector_status status); + /** * @hdmi_tmds_char_rate_valid: * From 71fb499659df7567642a02ca7709d1c9b75774db Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:29:45 +0100 Subject: [PATCH 107/218] drm/bridge: synopsys: dw-dp: Support software triggered OOB HPD Add support for USB-C DP AltMode out-of-band hotplug handling. The handling itself is implemented in the platform specific driver as the registers to force HPD state are not part of the Designware DisplayPort IP itself. Instead the platform integration might provide the necessary functionality to mux the HPD signal. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 38 ++++++++++++++++++++++++- include/drm/bridge/dw_dp.h | 3 ++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 6150d7786bf777..d613f90c8fba88 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1817,6 +1817,19 @@ static struct drm_bridge_state *dw_dp_bridge_atomic_duplicate_state(struct drm_b return &state->base; } +static void dw_dp_bridge_oob_notify(struct drm_bridge *bridge, + struct drm_connector *connector, + enum drm_connector_status status) +{ + bool hpd_high = status != connector_status_disconnected; + struct dw_dp *dp = bridge_to_dp(bridge); + + if (dp->plat_data.hpd_sw_cfg) + dp->plat_data.hpd_sw_cfg(dp->plat_data.data, hpd_high); + else + dev_err_once(dp->dev, "Missing platform handler for OOB HPD handling\n"); +} + static const struct drm_bridge_funcs dw_dp_bridge_funcs = { .atomic_duplicate_state = dw_dp_bridge_atomic_duplicate_state, .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, @@ -1829,6 +1842,7 @@ static const struct drm_bridge_funcs dw_dp_bridge_funcs = { .atomic_disable = dw_dp_bridge_atomic_disable, .detect = dw_dp_bridge_detect, .edid_read = dw_dp_bridge_edid_read, + .oob_notify = dw_dp_bridge_oob_notify, }; static int dw_dp_link_retrain(struct dw_dp *dp) @@ -1965,6 +1979,19 @@ static void dw_dp_phy_exit(void *data) phy_exit(dp->phy); } +static bool dw_dp_is_routed_to_usb_c(struct drm_encoder *encoder) +{ + struct drm_bridge *last_bridge __free(drm_bridge_put) = NULL; + struct fwnode_handle *fwnode; + + last_bridge = drm_bridge_chain_get_last_bridge(encoder); + if (!last_bridge) + return false; + + fwnode = of_fwnode_handle(last_bridge->of_node); + return fwnode_device_is_compatible(fwnode, "usb-c-connector"); +} + struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, const struct dw_dp_plat_data *plat_data) { @@ -1980,7 +2007,9 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, dp->dev = dev; dp->pixel_mode = plat_data->pixel_mode; - + dp->plat_data.hpd_sw_sel = plat_data->hpd_sw_sel; + dp->plat_data.hpd_sw_cfg = plat_data->hpd_sw_cfg; + dp->plat_data.data = plat_data->data; dp->plat_data.max_link_rate = plat_data->max_link_rate; bridge = &dp->bridge; mutex_init(&dp->irq_lock); @@ -2078,6 +2107,13 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, goto unregister_aux; } + if (dw_dp_is_routed_to_usb_c(encoder)) { + dev_dbg(dev, "USB-C mode\n"); + + if (dp->plat_data.hpd_sw_sel) + dp->plat_data.hpd_sw_sel(dp->plat_data.data, 1); + } + dw_dp_init_hw(dp); ret = phy_init(dp->phy); diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index 22105c3e8e4d66..2127afa26b2cfd 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -20,6 +20,9 @@ enum { struct dw_dp_plat_data { u32 max_link_rate; u8 pixel_mode; + void *data; + void (*hpd_sw_sel)(void *data, bool hpd); + void (*hpd_sw_cfg)(void *data, bool hpd); }; struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, From d9c48130d01ad4628d5130d2a78afd5a7c585d59 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:34:43 +0100 Subject: [PATCH 108/218] drm/rockchip: dw_dp: Implement out-of-band HPD handling Implement out-of-band hotplug handling, which will be used to receive external hotplug information from the USB-C state machine. This is currently handled by the USBDP PHY, which brings quite some trouble as the register being accessed requires the power-domain from the DP controller and also requires custom TypeC HPD info parsing in the USBDP PHY driver. In contrast to the USBDP PHY this does not just enable the hotplug signal when a DP AltMode capable adapter is plugged in, but instead properly detects if a cable is plugged in for things like USB-C to HDMI adapters. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 126 ++++++++++++++++++++-- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 56658a45b77327..b62b27df9d675b 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -7,8 +7,11 @@ */ #include +#include +#include #include #include +#include #include #include #include @@ -25,12 +28,54 @@ #include "rockchip_drm_drv.h" #include "rockchip_drm_vop.h" +#define ROCKCHIP_MAX_CTRLS 2 + +#define ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL BIT(10) +#define ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG BIT(11) + +struct rockchip_dw_dp_plat_data { + u8 num_ctrls; + u32 ctrl_ids[ROCKCHIP_MAX_CTRLS]; + u32 max_link_rate; + u8 pixel_mode; + u32 hpd_reg[ROCKCHIP_MAX_CTRLS]; +}; + struct rockchip_dw_dp { struct dw_dp *base; struct device *dev; + const struct rockchip_dw_dp_plat_data *pdata; + struct regmap *vo_grf; struct rockchip_encoder encoder; + int id; + bool hpd_sel; + bool hpd_cfg; }; +static void dw_dp_rockchip_hpd_sw_sel(void *data, bool force_hpd_from_sw) +{ + struct rockchip_dw_dp *dp = data; + u32 hpd_reg = dp->pdata->hpd_reg[dp->id]; + + dp->hpd_sel = force_hpd_from_sw; + + regmap_write(dp->vo_grf, hpd_reg, + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL, dp->hpd_sel)); +} + +static void dw_dp_rockchip_hpd_sw_cfg(void *data, bool hpd) +{ + struct rockchip_dw_dp *dp = data; + u32 hpd_reg = dp->pdata->hpd_reg[dp->id]; + + dev_dbg(dp->dev, "Force HPD connected=%s\n", str_yes_no(hpd)); + + dp->hpd_cfg = hpd; + + regmap_write(dp->vo_grf, hpd_reg, + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG, dp->hpd_cfg)); +} + static int dw_dp_encoder_atomic_check(struct drm_encoder *encoder, struct drm_crtc_state *crtc_state, struct drm_connector_state *conn_state) @@ -73,14 +118,49 @@ static const struct drm_encoder_helper_funcs dw_dp_encoder_helper_funcs = { .atomic_check = dw_dp_encoder_atomic_check, }; +static struct regmap *dp_dp_rockchip_get_vo_grf(struct rockchip_dw_dp *dp) +{ + struct device_node *np = dev_of_node(dp->dev); + struct of_phandle_args args; + struct regmap *regmap; + int ret; + + ret = of_parse_phandle_with_args(np, "phys", "#phy-cells", 0, &args); + if (ret) + return ERR_PTR(-ENODEV); + + /* + * Limit this workaround to RK3576 and RK3588, new platforms should + * add a VO GRF phandle in the DisplayPort DT node. + */ + if (!of_device_is_compatible(args.np, "rockchip,rk3576-usbdp-phy") && + !of_device_is_compatible(args.np, "rockchip,rk3588-usbdp-phy")) { + regmap = ERR_PTR(-ENODEV); + goto out_put_node; + } + + regmap = syscon_regmap_lookup_by_phandle(args.np, "rockchip,vo-grf"); + +out_put_node: + of_node_put(args.np); + return regmap; +} + static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void *data) { - const struct dw_dp_plat_data *plat_data; + const struct rockchip_dw_dp_plat_data *plat_data_const; + struct platform_device *pdev = to_platform_device(dev); + struct dw_dp_plat_data *plat_data; struct drm_device *drm_dev = data; struct rockchip_dw_dp *dp; struct drm_encoder *encoder; struct drm_connector *connector; - int ret; + struct resource *res; + int ret, id; + + plat_data = drmm_kzalloc(drm_dev, sizeof(*plat_data), GFP_KERNEL); + if (!plat_data) + return -ENOMEM; dp = drmm_kzalloc(drm_dev, sizeof(*dp), GFP_KERNEL); if (!dp) @@ -89,10 +169,38 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * dp->dev = dev; dev_set_drvdata(dev, dp); - plat_data = of_device_get_match_data(dev); - if (!plat_data) + plat_data_const = device_get_match_data(dev); + if (!plat_data_const) return -ENODEV; + dp->pdata = plat_data_const; + + res = platform_get_mem_or_io(pdev, 0); + if (IS_ERR(res)) + return PTR_ERR(res); + + /* find the DisplayPort ID from the io address */ + dp->id = -ENODEV; + for (id = 0; id < plat_data_const->num_ctrls; id++) { + if (res->start == plat_data_const->ctrl_ids[id]) { + dp->id = id; + break; + } + } + + if (dp->id < 0) + return dp->id; + + dp->vo_grf = dp_dp_rockchip_get_vo_grf(dp); + if (IS_ERR(dp->vo_grf)) + return PTR_ERR(dp->vo_grf); + + plat_data->max_link_rate = plat_data_const->max_link_rate; + plat_data->pixel_mode = plat_data_const->pixel_mode; + plat_data->hpd_sw_sel = dw_dp_rockchip_hpd_sw_sel; + plat_data->hpd_sw_cfg = dw_dp_rockchip_hpd_sw_cfg; + plat_data->data = dp; + encoder = &dp->encoder.encoder; encoder->possible_crtcs = drm_of_find_possible_crtcs(drm_dev, dev->of_node); rockchip_drm_encoder_set_crtc_endpoint_id(&dp->encoder, dev->of_node, 0, 0); @@ -142,14 +250,20 @@ static void dw_dp_remove(struct platform_device *pdev) component_del(&pdev->dev, &dw_dp_rockchip_component_ops); } -static const struct dw_dp_plat_data rk3588_dp_plat_data = { +static const struct rockchip_dw_dp_plat_data rk3588_dp_plat_data = { + .num_ctrls = 2, + .ctrl_ids = {0xfde50000, 0xfde60000}, .max_link_rate = 810000, .pixel_mode = DW_DP_MP_QUAD_PIXEL, + .hpd_reg = {0x0000, 0x0008}, }; -static const struct dw_dp_plat_data rk3576_dp_plat_data = { +static const struct rockchip_dw_dp_plat_data rk3576_dp_plat_data = { + .num_ctrls = 1, + .ctrl_ids = {0x27e40000}, .max_link_rate = 810000, .pixel_mode = DW_DP_MP_DUAL_PIXEL, + .hpd_reg = {0x0000}, }; static const struct of_device_id dw_dp_of_match[] = { From 46018a47d57a0117434206e75fb843be837b3228 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:40:50 +0100 Subject: [PATCH 109/218] drm/bridge: synopsys: dw-dp: Add Runtime PM support Add runtime PM stubs to the Synopsys DesignWare DisplayPort bridge driver. Support is not enabled automatically and must be hooked up in the vendor specific glue code. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 27 +++++++++++++++++++++++++ include/drm/bridge/dw_dp.h | 3 +++ 2 files changed, 30 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index d613f90c8fba88..c721453ccdf324 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1465,6 +1465,8 @@ static ssize_t dw_dp_aux_transfer(struct drm_dp_aux *aux, if (WARN_ON(msg->size > 16)) return -E2BIG; + ACQUIRE(pm_runtime_active_auto, pm)(dp->dev); + switch (msg->request & ~DP_AUX_I2C_MOT) { case DP_AUX_NATIVE_WRITE: case DP_AUX_I2C_WRITE: @@ -1655,6 +1657,8 @@ static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_connector_state *conn_state; int ret; + pm_runtime_get_sync(dp->dev); + connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); if (!connector) { dev_err(dp->dev, "failed to get connector\n"); @@ -1709,6 +1713,7 @@ static void dw_dp_bridge_atomic_disable(struct drm_bridge *bridge, dw_dp_link_disable(dp); bitmap_zero(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); dw_dp_reset(dp); + pm_runtime_put_autosuspend(dp->dev); } static bool dw_dp_hpd_detect_link(struct dw_dp *dp, struct drm_connector *connector) @@ -1729,6 +1734,8 @@ static enum drm_connector_status dw_dp_bridge_detect(struct drm_bridge *bridge, { struct dw_dp *dp = bridge_to_dp(bridge); + ACQUIRE(pm_runtime_active_auto, pm)(dp->dev); + if (!dw_dp_hpd_detect(dp)) return connector_status_disconnected; @@ -2153,6 +2160,26 @@ void dw_dp_unbind(struct dw_dp *dp) } EXPORT_SYMBOL_GPL(dw_dp_unbind); +int dw_dp_runtime_suspend(struct dw_dp *dp) +{ + clk_disable_unprepare(dp->aux_clk); + clk_disable_unprepare(dp->apb_clk); + + return 0; +} +EXPORT_SYMBOL_GPL(dw_dp_runtime_suspend); + +int dw_dp_runtime_resume(struct dw_dp *dp) +{ + clk_prepare_enable(dp->apb_clk); + clk_prepare_enable(dp->aux_clk); + + dw_dp_init_hw(dp); + + return 0; +} +EXPORT_SYMBOL_GPL(dw_dp_runtime_resume); + MODULE_AUTHOR("Andy Yan "); MODULE_DESCRIPTION("DW DP Core Library"); MODULE_LICENSE("GPL"); diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index 2127afa26b2cfd..3037e0290861eb 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -28,4 +28,7 @@ struct dw_dp_plat_data { struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, const struct dw_dp_plat_data *plat_data); void dw_dp_unbind(struct dw_dp *dp); + +int dw_dp_runtime_suspend(struct dw_dp *dp); +int dw_dp_runtime_resume(struct dw_dp *dp); #endif /* __DW_DP__ */ From 0e6277637d74d01f82ce5291a6946544c0ca67b9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:44:22 +0100 Subject: [PATCH 110/218] drm/rockchip: dw_dp: Add runtime PM support Add support for runtime PM to the Rockchip RK3576/3588 Synopsys DesignWare DisplayPort driver. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index b62b27df9d675b..b02939107ad1de 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,8 @@ static void dw_dp_rockchip_hpd_sw_sel(void *data, bool force_hpd_from_sw) dp->hpd_sel = force_hpd_from_sw; + ACQUIRE(pm_runtime_active_auto, pm)(dp->dev); + regmap_write(dp->vo_grf, hpd_reg, FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL, dp->hpd_sel)); } @@ -72,6 +75,8 @@ static void dw_dp_rockchip_hpd_sw_cfg(void *data, bool hpd) dp->hpd_cfg = hpd; + ACQUIRE(pm_runtime_active_auto, pm)(dp->dev); + regmap_write(dp->vo_grf, hpd_reg, FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG, dp->hpd_cfg)); } @@ -214,6 +219,12 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * if (IS_ERR(dp->base)) return PTR_ERR(dp->base); + pm_runtime_use_autosuspend(dev); + pm_runtime_set_autosuspend_delay(dev, 500); + ret = devm_pm_runtime_enable(dev); + if (ret) + return dev_err_probe(dev, ret, "failed to enable runtime PM\n"); + connector = drm_bridge_connector_init(drm_dev, encoder); if (IS_ERR(connector)) ret = dev_err_probe(dev, PTR_ERR(connector), @@ -250,6 +261,34 @@ static void dw_dp_remove(struct platform_device *pdev) component_del(&pdev->dev, &dw_dp_rockchip_component_ops); } +static int dw_dp_rockchip_runtime_suspend(struct device *dev) +{ + struct rockchip_dw_dp *dp = dev_get_drvdata(dev); + + return dw_dp_runtime_suspend(dp->base); +} + +static int dw_dp_rockchip_runtime_resume(struct device *dev) +{ + struct rockchip_dw_dp *dp = dev_get_drvdata(dev); + u32 hpd_reg = dp->pdata->hpd_reg[dp->id]; + int ret; + + ret = dw_dp_runtime_resume(dp->base); + if (ret) + return ret; + + regmap_write(dp->vo_grf, hpd_reg, + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL, dp->hpd_sel) | + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG, dp->hpd_cfg)); + + return 0; +} + +static const struct dev_pm_ops dw_dp_pm_ops = { + RUNTIME_PM_OPS(dw_dp_rockchip_runtime_suspend, dw_dp_rockchip_runtime_resume, NULL) +}; + static const struct rockchip_dw_dp_plat_data rk3588_dp_plat_data = { .num_ctrls = 2, .ctrl_ids = {0xfde50000, 0xfde60000}, @@ -284,5 +323,6 @@ struct platform_driver dw_dp_driver = { .driver = { .name = "dw-dp", .of_match_table = dw_dp_of_match, + .pm = pm_ptr(&dw_dp_pm_ops), }, }; From 4424c8f6de79f6b8192e92a7a8097dd48dbbd2df Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 23 Jan 2026 23:10:22 +0100 Subject: [PATCH 111/218] [RFC] dt-bindings: display: rockchip: dw-dp: fix sound DAI cells The RK3588 and RK3576 DesignWare DisplayPort controllers both have two possible DAI interfaces: I2S and S/PDIF. Thus it is needed to have an argument to select the right interface. In case of RK3576 this is not enough though. The RK3576 has the same IP as RK3588, but configured with Multi Stream Transport (MST) enabled for up to 3 displays and thus has a total of 6 DAI interfaces (I2S and S/PDIF for each possible stream. Meanwhile the RK3588 does not support MST and thus has only 2 DAI interfaces. The binding update right now only supports the simple single stream transport (SST) setup. To avoid further DT ABI breakage (or complicated bindings supporting different number of arguments), it's probably a good idea to take MST into account now even though the upstream Linux driver does not yet support it. I see two options: 1. Adding yet another cell, so that we have the following: <&dp_ctrl [display_stream] [i2s_or_spdif]>; potentially append extra input ports for MST video data to existing ports node (e.g. port@2). I would only handle the sound DAI part in my patch and basically use '0' for the display stream and just leave the option of using '1' or '2' once MST support is added. 2. The vendor kernel creates a sub-node for each supported display stream and puts the ports mapping as well as the DAI reference into that. This bundles all information for one display stream together, which creates a clean look but the subnode does not really describe any real thing in the hardware. As upstream MST support seems to be quite limited, I wish for some feedback about the preferred way to handle this. Signed-off-by: Sebastian Reichel --- .../devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml b/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml index 2b0d9e23e9432f..1303d0e2145a97 100644 --- a/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml +++ b/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml @@ -83,7 +83,8 @@ properties: maxItems: 1 "#sound-dai-cells": - const: 0 + const: 1 + description: 0 for I2S, 1 for SPDIF required: - compatible @@ -144,7 +145,7 @@ examples: resets = <&cru SRST_DP0>; phys = <&usbdp_phy0 PHY_TYPE_DP>; power-domains = <&power RK3588_PD_VO0>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; ports { #address-cells = <1>; From 215bbad29f6a3fec7a4a9c183ec1ebe32ed3283e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 05:22:05 +0100 Subject: [PATCH 112/218] drm/bridge: synopsys: dw-dp: Add audio support Implement audio support for the Synopsys DesignWare DisplayPort controller. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 221 +++++++++++++++++++++++- 1 file changed, 220 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index c721453ccdf324..3ac46e4f43a7c2 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -23,17 +23,21 @@ #include #include #include +#include #include #include #include #include #include +#include + #define DW_DP_VERSION_NUMBER 0x0000 #define DW_DP_VERSION_TYPE 0x0004 #define DW_DP_ID 0x0008 #define DW_DP_CONFIG_REG1 0x0100 +#define AUDIO_SELECT GENMASK(2, 1) #define DW_DP_CONFIG_REG2 0x0104 #define DW_DP_CONFIG_REG3 0x0108 @@ -110,6 +114,10 @@ #define HBR_MODE_ENABLE BIT(10) #define AUDIO_DATA_WIDTH GENMASK(9, 5) #define AUDIO_DATA_IN_EN GENMASK(4, 1) +#define AUDIO_DATA_IN_EN_CHANNEL12 BIT(0) +#define AUDIO_DATA_IN_EN_CHANNEL34 BIT(1) +#define AUDIO_DATA_IN_EN_CHANNEL56 BIT(2) +#define AUDIO_DATA_IN_EN_CHANNEL78 BIT(3) #define AUDIO_INF_SELECT BIT(0) #define DW_DP_SDP_VERTICAL_CTRL 0x0500 @@ -253,6 +261,8 @@ #define SDP_REG_BANK_SIZE 16 +#define DW_DP_SDP_VERSION 0x12 + struct dw_dp_link_caps { bool enhanced_framing; bool tps3_supported; @@ -305,6 +315,19 @@ struct dw_dp_hotplug { bool long_hpd; }; +enum dw_dp_audio_interface_support { + DW_DP_AUDIO_I2S_ONLY = 0, + DW_DP_AUDIO_SPDIF_ONLY = 1, + DW_DP_AUDIO_I2S_AND_SPDIF = 2, + DW_DP_AUDIO_NONE = 3, +}; + +enum dw_dp_audio_interface { + DW_DP_AUDIO_I2S = 0, + DW_DP_AUDIO_SPDIF = 1, + DW_DP_AUDIO_UNUSED, +}; + struct dw_dp { struct drm_bridge bridge; struct device *dev; @@ -320,6 +343,8 @@ struct dw_dp { int irq; struct work_struct hpd_work; struct dw_dp_hotplug hotplug; + enum dw_dp_audio_interface audio_interface; + int audio_channels; /* Serialize hpd status access */ struct mutex irq_lock; @@ -1837,6 +1862,186 @@ static void dw_dp_bridge_oob_notify(struct drm_bridge *bridge, dev_err_once(dp->dev, "Missing platform handler for OOB HPD handling\n"); } +static int dw_dp_audio_infoframe_send(struct dw_dp *dp) +{ + struct hdmi_audio_infoframe frame; + struct dw_dp_sdp sdp; + int ret; + + ret = hdmi_audio_infoframe_init(&frame); + if (ret < 0) + return ret; + + frame.coding_type = HDMI_AUDIO_CODING_TYPE_STREAM; + frame.sample_frequency = HDMI_AUDIO_SAMPLE_FREQUENCY_STREAM; + frame.sample_size = HDMI_AUDIO_SAMPLE_SIZE_STREAM; + frame.channels = dp->audio_channels; + + ret = hdmi_audio_infoframe_pack_for_dp(&frame, &sdp.base, DW_DP_SDP_VERSION); + if (ret < 0) + return ret; + + sdp.flags = DW_DP_SDP_VERTICAL_INTERVAL; + + return dw_dp_send_sdp(dp, &sdp); +} + +static int dw_dp_audio_startup(struct drm_bridge *bridge, + struct drm_connector *connector) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + dev_dbg(dp->dev, "audio startup\n"); + pm_runtime_get_sync(dp->dev); + + return 0; +} + +static void dw_dp_audio_unprepare(struct drm_bridge *bridge, + struct drm_connector *connector) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + /* Disable all audio streams */ + regmap_update_bits(dp->regmap, DW_DP_AUD_CONFIG1, AUDIO_DATA_IN_EN, + FIELD_PREP(AUDIO_DATA_IN_EN, 0)); + + if (dp->audio_interface == DW_DP_AUDIO_SPDIF) + clk_disable_unprepare(dp->spdif_clk); + else if (dp->audio_interface == DW_DP_AUDIO_I2S) + clk_disable_unprepare(dp->i2s_clk); + + dp->audio_interface = DW_DP_AUDIO_UNUSED; +} + +static int dw_dp_audio_prepare(struct drm_bridge *bridge, + struct drm_connector *connector, + struct hdmi_codec_daifmt *daifmt, + struct hdmi_codec_params *params) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + u8 audio_data_in_en, supported_audio_interfaces; + u32 cfg1; + int ret; + + /* + * prepare might be called multiple times, so release the clocks + * from previous calls to keep the calls in balance. + */ + if (dp->audio_interface != DW_DP_AUDIO_UNUSED) + dw_dp_audio_unprepare(bridge, connector); + + dp->audio_channels = params->cea.channels; + switch (params->cea.channels) { + case 1: + case 2: + audio_data_in_en = AUDIO_DATA_IN_EN_CHANNEL12; + break; + case 8: + audio_data_in_en = AUDIO_DATA_IN_EN_CHANNEL12 | + AUDIO_DATA_IN_EN_CHANNEL34 | + AUDIO_DATA_IN_EN_CHANNEL56 | + AUDIO_DATA_IN_EN_CHANNEL78; + break; + default: + dev_err(dp->dev, "invalid audio channels %d\n", dp->audio_channels); + return -EINVAL; + } + + switch (daifmt->fmt) { + case HDMI_SPDIF: + dp->audio_interface = DW_DP_AUDIO_SPDIF; + break; + case HDMI_I2S: + /* + * It is recommended to use SPDIF instead of I2S, since I2S mode requires + * manually inserting PCUV control bits from userspace and this is done + * automatically in hardware for SPDIF mode. + */ + dp->audio_interface = DW_DP_AUDIO_I2S; + break; + default: + dev_err(dp->dev, "invalid DAI format %d\n", daifmt->fmt); + return -EINVAL; + } + + regmap_read(dp->regmap, DW_DP_CONFIG_REG1, &cfg1); + supported_audio_interfaces = FIELD_GET(AUDIO_SELECT, cfg1); + + if (supported_audio_interfaces != DW_DP_AUDIO_I2S_AND_SPDIF && + supported_audio_interfaces != dp->audio_interface) { + dev_err(dp->dev, "unsupported DAI %d\n", daifmt->fmt); + return -EINVAL; + } + + clk_prepare_enable(dp->spdif_clk); + clk_prepare_enable(dp->i2s_clk); + + regmap_update_bits(dp->regmap, DW_DP_AUD_CONFIG1, + AUDIO_DATA_IN_EN | NUM_CHANNELS | AUDIO_DATA_WIDTH | + AUDIO_INF_SELECT | HBR_MODE_ENABLE, + FIELD_PREP(AUDIO_DATA_IN_EN, audio_data_in_en) | + FIELD_PREP(NUM_CHANNELS, dp->audio_channels - 1) | + FIELD_PREP(AUDIO_DATA_WIDTH, params->sample_width) | + FIELD_PREP(AUDIO_INF_SELECT, dp->audio_interface) | + FIELD_PREP(HBR_MODE_ENABLE, 0)); + + /* Wait for inf switch */ + usleep_range(20, 40); + + if (dp->audio_interface == DW_DP_AUDIO_I2S) + clk_disable_unprepare(dp->spdif_clk); + else if (dp->audio_interface == DW_DP_AUDIO_SPDIF) + clk_disable_unprepare(dp->i2s_clk); + + /* + * Send audio stream during vertical and horizontal blanking periods. + * Send out audio timestamp SDP once per video frame during the vertical + * blanking period + */ + regmap_update_bits(dp->regmap, DW_DP_SDP_VERTICAL_CTRL, + EN_AUDIO_STREAM_SDP | EN_AUDIO_TIMESTAMP_SDP, + FIELD_PREP(EN_AUDIO_STREAM_SDP, 1) | + FIELD_PREP(EN_AUDIO_TIMESTAMP_SDP, 1)); + regmap_update_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, + EN_AUDIO_STREAM_SDP, + FIELD_PREP(EN_AUDIO_STREAM_SDP, 1)); + + ret = dw_dp_audio_infoframe_send(dp); + if (ret < 0) + dev_err(dp->dev, "failed to send audio infoframe\n"); + + dev_dbg(dp->dev, "audio prepare with %d channels using DAI=%d\n", + dp->audio_channels, dp->audio_interface); + + return 0; +} + +static void dw_dp_audio_shutdown(struct drm_bridge *bridge, + struct drm_connector *connector) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + dev_dbg(dp->dev, "audio shutdown\n"); + + dw_dp_audio_unprepare(bridge, connector); + pm_runtime_put_autosuspend(dp->dev); +} + +static int dw_dp_audio_mute_stream(struct drm_bridge *bridge, + struct drm_connector *connector, + bool enable, int direction) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + dev_dbg(dp->dev, "audio %smute\n", enable ? "" : "un"); + + regmap_update_bits(dp->regmap, DW_DP_AUD_CONFIG1, AUDIO_MUTE, + FIELD_PREP(AUDIO_MUTE, enable)); + + return 0; +} + static const struct drm_bridge_funcs dw_dp_bridge_funcs = { .atomic_duplicate_state = dw_dp_bridge_atomic_duplicate_state, .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, @@ -1850,6 +2055,11 @@ static const struct drm_bridge_funcs dw_dp_bridge_funcs = { .detect = dw_dp_bridge_detect, .edid_read = dw_dp_bridge_edid_read, .oob_notify = dw_dp_bridge_oob_notify, + + .dp_audio_startup = dw_dp_audio_startup, + .dp_audio_prepare = dw_dp_audio_prepare, + .dp_audio_shutdown = dw_dp_audio_shutdown, + .dp_audio_mute_stream = dw_dp_audio_mute_stream, }; static int dw_dp_link_retrain(struct dw_dp *dp) @@ -2076,10 +2286,19 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, } bridge->of_node = dev->of_node; - bridge->ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID | DRM_BRIDGE_OP_HPD; + bridge->ops = DRM_BRIDGE_OP_DP_AUDIO | + DRM_BRIDGE_OP_DETECT | + DRM_BRIDGE_OP_EDID | + DRM_BRIDGE_OP_HPD; bridge->type = DRM_MODE_CONNECTOR_DisplayPort; bridge->ycbcr_420_allowed = true; + dp->audio_interface = DW_DP_AUDIO_UNUSED; + bridge->hdmi_audio_dev = dev; + bridge->hdmi_audio_max_i2s_playback_channels = 8; + bridge->hdmi_audio_dai_port = 1; + bridge->hdmi_audio_spdif_playback = true; + ret = devm_drm_bridge_add(dev, bridge); if (ret) return ERR_PTR(ret); From 72ec117f26d86873f5c61996ec015f502367ffcb Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:01:21 +0100 Subject: [PATCH 113/218] arm64: dts: rockchip: Add DP sound support to RK3576 device tree Add support for enabling sound for the DisplayPort to the RK3576 DT. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index 28175d8200d57c..f62a0d01fe0549 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -344,6 +344,21 @@ }; }; + dp0_sound: dp0-sound { + compatible = "simple-audio-card"; + simple-audio-card,mclk-fs = <512>; + simple-audio-card,name = "DP0"; + status = "disabled"; + + simple-audio-card,codec { + sound-dai = <&dp 1>; + }; + + simple-audio-card,cpu { + sound-dai = <&spdif_tx3>; + }; + }; + gpu_opp_table: opp-table-gpu { compatible = "operating-points-v2"; @@ -1508,6 +1523,7 @@ resets = <&cru SRST_DP0>; phys = <&usbdp_phy PHY_TYPE_DP>; power-domains = <&power RK3576_PD_VO1>; + #sound-dai-cells = <1>; status = "disabled"; ports { From 048cd4b2164b40ea78bf815fa2256cc8c6c6f518 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:02:48 +0100 Subject: [PATCH 114/218] arm64: dts: rockchip: Add DP audio for ArmSom Sige5 Add audio support to the USB-C DisplayPort Alternate Mode. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index 411253b6eff038..b76b9c9fe0110b 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -288,6 +288,10 @@ }; }; +&dp0_sound { + status = "okay"; +}; + &gmac0 { phy-mode = "rgmii-id"; clock_in_out = "output"; @@ -966,6 +970,10 @@ status = "okay"; }; +&spdif_tx3 { + status = "okay"; +}; + &u2phy0 { status = "okay"; }; From 0d1ecfcc0dde7be3bb69a5c1d1994d67138fdd6f Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 22:22:54 +0100 Subject: [PATCH 115/218] arm64: dts: rockchip: Fix DP sound-dai-cells on RK3588 The DP controller has a I2S and SPDIF interface and thus needs one cell to specify the interface being used. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3588-base.dtsi | 2 +- arch/arm64/boot/dts/rockchip/rk3588-extra.dtsi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi index e8855b26267d88..43c81727354fab 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1946,7 +1946,7 @@ phys = <&usbdp_phy0 PHY_TYPE_DP>; power-domains = <&power RK3588_PD_VO0>; resets = <&cru SRST_DP0>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; status = "disabled"; ports { diff --git a/arch/arm64/boot/dts/rockchip/rk3588-extra.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-extra.dtsi index a2640014ee0421..b4d7843918cce7 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-extra.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-extra.dtsi @@ -223,7 +223,7 @@ phys = <&usbdp_phy1 PHY_TYPE_DP>; power-domains = <&power RK3588_PD_VO0>; resets = <&cru SRST_DP1>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; status = "disabled"; ports { From 398a951c6c0fda3fa4ae21b7f4f05d9b681b3314 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 16:23:15 +0200 Subject: [PATCH 116/218] usb: typec: tcpm: add device managed port registration A few drivers would benefit from having device managed TCPM port registration, so that their probe routine does not mix device managed and non-device managed calls. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 24 ++++++++++++++++++++++++ include/linux/usb/tcpm.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index b8c36f9a880527..0ab22eafaf35f0 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -8659,6 +8659,30 @@ void tcpm_unregister_port(struct tcpm_port *port) } EXPORT_SYMBOL_GPL(tcpm_unregister_port); +static void devm_tcpm_unregister_port(void *data) +{ + struct tcpm_port *port = data; + tcpm_unregister_port(port); +} + +struct tcpm_port *devm_tcpm_register_port(struct device *dev, + struct tcpc_dev *tcpc) +{ + struct tcpm_port *result; + int ret; + + result = tcpm_register_port(dev, tcpc); + if (IS_ERR(result)) + return result; + + ret = devm_add_action_or_reset(dev, devm_tcpm_unregister_port, result); + if (ret < 0) + return ERR_PTR(ret); + + return result; +} +EXPORT_SYMBOL_GPL(devm_tcpm_register_port); + MODULE_AUTHOR("Guenter Roeck "); MODULE_DESCRIPTION("USB Type-C Port Manager"); MODULE_LICENSE("GPL"); diff --git a/include/linux/usb/tcpm.h b/include/linux/usb/tcpm.h index 93079450bba01f..fc60980f9f41e8 100644 --- a/include/linux/usb/tcpm.h +++ b/include/linux/usb/tcpm.h @@ -177,6 +177,9 @@ struct tcpm_port; struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc); void tcpm_unregister_port(struct tcpm_port *port); +struct tcpm_port *devm_tcpm_register_port(struct device *dev, + struct tcpc_dev *tcpc); + void tcpm_vbus_change(struct tcpm_port *port); void tcpm_cc_change(struct tcpm_port *port); void tcpm_sink_frs(struct tcpm_port *port); From 2e23c5adf53ca8c7d27e6db50fab7993ce71999e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 17:41:17 +0200 Subject: [PATCH 117/218] usb: typec: fusb302: Switch to device managed resources The driver's probe routine is currently mixing registration calls for device managed resources with unmananged ones. This results in issues such as resource leaks or use-after-free. Fix this up by fully converting the driver to device managed resources. Resource acquisition has been reordered a bit, so that the work is initialized after the TCPM port is registered as the work uses the TCPM devices. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 112 +++++++++++++++++-------------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index 34354c47c49547..69b1af1358d11f 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -212,20 +213,28 @@ static int fusb302_debug_show(struct seq_file *s, void *v) } DEFINE_SHOW_ATTRIBUTE(fusb302_debug); -static void fusb302_debugfs_init(struct fusb302_chip *chip) +static void fusb302_debugfs_exit(void *data) +{ + struct fusb302_chip *chip = data; + + debugfs_remove(chip->dentry); +} + +static int fusb302_debugfs_init(struct fusb302_chip *chip) { char name[NAME_MAX]; + int ret; + + ret = devm_mutex_init(chip->dev, &chip->logbuffer_lock); + if (ret < 0) + return ret; - mutex_init(&chip->logbuffer_lock); snprintf(name, NAME_MAX, "fusb302-%s", dev_name(chip->dev)); chip->dentry = debugfs_create_dir(name, usb_debug_root); debugfs_create_file("log", S_IFREG | 0444, chip->dentry, chip, &fusb302_debug_fops); -} -static void fusb302_debugfs_exit(struct fusb302_chip *chip) -{ - debugfs_remove(chip->dentry); + return devm_add_action_or_reset(chip->dev, fusb302_debugfs_exit, chip); } #else @@ -233,7 +242,6 @@ static void fusb302_debugfs_exit(struct fusb302_chip *chip) static void fusb302_log(const struct fusb302_chip *chip, const char *fmt, ...) { } static void fusb302_debugfs_init(const struct fusb302_chip *chip) { } -static void fusb302_debugfs_exit(const struct fusb302_chip *chip) { } #endif @@ -1688,6 +1696,13 @@ static struct fwnode_handle *fusb302_fwnode_get(struct device *dev) return fwnode; } +static void fusb302_fwnode_put(void *data) +{ + struct fusb302_chip *chip = data; + + fwnode_handle_put(chip->tcpc_dev.fwnode); +} + static int fusb302_probe(struct i2c_client *client) { struct fusb302_chip *chip; @@ -1708,7 +1723,10 @@ static int fusb302_probe(struct i2c_client *client) chip->i2c_client = client; chip->dev = &client->dev; - mutex_init(&chip->lock); + + ret = devm_mutex_init(dev, &chip->lock); + if (ret < 0) + return ret; /* * Devicetree platforms should get extcon via phandle (not yet @@ -1727,67 +1745,68 @@ static int fusb302_probe(struct i2c_client *client) if (IS_ERR(chip->vbus)) return PTR_ERR(chip->vbus); - chip->wq = create_singlethread_workqueue(dev_name(chip->dev)); + chip->wq = devm_alloc_ordered_workqueue(dev, dev_name(dev), 0); if (!chip->wq) return -ENOMEM; spin_lock_init(&chip->irq_lock); - INIT_WORK(&chip->irq_work, fusb302_irq_work); - INIT_DELAYED_WORK(&chip->bc_lvl_handler, fusb302_bc_lvl_handler_work); init_tcpc_dev(&chip->tcpc_dev); - fusb302_debugfs_init(chip); + + ret = fusb302_debugfs_init(chip); + if (ret < 0) + return ret; if (client->irq) { chip->gpio_int_n_irq = client->irq; } else { ret = init_gpio(chip); if (ret < 0) - goto destroy_workqueue; + return ret; } chip->tcpc_dev.fwnode = fusb302_fwnode_get(dev); if (IS_ERR(chip->tcpc_dev.fwnode)) { ret = PTR_ERR(chip->tcpc_dev.fwnode); - goto destroy_workqueue; + return ret; } + ret = devm_add_action_or_reset(dev, fusb302_fwnode_put, chip); + if (ret < 0) + return ret; + bridge_dev = devm_drm_dp_hpd_bridge_alloc(chip->dev, to_of_node(chip->tcpc_dev.fwnode)); - if (IS_ERR(bridge_dev)) { - ret = dev_err_probe(chip->dev, PTR_ERR(bridge_dev), - "failed to alloc bridge\n"); - goto fwnode_put; - } + if (IS_ERR(bridge_dev)) + return dev_err_probe(chip->dev, PTR_ERR(bridge_dev), + "failed to alloc bridge\n"); - chip->tcpm_port = tcpm_register_port(&client->dev, &chip->tcpc_dev); - if (IS_ERR(chip->tcpm_port)) { - ret = dev_err_probe(dev, PTR_ERR(chip->tcpm_port), + chip->tcpm_port = devm_tcpm_register_port(&client->dev, &chip->tcpc_dev); + if (IS_ERR(chip->tcpm_port)) + return dev_err_probe(dev, PTR_ERR(chip->tcpm_port), "cannot register tcpm port\n"); - goto fwnode_put; - } - ret = devm_drm_dp_hpd_bridge_add(chip->dev, bridge_dev); - if (ret) - goto tcpm_unregister_port; + ret = devm_delayed_work_autocancel(dev, &chip->bc_lvl_handler, + fusb302_bc_lvl_handler_work); + if (ret < 0) + return ret; + + ret = devm_work_autocancel(dev, &chip->irq_work, fusb302_irq_work); + if (ret < 0) + return ret; + + ret = devm_request_threaded_irq(dev, chip->gpio_int_n_irq, NULL, + fusb302_irq_intn, + IRQF_ONESHOT | IRQF_TRIGGER_LOW, + "fsc_interrupt_int_n", chip); + if (ret < 0) + return dev_err_probe(dev, ret, "failed to request IRQ"); - ret = request_threaded_irq(chip->gpio_int_n_irq, NULL, fusb302_irq_intn, - IRQF_ONESHOT | IRQF_TRIGGER_LOW, - "fsc_interrupt_int_n", chip); - if (ret < 0) { - dev_err(dev, "cannot request IRQ for GPIO Int_N, ret=%d", ret); - goto tcpm_unregister_port; - } - enable_irq_wake(chip->gpio_int_n_irq); i2c_set_clientdata(client, chip); - return 0; + ret = devm_drm_dp_hpd_bridge_add(chip->dev, bridge_dev); + if (ret) + return ret; -tcpm_unregister_port: - tcpm_unregister_port(chip->tcpm_port); -fwnode_put: - fwnode_handle_put(chip->tcpc_dev.fwnode); -destroy_workqueue: - fusb302_debugfs_exit(chip); - destroy_workqueue(chip->wq); + enable_irq_wake(chip->gpio_int_n_irq); return ret; } @@ -1797,13 +1816,6 @@ static void fusb302_remove(struct i2c_client *client) struct fusb302_chip *chip = i2c_get_clientdata(client); disable_irq_wake(chip->gpio_int_n_irq); - free_irq(chip->gpio_int_n_irq, chip); - cancel_work_sync(&chip->irq_work); - cancel_delayed_work_sync(&chip->bc_lvl_handler); - tcpm_unregister_port(chip->tcpm_port); - fwnode_handle_put(chip->tcpc_dev.fwnode); - destroy_workqueue(chip->wq); - fusb302_debugfs_exit(chip); } static int fusb302_pm_suspend(struct device *dev) From d9647afb7d2ed4b6471efaf26d52aa35956ba71f Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:01:37 +0200 Subject: [PATCH 118/218] usb: typec: fusb302: rename init_gpio into fusb302_init_irq Add the fusb302_ prefix to init_gpio, since it is used by all the other functions and allows easy figuring out that it is a local function. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index 69b1af1358d11f..6959c3459d6abd 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -1644,7 +1644,7 @@ static void fusb302_irq_work(struct work_struct *work) enable_irq(chip->gpio_int_n_irq); } -static int init_gpio(struct fusb302_chip *chip) +static int fusb302_init_irq(struct fusb302_chip *chip) { struct device *dev = chip->dev; int ret = 0; @@ -1759,7 +1759,7 @@ static int fusb302_probe(struct i2c_client *client) if (client->irq) { chip->gpio_int_n_irq = client->irq; } else { - ret = init_gpio(chip); + ret = fusb302_init_irq(chip); if (ret < 0) return ret; } From 179ffa3c052aa0d16fe51156db2c20d4487bebe6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:03:15 +0200 Subject: [PATCH 119/218] usb: typec: fusb302: move gpio_int_n into function local scope The driver only cares about the interrupt and the GPIO itself is device managed, so drop the reference from the device structure and only keep it in the function scope. While at it cleanup the error handling by using dev_err_probe. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index 6959c3459d6abd..6334d5e3fd81c8 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -85,7 +85,6 @@ struct fusb302_chip { struct work_struct irq_work; bool irq_suspended; bool irq_while_suspended; - struct gpio_desc *gpio_int_n; int gpio_int_n_irq; struct extcon_dev *extcon; @@ -1647,19 +1646,17 @@ static void fusb302_irq_work(struct work_struct *work) static int fusb302_init_irq(struct fusb302_chip *chip) { struct device *dev = chip->dev; + struct gpio_desc *int_gpio; int ret = 0; - chip->gpio_int_n = devm_gpiod_get(dev, "fcs,int_n", GPIOD_IN); - if (IS_ERR(chip->gpio_int_n)) { - dev_err(dev, "failed to request gpio_int_n\n"); - return PTR_ERR(chip->gpio_int_n); - } - ret = gpiod_to_irq(chip->gpio_int_n); - if (ret < 0) { - dev_err(dev, - "cannot request IRQ for GPIO Int_N, ret=%d", ret); - return ret; - } + int_gpio = devm_gpiod_get(dev, "fcs,int_n", GPIOD_IN); + if (IS_ERR(int_gpio)) + return dev_err_probe(dev, PTR_ERR(int_gpio), "failed to request interrupt GPIO\n"); + + ret = gpiod_to_irq(int_gpio); + if (ret < 0) + return dev_err_probe(dev, ret, "cannot request IRQ for GPIO\n"); + chip->gpio_int_n_irq = ret; return 0; } From b2960dad40f722148096c05adcb66a08d8a3a1c8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:10:26 +0200 Subject: [PATCH 120/218] usb: typec: fusb302: rework gpio_int_n_irq handling Improve the code readability, so that one does not need to jump into fusb302_init_irq() to understand that chip->gpio_int_n_irq is guranteed to be initialized. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index 6334d5e3fd81c8..f1de6d9cff1ce6 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -1657,8 +1657,7 @@ static int fusb302_init_irq(struct fusb302_chip *chip) if (ret < 0) return dev_err_probe(dev, ret, "cannot request IRQ for GPIO\n"); - chip->gpio_int_n_irq = ret; - return 0; + return ret; } #define PDO_FIXED_FLAGS \ @@ -1753,13 +1752,13 @@ static int fusb302_probe(struct i2c_client *client) if (ret < 0) return ret; - if (client->irq) { + if (client->irq) chip->gpio_int_n_irq = client->irq; - } else { - ret = fusb302_init_irq(chip); - if (ret < 0) - return ret; - } + else + chip->gpio_int_n_irq = fusb302_init_irq(chip); + + if (chip->gpio_int_n_irq < 0) + return chip->gpio_int_n_irq; chip->tcpc_dev.fwnode = fusb302_fwnode_get(dev); if (IS_ERR(chip->tcpc_dev.fwnode)) { From 3f21f18204f61c44db6477a897a19d3d73964447 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:17:02 +0200 Subject: [PATCH 121/218] usb: typec: fusb302: drop custom gpio interrupt logic When the driver was upstreamed it contained the logic to fetch a "fcs,int_n" GPIO from device-tree, convert it into an interrupt and use it. This was never part of the binding and there was only a single upstream user, which got converted to follow the proper bindings in 2020. Drop the custom logic and only allow the properly documented ABI. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/fusb302.c | 41 ++++++++------------------------ 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index f1de6d9cff1ce6..5a166caff54676 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -85,7 +84,7 @@ struct fusb302_chip { struct work_struct irq_work; bool irq_suspended; bool irq_while_suspended; - int gpio_int_n_irq; + int irq; struct extcon_dev *extcon; struct workqueue_struct *wq; @@ -1496,7 +1495,7 @@ static irqreturn_t fusb302_irq_intn(int irq, void *dev_id) unsigned long flags; /* Disable our level triggered IRQ until our irq_work has cleared it */ - disable_irq_nosync(chip->gpio_int_n_irq); + disable_irq_nosync(chip->irq); spin_lock_irqsave(&chip->irq_lock, flags); if (chip->irq_suspended) @@ -1640,24 +1639,7 @@ static void fusb302_irq_work(struct work_struct *work) } done: mutex_unlock(&chip->lock); - enable_irq(chip->gpio_int_n_irq); -} - -static int fusb302_init_irq(struct fusb302_chip *chip) -{ - struct device *dev = chip->dev; - struct gpio_desc *int_gpio; - int ret = 0; - - int_gpio = devm_gpiod_get(dev, "fcs,int_n", GPIOD_IN); - if (IS_ERR(int_gpio)) - return dev_err_probe(dev, PTR_ERR(int_gpio), "failed to request interrupt GPIO\n"); - - ret = gpiod_to_irq(int_gpio); - if (ret < 0) - return dev_err_probe(dev, ret, "cannot request IRQ for GPIO\n"); - - return ret; + enable_irq(chip->irq); } #define PDO_FIXED_FLAGS \ @@ -1752,13 +1734,10 @@ static int fusb302_probe(struct i2c_client *client) if (ret < 0) return ret; - if (client->irq) - chip->gpio_int_n_irq = client->irq; - else - chip->gpio_int_n_irq = fusb302_init_irq(chip); - - if (chip->gpio_int_n_irq < 0) - return chip->gpio_int_n_irq; + chip->irq = client->irq; + if (!chip->irq) + return dev_err_probe(chip->dev, -ENXIO, + "missing interrupt\n"); chip->tcpc_dev.fwnode = fusb302_fwnode_get(dev); if (IS_ERR(chip->tcpc_dev.fwnode)) { @@ -1789,7 +1768,7 @@ static int fusb302_probe(struct i2c_client *client) if (ret < 0) return ret; - ret = devm_request_threaded_irq(dev, chip->gpio_int_n_irq, NULL, + ret = devm_request_threaded_irq(dev, chip->irq, NULL, fusb302_irq_intn, IRQF_ONESHOT | IRQF_TRIGGER_LOW, "fsc_interrupt_int_n", chip); @@ -1802,7 +1781,7 @@ static int fusb302_probe(struct i2c_client *client) if (ret) return ret; - enable_irq_wake(chip->gpio_int_n_irq); + enable_irq_wake(chip->irq); return ret; } @@ -1811,7 +1790,7 @@ static void fusb302_remove(struct i2c_client *client) { struct fusb302_chip *chip = i2c_get_clientdata(client); - disable_irq_wake(chip->gpio_int_n_irq); + disable_irq_wake(chip->irq); } static int fusb302_pm_suspend(struct device *dev) From 36cb98d29f655e2a2430c6a788b07194b10c05cc Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:17:57 +0200 Subject: [PATCH 122/218] phy: hdmi: Add optional FRL TxFFE config options During HDMI 2.1 FRL link training, the source and sink can negotiate a Transmitter Feed-Forward Equalization (TxFFE) level to improve the signal quality. Starting from zero, the source may increment the TxFFE level up to a maximum agreed during the LTS3 stage if the sink keeps reporting FLT failures. TxFFE adjustment is optional and only attempted when both the source and the connected sink support it. Since the existing HDMI PHY configuration API covers the FRL rate/lane selection only, provide the following fields to the frl sub-struct of phy_configure_opts_hdmi: * ffe_level: the TxFFE level to apply, only meaningful when set_ffe_level is set. * set_ffe_level: a 1-bit flag that changes the semantics of the phy_configure() call, i.e. when set, the PHY driver must apply the new ffe_level and ignore the other frl related fields. The flag-based approach reflects an important invariant in the link training process: whenever the FRL rate or lane count changes, the TxFFE level must be reset to zero. A separate phy_configure() call with set_ffe_level can only follow after the rate has been established, making the two operations deliberately distinct. Signed-off-by: Cristian Ciocaltea --- include/linux/phy/phy-hdmi.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/linux/phy/phy-hdmi.h b/include/linux/phy/phy-hdmi.h index d4cf4430ee8f3b..1d4b6247507979 100644 --- a/include/linux/phy/phy-hdmi.h +++ b/include/linux/phy/phy-hdmi.h @@ -19,6 +19,10 @@ enum phy_hdmi_mode { * @tmds_char_rate: HDMI TMDS Character Rate in Hertz. * @frl.rate_per_lane: HDMI FRL Rate per Lane in Gbps. * @frl.lanes: HDMI FRL lanes count. + * @frl.ffe_level: Transmitter Feed Forward Equalizer Level. + * Optional, only meaningful when set_ffe_level flag is on. + * @frl.set_ffe_level: Flag indicating whether or not to reconfigure ffe_level. + * All the other struct fields must be ignored when this is used. * * This structure is used to represent the configuration state of a HDMI phy. */ @@ -29,6 +33,8 @@ struct phy_configure_opts_hdmi { struct { u8 rate_per_lane; u8 lanes; + u8 ffe_level; + u8 set_ffe_level : 1; } frl; }; }; From 54afcd52cf595ffe388394af2d3b8f44e3a87d9e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:43:24 +0200 Subject: [PATCH 123/218] phy: rockchip: samsung-hdptx: Add support for FRL TxFFE level control During HDMI 2.1 FRL link training, the source may need to incrementally raise the TxFFE level in response to persistent link failures reported by the sink during LTS3. The phy_configure_opts_hdmi struct now carries ffe_level and set_ffe_level fields to convey such an update independently of a full rate reconfiguration. Wire up the optional TxFFE control in the Samsung HDPTX PHY driver. Signed-off-by: Cristian Ciocaltea --- .../phy/rockchip/phy-rockchip-samsung-hdptx.c | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 0684e1a0d1a17d..64787d47025f93 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -333,6 +333,7 @@ #define FRL_3G3L_RATE 900000000 #define FRL_6G3L_RATE 1800000000 #define FRL_8G4L_RATE 3200000000 +#define FRL_FFE_MAX_LEVEL 3 enum dp_link_rate { DP_BW_RBR, @@ -466,6 +467,16 @@ static const struct ropll_config rk_hdptx_tmds_ropll_cfg[] = { { 25175000ULL, 84, 84, 1, 1, 15, 1, 168, 1, 16, 4, 1, 1, }, }; +static const struct ffe_config { + u8 pre_shoot; + u8 de_emphasis; +} rk_hdptx_frl_ffe_cfg[FRL_FFE_MAX_LEVEL + 1] = { + { 0x3, 0x4 }, + { 0x3, 0x6 }, + { 0x3, 0x8 }, + { 0x3, 0x9 }, +}; + static const struct reg_sequence rk_hdptx_common_cmn_init_seq[] = { REG_SEQ0(CMN_REG(0009), 0x0c), REG_SEQ0(CMN_REG(000a), 0x83), @@ -1323,6 +1334,45 @@ static int rk_hdptx_tmds_ropll_mode_config(struct rk_hdptx_phy *hdptx) return rk_hdptx_post_enable_lane(hdptx); } +static int rk_hdptx_frl_ffe_config(struct rk_hdptx_phy *hdptx, u8 ffe_level) +{ + u8 val; + + if (ffe_level > FRL_FFE_MAX_LEVEL) + return -EINVAL; + + val = rk_hdptx_frl_ffe_cfg[ffe_level].pre_shoot; + + regmap_update_bits(hdptx->regmap, LANE_REG(0305), + LN_TX_DRV_PRE_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_PRE_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0405), + LN_TX_DRV_PRE_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_PRE_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0505), + LN_TX_DRV_PRE_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_PRE_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0605), + LN_TX_DRV_PRE_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_PRE_LVL_CTRL_MASK, val)); + + val = rk_hdptx_frl_ffe_cfg[ffe_level].de_emphasis; + + regmap_update_bits(hdptx->regmap, LANE_REG(0304), + LN_TX_DRV_POST_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_POST_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0404), + LN_TX_DRV_POST_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_POST_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0504), + LN_TX_DRV_POST_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_POST_LVL_CTRL_MASK, val)); + regmap_update_bits(hdptx->regmap, LANE_REG(0604), + LN_TX_DRV_POST_LVL_CTRL_MASK, + FIELD_PREP(LN_TX_DRV_POST_LVL_CTRL_MASK, val)); + return 0; +} + static void rk_hdptx_dp_reset(struct rk_hdptx_phy *hdptx) { reset_control_assert(hdptx->rsts[RST_LANE].rstc); @@ -1734,6 +1784,13 @@ static int rk_hdptx_phy_verify_hdmi_config(struct rk_hdptx_phy *hdptx, unsigned long long frl_rate = 100000000ULL * hdmi_in->frl.lanes * hdmi_in->frl.rate_per_lane; + if (hdmi_in->frl.set_ffe_level) { + if (hdmi_in->frl.ffe_level > FRL_FFE_MAX_LEVEL) + return -EINVAL; + + return 0; + } + switch (hdmi_in->frl.rate_per_lane) { case 3: case 6: @@ -2080,11 +2137,18 @@ static int rk_hdptx_phy_configure(struct phy *phy, union phy_configure_opts *opt if (ret) { dev_err(hdptx->dev, "invalid hdmi params for phy configure\n"); } else { - hdptx->pll_config_dirty = true; - - dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__, - hdptx->hdmi_cfg.mode ? "FRL" : "TMDS", - hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc); + if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL && + opts->hdmi.frl.set_ffe_level) { + dev_dbg(hdptx->dev, "%s ffe_level=%u\n", __func__, + opts->hdmi.frl.ffe_level); + ret = rk_hdptx_frl_ffe_config(hdptx, opts->hdmi.frl.ffe_level); + } else { + hdptx->pll_config_dirty = true; + + dev_dbg(hdptx->dev, "%s %s rate=%llu bpc=%u\n", __func__, + hdptx->hdmi_cfg.mode ? "FRL" : "TMDS", + hdptx->hdmi_cfg.rate, hdptx->hdmi_cfg.bpc); + } } return ret; From e26bd811fc28dee80baee6651090bcb141481182 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Apr 2026 04:19:28 +0300 Subject: [PATCH 124/218] [WIP-update-fixes] phy: rockchip: samsung-hdptx: Handle PHY config after module reload The pll_config_dirty mechanism introduced in commit e63ea089a8ab ("phy: rockchip: samsung-hdptx: Handle uncommitted PHY config changes") invalidates the clock rate in determine_rate() by resetting req->rate to zero, ensuring CCF will invoke set_rate() to program pending PLL configuration changes into hardware. However, after a module reload cycle the PHY PLL clock gets re-registered with CCF, which causes the framework's cached rate to also be zero. Setting req->rate to zero then has no effect, since CCF sees no difference between the requested and current rates and skips calling set_rate(), leaving the PLL unconfigured. Address this by first computing the actual target rate from the HDMI link configuration, and only then invalidating it when it matches the CCF cached rate. Fixes: e63ea089a8ab ("phy: rockchip: samsung-hdptx: Handle uncommitted PHY config changes") Signed-off-by: Cristian Ciocaltea --- drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c index 64787d47025f93..7e7e51f8e7e54f 100644 --- a/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c +++ b/drivers/phy/rockchip/phy-rockchip-samsung-hdptx.c @@ -2387,14 +2387,16 @@ static int rk_hdptx_phy_clk_determine_rate(struct clk_hw *hw, * to ensure rk_hdptx_phy_clk_set_rate() will be always invoked. * Otherwise, restrict the rate according to the PHY link setup. */ - if (hdptx->pll_config_dirty) - req->rate = 0; - else if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) + + if (hdptx->hdmi_cfg.mode == PHY_HDMI_MODE_FRL) req->rate = hdptx->hdmi_cfg.rate; else req->rate = DIV_ROUND_CLOSEST_ULL(hdptx->hdmi_cfg.rate * 8, hdptx->hdmi_cfg.bpc); + if (hdptx->pll_config_dirty && req->rate == clk_hw_get_rate(hw)) + req->rate = 0; + return 0; } From b4abe90e7ad43c05967cbbcfb68e39937c8a8e8d Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sun, 2 Mar 2025 19:47:18 +0200 Subject: [PATCH 125/218] [DEBUG] drm/rockchip: vop2: Log DCLK setup Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 1ad6369336d2fb..9f30d2c7a2b400 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -958,8 +958,10 @@ static void vop2_crtc_atomic_disable(struct drm_crtc *crtc, vop2_crtc_disable_irq(vp, VP_INT_DSP_HOLD_VALID); - if (vp->dclk_src) + if (vp->dclk_src) { + dev_info(vop2->dev, "reseting dclk parent\n"); clk_set_parent(vp->dclk, vp->dclk_src); + } clk_disable_unprepare(vp->dclk); @@ -1792,6 +1794,7 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, if (!vop2->pll_hdmiphy0) break; + dev_info(vop2->dev, "reparenting HDMI0 dclk\n"); if (!vp->dclk_src) vp->dclk_src = clk_get_parent(vp->dclk); @@ -1807,6 +1810,7 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, if (!vop2->pll_hdmiphy1) break; + dev_info(vop2->dev, "reparenting HDMI1 dclk\n"); if (!vp->dclk_src) vp->dclk_src = clk_get_parent(vp->dclk); @@ -1821,6 +1825,7 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, } } + dev_info(vop2->dev, "setting dclk rate=%lu (crt=%lu)\n", clock, clk_get_rate(vp->dclk)); clk_set_rate(vp->dclk, clock); vop2_post_config(crtc); From b899fa9b9840bdb06ac1a960353941c5801cb6e8 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 23:51:17 +0300 Subject: [PATCH 126/218] drm/bridge: Remove redundant error check in drm_bridge_helper_reset_crtc() Remove the no-op error check after drm_atomic_helper_reset_crtc() since the goto target is the immediately following label and the return value is already propagated correctly without it. Reviewed-by: Dmitry Baryshkov Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/drm_bridge_helper.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/gpu/drm/drm_bridge_helper.c b/drivers/gpu/drm/drm_bridge_helper.c index 420f29cf3e5435..0a3c8fee66b325 100644 --- a/drivers/gpu/drm/drm_bridge_helper.c +++ b/drivers/gpu/drm/drm_bridge_helper.c @@ -50,8 +50,6 @@ int drm_bridge_helper_reset_crtc(struct drm_bridge *bridge, crtc = connector->state->crtc; ret = drm_atomic_helper_reset_crtc(crtc, ctx); - if (ret) - goto out; out: drm_modeset_unlock(&dev->mode_config.connection_mutex); From bd67fd2951572da184372207ecd586ceab6e4f2c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 22:48:01 +0200 Subject: [PATCH 127/218] drm/bridge: Add detect_ctx hook and drm_bridge_detect_ctx() helper Add an atomic-aware .detect_ctx() callback to drm_bridge_funcs and a drm_bridge_detect_ctx() helper that accepts an optional drm_modeset_acquire_ctx. This enables bridge drivers to perform operations requiring modeset locking during connector detection, such as SCDC management for HDMI 2.0. When both ->detect_ctx and ->detect are defined, the former takes precedence. When ctx is NULL, locking is managed internally with EDEADLK retry. Tested-by: Diederik de Haas Tested-by: Maud Spierings Acked-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/drm_bridge.c | 66 +++++++++++++++++++++++++++++++++--- include/drm/drm_bridge.h | 43 +++++++++++++++++++---- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index 38d52004356dc9..43d834d79fc6e1 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -1360,9 +1360,9 @@ EXPORT_SYMBOL(drm_atomic_bridge_chain_check); * @connector: attached connector * * If the bridge supports output detection, as reported by the - * DRM_BRIDGE_OP_DETECT bridge ops flag, call &drm_bridge_funcs.detect for the - * bridge and return the connection status. Otherwise return - * connector_status_unknown. + * DRM_BRIDGE_OP_DETECT bridge ops flag, call &drm_bridge_funcs.detect_ctx + * or &drm_bridge_funcs.detect for the bridge and return the connection status. + * Otherwise return connector_status_unknown. * * RETURNS: * The detection status on success, or connector_status_unknown if the bridge @@ -1371,12 +1371,68 @@ EXPORT_SYMBOL(drm_atomic_bridge_chain_check); enum drm_connector_status drm_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector) { + return drm_bridge_detect_ctx(bridge, connector, NULL); +} +EXPORT_SYMBOL_GPL(drm_bridge_detect); + +/** + * drm_bridge_detect_ctx - check if anything is attached to the bridge output + * @bridge: bridge control structure + * @connector: attached connector + * @ctx: acquire_ctx, or NULL to let this function handle locking + * + * If the bridge supports output detection, as reported by the + * DRM_BRIDGE_OP_DETECT bridge ops flag, call &drm_bridge_funcs.detect_ctx + * or &drm_bridge_funcs.detect for the bridge and return the connection status. + * Otherwise return connector_status_unknown. + * + * RETURNS: + * The detection status on success, or connector_status_unknown if the bridge + * doesn't support output detection. + * If @ctx is set, it might also return -EDEADLK. + */ +int drm_bridge_detect_ctx(struct drm_bridge *bridge, + struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx) +{ + struct drm_modeset_acquire_ctx br_ctx; + int ret; + if (!(bridge->ops & DRM_BRIDGE_OP_DETECT)) return connector_status_unknown; - return bridge->funcs->detect(bridge, connector); + if (!bridge->funcs->detect_ctx) + return bridge->funcs->detect(bridge, connector); + + if (ctx) { + ret = bridge->funcs->detect_ctx(bridge, connector, ctx); + if (ret == -EDEADLK) + return ret; + + goto out; + } + + drm_modeset_acquire_init(&br_ctx, 0); +retry: + ret = drm_modeset_lock(&connector->dev->mode_config.connection_mutex, + &br_ctx); + if (!ret) + ret = bridge->funcs->detect_ctx(bridge, connector, &br_ctx); + + if (ret == -EDEADLK) { + drm_modeset_backoff(&br_ctx); + goto retry; + } + + drm_modeset_drop_locks(&br_ctx); + drm_modeset_acquire_fini(&br_ctx); +out: + if (WARN_ON(ret < 0)) + ret = connector_status_unknown; + + return ret; } -EXPORT_SYMBOL_GPL(drm_bridge_detect); +EXPORT_SYMBOL_GPL(drm_bridge_detect_ctx); /** * drm_bridge_get_modes - fill all modes currently valid for the sink into the diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 2946caf00bb9de..619a52552134b8 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -535,10 +535,12 @@ struct drm_bridge_funcs { * * Check if anything is attached to the bridge output. * - * This callback is optional, if not implemented the bridge will be - * considered as always having a component attached to its output. - * Bridges that implement this callback shall set the - * DRM_BRIDGE_OP_DETECT flag in their &drm_bridge->ops. + * This is the non-atomic version of detect_ctx() callback, and is + * optional. If both are implemented, it is ignored. If none is + * implemented, the bridge will be considered as always having a + * component attached to its output. Bridges that implement this + * callback shall set the DRM_BRIDGE_OP_DETECT flag in their + * &drm_bridge->ops. * * RETURNS: * @@ -547,6 +549,32 @@ struct drm_bridge_funcs { enum drm_connector_status (*detect)(struct drm_bridge *bridge, struct drm_connector *connector); + /** + * @detect_ctx: + * + * Check if anything is attached to the bridge output. + * + * This is the atomic version of detect() callback, and is optional. + * If both are implemented, it takes precedence. If none is implemented, + * the bridge will be considered as always having a component attached + * to its output. Bridges that implement this callback shall set the + * DRM_BRIDGE_OP_DETECT flag in their &drm_bridge->ops. + * + * To avoid races against concurrent connector state updates, the + * helper libraries always call this with ctx set to a valid context, + * and &drm_mode_config.connection_mutex will always be locked with + * the ctx parameter set to this ctx. This allows taking additional + * locks as required. + * + * RETURNS: + * + * &drm_connector_status indicating the bridge output status, + * or the error code returned by drm_modeset_lock(), -EDEADLK. + */ + int (*detect_ctx)(struct drm_bridge *bridge, + struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx); + /** * @get_modes: * @@ -1016,8 +1044,8 @@ struct drm_bridge_timings { enum drm_bridge_ops { /** * @DRM_BRIDGE_OP_DETECT: The bridge can detect displays connected to - * its output. Bridges that set this flag shall implement the - * &drm_bridge_funcs->detect callback. + * its output. Bridges that set this flag shall implement either the + * &drm_bridge_funcs->detect or &drm_bridge_funcs->detect_ctx callbacks. */ DRM_BRIDGE_OP_DETECT = BIT(0), /** @@ -1565,6 +1593,9 @@ drm_atomic_helper_bridge_propagate_bus_fmt(struct drm_bridge *bridge, enum drm_connector_status drm_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector); +int drm_bridge_detect_ctx(struct drm_bridge *bridge, + struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx); int drm_bridge_get_modes(struct drm_bridge *bridge, struct drm_connector *connector); const struct drm_edid *drm_bridge_edid_read(struct drm_bridge *bridge, From 891c454952c4fdcbc873d050b696a76262fbe176 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 22 Apr 2026 01:26:32 +0300 Subject: [PATCH 128/218] drm/bridge-connector: Use cached connector status in .get_modes() Replace the active drm_bridge_connector_detect() call in get_modes() with a read of the already-cached connector->status. The .get_modes() callback is only invoked from drm_helper_probe_single_connector_modes(), which has already retrieved the connector status. Calling detect again is redundant and triggers a duplicate hotplug event. This is also a prerequisite for switching to the .detect_ctx() hook, which requires a drm_modeset_acquire_ctx not available in the .get_modes() path. Reviewed-by: Dmitry Baryshkov Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/display/drm_bridge_connector.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c index 4333cc66073ecf..2b74373cac5d53 100644 --- a/drivers/gpu/drm/display/drm_bridge_connector.c +++ b/drivers/gpu/drm/display/drm_bridge_connector.c @@ -300,12 +300,10 @@ static const struct drm_connector_funcs drm_bridge_connector_funcs = { static int drm_bridge_connector_get_modes_edid(struct drm_connector *connector, struct drm_bridge *bridge) { - enum drm_connector_status status; const struct drm_edid *drm_edid; int n; - status = drm_bridge_connector_detect(connector, false); - if (status != connector_status_connected) + if (connector->status != connector_status_connected) goto no_edid; drm_edid = drm_bridge_edid_read(bridge, connector); From ed28b1fa34736b5a466ff0d4eae4a55249e9a2cc Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 23:04:23 +0200 Subject: [PATCH 129/218] drm/bridge-connector: Switch to .detect_ctx() for connector detection Use the atomic .detect_ctx() connector helper hook and invoke drm_bridge_detect_ctx() to propagate the modeset acquire context to bridge drivers. This enables bridge drivers to perform modeset operations during detection, which is needed for managing SCDC state lost on sink disconnects in HDMI 2.0 scenarios. Tested-by: Diederik de Haas Tested-by: Maud Spierings Reviewed-by: Dmitry Baryshkov Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/display/drm_bridge_connector.c | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c index 2b74373cac5d53..d3aaa81838320e 100644 --- a/drivers/gpu/drm/display/drm_bridge_connector.c +++ b/drivers/gpu/drm/display/drm_bridge_connector.c @@ -214,39 +214,6 @@ static void drm_bridge_connector_disable_hpd(struct drm_connector *connector) * Bridge Connector Functions */ -static enum drm_connector_status -drm_bridge_connector_detect(struct drm_connector *connector, bool force) -{ - struct drm_bridge_connector *bridge_connector = - to_drm_bridge_connector(connector); - struct drm_bridge *detect = bridge_connector->bridge_detect; - struct drm_bridge *hdmi = bridge_connector->bridge_hdmi; - enum drm_connector_status status; - - if (detect) { - status = detect->funcs->detect(detect, connector); - - if (hdmi) - drm_atomic_helper_connector_hdmi_hotplug(connector, status); - - drm_bridge_connector_hpd_notify(connector, status); - } else { - switch (connector->connector_type) { - case DRM_MODE_CONNECTOR_DPI: - case DRM_MODE_CONNECTOR_LVDS: - case DRM_MODE_CONNECTOR_DSI: - case DRM_MODE_CONNECTOR_eDP: - status = connector_status_connected; - break; - default: - status = connector_status_unknown; - break; - } - } - - return status; -} - static void drm_bridge_connector_force(struct drm_connector *connector) { struct drm_bridge_connector *bridge_connector = @@ -284,7 +251,6 @@ static void drm_bridge_connector_reset(struct drm_connector *connector) static const struct drm_connector_funcs drm_bridge_connector_funcs = { .reset = drm_bridge_connector_reset, - .detect = drm_bridge_connector_detect, .force = drm_bridge_connector_force, .fill_modes = drm_helper_probe_single_connector_modes, .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, @@ -297,6 +263,42 @@ static const struct drm_connector_funcs drm_bridge_connector_funcs = { * Bridge Connector Helper Functions */ +static int drm_bridge_connector_detect_ctx(struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx, + bool force) +{ + struct drm_bridge_connector *bridge_connector = + to_drm_bridge_connector(connector); + struct drm_bridge *detect = bridge_connector->bridge_detect; + struct drm_bridge *hdmi = bridge_connector->bridge_hdmi; + int ret; + + if (detect) { + ret = drm_bridge_detect_ctx(detect, connector, ctx); + if (ret < 0) + return ret; + + if (hdmi) + drm_atomic_helper_connector_hdmi_hotplug(connector, ret); + + drm_bridge_connector_hpd_notify(connector, ret); + } else { + switch (connector->connector_type) { + case DRM_MODE_CONNECTOR_DPI: + case DRM_MODE_CONNECTOR_LVDS: + case DRM_MODE_CONNECTOR_DSI: + case DRM_MODE_CONNECTOR_eDP: + ret = connector_status_connected; + break; + default: + ret = connector_status_unknown; + break; + } + } + + return ret; +} + static int drm_bridge_connector_get_modes_edid(struct drm_connector *connector, struct drm_bridge *bridge) { @@ -388,6 +390,7 @@ static int drm_bridge_connector_atomic_check(struct drm_connector *connector, static const struct drm_connector_helper_funcs drm_bridge_connector_helper_funcs = { .get_modes = drm_bridge_connector_get_modes, + .detect_ctx = drm_bridge_connector_detect_ctx, .mode_valid = drm_bridge_connector_mode_valid, .enable_hpd = drm_bridge_connector_enable_hpd, .disable_hpd = drm_bridge_connector_disable_hpd, From 1be6f2569e4df43b3418f02d3b5de3d9c5e491f7 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 13 Sep 2024 17:30:35 +0300 Subject: [PATCH 130/218] drm/bridge: dw-hdmi-qp: Add HDMI 2.0 SCDC scrambling and high TMDS clock ratio support Enable HDMI 2.0 display modes (e.g. 4K@60Hz) by adding SCDC management for the high TMDS clock ratio and scrambling, required when the TMDS character rate exceeds the 340 MHz HDMI 1.4b limit. A periodic work item monitors the sink's scrambling status to recover from sink-side resets. On hotplug detect, if SCDC scrambling state is out of sync with the driver, trigger a CRTC reset to re-establish the link. Reject modes requiring TMDS rates above 600 MHz, as those fall in the HDMI 2.1 FRL domain which is not supported. In no_hpd configurations, further restrict to 340 MHz since SCDC requires a connected sink. Tested-by: Diederik de Haas Tested-by: Maud Spierings Acked-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 188 +++++++++++++++++-- 1 file changed, 172 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index d649a1cf07f5cf..c482a8e7da259e 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -2,6 +2,7 @@ /* * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd. * Copyright (c) 2024 Collabora Ltd. + * Copyright (c) 2025 Amazon.com, Inc. or its affiliates. * * Author: Algea Cao * Author: Cristian Ciocaltea @@ -21,9 +22,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -39,7 +42,9 @@ #define DDC_SEGMENT_ADDR 0x30 #define HDMI14_MAX_TMDSCLK 340000000 +#define HDMI20_MAX_TMDSRATE 600000000 +#define SCDC_MAX_SOURCE_VERSION 0x1 #define SCRAMB_POLL_DELAY_MS 3000 /* @@ -164,6 +169,11 @@ struct dw_hdmi_qp { } phy; unsigned long ref_clk_rate; + + struct drm_connector *curr_conn; + struct delayed_work scramb_work; + bool scramb_enabled; + struct regmap *regm; int main_irq; @@ -749,28 +759,124 @@ static struct i2c_adapter *dw_hdmi_qp_i2c_adapter(struct dw_hdmi_qp *hdmi) return adap; } +static bool dw_hdmi_qp_supports_scrambling(struct drm_display_info *display) +{ + if (!display->is_hdmi) + return false; + + return display->hdmi.scdc.supported && + display->hdmi.scdc.scrambling.supported; +} + +static int dw_hdmi_qp_set_scramb(struct dw_hdmi_qp *hdmi) +{ + bool done; + + dev_dbg(hdmi->dev, "set scrambling\n"); + + done = drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, true); + if (!done) + return -EIO; + + done = drm_scdc_set_scrambling(hdmi->curr_conn, true); + if (!done) { + drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, false); + return -EIO; + } + + schedule_delayed_work(&hdmi->scramb_work, + msecs_to_jiffies(SCRAMB_POLL_DELAY_MS)); + return 0; +} + +static void dw_hdmi_qp_scramb_work(struct work_struct *work) +{ + struct dw_hdmi_qp *hdmi = container_of(to_delayed_work(work), + struct dw_hdmi_qp, + scramb_work); + if (READ_ONCE(hdmi->scramb_enabled) && + !drm_scdc_get_scrambling_status(hdmi->curr_conn)) + dw_hdmi_qp_set_scramb(hdmi); +} + +static void dw_hdmi_qp_enable_scramb(struct dw_hdmi_qp *hdmi) +{ + int ret; + u8 ver; + + if (!dw_hdmi_qp_supports_scrambling(&hdmi->curr_conn->display_info)) + return; + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_SINK_VERSION, &ver); + if (ret) { + dev_err(hdmi->dev, "Failed to read SCDC_SINK_VERSION: %d\n", ret); + return; + } + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_SOURCE_VERSION, + min_t(u8, ver, SCDC_MAX_SOURCE_VERSION)); + if (ret) { + dev_err(hdmi->dev, "Failed to write SCDC_SOURCE_VERSION: %d\n", ret); + return; + } + + WRITE_ONCE(hdmi->scramb_enabled, true); + + ret = dw_hdmi_qp_set_scramb(hdmi); + if (ret) { + hdmi->scramb_enabled = false; + return; + } + + dw_hdmi_qp_write(hdmi, 1, SCRAMB_CONFIG0); + + /* Wait at least 1 ms before resuming TMDS transmission */ + usleep_range(1000, 5000); +} + +static void dw_hdmi_qp_disable_scramb(struct dw_hdmi_qp *hdmi) +{ + if (!hdmi->scramb_enabled) + return; + + dev_dbg(hdmi->dev, "disable scrambling\n"); + + WRITE_ONCE(hdmi->scramb_enabled, false); + cancel_delayed_work_sync(&hdmi->scramb_work); + + dw_hdmi_qp_write(hdmi, 0, SCRAMB_CONFIG0); + + if (hdmi->curr_conn->status == connector_status_connected) { + drm_scdc_set_scrambling(hdmi->curr_conn, false); + drm_scdc_set_high_tmds_clock_ratio(hdmi->curr_conn, false); + } +} + static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_atomic_state *state) { struct dw_hdmi_qp *hdmi = bridge->driver_private; struct drm_connector_state *conn_state; - struct drm_connector *connector; unsigned int op_mode; - connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); - if (WARN_ON(!connector)) + hdmi->curr_conn = drm_atomic_get_new_connector_for_encoder(state, + bridge->encoder); + if (WARN_ON(!hdmi->curr_conn)) return; - conn_state = drm_atomic_get_new_connector_state(state, connector); + conn_state = drm_atomic_get_new_connector_state(state, hdmi->curr_conn); if (WARN_ON(!conn_state)) return; - if (connector->display_info.is_hdmi) { + if (hdmi->curr_conn->display_info.is_hdmi) { dev_dbg(hdmi->dev, "%s mode=HDMI %s rate=%llu bpc=%u\n", __func__, drm_hdmi_connector_get_output_format_name(conn_state->hdmi.output_format), conn_state->hdmi.tmds_char_rate, conn_state->hdmi.output_bpc); op_mode = 0; hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate; + + if (conn_state->hdmi.tmds_char_rate > HDMI14_MAX_TMDSCLK) + dw_hdmi_qp_enable_scramb(hdmi); } else { dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__); op_mode = OPMODE_DVI; @@ -781,7 +887,7 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, dw_hdmi_qp_mod(hdmi, HDCP2_BYPASS, HDCP2_BYPASS, HDCP2LOGIC_CONFIG0); dw_hdmi_qp_mod(hdmi, op_mode, OPMODE_DVI, LINK_CONFIG0); - drm_atomic_helper_connector_hdmi_update_infoframes(connector, state); + drm_atomic_helper_connector_hdmi_update_infoframes(hdmi->curr_conn, state); } static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge, @@ -791,14 +897,49 @@ static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge, hdmi->tmds_char_rate = 0; + dw_hdmi_qp_disable_scramb(hdmi); + + hdmi->curr_conn = NULL; hdmi->phy.ops->disable(hdmi, hdmi->phy.data); } -static enum drm_connector_status -dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connector) +static int dw_hdmi_qp_reset_crtc(struct dw_hdmi_qp *hdmi, + struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx) +{ + u8 config; + int ret; + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_TMDS_CONFIG, &config); + if (ret < 0) { + dev_err(hdmi->dev, "Failed to read TMDS config: %d\n", ret); + return ret; + } + + if (!!(config & SCDC_SCRAMBLING_ENABLE) == hdmi->scramb_enabled) + return 0; + + drm_atomic_helper_connector_hdmi_hotplug(connector, + connector_status_connected); + /* + * Conform to HDMI 2.0 spec by ensuring scrambled data is not sent + * before configuring the sink scrambling, as well as suspending any + * TMDS transmission while changing the TMDS clock rate in the sink. + */ + + dev_dbg(hdmi->dev, "resetting crtc\n"); + + return drm_bridge_helper_reset_crtc(&hdmi->bridge, ctx); +} + +static int dw_hdmi_qp_bridge_detect_ctx(struct drm_bridge *bridge, + struct drm_connector *connector, + struct drm_modeset_acquire_ctx *ctx) { struct dw_hdmi_qp *hdmi = bridge->driver_private; + enum drm_connector_status status; const struct drm_edid *drm_edid; + int ret; if (hdmi->no_hpd) { drm_edid = drm_edid_read_ddc(connector, bridge->ddc); @@ -808,7 +949,20 @@ dw_hdmi_qp_bridge_detect(struct drm_bridge *bridge, struct drm_connector *connec return connector_status_disconnected; } - return hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data); + status = hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data); + + dev_dbg(hdmi->dev, "%s status=%d scramb=%d\n", __func__, + status, hdmi->scramb_enabled); + + if (status == connector_status_connected && hdmi->scramb_enabled) { + ret = dw_hdmi_qp_reset_crtc(hdmi, connector, ctx); + if (ret == -EDEADLK) + return ret; + if (ret < 0) + status = connector_status_unknown; + } + + return status; } static const struct drm_edid * @@ -832,12 +986,12 @@ dw_hdmi_qp_bridge_tmds_char_rate_valid(const struct drm_bridge *bridge, { struct dw_hdmi_qp *hdmi = bridge->driver_private; - /* - * TODO: when hdmi->no_hpd is 1 we must not support modes that - * require scrambling, including every mode with a clock above - * HDMI14_MAX_TMDSCLK. - */ - if (rate > HDMI14_MAX_TMDSCLK) { + if (hdmi->no_hpd && rate > HDMI14_MAX_TMDSCLK) { + dev_dbg(hdmi->dev, "Unsupported TMDS char rate in no_hpd mode: %lld\n", rate); + return MODE_CLOCK_HIGH; + } + + if (rate > HDMI20_MAX_TMDSRATE) { dev_dbg(hdmi->dev, "Unsupported TMDS char rate: %lld\n", rate); return MODE_CLOCK_HIGH; } @@ -1197,7 +1351,7 @@ static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = { .atomic_reset = drm_atomic_helper_bridge_reset, .atomic_enable = dw_hdmi_qp_bridge_atomic_enable, .atomic_disable = dw_hdmi_qp_bridge_atomic_disable, - .detect = dw_hdmi_qp_bridge_detect, + .detect_ctx = dw_hdmi_qp_bridge_detect_ctx, .edid_read = dw_hdmi_qp_bridge_edid_read, .hdmi_tmds_char_rate_valid = dw_hdmi_qp_bridge_tmds_char_rate_valid, .hdmi_clear_avi_infoframe = dw_hdmi_qp_bridge_clear_avi_infoframe, @@ -1287,6 +1441,8 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev, if (IS_ERR(hdmi)) return ERR_CAST(hdmi); + INIT_DELAYED_WORK(&hdmi->scramb_work, dw_hdmi_qp_scramb_work); + hdmi->dev = dev; regs = devm_platform_ioremap_resource(pdev, 0); From eeaf158013db988a8f5330f576e95807d10153c7 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:38 +0200 Subject: [PATCH 131/218] dt-bindings: pwm: Add a new binding for rockchip,rk3576-pwm The Rockchip RK3576 SoC has a newer PWM controller IP revision than previous Rockchip SoCs. This IP, called "PWMv4" by Rockchip, introduces several new features, and consequently differs in its bindings. Instead of expanding the ever-growing rockchip-pwm binding that already has an if-condition, add an entirely new binding to handle this. There are two additional clocks, "osc" and "rc". These are available for every PWM instance, and the PWM hardware can switch between the "pwm", "osc" and "rc" clock at runtime. The PWM controller also comes with an interrupt now. This interrupt is used to signal various conditions. Reviewed-by: Conor Dooley Reviewed-by: Rob Herring (Arm) Signed-off-by: Nicolas Frattaroli --- .../bindings/pwm/rockchip,rk3576-pwm.yaml | 77 +++++++++++++++++++ MAINTAINERS | 7 ++ 2 files changed, 84 insertions(+) create mode 100644 Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml diff --git a/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml new file mode 100644 index 00000000000000..48d5055c8b069f --- /dev/null +++ b/Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/pwm/rockchip,rk3576-pwm.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Rockchip PWMv4 controller + +maintainers: + - Nicolas Frattaroli + +description: | + The Rockchip PWMv4 controller is a PWM controller found on several Rockchip + SoCs, such as the RK3576. + + It supports both generating and capturing PWM signals. + +allOf: + - $ref: pwm.yaml# + +properties: + compatible: + items: + - const: rockchip,rk3576-pwm + + reg: + maxItems: 1 + + clocks: + items: + - description: Used to derive the PWM signal. + - description: Used as the APB bus clock. + - description: Used as an alternative to derive the PWM signal. + - description: Used as another alternative to derive the PWM signal. + + clock-names: + items: + - const: pwm + - const: pclk + - const: osc + - const: rc + + interrupts: + maxItems: 1 + + "#pwm-cells": + const: 3 + +required: + - compatible + - reg + - clocks + - clock-names + - interrupts + +additionalProperties: false + +examples: + - | + #include + #include + #include + + soc { + #address-cells = <2>; + #size-cells = <2>; + + pwm@2add0000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add0000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, <&cru CLK_OSC_PWM1>, + <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + #pwm-cells = <3>; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 4f5f21080fddea..6d12598d8a198a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23156,6 +23156,13 @@ F: Documentation/userspace-api/media/v4l/metafmt-rkisp1.rst F: drivers/media/platform/rockchip/rkisp1 F: include/uapi/linux/rkisp1-config.h +ROCKCHIP MFPWM +M: Nicolas Frattaroli +L: linux-rockchip@lists.infradead.org +L: linux-pwm@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml + ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT M: Daniel Golle M: Aurelien Jarno From d1295f05e0335f5f5e940e65010924a61c6a6c34 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:39 +0200 Subject: [PATCH 132/218] mfd: Add Rockchip mfpwm driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the Rockchip RK3576, the PWM IP used by Rockchip has changed substantially. Looking at both the downstream pwm-rockchip driver as well as the mainline pwm-rockchip driver made it clear that with all its additional features and its differences from previous IP revisions, it is best supported in a new driver. This brings us to the question as to what such a new driver should be. To me, it soon became clear that it should actually be several new drivers, most prominently when Uwe Kleine-König let me know that I should not implement the pwm subsystem's capture callback, but instead write a counter driver for this functionality. Combined with the other as-of-yet unimplemented functionality of this new IP, it became apparent that it needs to be spread across several subsystems. For this reason, we add a new MFD core driver, called mfpwm (short for "Multi-function PWM"). This "parent" driver makes sure that only one device function driver is using the device at a time, and is in charge of registering the MFD cell devices for the individual device functions offered by the device. An acquire/release pattern is used to guarantee that device function drivers don't step on each other's toes. Signed-off-by: Nicolas Frattaroli --- MAINTAINERS | 2 + drivers/mfd/Kconfig | 16 + drivers/mfd/Makefile | 1 + drivers/mfd/rockchip-mfpwm.c | 357 ++++++++++++++++++++++ include/linux/mfd/rockchip-mfpwm.h | 470 +++++++++++++++++++++++++++++ 5 files changed, 846 insertions(+) create mode 100644 drivers/mfd/rockchip-mfpwm.c create mode 100644 include/linux/mfd/rockchip-mfpwm.h diff --git a/MAINTAINERS b/MAINTAINERS index 6d12598d8a198a..9e9e3c029b050b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23162,6 +23162,8 @@ L: linux-rockchip@lists.infradead.org L: linux-pwm@vger.kernel.org S: Maintained F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml +F: drivers/mfd/rockchip-mfpwm.c +F: include/linux/mfd/rockchip-mfpwm.h ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT M: Daniel Golle diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 7192c9d1d268e9..80b4e82c4937fd 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -1378,6 +1378,22 @@ config MFD_RC5T583 Additional drivers must be enabled in order to use the different functionality of the device. +config MFD_ROCKCHIP_MFPWM + tristate "Rockchip multi-function PWM controller" + depends on ARCH_ROCKCHIP || COMPILE_TEST + depends on OF + depends on HAS_IOMEM + depends on COMMON_CLK + select MFD_CORE + help + Some Rockchip SoCs, such as the RK3576, use a PWM controller that has + several different functions, such as generating PWM waveforms but also + counting waveforms. + + This driver manages the overall device, and selects between different + functionalities at runtime as needed. Drivers for them are implemented + in their respective subsystems. + config MFD_RK8XX tristate select MFD_CORE diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index e75e8045c28afa..ebadbaea9e4a09 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -231,6 +231,7 @@ obj-$(CONFIG_MFD_PALMAS) += palmas.o obj-$(CONFIG_MFD_VIPERBOARD) += viperboard.o obj-$(CONFIG_MFD_NTXEC) += ntxec.o obj-$(CONFIG_MFD_RC5T583) += rc5t583.o rc5t583-irq.o +obj-$(CONFIG_MFD_ROCKCHIP_MFPWM) += rockchip-mfpwm.o obj-$(CONFIG_MFD_RK8XX) += rk8xx-core.o obj-$(CONFIG_MFD_RK8XX_I2C) += rk8xx-i2c.o obj-$(CONFIG_MFD_RK8XX_SPI) += rk8xx-spi.o diff --git a/drivers/mfd/rockchip-mfpwm.c b/drivers/mfd/rockchip-mfpwm.c new file mode 100644 index 00000000000000..72d04982b9615e --- /dev/null +++ b/drivers/mfd/rockchip-mfpwm.c @@ -0,0 +1,357 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2025 Collabora Ltd. + * + * A driver to manage all the different functionalities exposed by Rockchip's + * PWMv4 hardware. + * + * This driver is chiefly focused on guaranteeing non-concurrent operation + * between the different device functions, as well as setting the clocks. + * It registers the device function platform devices, e.g. PWM output or + * PWM capture. + * + * Authors: + * Nicolas Frattaroli + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * struct rockchip_mfpwm - private mfpwm driver instance state struct + * @pdev: pointer to this instance's &struct platform_device + * @base: pointer to the memory mapped registers of this device + * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from + * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from + * @rc_clk: pointer to the RC oscillator the PWM signal may be derived from + * @chosen_clk: a clk-mux of pwm_clk, osc_clk and rc_clk + * @pclk: pointer to the APB bus clock needed for mmio register access + * @active_func: pointer to the currently active device function, or %NULL if no + * device function is currently actively using any of the shared + * resources. May only be checked/modified with @state_lock held. + * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d + * it. Must only be checked or modified while holding @state_lock. + * @state_lock: this lock is held while either the active device function, the + * enable register, or the chosen clock is being changed. + * @irq: the IRQ number of this device + */ +struct rockchip_mfpwm { + struct platform_device *pdev; + void __iomem *base; + struct clk *pwm_clk; + struct clk *osc_clk; + struct clk *rc_clk; + struct clk *chosen_clk; + struct clk *pclk; + struct rockchip_mfpwm_func *active_func; + unsigned int acquire_cnt; + spinlock_t state_lock; + int irq; +}; + +static atomic_t subdev_id = ATOMIC_INIT(0); + +static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev) +{ + return platform_get_drvdata(pdev); +} + +static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf, + const char *fname) +{ + struct device *dev = &pwmf->parent->pdev->dev; + + if (IS_ERR_OR_NULL(pwmf)) { + dev_warn(dev, "called %s with an erroneous handle, no effect\n", + fname); + return -EINVAL; + } + + if (IS_ERR_OR_NULL(pwmf->parent)) { + dev_warn(dev, "called %s with an erroneous mfpwm_func parent, no effect\n", + fname); + return -EINVAL; + } + + return 0; +} + +__attribute__((nonnull)) +static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf) +{ + struct rockchip_mfpwm *mfpwm = pwmf->parent; + unsigned int cnt; + + if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id) + return -EBUSY; + + if (!mfpwm->active_func) + mfpwm->active_func = pwmf; + + if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) { + mfpwm->acquire_cnt = cnt; + } else { + dev_warn(&mfpwm->pdev->dev, "prevented acquire counter overflow in %s\n", + __func__); + return -EOVERFLOW; + } + + dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n", + pwmf->id, mfpwm->acquire_cnt); + + return clk_enable(mfpwm->pclk); +} + +int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf) +{ + struct rockchip_mfpwm *mfpwm; + unsigned long flags; + int ret = 0; + + ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire"); + if (ret) + return ret; + + mfpwm = pwmf->parent; + dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id); + + if (!spin_trylock_irqsave(&mfpwm->state_lock, flags)) + return -EBUSY; + + ret = mfpwm_do_acquire(pwmf); + + spin_unlock_irqrestore(&mfpwm->state_lock, flags); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM"); + +__attribute__((nonnull)) +static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf) +{ + struct rockchip_mfpwm *mfpwm = pwmf->parent; + + if (!mfpwm->active_func) + return; + + if (mfpwm->active_func->id != pwmf->id) + return; + + /* + * No need to check_sub_overflow here, !mfpwm->active_func above catches + * this type of problem already. + */ + mfpwm->acquire_cnt--; + + if (!mfpwm->acquire_cnt) + mfpwm->active_func = NULL; + + clk_disable(mfpwm->pclk); +} + +void mfpwm_release(const struct rockchip_mfpwm_func *pwmf) +{ + struct rockchip_mfpwm *mfpwm; + unsigned long flags; + + if (mfpwm_check_pwmf(pwmf, "mfpwm_release")) + return; + + mfpwm = pwmf->parent; + + spin_lock_irqsave(&mfpwm->state_lock, flags); + mfpwm_do_release(pwmf); + dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n", + pwmf->id, mfpwm->acquire_cnt); + spin_unlock_irqrestore(&mfpwm->state_lock, flags); +} +EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM"); + +int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf) +{ + struct rockchip_mfpwm *mfpwm; + int ret; + + ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire"); + if (ret) + return ret; + + mfpwm = pwmf->parent; + + guard(spinlock_irqsave)(&mfpwm->state_lock); + + if (!rockchip_pwm_v4_is_enabled(mfpwm_reg_read(mfpwm->base, PWMV4_REG_ENABLE))) + return -1; + + return mfpwm_reg_read(mfpwm->base, PWMV4_REG_CTRL) & PWMV4_MODE_MASK; +} +EXPORT_SYMBOL_NS_GPL(mfpwm_get_mode, "ROCKCHIP_MFPWM"); + +/** + * mfpwm_register_subdev - register a single mfpwm_func + * @mfpwm: pointer to the parent &struct rockchip_mfpwm + * @name: sub-device name string + * + * Allocate a single &struct mfpwm_func, fill its members with appropriate data, + * and register a new mfd cell. + * + * Returns: 0 on success, negative errno on error + */ +static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm, + const char *name) +{ + struct rockchip_mfpwm_func *func; + struct mfd_cell cell = {}; + + func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL); + if (IS_ERR(func)) + return PTR_ERR(func); + func->irq = mfpwm->irq; + func->parent = mfpwm; + func->id = atomic_inc_return(&subdev_id); + func->base = mfpwm->base; + func->core = mfpwm->chosen_clk; + cell.name = name; + cell.platform_data = func; + cell.pdata_size = sizeof(*func); + + return devm_mfd_add_devices(&mfpwm->pdev->dev, func->id, &cell, 1, NULL, + 0, NULL); +} + +static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm) +{ + int ret; + + ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-v4"); + if (ret) + return ret; + + ret = mfpwm_register_subdev(mfpwm, "rockchip-pwm-capture"); + if (ret) + return ret; + + return 0; +} + +static int rockchip_mfpwm_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct rockchip_mfpwm *mfpwm; + char *clk_mux_name; + const char *mux_p_names[3]; + int ret = 0; + + mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL); + if (IS_ERR(mfpwm)) + return PTR_ERR(mfpwm); + + mfpwm->pdev = pdev; + + spin_lock_init(&mfpwm->state_lock); + + mfpwm->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(mfpwm->base)) + return dev_err_probe(dev, PTR_ERR(mfpwm->base), + "failed to ioremap address\n"); + + mfpwm->pclk = devm_clk_get_prepared(dev, "pclk"); + if (IS_ERR(mfpwm->pclk)) + return dev_err_probe(dev, PTR_ERR(mfpwm->pclk), + "couldn't get and prepare 'pclk' clock\n"); + + mfpwm->irq = platform_get_irq(pdev, 0); + if (mfpwm->irq < 0) + return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n"); + + mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm"); + if (IS_ERR(mfpwm->pwm_clk)) + return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk), + "couldn't get and prepare 'pwm' clock\n"); + + mfpwm->osc_clk = devm_clk_get_prepared(dev, "osc"); + if (IS_ERR(mfpwm->osc_clk)) + return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk), + "couldn't get and prepare 'osc' clock\n"); + + mfpwm->rc_clk = devm_clk_get_prepared(dev, "rc"); + if (IS_ERR(mfpwm->rc_clk)) + return dev_err_probe(dev, PTR_ERR(mfpwm->rc_clk), + "couldn't get and prepare 'rc' clock\n"); + + clk_mux_name = devm_kasprintf(dev, GFP_KERNEL, "%s_chosen", dev_name(dev)); + if (!clk_mux_name) + return -ENOMEM; + + mux_p_names[0] = __clk_get_name(mfpwm->pwm_clk); + mux_p_names[1] = __clk_get_name(mfpwm->osc_clk); + mux_p_names[2] = __clk_get_name(mfpwm->rc_clk); + mfpwm->chosen_clk = clk_register_mux(dev, clk_mux_name, mux_p_names, + ARRAY_SIZE(mux_p_names), + CLK_SET_RATE_PARENT, + mfpwm->base + PWMV4_REG_CLK_CTRL, + PWMV4_CLK_SRC_SHIFT, PWMV4_CLK_SRC_WIDTH, + CLK_MUX_HIWORD_MASK, NULL); + ret = clk_prepare(mfpwm->chosen_clk); + if (ret) { + dev_err(dev, "failed to prepare PWM clock mux: %pe\n", + ERR_PTR(ret)); + return ret; + } + + platform_set_drvdata(pdev, mfpwm); + + ret = mfpwm_register_subdevs(mfpwm); + if (ret) { + dev_err(dev, "failed to register sub-devices: %pe\n", + ERR_PTR(ret)); + return ret; + } + + return ret; +} + +static void rockchip_mfpwm_remove(struct platform_device *pdev) +{ + struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev); + unsigned long flags; + + spin_lock_irqsave(&mfpwm->state_lock, flags); + + if (mfpwm->chosen_clk) { + clk_unprepare(mfpwm->chosen_clk); + clk_unregister_mux(mfpwm->chosen_clk); + } + + spin_unlock_irqrestore(&mfpwm->state_lock, flags); +} + +static const struct of_device_id rockchip_mfpwm_of_match[] = { + { + .compatible = "rockchip,rk3576-pwm", + }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match); + +static struct platform_driver rockchip_mfpwm_driver = { + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = rockchip_mfpwm_of_match, + }, + .probe = rockchip_mfpwm_probe, + .remove = rockchip_mfpwm_remove, +}; +module_platform_driver(rockchip_mfpwm_driver); + +MODULE_AUTHOR("Nicolas Frattaroli "); +MODULE_DESCRIPTION("Rockchip MFPWM Driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/mfd/rockchip-mfpwm.h b/include/linux/mfd/rockchip-mfpwm.h new file mode 100644 index 00000000000000..dbf1588a438295 --- /dev/null +++ b/include/linux/mfd/rockchip-mfpwm.h @@ -0,0 +1,470 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (c) 2025 Collabora Ltd. + * + * Common header file for all the Rockchip Multi-function PWM controller + * drivers that are spread across subsystems. + * + * Authors: + * Nicolas Frattaroli + */ + +#ifndef __SOC_ROCKCHIP_MFPWM_H__ +#define __SOC_ROCKCHIP_MFPWM_H__ + +#include +#include +#include +#include +#include + +struct rockchip_mfpwm; + +/** + * struct rockchip_mfpwm_func - struct representing a single function driver + * + * @id: unique id for this function driver instance + * @base: pointer to start of MMIO registers + * @parent: a pointer to the parent mfpwm struct + * @irq: the shared IRQ gotten from the parent mfpwm device + * @core: a pointer to the clk mux that drives this channel's PWM + */ +struct rockchip_mfpwm_func { + int id; + void __iomem *base; + struct rockchip_mfpwm *parent; + int irq; + struct clk *core; +}; + +/* + * PWMV4 Register Definitions + * -------------------------- + * + * Attributes: + * RW - Read-Write + * RO - Read-Only + * WO - Write-Only + * W1T - Write high, Self-clearing + * W1C - Write high to clear interrupt + * + * Bit ranges to be understood with Verilog-like semantics, + * e.g. [03:00] is 4 bits: 0, 1, 2 and 3. + * + * All registers must be accessed with 32-bit width accesses only + */ + +#define PWMV4_REG_VERSION 0x000 +/* + * VERSION Register Description + * [31:24] RO | Hardware Major Version + * [23:16] RO | Hardware Minor Version + * [15:15] RO | Reserved + * [14:14] RO | Hardware supports biphasic counters + * [13:13] RO | Hardware supports filters + * [12:12] RO | Hardware supports waveform generation + * [11:11] RO | Hardware supports counter + * [10:10] RO | Hardware supports frequency metering + * [09:09] RO | Hardware supports power key functionality + * [08:08] RO | Hardware supports infrared transmissions + * [07:04] RO | Channel index of this instance + * [03:00] RO | Number of channels the base instance supports + */ + +#define PWMV4_REG_ENABLE 0x004 +/* + * ENABLE Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:06] RO | Reserved + * [05:05] RW | PWM Channel Counter Read Enable, 1 = enabled + */ +#define PWMV4_CHN_CNT_RD_EN(v) FIELD_PREP_WM16(BIT(5), (v)) +/* + * [04:04] W1T | PWM Globally Joined Control Enable + * 1 = this PWM channel will be enabled by a global pwm enable + * bit instead of the PWM Enable bit. + */ +#define PWMV4_GLOBAL_CTRL_EN(v) FIELD_PREP_WM16(BIT(4), (v)) +/* + * [03:03] RW | Force Clock Enable + * 0 = disabled, if the PWM channel is inactive then so is the + * clock prescale module + */ +#define PWMV4_FORCE_CLK_EN(v) FIELD_PREP_WM16(BIT(3), (v)) +/* + * [02:02] W1T | PWM Control Update Enable + * 1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and + * _OFFSET registers once 1 is written to it + */ +#define PWMV4_CTRL_UPDATE_EN FIELD_PREP_WM16_CONST(BIT(2), 1) +/* + * [01:01] RW | PWM Enable, 1 = enabled + * If in one-shot mode, clears after end of operation + */ +#define PWMV4_EN_MASK BIT(1) +#define PWMV4_EN(v) FIELD_PREP_WM16(PWMV4_EN_MASK, \ + ((v) ? 1 : 0)) +/* + * [00:00] RW | PWM Clock Enable, 1 = enabled + * If in one-shot mode, clears after end of operation + */ +#define PWMV4_CLK_EN_MASK BIT(0) +#define PWMV4_CLK_EN(v) FIELD_PREP_WM16(PWMV4_CLK_EN_MASK, \ + ((v) ? 1 : 0)) +#define PWMV4_EN_BOTH_MASK (PWMV4_EN_MASK | PWMV4_CLK_EN_MASK) +static inline __pure bool rockchip_pwm_v4_is_enabled(unsigned int val) +{ + return (val & PWMV4_EN_BOTH_MASK); +} + +#define PWMV4_REG_CLK_CTRL 0x008 +/* + * CLK_CTRL Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:15] RW | Clock Global Selection + * 0 = current channel scale clock + * 1 = global channel scale clock + */ +#define PWMV4_CLK_GLOBAL(v) FIELD_PREP_WM16(BIT(15), (v)) +/* + * [14:13] RW | Clock Source Selection + * 0 = Clock from PLL, frequency can be configured + * 1 = Clock from crystal oscillator, frequency is fixed + * 2 = Clock from RC oscillator, frequency is fixed + * 3 = Reserved + * NOTE: The purpose for this clock-mux-outside-CRU construct is + * to let the SoC go into a sleep state with the PWM + * hardware still having a clock signal for IR input, which + * can then wake up the SoC. + */ +#define PWMV4_CLK_SRC_PLL 0x0U +#define PWMV4_CLK_SRC_CRYSTAL 0x1U +#define PWMV4_CLK_SRC_RC 0x2U +#define PWMV4_CLK_SRC_SHIFT 13 +#define PWMV4_CLK_SRC_WIDTH 2 +/* + * [12:04] RW | Scale Factor to apply to pre-scaled clock + * 1 <= v <= 256, v means clock divided by 2*v + */ +#define PWMV4_CLK_SCALE_F(v) FIELD_PREP_WM16(GENMASK(12, 4), (v)) +/* + * [03:03] RO | Reserved + * [02:00] RW | Prescale Factor + * v here means the input clock is divided by pow(2, v) + */ +#define PWMV4_CLK_PRESCALE_F(v) FIELD_PREP_WM16(GENMASK(2, 0), (v)) + +#define PWMV4_REG_CTRL 0x00C +/* + * CTRL Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:09] RO | Reserved + * [08:06] RW | PWM Input Channel Selection + * By default, the channel selects its own input, but writing v + * here selects PWM input from channel v instead. + */ +#define PWMV4_CTRL_IN_SEL(v) FIELD_PREP_WM16(GENMASK(8, 6), (v)) +/* [05:05] RW | Aligned Mode, 0 = Valid, 1 = Invalid */ +#define PWMV4_CTRL_UNALIGNED(v) FIELD_PREP_WM16(BIT(5), (v)) +/* [04:04] RW | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */ +#define PWMV4_LEFT_ALIGNED 0x0U +#define PWMV4_CENTRE_ALIGNED 0x1U +#define PWMV4_CTRL_OUT_MODE(v) FIELD_PREP_WM16(BIT(4), (v)) +/* + * [03:03] RW | Inactive Polarity for when the channel is either disabled or + * has completed outputting the entire waveform in one-shot mode. + * 0 = Negative, 1 = Positive + */ +#define PWMV4_POLARITY_N 0x0U +#define PWMV4_POLARITY_P 0x1U +#define PWMV4_INACTIVE_POL(v) FIELD_PREP_WM16(BIT(3), (v)) +/* + * [02:02] RW | Duty Cycle Polarity to use at the start of the waveform. + * 0 = Negative, 1 = Positive + */ +#define PWMV4_DUTY_POL_SHIFT 2 +#define PWMV4_DUTY_POL_MASK BIT(PWMV4_DUTY_POL_SHIFT) +#define PWMV4_DUTY_POL(v) FIELD_PREP_WM16(PWMV4_DUTY_POL_MASK, \ + (v)) +/* + * [01:00] RW | PWM Mode + * 0 = One-shot mode, PWM generates waveform RPT times + * 1 = Continuous mode + * 2 = Capture mode, PWM measures cycles of input waveform + * 3 = Reserved + */ +#define PWMV4_MODE_ONESHOT 0x0U +#define PWMV4_MODE_CONT 0x1U +#define PWMV4_MODE_CAPTURE 0x2U +#define PWMV4_MODE_MASK GENMASK(1, 0) +#define PWMV4_MODE(v) FIELD_PREP_WM16(PWMV4_MODE_MASK, (v)) +#define PWMV4_CTRL_COM_FLAGS (PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \ + PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \ + PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \ + PWMV4_CTRL_UNALIGNED(true)) +#define PWMV4_CTRL_CONT_FLAGS (PWMV4_MODE(PWMV4_MODE_CONT) | \ + PWMV4_CTRL_COM_FLAGS) +#define PWMV4_CTRL_CAP_FLAGS (PWMV4_MODE(PWMV4_MODE_CAPTURE) | \ + PWMV4_CTRL_COM_FLAGS) + +#define PWMV4_REG_PERIOD 0x010 +/* + * PERIOD Register Description + * [31:00] RW | Period of the output waveform + * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED + */ + +#define PWMV4_REG_DUTY 0x014 +/* + * DUTY Register Description + * [31:00] RW | Duty cycle of the output waveform + * Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED + */ + +#define PWMV4_REG_OFFSET 0x018 +/* + * OFFSET Register Description + * [31:00] RW | Offset of the output waveform, based on the PWM clock + * Constraints: 0 <= v <= (PERIOD - DUTY) + */ + +#define PWMV4_REG_RPT 0x01C +/* + * RPT Register Description + * [31:16] RW | Second dimensional of the effective number of waveform + * repetitions. Increases by one every first dimensional times. + * Value `n` means `n + 1` repetitions. The final number of + * repetitions of the waveform in one-shot mode is: + * `(first_dimensional + 1) * (second_dimensional + 1)` + * [15:00] RW | First dimensional of the effective number of waveform + * repetitions. Value `n` means `n + 1` repetitions. + */ + +#define PWMV4_REG_FILTER_CTRL 0x020 +/* + * FILTER_CTRL Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:10] RO | Reserved + * [09:04] RW | Filter window number + * [03:01] RO | Reserved + * [00:00] RW | Filter Enable, 0 = disabled, 1 = enabled + */ + +#define PWMV4_REG_CNT 0x024 +/* + * CNT Register Description + * [31:00] RO | Current value of the PWM Channel 0 counter in pwm clock cycles, + * 0 <= v <= 2^32-1 + */ + +#define PWMV4_REG_ENABLE_DELAY 0x028 +/* + * ENABLE_DELAY Register Description + * [31:16] RO | Reserved + * [15:00] RW | PWM enable delay, in an unknown unit but probably cycles + */ + +#define PWMV4_REG_HPC 0x02C +/* + * HPC Register Description + * [31:00] RW | Number of effective high polarity cycles of the input waveform + * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1 + */ + +#define PWMV4_REG_LPC 0x030 +/* + * LPC Register Description + * [31:00] RW | Number of effective low polarity cycles of the input waveform + * in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1 + */ + +#define PWMV4_REG_BIPHASIC_CNT_CTRL0 0x040 +/* + * BIPHASIC_CNT_CTRL0 Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:10] RO | Reserved + * [09:09] RW | Biphasic Counter Phase Edge Selection for mode 0, + * 0 = rising edge (posedge), 1 = falling edge (negedge) + * [08:08] RW | Biphasic Counter Clock force enable, 1 = force enable + * [07:07] W1T | Synchronous Enable + * [06:06] W1T | Mode Switch + * 0 = Normal Mode, 1 = Switch timer clock and measured clock + * Constraints: "Biphasic Counter Mode" must be 0 if this is 1 + * [05:03] RW | Biphasic Counter Mode + * 0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3, + * 0x4 = Mode 4, 0x5 = Reserved + * [02:02] RW | Biphasic Counter Clock Selection + * 0 = clock is from PLL and frequency can be configured + * 1 = clock is from crystal oscillator and frequency is fixed + * [01:01] RW | Biphasic Counter Continuous Mode + * [00:00] W1T | Biphasic Counter Enable + */ + +#define PWMV4_REG_BIPHASIC_CNT_CTRL1 0x044 +/* + * BIPHASIC_CNT_CTRL1 Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:11] RO | Reserved + * [10:04] RW | Biphasic Counter Filter Window Number + * [03:01] RO | Reserved + * [00:00] RW | Biphasic Counter Filter Enable + */ + +#define PWMV4_REG_BIPHASIC_CNT_TIMER 0x048 +/* + * BIPHASIC_CNT_TIMER Register Description + * [31:00] RW | Biphasic Counter Timer Value, in number of biphasic counter + * timer clock cycles + */ + +#define PWMV4_REG_BIPHASIC_CNT_RES 0x04C +/* + * BIPHASIC_CNT_RES Register Description + * [31:00] RO | Biphasic Counter Result Value + * Constraints: Can only be read after INTSTS[9] is asserted + */ + +#define PWMV4_REG_BIPHASIC_CNT_RES_S 0x050 +/* + * BIPHASIC_CNT_RES_S Register Description + * [31:00] RO | Biphasic Counter Result Value with synchronised processing + * Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1 + */ + +#define PWMV4_REG_INTSTS 0x070 +/* + * INTSTS Register Description + * [31:10] RO | Reserved + * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted + * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted + * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted + * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted + * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted + * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted + * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted + * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted + * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted + * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted + */ +#define PWMV4_INT_LPC BIT(0) +#define PWMV4_INT_HPC BIT(1) +#define PWMV4_INT_LPC_W(v) FIELD_PREP_WM16(PWMV4_INT_LPC, \ + ((v) ? 1 : 0)) +#define PWMV4_INT_HPC_W(v) FIELD_PREP_WM16(PWMV4_INT_HPC, \ + ((v) ? 1 : 0)) + +#define PWMV4_REG_INT_EN 0x074 +/* + * INT_EN Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:10] RO | Reserved + * [09:09] RW | Biphasic Counter Interrupt Enable, 1 = enabled + * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled + * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled + * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled + * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled + * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled + * [03:03] W1C | Reload Interrupt Enable, 1 = enabled + * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled + * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled + * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled + */ + +#define PWMV4_REG_INT_MASK 0x078 +/* + * INT_MASK Register Description + * [31:16] WO | Write Enable Mask for the lower half of the register + * Set bit `n` here to 1 if you wish to modify bit `n >> 16` in + * the same write operation + * [15:10] RO | Reserved + * [09:09] RW | Biphasic Counter Interrupt Masked, 1 = masked + * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked + * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked + * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked + * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked + * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked + * [03:03] W1C | Reload Interrupt Masked, 1 = masked + * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked + * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked + * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked + */ + +static inline u32 mfpwm_reg_read(void __iomem *base, u32 reg) +{ + return readl(base + reg); +} + +static inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val) +{ + writel(val, base + reg); +} + +/** + * mfpwm_acquire - try becoming the active mfpwm function device + * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func + * + * mfpwm device "function" drivers must call this function before doing anything + * that either modifies or relies on the parent device's state, such as clocks, + * enabling/disabling outputs, modifying shared regs etc. + * + * The return statues should always be checked. + * + * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release() + * calls once the device is no longer making changes that affect other devices, + * or stops producing user-visible effects that depend on the current device + * state being kept as-is. (e.g. after the PWM output signal is stopped) + * + * The same device function may mfpwm_acquire() multiple times while it already + * is active, i.e. it is re-entrant, though it needs to balance this with the + * same number of mfpwm_release() calls. + * + * Context: This function does not sleep. + * + * Return: + * * %0 - success + * * %-EBUSY - a different device function is active + * * %-EOVERFLOW - the acquire counter is at its maximum + */ +extern int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf); + +/** + * mfpwm_release - drop usage of active mfpwm device function by 1 + * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func + * + * This is the balancing call to mfpwm_acquire(). If no users of the device + * function remain, set the mfpwm device to have no active device function, + * allowing other device functions to claim it. + */ +extern void mfpwm_release(const struct rockchip_mfpwm_func *pwmf); + +/** + * mfpwm_get_mode - get the current mode the hardware is in + * @pwmf: pointer to a &struct rockchip_mfpwm_func + * + * Check the hardware registers of the PWM hardware to determine which mode it + * is currently operating in, if any. + * + * Returns: + * - %-EINVAL if @pwmf is %NULL or an error pointer + * - %-1 if the PWM hardware is off, regardless of operating mode + * - %PWMV4_MODE_ONESHOT if PWM hardware is in one-shot output mode + * - %PWMV4_MODE_CONT if PWM hardware is in continuous output mode + * - %PWMV4_MODE_CAPTURE if PWM hardware is in capture mode + */ +extern int mfpwm_get_mode(const struct rockchip_mfpwm_func *pwmf); + +#endif /* __SOC_ROCKCHIP_MFPWM_H__ */ From e62cee3eaf9c88cbce3960ae9b1b38ab19082a76 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:40 +0200 Subject: [PATCH 133/218] pwm: Add rockchip PWMv4 driver The Rockchip RK3576 brings with it a new PWM IP, in downstream code referred to as "v4". This new IP is different enough from the previous Rockchip IP that I felt it necessary to add a new driver for it, instead of shoehorning it in the old one. Add this new driver, based on the PWM core's waveform APIs. Its platform device is registered by the parent mfpwm driver, from which it also receives a little platform data struct, so that mfpwm can guarantee that all the platform device drivers spread across different subsystems for this specific hardware IP do not interfere with each other. Signed-off-by: Nicolas Frattaroli --- MAINTAINERS | 1 + drivers/pwm/Kconfig | 11 + drivers/pwm/Makefile | 1 + drivers/pwm/pwm-rockchip-v4.c | 383 ++++++++++++++++++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 drivers/pwm/pwm-rockchip-v4.c diff --git a/MAINTAINERS b/MAINTAINERS index 9e9e3c029b050b..3218866859f6a3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23163,6 +23163,7 @@ L: linux-pwm@vger.kernel.org S: Maintained F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml F: drivers/mfd/rockchip-mfpwm.c +F: drivers/pwm/pwm-rockchip-v4.c F: include/linux/mfd/rockchip-mfpwm.h ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 6f3147518376a0..3fe7993bf12ba7 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -625,6 +625,17 @@ config PWM_ROCKCHIP Generic PWM framework driver for the PWM controller found on Rockchip SoCs. +config PWM_ROCKCHIP_V4 + tristate "Rockchip PWM v4 support" + depends on MFD_ROCKCHIP_MFPWM + help + Generic PWM framework driver for the PWM controller found on + later Rockchip SoCs such as the RK3576. + + Uses the Rockchip Multi-function PWM controller driver infrastructure + to guarantee fearlessly concurrent operation with other functions of + the same device implemented by drivers in other subsystems. + config PWM_SAMSUNG tristate "Samsung PWM support" depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index 0dc0d2b69025db..a234027dbbc651 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -56,6 +56,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o +obj-$(CONFIG_PWM_ROCKCHIP_V4) += pwm-rockchip-v4.o obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o diff --git a/drivers/pwm/pwm-rockchip-v4.c b/drivers/pwm/pwm-rockchip-v4.c new file mode 100644 index 00000000000000..b7de72c433c5bd --- /dev/null +++ b/drivers/pwm/pwm-rockchip-v4.c @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2025 Collabora Ltd. + * + * A Pulse-Width-Modulation (PWM) generator driver for the generators found in + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". Uses + * the MFPWM infrastructure to guarantee exclusive use over the device without + * other functions of the device from different drivers interfering with its + * operation while it's active. + * + * Technical Reference Manual: Chapter 31 of the RK3506 TRM Part 1, a SoC which + * uses the same PWM hardware and has a publicly available TRM. + * https://opensource.rock-chips.com/images/3/36/Rockchip_RK3506_TRM_Part_1_V1.2-20250811.pdf + * + * Authors: + * Nicolas Frattaroli + * + * Limitations: + * - The hardware supports both completing the currently running period + * on disable (by switching to oneshot mode with a single repetition and + * only disable when the complete irq fires), and abrupt disable (freeze). + * Only the latter is implemented in the driver. + * - When the output is disabled, the pin will remain driven to whatever state + * it last had. + * - Adjustments to the duty cycle will only take effect during the next period. + * - Adjustments to the period length will only take effect during the next + * period. + * - The hardware only supports offsets in [0, period - duty_cycle] + */ + +#include +#include +#include +#include + +struct rockchip_pwm_v4 { + struct rockchip_mfpwm_func *pwmf; + struct pwm_chip chip; +}; + +struct __packed rockchip_pwm_v4_wf { + u32 period; + u32 duty; + u32 offset; + unsigned long rate; +}; + +static inline struct rockchip_pwm_v4 *to_rockchip_pwm_v4(struct pwm_chip *chip) +{ + return pwmchip_get_drvdata(chip); +} + +/** + * rockchip_pwm_v4_round_single - convert a PWM parameter to hardware + * @rate: clock rate of the PWM clock, as per clk_get_rate + * Assumed to be <= 1GHz for overflow considerations + * @in_val: parameter in nanoseconds to convert + * + * Returns the rounded value, saturating at U32_MAX if too large + */ +static u32 rockchip_pwm_v4_round_single(unsigned long rate, u64 in_val) +{ + u64 tmp; + + tmp = mul_u64_u64_div_u64(rate, in_val, NSEC_PER_SEC); + if (tmp > U32_MAX) + tmp = U32_MAX; + + return tmp; +} + +/** + * rockchip_pwm_v4_round_params - convert PWM parameters to hardware + * @rate: PWM clock rate to do the calculations at + * @wf: pointer to the generic &struct pwm_waveform input parameters + * @wfhw: pointer to the hardware-specific &struct rockchip_pwm_v4_wf output + * parameters that the results will be stored in + * + * Convert nanosecond-based duty/period/offset parameters to the PWM hardware's + * native rounded representation in number of cycles at clock rate @rate. Should + * any of the input parameters be out of range for the hardware, the + * corresponding output parameter is the maximum permissible value for said + * parameter with considerations to the others. + */ +static void rockchip_pwm_v4_round_params(unsigned long rate, + const struct pwm_waveform *wf, + struct rockchip_pwm_v4_wf *wfhw) +{ + wfhw->period = rockchip_pwm_v4_round_single(rate, wf->period_length_ns); + + wfhw->duty = rockchip_pwm_v4_round_single(rate, wf->duty_length_ns); + + /* As per TRM, PWM_OFFSET: "The value ranges from 0 to (period-duty)" */ + wfhw->offset = rockchip_pwm_v4_round_single(rate, wf->duty_offset_ns); + if (!wfhw->period) /* Don't underflow when pwm disabled */ + wfhw->offset = 0; + else if (wfhw->offset > wfhw->period - wfhw->duty) + wfhw->offset = wfhw->period - wfhw->duty; +} + +static int rockchip_pwm_v4_round_wf_tohw(struct pwm_chip *chip, + struct pwm_device *pwm, + const struct pwm_waveform *wf, + void *_wfhw) +{ + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip); + struct rockchip_pwm_v4_wf *wfhw = _wfhw; + unsigned long rate; + + rate = clk_get_rate(pc->pwmf->core); + + /* + * It's unlikely this code path is ever taken, as current hardware does + * not expose a clock that comes anywhere close to 1GHz. However, in + * order to avoid even a theoretical overflow in parameter rounding, + * error out if this ever happens to be the case. + */ + if (rate > NSEC_PER_SEC) + return -ERANGE; + + rockchip_pwm_v4_round_params(rate, wf, wfhw); + + if (wf->period_length_ns > 0) + wfhw->rate = rate; + else + wfhw->rate = 0; + + dev_dbg(&chip->dev, + "tohw: pwm#%u: %lld/%lld [+%lld] @%lu -> DUTY: %08x, PERIOD: %08x, OFFSET: %08x\n", + pwm->hwpwm, wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns, + rate, wfhw->duty, wfhw->period, wfhw->offset); + + return 0; +} + +static int rockchip_pwm_v4_round_wf_fromhw(struct pwm_chip *chip, + struct pwm_device *pwm, + const void *_wfhw, + struct pwm_waveform *wf) +{ + const struct rockchip_pwm_v4_wf *wfhw = _wfhw; + unsigned long rate = wfhw->rate; + + if (rate) { + wf->period_length_ns = DIV_ROUND_UP((u64)wfhw->period * NSEC_PER_SEC, rate); + wf->duty_length_ns = DIV_ROUND_UP((u64)wfhw->duty * NSEC_PER_SEC, rate); + wf->duty_offset_ns = DIV_ROUND_UP((u64)wfhw->offset * NSEC_PER_SEC, rate); + } else { + wf->period_length_ns = 0; + wf->duty_length_ns = 0; + wf->duty_offset_ns = 0; + } + + dev_dbg(&chip->dev, + "fromhw: pwm#%u: DUTY: %08x, PERIOD: %08x, OFFSET: %08x @%lu -> %lld/%lld [+%lld]\n", + pwm->hwpwm, wfhw->duty, wfhw->period, wfhw->offset, rate, + wf->duty_length_ns, wf->period_length_ns, wf->duty_offset_ns); + + return 0; +} + +static int rockchip_pwm_v4_read_wf(struct pwm_chip *chip, struct pwm_device *pwm, + void *_wfhw) +{ + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip); + struct rockchip_pwm_v4_wf *wfhw = _wfhw; + unsigned long rate; + int ret; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + return ret; + + rate = clk_get_rate(pc->pwmf->core); + + wfhw->period = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_PERIOD); + wfhw->duty = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_DUTY); + wfhw->offset = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_OFFSET); + if (rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_ENABLE))) + wfhw->rate = rate; + else + wfhw->rate = 0; + + mfpwm_release(pc->pwmf); + + return 0; +} + +static int rockchip_pwm_v4_write_wf(struct pwm_chip *chip, struct pwm_device *pwm, + const void *_wfhw) +{ + struct rockchip_pwm_v4 *pc = to_rockchip_pwm_v4(chip); + const struct rockchip_pwm_v4_wf *wfhw = _wfhw; + bool was_enabled; + int ret; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + return ret; + + was_enabled = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base, + PWMV4_REG_ENABLE)); + + /* + * "But Nicolas", you ask with valid concerns, "why would you enable the + * PWM before setting all the parameter registers?" + * + * Excellent question, Mr. Reader M. Strawman! The RK3576 TRM Part 1 + * Section 34.6.3 specifies that this is the intended order of writes. + * Doing the PWM_EN and PWM_CLK_EN writes after the params but before + * the CTRL_UPDATE_EN, or even after the CTRL_UPDATE_EN, results in + * erratic behaviour where repeated turning on and off of the PWM may + * not turn it off under all circumstances. This is also why we don't + * use relaxed writes; it's not worth the footgun. + */ + if (wfhw->rate) + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, + PWMV4_EN_BOTH_MASK)); + else + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + FIELD_PREP_WM16(PWMV4_EN_BOTH_MASK, 0)); + + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_PERIOD, wfhw->period); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_DUTY, wfhw->duty); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_OFFSET, wfhw->offset); + + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, PWMV4_CTRL_CONT_FLAGS); + + /* Commit new configuration to hardware output. */ + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_CTRL_UPDATE_EN); + + if (wfhw->rate) { + if (!was_enabled) { + dev_dbg(&chip->dev, "Enabling PWM output\n"); + ret = clk_enable(pc->pwmf->core); + if (ret) + goto err_mfpwm_release; + ret = clk_set_rate_exclusive(pc->pwmf->core, wfhw->rate); + if (ret) { + clk_disable(pc->pwmf->core); + goto err_mfpwm_release; + } + + /* + * Output should be on now, acquire device to guarantee + * exclusion with other device functions while it's on. + * + * It's highly unlikely that this fails, as mfpwm has + * already been acquired before, and this is just a + * usage counter increase. Not worth the added + * complexity of clearing the PWMV4_REG_ENABLE again, + * especially considering the CTRL_UPDATE_EN behaviour. + */ + ret = mfpwm_acquire(pc->pwmf); + if (ret) { + clk_rate_exclusive_put(pc->pwmf->core); + clk_disable(pc->pwmf->core); + goto err_mfpwm_release; + } + } + } else if (was_enabled) { + dev_dbg(&chip->dev, "Disabling PWM output\n"); + clk_rate_exclusive_put(pc->pwmf->core); + clk_disable(pc->pwmf->core); + /* Output is off now, extra release to balance extra acquire */ + mfpwm_release(pc->pwmf); + } + +err_mfpwm_release: + mfpwm_release(pc->pwmf); + + return ret; +} + +static const struct pwm_ops rockchip_pwm_v4_ops = { + .sizeof_wfhw = sizeof(struct rockchip_pwm_v4_wf), + .round_waveform_tohw = rockchip_pwm_v4_round_wf_tohw, + .round_waveform_fromhw = rockchip_pwm_v4_round_wf_fromhw, + .read_waveform = rockchip_pwm_v4_read_wf, + .write_waveform = rockchip_pwm_v4_write_wf, +}; + +static bool rockchip_pwm_v4_on_and_continuous(struct rockchip_pwm_v4 *pc) +{ + bool en; + u32 val; + + en = rockchip_pwm_v4_is_enabled(mfpwm_reg_read(pc->pwmf->base, + PWMV4_REG_ENABLE)); + val = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_CTRL); + + return en && ((val & PWMV4_MODE_MASK) == PWMV4_MODE_CONT); +} + +static int rockchip_pwm_v4_probe(struct platform_device *pdev) +{ + struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev); + struct rockchip_pwm_v4 *pc; + struct pwm_chip *chip; + struct device *dev = &pdev->dev; + int ret; + + /* + * For referencing the PWM in the DT to work, we need the parent MFD + * device's OF node. + */ + dev->of_node_reused = true; + device_set_node(dev, of_fwnode_handle(dev->parent->of_node)); + + chip = devm_pwmchip_alloc(dev, 1, sizeof(*pc)); + if (IS_ERR(chip)) + return PTR_ERR(chip); + + pc = to_rockchip_pwm_v4(chip); + pc->pwmf = pwmf; + + ret = mfpwm_acquire(pwmf); + if (ret) + return dev_err_probe(dev, ret, "Couldn't acquire mfpwm in probe\n"); + + if (!rockchip_pwm_v4_on_and_continuous(pc)) + mfpwm_release(pwmf); + else { + dev_dbg(dev, "PWM was already on at probe time\n"); + ret = clk_enable(pwmf->core); + if (ret) { + dev_err_probe(dev, ret, "Enabling pwm clock failed\n"); + goto err_mfpwm_release; + } + ret = clk_rate_exclusive_get(pc->pwmf->core); + if (ret) { + dev_err_probe(dev, ret, "Protecting pwm clock failed\n"); + goto err_clk_disable; + } + } + + platform_set_drvdata(pdev, chip); + + chip->ops = &rockchip_pwm_v4_ops; + + ret = devm_pwmchip_add(dev, chip); + if (ret) { + dev_err_probe(dev, ret, "Failed to add PWM chip\n"); + if (rockchip_pwm_v4_on_and_continuous(pc)) + goto err_rate_put; + + return ret; + } + + return 0; + +err_rate_put: + clk_rate_exclusive_put(pwmf->core); +err_clk_disable: + clk_disable(pwmf->core); +err_mfpwm_release: + mfpwm_release(pwmf); + + return ret; +} + +static const struct platform_device_id rockchip_pwm_v4_ids[] = { + { .name = "rockchip-pwm-v4", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(platform, rockchip_pwm_v4_ids); + +static struct platform_driver rockchip_pwm_v4_driver = { + .probe = rockchip_pwm_v4_probe, + .driver = { + .name = "rockchip-pwm-v4", + }, + .id_table = rockchip_pwm_v4_ids, +}; +module_platform_driver(rockchip_pwm_v4_driver); + +MODULE_AUTHOR("Nicolas Frattaroli "); +MODULE_DESCRIPTION("Rockchip PWMv4 Driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("ROCKCHIP_MFPWM"); +MODULE_ALIAS("platform:pwm-rockchip-v4"); From 1ddd5b5965eb6a0254365c065eb306e68945fda4 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:41 +0200 Subject: [PATCH 134/218] counter: Add rockchip-pwm-capture driver Among many other things, Rockchip's new PWMv4 IP in the RK3576 supports PWM capture functionality. Add a basic driver for this that works to expose HPC/LPC counts and state change events to userspace through the counter framework. It's quite basic, but works well enough to demonstrate the device function exclusion stuff that mfpwm does, in order to eventually support all the functions of this device in drivers within their appropriate subsystems, without them interfering with each other. Signed-off-by: Nicolas Frattaroli --- MAINTAINERS | 1 + drivers/counter/Kconfig | 11 + drivers/counter/Makefile | 1 + drivers/counter/rockchip-pwm-capture.c | 307 +++++++++++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 drivers/counter/rockchip-pwm-capture.c diff --git a/MAINTAINERS b/MAINTAINERS index 3218866859f6a3..1ebbc6c8d6b52f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23162,6 +23162,7 @@ L: linux-rockchip@lists.infradead.org L: linux-pwm@vger.kernel.org S: Maintained F: Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml +F: drivers/counter/rockchip-pwm-capture.c F: drivers/mfd/rockchip-mfpwm.c F: drivers/pwm/pwm-rockchip-v4.c F: include/linux/mfd/rockchip-mfpwm.h diff --git a/drivers/counter/Kconfig b/drivers/counter/Kconfig index d30d22dfe57741..85adeb41aeedad 100644 --- a/drivers/counter/Kconfig +++ b/drivers/counter/Kconfig @@ -90,6 +90,17 @@ config MICROCHIP_TCB_CAPTURE To compile this driver as a module, choose M here: the module will be called microchip-tcb-capture. +config ROCKCHIP_PWM_CAPTURE + tristate "Rockchip PWM Counter Capture driver" + depends on MFD_ROCKCHIP_MFPWM + help + Generic counter framework driver for the multi-function PWM on + Rockchip SoCs such as the RK3576. + + Uses the Rockchip Multi-function PWM controller driver infrastructure + to guarantee exclusive operation with other functions of the same + device implemented by drivers in other subsystems. + config RZ_MTU3_CNT tristate "Renesas RZ/G2L MTU3a counter driver" depends on RZ_MTU3 diff --git a/drivers/counter/Makefile b/drivers/counter/Makefile index fa3c1d08f70688..2bfcfc2c584bd1 100644 --- a/drivers/counter/Makefile +++ b/drivers/counter/Makefile @@ -17,3 +17,4 @@ obj-$(CONFIG_FTM_QUADDEC) += ftm-quaddec.o obj-$(CONFIG_MICROCHIP_TCB_CAPTURE) += microchip-tcb-capture.o obj-$(CONFIG_INTEL_QEP) += intel-qep.o obj-$(CONFIG_TI_ECAP_CAPTURE) += ti-ecap-capture.o +obj-$(CONFIG_ROCKCHIP_PWM_CAPTURE) += rockchip-pwm-capture.o diff --git a/drivers/counter/rockchip-pwm-capture.c b/drivers/counter/rockchip-pwm-capture.c new file mode 100644 index 00000000000000..09a92f2bc40988 --- /dev/null +++ b/drivers/counter/rockchip-pwm-capture.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2025 Collabora Ltd. + * + * A counter driver for the Pulse-Width-Modulation (PWM) hardware found on + * Rockchip SoCs such as the RK3576, internally referred to as "PWM v4". It + * allows for measuring the high cycles and low cycles of a PWM signal through + * the generic counter framework, while guaranteeing exclusive use over the + * MFPWM device while the counter is enabled. + * + * Authors: + * Nicolas Frattaroli + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RKPWMC_INT_MASK (PWMV4_INT_LPC | PWMV4_INT_HPC) + +struct rockchip_pwm_capture { + struct rockchip_mfpwm_func *pwmf; + struct counter_device *counter; +}; + +static struct counter_signal rkpwmc_signals[] = { + { + .id = 0, + .name = "PWM Clock" + }, +}; + +static const enum counter_synapse_action rkpwmc_hpc_lpc_actions[] = { + COUNTER_SYNAPSE_ACTION_BOTH_EDGES, + COUNTER_SYNAPSE_ACTION_NONE, +}; + +static struct counter_synapse rkpwmc_pwm_synapses[] = { + { + .actions_list = rkpwmc_hpc_lpc_actions, + .num_actions = ARRAY_SIZE(rkpwmc_hpc_lpc_actions), + .signal = &rkpwmc_signals[0] + }, +}; + +static const enum counter_function rkpwmc_functions[] = { + COUNTER_FUNCTION_INCREASE, +}; + +static inline bool rkpwmc_is_enabled(struct rockchip_mfpwm_func *pwmf) +{ + return mfpwm_get_mode(pwmf) == PWMV4_MODE_CAPTURE; +} + +static bool rkpwmc_acquire_if_enabled(struct rockchip_pwm_capture *pc) +{ + int ret; + + ret = mfpwm_acquire(pc->pwmf); + if (ret < 0) + return false; + + if (rkpwmc_is_enabled(pc->pwmf)) + return true; + + mfpwm_release(pc->pwmf); + + return false; +} + +static int rkpwmc_enable_read(struct counter_device *counter, + struct counter_count *count, + u8 *enable) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + + *enable = rkpwmc_is_enabled(pc->pwmf); + + return 0; +} + +static int rkpwmc_enable_write(struct counter_device *counter, + struct counter_count *count, + u8 enable) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + int ret; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + return ret; + + if (!!enable != rkpwmc_is_enabled(pc->pwmf)) { + if (enable) { + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(false)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_CTRL, + PWMV4_CTRL_CAP_FLAGS); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN, + PWMV4_INT_LPC_W(true) | + PWMV4_INT_HPC_W(true)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(true) | PWMV4_CLK_EN(true)); + + ret = clk_enable(pc->pwmf->core); + if (ret) + goto err_release; + + ret = clk_rate_exclusive_get(pc->pwmf->core); + if (ret) + goto err_disable_pwm_clk; + + ret = mfpwm_acquire(pc->pwmf); + if (ret) + goto err_unprotect_pwm_clk; + } else { + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INT_EN, + PWMV4_INT_LPC_W(false) | + PWMV4_INT_HPC_W(false)); + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_ENABLE, + PWMV4_EN(false) | PWMV4_CLK_EN(false)); + clk_rate_exclusive_put(pc->pwmf->core); + clk_disable(pc->pwmf->core); + mfpwm_release(pc->pwmf); + } + } + + mfpwm_release(pc->pwmf); + + return 0; + +err_unprotect_pwm_clk: + clk_rate_exclusive_put(pc->pwmf->core); +err_disable_pwm_clk: + clk_disable(pc->pwmf->core); +err_release: + mfpwm_release(pc->pwmf); + + return ret; +} + +static struct counter_comp rkpwmc_ext[] = { + COUNTER_COMP_ENABLE(rkpwmc_enable_read, rkpwmc_enable_write), +}; + +enum rkpwmc_count_id { + COUNT_LPC = 0, + COUNT_HPC = 1, +}; + +static struct counter_count rkpwmc_counts[] = { + { + .id = COUNT_LPC, + .name = "Low Polarity Capture", + .functions_list = rkpwmc_functions, + .num_functions = ARRAY_SIZE(rkpwmc_functions), + .synapses = rkpwmc_pwm_synapses, + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses), + .ext = rkpwmc_ext, + .num_ext = ARRAY_SIZE(rkpwmc_ext), + }, + { + .id = COUNT_HPC, + .name = "High Polarity Capture", + .functions_list = rkpwmc_functions, + .num_functions = ARRAY_SIZE(rkpwmc_functions), + .synapses = rkpwmc_pwm_synapses, + .num_synapses = ARRAY_SIZE(rkpwmc_pwm_synapses), + .ext = rkpwmc_ext, + .num_ext = ARRAY_SIZE(rkpwmc_ext), + }, +}; + +static int rkpwmc_count_read(struct counter_device *counter, + struct counter_count *count, u64 *value) +{ + struct rockchip_pwm_capture *pc = counter_priv(counter); + + switch (count->id) { + case COUNT_LPC: + if (rkpwmc_acquire_if_enabled(pc)) { + *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_LPC); + mfpwm_release(pc->pwmf); + } else { + *value = 0; + } + return 0; + case COUNT_HPC: + if (rkpwmc_acquire_if_enabled(pc)) { + *value = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_HPC); + mfpwm_release(pc->pwmf); + } else { + *value = 0; + } + return 0; + default: + return -EINVAL; + } +} + +static const struct counter_ops rkpwmc_ops = { + .count_read = rkpwmc_count_read, +}; + +static irqreturn_t rkpwmc_irq_handler(int irq, void *data) +{ + struct rockchip_pwm_capture *pc = data; + u32 intsts; + u32 clr = 0; + + intsts = mfpwm_reg_read(pc->pwmf->base, PWMV4_REG_INTSTS); + + if (!(intsts & RKPWMC_INT_MASK)) + return IRQ_NONE; + + if (intsts & PWMV4_INT_LPC) { + clr |= PWMV4_INT_LPC; + counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 0); + } + + if (intsts & PWMV4_INT_HPC) { + clr |= PWMV4_INT_HPC; + counter_push_event(pc->counter, COUNTER_EVENT_CHANGE_OF_STATE, 1); + } + + if (clr) + mfpwm_reg_write(pc->pwmf->base, PWMV4_REG_INTSTS, clr); + + /* If other interrupt status bits are set, they're not for this driver */ + if (intsts != clr) + return IRQ_NONE; + + return IRQ_HANDLED; +} + +static int rockchip_pwm_capture_probe(struct platform_device *pdev) +{ + struct rockchip_mfpwm_func *pwmf = dev_get_platdata(&pdev->dev); + struct rockchip_pwm_capture *pc; + struct counter_device *counter; + int ret; + + /* Set our (still unset) OF node to the parent MFD device's OF node */ + pdev->dev.parent->of_node_reused = true; + device_set_node(&pdev->dev, + of_fwnode_handle(no_free_ptr(pdev->dev.parent->of_node))); + + counter = devm_counter_alloc(&pdev->dev, sizeof(*pc)); + if (IS_ERR(counter)) + return PTR_ERR(counter); + + pc = counter_priv(counter); + pc->pwmf = pwmf; + + platform_set_drvdata(pdev, pc); + + /* If the counter is on at module probe, acquire it */ + rkpwmc_acquire_if_enabled(pc); + + counter->name = pdev->name; + counter->signals = rkpwmc_signals; + counter->num_signals = ARRAY_SIZE(rkpwmc_signals); + counter->ops = &rkpwmc_ops; + counter->counts = rkpwmc_counts; + counter->num_counts = ARRAY_SIZE(rkpwmc_counts); + + pc->counter = counter; + + ret = devm_counter_add(&pdev->dev, counter); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, "Failed to add counter\n"); + + ret = devm_request_irq(&pdev->dev, pwmf->irq, rkpwmc_irq_handler, + IRQF_SHARED, pdev->name, pc); + if (ret) + return dev_err_probe(&pdev->dev, ret, "Failed requesting IRQ\n"); + + return 0; +} + +static const struct platform_device_id rockchip_pwm_capture_id_table[] = { + { .name = "rockchip-pwm-capture", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(platform, rockchip_pwm_capture_id_table); + +static struct platform_driver rockchip_pwm_capture_driver = { + .probe = rockchip_pwm_capture_probe, + .id_table = rockchip_pwm_capture_id_table, + .driver = { + .name = "rockchip-pwm-capture", + }, +}; +module_platform_driver(rockchip_pwm_capture_driver); + +MODULE_AUTHOR("Nicolas Frattaroli "); +MODULE_DESCRIPTION("Rockchip PWM Counter Capture Driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("ROCKCHIP_MFPWM"); +MODULE_IMPORT_NS("COUNTER"); +MODULE_ALIAS("platform:rockchip-pwm-capture"); From 16f67cd32c78f9960c2955ed75ce80819f5b3e0c Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:42 +0200 Subject: [PATCH 135/218] arm64: dts: rockchip: add PWM nodes to RK3576 SoC dtsi The RK3576 SoC features three distinct PWM controllers, with variable numbers of channels. Add each channel as a separate node to the SoC's device tree, as they don't really overlap in register ranges. Signed-off-by: Nicolas Frattaroli --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 208 +++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index f62a0d01fe0549..f98d7803dc3236 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -1047,6 +1047,32 @@ status = "disabled"; }; + pwm0_2ch_0: pwm@27330000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x27330000 0x0 0x1000>; + clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>, + <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm0m0_ch0>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm0_2ch_1: pwm@27331000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x27331000 0x0 0x1000>; + clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>, + <&cru CLK_PMU1PWM_OSC>, <&cru CLK_PMU1PWM_RC>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm0m0_ch1>; + #pwm-cells = <3>; + status = "disabled"; + }; + pmu: power-management@27380000 { compatible = "rockchip,rk3576-pmu", "syscon", "simple-mfd"; reg = <0x0 0x27380000 0x0 0x800>; @@ -2646,6 +2672,188 @@ status = "disabled"; }; + pwm1_6ch_0: pwm@2add0000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add0000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch0>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm1_6ch_1: pwm@2add1000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add1000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch1>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm1_6ch_2: pwm@2add2000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add2000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch2>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm1_6ch_3: pwm@2add3000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add3000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch3>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm1_6ch_4: pwm@2add4000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add4000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch4>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm1_6ch_5: pwm@2add5000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2add5000 0x0 0x1000>; + clocks = <&cru CLK_PWM1>, <&cru PCLK_PWM1>, + <&cru CLK_OSC_PWM1>, <&cru CLK_RC_PWM1>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm1m0_ch5>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_0: pwm@2ade0000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade0000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch0>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_1: pwm@2ade1000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade1000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch1>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_2: pwm@2ade2000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade2000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch2>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_3: pwm@2ade3000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade3000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch3>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_4: pwm@2ade4000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade4000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch4>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_5: pwm@2ade5000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade5000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch5>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_6: pwm@2ade6000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade6000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch6>; + #pwm-cells = <3>; + status = "disabled"; + }; + + pwm2_8ch_7: pwm@2ade7000 { + compatible = "rockchip,rk3576-pwm"; + reg = <0x0 0x2ade7000 0x0 0x1000>; + clocks = <&cru CLK_PWM2>, <&cru PCLK_PWM2>, + <&cru CLK_OSC_PWM2>, <&cru CLK_RC_PWM2>; + clock-names = "pwm", "pclk", "osc", "rc"; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&pwm2m0_ch7>; + #pwm-cells = <3>; + status = "disabled"; + }; + saradc: adc@2ae00000 { compatible = "rockchip,rk3576-saradc", "rockchip,rk3588-saradc"; reg = <0x0 0x2ae00000 0x0 0x10000>; From 4d010f841696641ee0788602fbde2061775ee6e4 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:43 +0200 Subject: [PATCH 136/218] arm64: dts: rockchip: Add cooling fan to ROCK 4D The ROCK 4D has a header to connect a small cooling fan. This fan is driven by one of the SoC's PWM outputs driving a transistor, that in turn controls the fan's power. With the introduction of PWM support, add a description of this cooling fan, as well as the additional trips and cooling-maps for it. Signed-off-by: Nicolas Frattaroli --- .../boot/dts/rockchip/rk3576-rock-4d.dts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts index 90c4c251a9537e..43a8b2153a8b6a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -46,6 +46,14 @@ shutdown-gpios = <&gpio2 RK_PD1 GPIO_ACTIVE_HIGH>; }; + fan: pwm-fan { + compatible = "pwm-fan"; + cooling-levels = <0 180 205 230 255>; + fan-supply = <&vcc_5v0_sys>; + pwms = <&pwm2_8ch_5 0 60000 0>; + #cooling-cells = <2>; + }; + es8388_sound: es8388-sound { compatible = "simple-audio-card"; simple-audio-card,format = "i2s"; @@ -789,6 +797,36 @@ }; }; +&package_thermal { + polling-delay = <100>; + + trips { + package_fan0: package-fan0 { + temperature = <50000>; + hysteresis = <2000>; + type = "active"; + }; + + package_fan1: package-fan1 { + temperature = <60000>; + hysteresis = <2000>; + type = "active"; + }; + }; + + cooling-maps { + map1 { + trip = <&package_fan0>; + cooling-device = <&fan THERMAL_NO_LIMIT 1>; + }; + + map2 { + trip = <&package_fan1>; + cooling-device = <&fan 2 THERMAL_NO_LIMIT>; + }; + }; +}; + &pcie0 { pinctrl-names = "default"; pinctrl-0 = <&pcie_reset>; @@ -798,6 +836,13 @@ }; &pinctrl { + fan { + fan_pwm: fan-pwm { + rockchip,pins = + <4 RK_PC5 14 &pcfg_pull_down_drv_level_5>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; @@ -848,6 +893,11 @@ }; }; +&pwm2_8ch_5 { + pinctrl-0 = <&fan_pwm>; + status = "okay"; +}; + &sai1 { pinctrl-names = "default"; pinctrl-0 = <&sai1m0_lrck From f912e5addc861e0448ff5fa4ef6872b1eedf7464 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:08:12 +0300 Subject: [PATCH 137/218] drm/bridge: dw-hdmi-qp: Rate limit i2c read error messages During EDID reads, repeated i2c errors can flood the kernel log: [ 25.361716] dwhdmiqp-rockchip fde80000.hdmi: i2c read error [ 25.363376] dwhdmiqp-rockchip fde80000.hdmi: i2c read error ... [ 25.368671] dwhdmiqp-rockchip fde80000.hdmi: i2c read error [ 25.369440] dwhdmiqp-rockchip fde80000.hdmi: failed to get edid Switch to dev_err_ratelimited() in dw_hdmi_qp_i2c_read() to reduce log spam while still reporting the condition. Reviewed-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index c482a8e7da259e..36fbd3831bc4d3 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -573,7 +573,7 @@ static int dw_hdmi_qp_i2c_read(struct dw_hdmi_qp *hdmi, dev_dbg_ratelimited(hdmi->dev, "i2c read timed out\n"); else - dev_err(hdmi->dev, "i2c read timed out\n"); + dev_err_ratelimited(hdmi->dev, "i2c read timed out\n"); dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); return -EAGAIN; } @@ -584,7 +584,7 @@ static int dw_hdmi_qp_i2c_read(struct dw_hdmi_qp *hdmi, dev_dbg_ratelimited(hdmi->dev, "i2c read error\n"); else - dev_err(hdmi->dev, "i2c read error\n"); + dev_err_ratelimited(hdmi->dev, "i2c read error\n"); dw_hdmi_qp_write(hdmi, 0x01, I2CM_CONTROL0); return -EIO; } From 7e29e4c6629d56f435981968dcdea60e90b31eac Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:21:36 +0300 Subject: [PATCH 138/218] drm/rockchip: dw_hdmi_qp: Add missing newlines in dev_err_probe() messages Add the missing trailing newlines to a couple of dev_err_probe() calls in dw_hdmi_qp_rockchip_bind(). Fixes: b6736a4ea3fa ("drm/rockchip: dw_hdmi_qp: Improve error handling with dev_err_probe()") Fixes: e1f7b7cbd74c ("drm/rockchip: dw_hdmi_qp: Switch to drmm_encoder_init()") Reviewed-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index c78db7f8ab6c76..d9333ad8996b8a 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -589,14 +589,14 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, drm_encoder_helper_add(encoder, &dw_hdmi_qp_rockchip_encoder_helper_funcs); ret = drmm_encoder_init(drm, encoder, NULL, DRM_MODE_ENCODER_TMDS, NULL); if (ret) - return dev_err_probe(hdmi->dev, ret, "Failed to init encoder"); + return dev_err_probe(hdmi->dev, ret, "Failed to init encoder\n"); platform_set_drvdata(pdev, hdmi); hdmi->hdmi = dw_hdmi_qp_bind(pdev, encoder, &plat_data); if (IS_ERR(hdmi->hdmi)) return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->hdmi), - "Failed to bind dw-hdmi-qp"); + "Failed to bind dw-hdmi-qp\n"); connector = drm_bridge_connector_init(drm, encoder); if (IS_ERR(connector)) From be25c57c2c9d83b3279597e88d41bc1e247bf735 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:38:26 +0300 Subject: [PATCH 139/218] drm/rockchip: dw_hdmi_qp: Use local dev variable consistently in bind() Replace indirect struct device accesses via hdmi->dev and pdev->dev with the local dev parameter already available in dw_hdmi_qp_rockchip_bind(), for consistency and readability. Reviewed-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index d9333ad8996b8a..618d2aaa5c7d8d 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -475,7 +475,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, struct clk *ref_clk; int ret, irq, i; - if (!pdev->dev.of_node) + if (!dev->of_node) return -ENODEV; hdmi = drmm_kzalloc(drm, sizeof(*hdmi), GFP_KERNEL); @@ -495,7 +495,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, return dev_err_probe(dev, -ENODEV, "Missing platform ctrl ops\n"); hdmi->ctrl_ops = cfg->ctrl_ops; - hdmi->dev = &pdev->dev; + hdmi->dev = dev; hdmi->port_id = -ENODEV; /* Identify port ID by matching base IO address */ @@ -506,7 +506,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, } } if (hdmi->port_id < 0) - return dev_err_probe(hdmi->dev, hdmi->port_id, + return dev_err_probe(dev, hdmi->port_id, "Failed to match HDMI port ID\n"); plat_data.phy_ops = cfg->phy_ops; @@ -530,37 +530,36 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, hdmi->regmap = syscon_regmap_lookup_by_phandle(dev->of_node, "rockchip,grf"); if (IS_ERR(hdmi->regmap)) - return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->regmap), + return dev_err_probe(dev, PTR_ERR(hdmi->regmap), "Unable to get rockchip,grf\n"); hdmi->vo_regmap = syscon_regmap_lookup_by_phandle(dev->of_node, "rockchip,vo-grf"); if (IS_ERR(hdmi->vo_regmap)) - return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->vo_regmap), + return dev_err_probe(dev, PTR_ERR(hdmi->vo_regmap), "Unable to get rockchip,vo-grf\n"); - ret = devm_clk_bulk_get_all_enabled(hdmi->dev, &clks); + ret = devm_clk_bulk_get_all_enabled(dev, &clks); if (ret < 0) - return dev_err_probe(hdmi->dev, ret, "Failed to get clocks\n"); + return dev_err_probe(dev, ret, "Failed to get clocks\n"); - ref_clk = clk_get(hdmi->dev, "ref"); + ref_clk = clk_get(dev, "ref"); if (IS_ERR(ref_clk)) - return dev_err_probe(hdmi->dev, PTR_ERR(ref_clk), + return dev_err_probe(dev, PTR_ERR(ref_clk), "Failed to get ref clock\n"); plat_data.ref_clk_rate = clk_get_rate(ref_clk); clk_put(ref_clk); - hdmi->frl_enable_gpio = devm_gpiod_get_optional(hdmi->dev, "frl-enable", + hdmi->frl_enable_gpio = devm_gpiod_get_optional(dev, "frl-enable", GPIOD_OUT_LOW); if (IS_ERR(hdmi->frl_enable_gpio)) - return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->frl_enable_gpio), + return dev_err_probe(dev, PTR_ERR(hdmi->frl_enable_gpio), "Failed to request FRL enable GPIO\n"); hdmi->phy = devm_of_phy_get_by_index(dev, dev->of_node, 0); if (IS_ERR(hdmi->phy)) - return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->phy), - "Failed to get phy\n"); + return dev_err_probe(dev, PTR_ERR(hdmi->phy), "Failed to get phy\n"); cfg->ctrl_ops->io_init(hdmi); @@ -578,7 +577,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, if (irq < 0) return irq; - ret = devm_request_threaded_irq(hdmi->dev, irq, + ret = devm_request_threaded_irq(dev, irq, cfg->ctrl_ops->hardirq_callback, cfg->ctrl_ops->irq_callback, IRQF_SHARED, "dw-hdmi-qp-hpd", @@ -589,18 +588,18 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, drm_encoder_helper_add(encoder, &dw_hdmi_qp_rockchip_encoder_helper_funcs); ret = drmm_encoder_init(drm, encoder, NULL, DRM_MODE_ENCODER_TMDS, NULL); if (ret) - return dev_err_probe(hdmi->dev, ret, "Failed to init encoder\n"); + return dev_err_probe(dev, ret, "Failed to init encoder\n"); platform_set_drvdata(pdev, hdmi); hdmi->hdmi = dw_hdmi_qp_bind(pdev, encoder, &plat_data); if (IS_ERR(hdmi->hdmi)) - return dev_err_probe(hdmi->dev, PTR_ERR(hdmi->hdmi), + return dev_err_probe(dev, PTR_ERR(hdmi->hdmi), "Failed to bind dw-hdmi-qp\n"); connector = drm_bridge_connector_init(drm, encoder); if (IS_ERR(connector)) - return dev_err_probe(hdmi->dev, PTR_ERR(connector), + return dev_err_probe(dev, PTR_ERR(connector), "Failed to init bridge connector\n"); return drm_connector_attach_encoder(connector, encoder); From 1c8462ec68597b9380d27f8b4ad389ab44856f45 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 23:55:39 +0300 Subject: [PATCH 140/218] drm/rockchip: dw_hdmi_qp: Register HPD IRQ after connector setup Move devm_request_threaded_irq() to the end of bind(), after drm_bridge_connector_init() and drm_connector_attach_encoder(), to ensure all DRM resources are ready before HPD interrupts can fire. While at it, add error handling for drm_connector_attach_encoder(). Tested-by: Diederik de Haas Tested-by: Maud Spierings Reviewed-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index 618d2aaa5c7d8d..fbbe26f8730ca0 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -577,14 +577,6 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, if (irq < 0) return irq; - ret = devm_request_threaded_irq(dev, irq, - cfg->ctrl_ops->hardirq_callback, - cfg->ctrl_ops->irq_callback, - IRQF_SHARED, "dw-hdmi-qp-hpd", - hdmi); - if (ret) - return ret; - drm_encoder_helper_add(encoder, &dw_hdmi_qp_rockchip_encoder_helper_funcs); ret = drmm_encoder_init(drm, encoder, NULL, DRM_MODE_ENCODER_TMDS, NULL); if (ret) @@ -602,7 +594,15 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, return dev_err_probe(dev, PTR_ERR(connector), "Failed to init bridge connector\n"); - return drm_connector_attach_encoder(connector, encoder); + ret = drm_connector_attach_encoder(connector, encoder); + if (ret) + return dev_err_probe(dev, ret, "Failed to attach connector\n"); + + return devm_request_threaded_irq(dev, irq, + cfg->ctrl_ops->hardirq_callback, + cfg->ctrl_ops->irq_callback, + IRQF_SHARED, "dw-hdmi-qp-hpd", + hdmi); } static void dw_hdmi_qp_rockchip_unbind(struct device *dev, From 2434e58d05d70ca5f7ff55ffd277f8af0ae1fc9c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 22:42:12 +0300 Subject: [PATCH 141/218] drm/rockchip: dw_hdmi_qp: Restrict HPD event to the affected connector Switch from drm_helper_hpd_irq_event(), which polls all connectors, to drm_connector_helper_hpd_irq_event(), which runs the detect cycle only on the affected connector. This avoids unnecessary work and redundant detect calls on unrelated connectors. Tested-by: Diederik de Haas Tested-by: Maud Spierings Reviewed-by: Heiko Stuebner Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index fbbe26f8730ca0..d6338195a5b751 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -93,6 +93,7 @@ struct rockchip_hdmi_qp { struct regmap *regmap; struct regmap *vo_regmap; struct rockchip_encoder encoder; + struct drm_connector *connector; struct dw_hdmi_qp *hdmi; struct phy *phy; struct gpio_desc *frl_enable_gpio; @@ -252,11 +253,10 @@ static void dw_hdmi_qp_rk3588_hpd_work(struct work_struct *work) struct rockchip_hdmi_qp *hdmi = container_of(work, struct rockchip_hdmi_qp, hpd_work.work); - struct drm_device *drm = hdmi->encoder.encoder.dev; bool changed; - if (drm) { - changed = drm_helper_hpd_irq_event(drm); + if (hdmi->connector) { + changed = drm_connector_helper_hpd_irq_event(hdmi->connector); if (changed) dev_dbg(hdmi->dev, "connector status changed\n"); } @@ -467,7 +467,6 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, struct dw_hdmi_qp_plat_data plat_data = {}; const struct rockchip_hdmi_qp_cfg *cfg; struct drm_device *drm = data; - struct drm_connector *connector; struct drm_encoder *encoder; struct rockchip_hdmi_qp *hdmi; struct resource *res; @@ -589,12 +588,12 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, return dev_err_probe(dev, PTR_ERR(hdmi->hdmi), "Failed to bind dw-hdmi-qp\n"); - connector = drm_bridge_connector_init(drm, encoder); - if (IS_ERR(connector)) - return dev_err_probe(dev, PTR_ERR(connector), + hdmi->connector = drm_bridge_connector_init(drm, encoder); + if (IS_ERR(hdmi->connector)) + return dev_err_probe(dev, PTR_ERR(hdmi->connector), "Failed to init bridge connector\n"); - ret = drm_connector_attach_encoder(connector, encoder); + ret = drm_connector_attach_encoder(hdmi->connector, encoder); if (ret) return dev_err_probe(dev, ret, "Failed to attach connector\n"); @@ -612,6 +611,8 @@ static void dw_hdmi_qp_rockchip_unbind(struct device *dev, struct rockchip_hdmi_qp *hdmi = dev_get_drvdata(dev); cancel_delayed_work_sync(&hdmi->hpd_work); + + hdmi->connector = NULL; } static const struct component_ops dw_hdmi_qp_rockchip_ops = { @@ -646,8 +647,8 @@ static int __maybe_unused dw_hdmi_qp_rockchip_resume(struct device *dev) dw_hdmi_qp_resume(dev, hdmi->hdmi); - if (hdmi->encoder.encoder.dev) - drm_helper_hpd_irq_event(hdmi->encoder.encoder.dev); + if (hdmi->connector) + drm_connector_helper_hpd_irq_event(hdmi->connector); return 0; } From 6025bc2f63b2008c542a13476adce718401e99f8 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 18:16:54 +0200 Subject: [PATCH 142/218] drm/display: scdc: Convert bit-field macros to use BIT() Make the code more robust and improve readability by replacing all open-coded bit-shift expressions with the BIT() macro. While at it, realign macro definitions so that the values line up consistently across the file. Moreover, SCDC_TMDS_BIT_CLOCK_RATIO_BY_10 is defined as (0 << 1), hence indicating a cleared bit. Since BIT() cannot meaningfully represent this, and the symbol has no users in the kernel tree, remove it. Signed-off-by: Cristian Ciocaltea --- include/drm/display/drm_scdc.h | 85 +++++++++++++++++----------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/include/drm/display/drm_scdc.h b/include/drm/display/drm_scdc.h index 3d58f37e8ed8ed..7ed40017e66068 100644 --- a/include/drm/display/drm_scdc.h +++ b/include/drm/display/drm_scdc.h @@ -24,65 +24,66 @@ #ifndef DRM_SCDC_H #define DRM_SCDC_H -#define SCDC_SINK_VERSION 0x01 +#include -#define SCDC_SOURCE_VERSION 0x02 +#define SCDC_SINK_VERSION 0x01 -#define SCDC_UPDATE_0 0x10 -#define SCDC_READ_REQUEST_TEST (1 << 2) -#define SCDC_CED_UPDATE (1 << 1) -#define SCDC_STATUS_UPDATE (1 << 0) +#define SCDC_SOURCE_VERSION 0x02 -#define SCDC_UPDATE_1 0x11 +#define SCDC_UPDATE_0 0x10 +#define SCDC_READ_REQUEST_TEST BIT(2) +#define SCDC_CED_UPDATE BIT(1) +#define SCDC_STATUS_UPDATE BIT(0) -#define SCDC_TMDS_CONFIG 0x20 -#define SCDC_TMDS_BIT_CLOCK_RATIO_BY_40 (1 << 1) -#define SCDC_TMDS_BIT_CLOCK_RATIO_BY_10 (0 << 1) -#define SCDC_SCRAMBLING_ENABLE (1 << 0) +#define SCDC_UPDATE_1 0x11 -#define SCDC_SCRAMBLER_STATUS 0x21 -#define SCDC_SCRAMBLING_STATUS (1 << 0) +#define SCDC_TMDS_CONFIG 0x20 +#define SCDC_TMDS_BIT_CLOCK_RATIO_BY_40 BIT(1) +#define SCDC_SCRAMBLING_ENABLE BIT(0) -#define SCDC_CONFIG_0 0x30 -#define SCDC_READ_REQUEST_ENABLE (1 << 0) +#define SCDC_SCRAMBLER_STATUS 0x21 +#define SCDC_SCRAMBLING_STATUS BIT(0) -#define SCDC_STATUS_FLAGS_0 0x40 -#define SCDC_CH2_LOCK (1 << 3) -#define SCDC_CH1_LOCK (1 << 2) -#define SCDC_CH0_LOCK (1 << 1) -#define SCDC_CH_LOCK_MASK (SCDC_CH2_LOCK | SCDC_CH1_LOCK | SCDC_CH0_LOCK) -#define SCDC_CLOCK_DETECT (1 << 0) +#define SCDC_CONFIG_0 0x30 +#define SCDC_READ_REQUEST_ENABLE BIT(0) -#define SCDC_STATUS_FLAGS_1 0x41 +#define SCDC_STATUS_FLAGS_0 0x40 +#define SCDC_CH2_LOCK BIT(3) +#define SCDC_CH1_LOCK BIT(2) +#define SCDC_CH0_LOCK BIT(1) +#define SCDC_CH_LOCK_MASK (SCDC_CH2_LOCK | SCDC_CH1_LOCK | SCDC_CH0_LOCK) +#define SCDC_CLOCK_DETECT BIT(0) -#define SCDC_ERR_DET_0_L 0x50 -#define SCDC_ERR_DET_0_H 0x51 -#define SCDC_ERR_DET_1_L 0x52 -#define SCDC_ERR_DET_1_H 0x53 -#define SCDC_ERR_DET_2_L 0x54 -#define SCDC_ERR_DET_2_H 0x55 -#define SCDC_CHANNEL_VALID (1 << 7) +#define SCDC_STATUS_FLAGS_1 0x41 -#define SCDC_ERR_DET_CHECKSUM 0x56 +#define SCDC_ERR_DET_0_L 0x50 +#define SCDC_ERR_DET_0_H 0x51 +#define SCDC_ERR_DET_1_L 0x52 +#define SCDC_ERR_DET_1_H 0x53 +#define SCDC_ERR_DET_2_L 0x54 +#define SCDC_ERR_DET_2_H 0x55 +#define SCDC_CHANNEL_VALID BIT(7) -#define SCDC_TEST_CONFIG_0 0xc0 -#define SCDC_TEST_READ_REQUEST (1 << 7) -#define SCDC_TEST_READ_REQUEST_DELAY(x) ((x) & 0x7f) +#define SCDC_ERR_DET_CHECKSUM 0x56 -#define SCDC_MANUFACTURER_IEEE_OUI 0xd0 -#define SCDC_MANUFACTURER_IEEE_OUI_SIZE 3 +#define SCDC_TEST_CONFIG_0 0xc0 +#define SCDC_TEST_READ_REQUEST BIT(7) +#define SCDC_TEST_READ_REQUEST_DELAY(x) ((x) & 0x7f) -#define SCDC_DEVICE_ID 0xd3 -#define SCDC_DEVICE_ID_SIZE 8 +#define SCDC_MANUFACTURER_IEEE_OUI 0xd0 +#define SCDC_MANUFACTURER_IEEE_OUI_SIZE 3 -#define SCDC_DEVICE_HARDWARE_REVISION 0xdb +#define SCDC_DEVICE_ID 0xd3 +#define SCDC_DEVICE_ID_SIZE 8 + +#define SCDC_DEVICE_HARDWARE_REVISION 0xdb #define SCDC_GET_DEVICE_HARDWARE_REVISION_MAJOR(x) (((x) >> 4) & 0xf) #define SCDC_GET_DEVICE_HARDWARE_REVISION_MINOR(x) (((x) >> 0) & 0xf) -#define SCDC_DEVICE_SOFTWARE_MAJOR_REVISION 0xdc -#define SCDC_DEVICE_SOFTWARE_MINOR_REVISION 0xdd +#define SCDC_DEVICE_SOFTWARE_MAJOR_REVISION 0xdc +#define SCDC_DEVICE_SOFTWARE_MINOR_REVISION 0xdd -#define SCDC_MANUFACTURER_SPECIFIC 0xde -#define SCDC_MANUFACTURER_SPECIFIC_SIZE 34 +#define SCDC_MANUFACTURER_SPECIFIC 0xde +#define SCDC_MANUFACTURER_SPECIFIC_SIZE 34 #endif From 31eb6b389d75ba1163535c0cd65346202ee8bde2 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 17:55:32 +0200 Subject: [PATCH 143/218] drm/display: scdc: Add HDMI 2.1 FRL register definitions HDMI 2.1 introduced Fixed Rate Link (FRL) as a new physical layer transport to replace TMDS for high-bandwidth modes requiring more than ~18 Gbps. Establishing a FRL link requires a training procedure known as Fixed Rate Link Training (FLT), which is coordinated between source and sink via the Status and Control Data Channel (SCDC). Add the register definitions and bit-field macros needed to implement this link training handshake. Signed-off-by: Cristian Ciocaltea --- include/drm/display/drm_scdc.h | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/include/drm/display/drm_scdc.h b/include/drm/display/drm_scdc.h index 7ed40017e66068..cd842940d634f0 100644 --- a/include/drm/display/drm_scdc.h +++ b/include/drm/display/drm_scdc.h @@ -31,6 +31,9 @@ #define SCDC_SOURCE_VERSION 0x02 #define SCDC_UPDATE_0 0x10 +#define SCDC_FLT_UPDATE BIT(5) +#define SCDC_FRL_START BIT(4) +#define SCDC_SOURCE_TEST_UPDATE BIT(3) #define SCDC_READ_REQUEST_TEST BIT(2) #define SCDC_CED_UPDATE BIT(1) #define SCDC_STATUS_UPDATE BIT(0) @@ -45,9 +48,25 @@ #define SCDC_SCRAMBLING_STATUS BIT(0) #define SCDC_CONFIG_0 0x30 +#define SCDC_FLT_NO_RETRAIN BIT(1) #define SCDC_READ_REQUEST_ENABLE BIT(0) +#define SCDC_CONFIG_1 0x31 +#define SCDC_FFE_LEVELS_MASK GENMASK(7, 4) +#define SCDC_FRL_RATE_MASK GENMASK(3, 0) +#define SCDC_FRL_RATE_DISABLE 0 +#define SCDC_FRL_RATE_3GBPS_3LANE 1 +#define SCDC_FRL_RATE_6GBPS_3LANE 2 +#define SCDC_FRL_RATE_6GBPS_4LANE 3 +#define SCDC_FRL_RATE_8GBPS_4LANE 4 +#define SCDC_FRL_RATE_10GBPS_4LANE 5 +#define SCDC_FRL_RATE_12GBPS_4LANE 6 + +#define SCDC_SOURCE_TEST_CONFIG 0x35 +#define SCDC_FLT_NO_TIMEOUT BIT(5) + #define SCDC_STATUS_FLAGS_0 0x40 +#define SCDC_FLT_READY BIT(6) #define SCDC_CH2_LOCK BIT(3) #define SCDC_CH1_LOCK BIT(2) #define SCDC_CH0_LOCK BIT(1) @@ -55,6 +74,12 @@ #define SCDC_CLOCK_DETECT BIT(0) #define SCDC_STATUS_FLAGS_1 0x41 +#define SCDC_FRL_LN1_LTP_REQ_MASK GENMASK(7, 4) +#define SCDC_FRL_LN0_LTP_REQ_MASK GENMASK(3, 0) + +#define SCDC_STATUS_FLAGS_2 0x42 +#define SCDC_FRL_LN3_LTP_REQ_MASK GENMASK(7, 4) +#define SCDC_FRL_LN2_LTP_REQ_MASK GENMASK(3, 0) #define SCDC_ERR_DET_0_L 0x50 #define SCDC_ERR_DET_0_H 0x51 From c9ee7ce5a0f610eb8a3fadf2cb78bade6675009a Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sat, 28 Mar 2026 22:08:28 +0200 Subject: [PATCH 144/218] drm/display: scdc-helper: Add FRL link training control functions Provide a few SCDC-side primitives to support implementing the Fixed Rate Link (FRL) link training handshake as mandated by the HDMI 2.1 specification: - drm_scdc_set_frl(): configures the FRL rate and FFE (Feed-Forward Equalization) level on the sink via the SCDC CONFIG_1 register, so the source can negotiate and apply a specific FRL operating point during link training - drm_scdc_calc_lower_frl(): computes the next-lower FRL bandwidth configuration given a current one, to support implementing the FRL link training fallback mechanism, where the source must step down to a lower rate when the sink signals training failure - drm_scdc_get_frl_ltp_request(): reads the Link Training Pattern (LTP) requests from the sink for all four FRL lanes via SCDC STATUS_FLAGS registers, to determine what pattern each lane expects during FRL link training so the source can respond appropriately Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/display/drm_scdc_helper.c | 186 ++++++++++++++++++++++ include/drm/display/drm_scdc_helper.h | 6 + 2 files changed, 192 insertions(+) diff --git a/drivers/gpu/drm/display/drm_scdc_helper.c b/drivers/gpu/drm/display/drm_scdc_helper.c index df878aad4a36b2..1f10f0fb7cabb7 100644 --- a/drivers/gpu/drm/display/drm_scdc_helper.c +++ b/drivers/gpu/drm/display/drm_scdc_helper.c @@ -21,6 +21,7 @@ * DEALINGS IN THE SOFTWARE. */ +#include #include #include #include @@ -276,3 +277,188 @@ bool drm_scdc_set_high_tmds_clock_ratio(struct drm_connector *connector, return true; } EXPORT_SYMBOL(drm_scdc_set_high_tmds_clock_ratio); + +static int drm_scdc_frl_config_to_rate(u8 config, u8 *rate_per_lane, u8 *lanes) +{ + switch (config) { + case SCDC_FRL_RATE_12GBPS_4LANE: + *rate_per_lane = 12; + *lanes = 4; + return 0; + case SCDC_FRL_RATE_10GBPS_4LANE: + *rate_per_lane = 10; + *lanes = 4; + return 0; + case SCDC_FRL_RATE_8GBPS_4LANE: + *rate_per_lane = 8; + *lanes = 4; + return 0; + case SCDC_FRL_RATE_6GBPS_4LANE: + *rate_per_lane = 6; + *lanes = 4; + return 0; + case SCDC_FRL_RATE_6GBPS_3LANE: + *rate_per_lane = 6; + *lanes = 3; + return 0; + case SCDC_FRL_RATE_3GBPS_3LANE: + *rate_per_lane = 3; + *lanes = 3; + return 0; + case SCDC_FRL_RATE_DISABLE: + *rate_per_lane = 0; + *lanes = 0; + return 0; + default: + return -EINVAL; + } +} + +static int drm_scdc_frl_rate_to_config(u8 rate_per_lane, u8 lanes) +{ + if (lanes != 0 && lanes != 3 && lanes != 4) + return -EINVAL; + + switch (rate_per_lane * lanes) { + case 48: + return SCDC_FRL_RATE_12GBPS_4LANE; + case 40: + return SCDC_FRL_RATE_10GBPS_4LANE; + case 32: + return SCDC_FRL_RATE_8GBPS_4LANE; + case 24: + return SCDC_FRL_RATE_6GBPS_4LANE; + case 18: + return SCDC_FRL_RATE_6GBPS_3LANE; + case 9: + return SCDC_FRL_RATE_3GBPS_3LANE; + case 0: + return SCDC_FRL_RATE_DISABLE; + default: + return -EINVAL; + } +} + +/** + * drm_scdc_set_frl - set FRL rate and FFE + * @connector: connector + * @rate_per_lane: FRL rate for a single lane (Gbps) + * @lanes: FRL lane count (3 or 4) + * @max_ffe_level: max TxFFE level for indicated FRL Rate (0..3) + * + * Writes over SCDC the FRL config register over SCDC channel, and sets + * FRL_Rate according to rate_per_lane x lanes, as well as FFE_levels + * according to max_ffe_level. + * + * Returns: + * True if write is successful, false otherwise. + */ +bool drm_scdc_set_frl(struct drm_connector *connector, + u8 rate_per_lane, u8 lanes, u8 max_ffe_level) +{ + u8 config; + int ret; + + ret = drm_scdc_frl_rate_to_config(rate_per_lane, lanes); + if (ret < 0 || max_ffe_level > 3) { + drm_dbg_kms(connector->dev, + "[CONNECTOR:%d:%s] Invalid FRL config: rate=%ux%u ffe=%u\n", + connector->base.id, connector->name, rate_per_lane, + lanes, max_ffe_level); + return false; + } + + config = FIELD_PREP(SCDC_FRL_RATE_MASK, ret) | + FIELD_PREP(SCDC_FFE_LEVELS_MASK, max_ffe_level); + + ret = drm_scdc_writeb(connector->ddc, SCDC_CONFIG_1, config); + if (ret < 0) { + drm_dbg_kms(connector->dev, + "[CONNECTOR:%d:%s] Failed to set FRL: %d\n", + connector->base.id, connector->name, ret); + return false; + } + + return true; +} +EXPORT_SYMBOL(drm_scdc_set_frl); + +/** + * drm_scdc_calc_lower_frl - compute a reduced bandwidth FRL rate + * @in_rate_per_lane: input FRL rate for a single lane (Gbps) + * @in_lanes: input FRL lane count (3 or 4) + * @out_rate_per_lane: output FRL rate for a single lane (Gbps) + * @out_lanes: output FRL lane count (3 or 4) + * + * Determinates the FRL rate configuration with the highest bandwidth that is + * still lower than the bandwidth corresponding to the given input configuration. + * The resulting configuration is stored in out_rate_per_lane and out_lanes. + * + * Returns: + * True if computation was successful, false otherwise. + */ +bool drm_scdc_calc_lower_frl(u8 in_rate_per_lane, u8 in_lanes, + u8 *out_rate_per_lane, u8 *out_lanes) +{ + int ret; + + ret = drm_scdc_frl_rate_to_config(in_rate_per_lane, in_lanes); + if (ret < 0) + return false; + + ret--; + + if (ret <= SCDC_FRL_RATE_DISABLE) + return false; + + ret = drm_scdc_frl_config_to_rate(ret, out_rate_per_lane, out_lanes); + + return ret == 0; +} +EXPORT_SYMBOL(drm_scdc_calc_lower_frl); + +/** + * drm_scdc_get_frl_ltp_request - read LTP requested by the Sink for FRL lanes + * @connector: connector + * @ln0: output LTP request for FRL lane 0 + * @ln1: output LTP request for FRL lane 1 + * @ln2: output LTP request for FRL lane 2 + * @ln3: output LTP request for FRL lane 3 + * + * Reads over SCDC the Link Training Pattern (LTP) requested by the Sink for + * each of the four FRL lanes and stores the values in ln0..3. + * + * Returns: + * True on success, false otherwise. + */ +bool drm_scdc_get_frl_ltp_request(struct drm_connector *connector, + u8 *ln0, u8 *ln1, u8 *ln2, u8 *ln3) +{ + u8 status; + int ret; + + ret = drm_scdc_readb(connector->ddc, SCDC_STATUS_FLAGS_1, &status); + if (ret < 0) { + drm_dbg_kms(connector->dev, + "[CONNECTOR:%d:%s] Failed to read LTP{0,1}: %d\n", + connector->base.id, connector->name, ret); + return false; + } + + *ln0 = FIELD_GET(SCDC_FRL_LN0_LTP_REQ_MASK, status); + *ln1 = FIELD_GET(SCDC_FRL_LN1_LTP_REQ_MASK, status); + + ret = drm_scdc_readb(connector->ddc, SCDC_STATUS_FLAGS_2, &status); + if (ret < 0) { + drm_dbg_kms(connector->dev, + "[CONNECTOR:%d:%s] Failed to read LTP{2,3}: %d\n", + connector->base.id, connector->name, ret); + return false; + } + + *ln2 = FIELD_GET(SCDC_FRL_LN2_LTP_REQ_MASK, status); + *ln3 = FIELD_GET(SCDC_FRL_LN3_LTP_REQ_MASK, status); + + return true; +} +EXPORT_SYMBOL(drm_scdc_get_frl_ltp_request); diff --git a/include/drm/display/drm_scdc_helper.h b/include/drm/display/drm_scdc_helper.h index 34600476a1b9c9..4bd1caee0d2817 100644 --- a/include/drm/display/drm_scdc_helper.h +++ b/include/drm/display/drm_scdc_helper.h @@ -77,4 +77,10 @@ bool drm_scdc_get_scrambling_status(struct drm_connector *connector); bool drm_scdc_set_scrambling(struct drm_connector *connector, bool enable); bool drm_scdc_set_high_tmds_clock_ratio(struct drm_connector *connector, bool set); +bool drm_scdc_set_frl(struct drm_connector *connector, + u8 rate_per_lane, u8 lanes, u8 max_ffe_level); +bool drm_scdc_calc_lower_frl(u8 in_rate_per_lane, u8 in_lanes, + u8 *out_rate_per_lane, u8 *out_lanes); +bool drm_scdc_get_frl_ltp_request(struct drm_connector *connector, + u8 *ln0, u8 *ln1, u8 *ln2, u8 *ln3); #endif From 22e9e1d634489b74b79f11bbe93511e0945f788f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:34:26 +0200 Subject: [PATCH 145/218] drm/display: hdmi: Provide hook for HDMI 2.1 FRL mode validation HDMI 2.1 sinks that support Fixed Rate Link (FRL) can accept pixel clocks that exceed their maximum TMDS character rate. The existing hdmi_clock_valid() helper unconditionally rejects any mode whose clock surpasses max_tmds_clock, making it impossible to validate modes that would be transported over FRL. Add the .frl_rate_valid() hook to struct drm_connector_hdmi_funcs, mirroring the existing .tmds_char_rate_valid() pattern. The callback receives both the required data rate and the sink's maximum link rate (both in bps) and returns a drm_mode_status value. Implementing the hook is mandatory for drivers that wish to expose FRL-capable modes. Extend hdmi_clock_valid() to fall through to an FRL validation path when the TMDS clock limit is exceeded: 1. Verify the sink advertises FRL capability via max_frl_rate_per_lane and max_lanes fields parsed from the HF-VSDB in the sink EDID. 2. Require the driver to implement the new .frl_rate_valid() callback; otherwise reject the mode, since FRL link management is driver-specific. 3. Compute the required FRL bandwidth using 16b/18b encoding and compare it against the sink's maximum FRL link capacity. Reject the mode if the required bandwidth exceeds what the sink can handle. 4. Delegate the final validation to the driver's .frl_rate_valid() callback, which can apply hardware-specific constraints on the achievable FRL lane/rate configuration. Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/display/drm_hdmi_state_helper.c | 26 +++++++++++++++++-- include/drm/drm_connector.h | 22 ++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/display/drm_hdmi_state_helper.c b/drivers/gpu/drm/display/drm_hdmi_state_helper.c index 9f3b696aceeb4b..208180ccffdad0 100644 --- a/drivers/gpu/drm/display/drm_hdmi_state_helper.c +++ b/drivers/gpu/drm/display/drm_hdmi_state_helper.c @@ -555,8 +555,30 @@ hdmi_clock_valid(const struct drm_connector *connector, const struct drm_connector_hdmi_funcs *funcs = connector->hdmi.funcs; const struct drm_display_info *info = &connector->display_info; - if (info->max_tmds_clock && clock > info->max_tmds_clock * 1000) - return MODE_CLOCK_HIGH; + if (info->max_tmds_clock && clock > info->max_tmds_clock * 1000) { + unsigned long long req_frl_bw, max_frl_bw; + + /* Check if FRL is supported by the sink */ + if (!info->hdmi.max_frl_rate_per_lane || !info->hdmi.max_lanes) + return MODE_CLOCK_HIGH; + + /* Mandate further (i.e. driver) FRL checks */ + if (!funcs || !funcs->frl_rate_valid) + return MODE_CLOCK_HIGH; + + /* + * Check required FRL bandwidth assuming 16b/18b encoding: + * bw (bps) = clock (Hz) * 3 (channels) * 8 (bit) * 18 / 16 + */ + req_frl_bw = clock * 27; + max_frl_bw = info->hdmi.max_frl_rate_per_lane * + info->hdmi.max_lanes * 1000000000ULL; + + if (req_frl_bw > max_frl_bw) + return MODE_CLOCK_HIGH; + + return funcs->frl_rate_valid(connector, mode, req_frl_bw, max_frl_bw); + } if (funcs && funcs->tmds_char_rate_valid) { enum drm_mode_status status; diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h index f83f28cae20757..9605896287fcb6 100644 --- a/include/drm/drm_connector.h +++ b/include/drm/drm_connector.h @@ -1343,6 +1343,28 @@ struct drm_connector_hdmi_funcs { const struct drm_display_mode *mode, unsigned long long tmds_rate); + /** + * @frl_rate_valid: + * + * This callback is invoked at atomic_check time to figure out + * whether a particular FRL data rate (given in bps) is supported + * by the driver by using a FRL link rate that doesn't exceed the + * maximum advertised by the sink (also expressed in bps). + * + * The @frl_rate_valid callback is only mandatory when drivers + * need to advertise FRL support. + * + * Returns: + * + * Either &drm_mode_status.MODE_OK or one of the failure reasons + * in &enum drm_mode_status. + */ + enum drm_mode_status + (*frl_rate_valid)(const struct drm_connector *connector, + const struct drm_display_mode *mode, + unsigned long long frl_data_rate, + unsigned long long frl_max_link_rate); + /** * @read_edid: * From 24ff47cb0d3aea7a33bf4064a219831aace2c871 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:27:05 +0200 Subject: [PATCH 146/218] drm/display: bridge-connector: Wire up rate validation for HDMI 2.1 bridges The bridge connector infrastructure acts as a generic glue layer between drm_bridge implementations and the DRM connector framework, delegating TMDS character rate validation to the underlying bridge via .hdmi_tmds_char_rate_valid(). FRL-capable bridges need the same treatment for the new .frl_rate_valid() connector hook introduced to support HDMI 2.1 mode validation. Add the corresponding .hdmi_frl_rate_valid() hook to struct drm_bridge_funcs, documented as optional and gated on DRM_BRIDGE_OP_HDMI, to be consistent with all other HDMI bridge callbacks. Provide drm_bridge_connector_frl_rate_valid(), which implements the .frl_rate_valid() callback in drm_connector_hdmi_funcs by forwarding the call to the HDMI bridge .hdmi_frl_rate_valid() hook. The delegation follows the same pattern as the TMDS equivalent, with one exception: if the bridge does not implement the callback, reject the FRL modes rather than silently permitting them. Unlike the TMDS path, FRL support must be explicit. Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/display/drm_bridge_connector.c | 22 ++++++++++++++++++ include/drm/drm_bridge.h | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c index d3aaa81838320e..d9e5f02ccb7f50 100644 --- a/drivers/gpu/drm/display/drm_bridge_connector.c +++ b/drivers/gpu/drm/display/drm_bridge_connector.c @@ -416,6 +416,27 @@ drm_bridge_connector_tmds_char_rate_valid(const struct drm_connector *connector, return MODE_OK; } +static enum drm_mode_status +drm_bridge_connector_frl_rate_valid(const struct drm_connector *connector, + const struct drm_display_mode *mode, + unsigned long long frl_data_rate, + unsigned long long frl_max_link_rate) +{ + struct drm_bridge_connector *bridge_connector = + to_drm_bridge_connector(connector); + struct drm_bridge *bridge; + + bridge = bridge_connector->bridge_hdmi; + if (!bridge) + return MODE_ERROR; + + if (bridge->funcs->hdmi_frl_rate_valid) + return bridge->funcs->hdmi_frl_rate_valid(bridge, mode, + frl_data_rate, + frl_max_link_rate); + return MODE_CLOCK_HIGH; +} + static int drm_bridge_connector_clear_avi_infoframe(struct drm_connector *connector) { struct drm_bridge_connector *bridge_connector = @@ -567,6 +588,7 @@ drm_bridge_connector_read_edid(struct drm_connector *connector) static const struct drm_connector_hdmi_funcs drm_bridge_connector_hdmi_funcs = { .tmds_char_rate_valid = drm_bridge_connector_tmds_char_rate_valid, + .frl_rate_valid = drm_bridge_connector_frl_rate_valid, .read_edid = drm_bridge_connector_read_edid, .avi = { .clear_infoframe = drm_bridge_connector_clear_avi_infoframe, diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 619a52552134b8..11ad4a8ce68e11 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -709,6 +709,29 @@ struct drm_bridge_funcs { const struct drm_display_mode *mode, unsigned long long tmds_rate); + /** + * @hdmi_frl_rate_valid: + * + * Check whether a particular FRL data rate (given in bps) is + * supported by the driver by using a FRL link rate that doesn't + * exceed the maximum advertised by the sink (also given in bps). + * + * This callback is optional and should only be implemented by the + * bridges that take part in the HDMI connector implementation and + * need to advertise FRL support. Bridges that implement it shall + * set the DRM_BRIDGE_OP_HDMI flag in their &drm_bridge->ops. + * + * Returns: + * + * Either &drm_mode_status.MODE_OK or one of the failure reasons + * in &enum drm_mode_status. + */ + enum drm_mode_status + (*hdmi_frl_rate_valid)(const struct drm_bridge *bridge, + const struct drm_display_mode *mode, + unsigned long long frl_data_rate, + unsigned long long frl_max_link_rate); + /** * @hdmi_clear_avi_infoframe: * From 074660620a7b285fa029ff9c353b4d179b2616bd Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 8 Apr 2026 00:23:35 +0300 Subject: [PATCH 147/218] drm/rockchip: vop2: Scale ACLK rate up for RK3588 FRL display modes At the default 500 MHz clock rate, the VOP2 AXI bus on RK3588 cannot sustain the memory bandwidth required by high-resolution FRL modes (e.g. 4K@120Hz), leading to rendering artifacts and POST_BUF_EMPTY interrupt errors. Increase the ACLK_VOP rate from 500 MHz to 750 MHz when any video port enters HDMI 2.1 FRL mode, and restore it to 500 MHz when the last FRL video port is disabled. In order for the FRL link state to be propagated from the HDMI encoder into rockchip_crtc_state, introduce a new frl_enabled flag, and use a reference counter (frl_vp_count) in struct vop2 to track how many video ports are driving FRL outputs, so the rate boost is applied for the first FRL VP and removed only after the last one goes idle. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/rockchip_drm_drv.h | 1 + drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 20 ++++++++++++++++++++ drivers/gpu/drm/rockchip/rockchip_drm_vop2.h | 1 + 3 files changed, 22 insertions(+) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_drv.h b/drivers/gpu/drm/rockchip/rockchip_drm_drv.h index 2e86ad00979c41..4ea8878e10a86d 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_drv.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_drv.h @@ -53,6 +53,7 @@ struct rockchip_crtc_state { u32 bus_format; u32 bus_flags; int color_space; + bool frl_enabled; }; #define to_rockchip_crtc_state(s) \ container_of(s, struct rockchip_crtc_state, base) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 9f30d2c7a2b400..9c802946c25022 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -104,6 +104,8 @@ enum vop2_afbc_format { }; #define VOP2_MAX_DCLK_RATE 600000000UL +#define VOP2_ACLK_RATE_DEFAULT 500000000UL +#define VOP2_ACLK_RATE_FRL 750000000UL /* * bus-format types. @@ -965,6 +967,17 @@ static void vop2_crtc_atomic_disable(struct drm_crtc *crtc, clk_disable_unprepare(vp->dclk); + if (vop2->version == VOP_VERSION_RK3588) { + struct rockchip_crtc_state *vcstate = to_rockchip_crtc_state(old_crtc_state); + + if (vcstate->frl_enabled) { + vop2->frl_vp_count--; + + if (!vop2->frl_vp_count) + clk_set_rate(vop2->aclk, VOP2_ACLK_RATE_DEFAULT); + } + } + vop2->enable_count--; if (!vop2->enable_count) @@ -1694,6 +1707,13 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, vop2->enable_count++; + if (vop2->version == VOP_VERSION_RK3588 && vcstate->frl_enabled) { + if (!vop2->frl_vp_count) + clk_set_rate(vop2->aclk, VOP2_ACLK_RATE_FRL); + + vop2->frl_vp_count++; + } + vcstate->yuv_overlay = is_yuv_output(vcstate->bus_format); vop2_crtc_enable_irq(vp, VP_INT_POST_BUF_EMPTY); diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h index 0296e35579a921..310e6547f3bc6e 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.h @@ -333,6 +333,7 @@ struct vop2 { * we need a ref counter here. */ unsigned int enable_count; + unsigned int frl_vp_count; struct clk *hclk; struct clk *aclk; struct clk *pclk; From 3398a9adf815fe2e43d2e4f0e13ffa7dabb84461 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 31 Mar 2026 17:52:43 +0300 Subject: [PATCH 148/218] drm/rockchip: dw_hdmi_qp: Consolidate link config into dedicated struct Abstract link configuration state into a dedicated dw_hdmi_qp_link_cfg struct by moving tmds_char_rate out of the main driver data struct into link_cfg, while also storing the configured PHY bit depth there. Eventually expose it to the bridge layer via a new .get_link_cfg() PHY operation. This refactoring is needed to give the bridge core a clean, extensible interface to query the active link parameters, a prerequisite for adding HDMI 2.1 FRL fields (rate, lane count, etc.) to the same structure without further scattering state across the driver. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 18 +++++++++++++++--- include/drm/bridge/dw_hdmi_qp.h | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index d6338195a5b751..b0a95ea711708d 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -96,11 +96,11 @@ struct rockchip_hdmi_qp { struct drm_connector *connector; struct dw_hdmi_qp *hdmi; struct phy *phy; + struct dw_hdmi_qp_link_cfg link_cfg; struct gpio_desc *frl_enable_gpio; struct delayed_work hpd_work; int port_id; const struct rockchip_hdmi_qp_ctrl_ops *ctrl_ops; - unsigned long long tmds_char_rate; }; struct rockchip_hdmi_qp_ctrl_ops { @@ -139,10 +139,11 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder, { struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder); struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state); + struct dw_hdmi_qp_link_cfg *lcfg = &hdmi->link_cfg; union phy_configure_opts phy_cfg = {}; int ret; - if (hdmi->tmds_char_rate == conn_state->hdmi.tmds_char_rate && + if (lcfg->tmds_char_rate == conn_state->hdmi.tmds_char_rate && s->output_bpc == conn_state->hdmi.output_bpc) return 0; @@ -151,7 +152,8 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder, ret = phy_configure(hdmi->phy, &phy_cfg); if (!ret) { - hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate; + hdmi->link_cfg.tmds_char_rate = conn_state->hdmi.tmds_char_rate; + hdmi->link_cfg.bpc = phy_cfg.hdmi.bpc; s->output_mode = ROCKCHIP_OUT_MODE_AAAA; s->output_type = DRM_MODE_CONNECTOR_HDMIA; s->output_bpc = conn_state->hdmi.output_bpc; @@ -210,11 +212,20 @@ static void dw_hdmi_qp_rk3588_setup_hpd(struct dw_hdmi_qp *dw_hdmi, void *data) regmap_write(hdmi->regmap, RK3588_GRF_SOC_CON2, val); } +static const struct dw_hdmi_qp_link_cfg * +dw_hdmi_qp_rk3588_get_link_cfg(struct dw_hdmi_qp *dw_hdmi, void *data) +{ + struct rockchip_hdmi_qp *hdmi = (struct rockchip_hdmi_qp *)data; + + return &hdmi->link_cfg; +} + static const struct dw_hdmi_qp_phy_ops rk3588_hdmi_phy_ops = { .init = dw_hdmi_qp_rk3588_phy_init, .disable = dw_hdmi_qp_rk3588_phy_disable, .read_hpd = dw_hdmi_qp_rk3588_read_hpd, .setup_hpd = dw_hdmi_qp_rk3588_setup_hpd, + .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, }; static enum drm_connector_status @@ -246,6 +257,7 @@ static const struct dw_hdmi_qp_phy_ops rk3576_hdmi_phy_ops = { .disable = dw_hdmi_qp_rk3588_phy_disable, .read_hpd = dw_hdmi_qp_rk3576_read_hpd, .setup_hpd = dw_hdmi_qp_rk3576_setup_hpd, + .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, }; static void dw_hdmi_qp_rk3588_hpd_work(struct work_struct *work) diff --git a/include/drm/bridge/dw_hdmi_qp.h b/include/drm/bridge/dw_hdmi_qp.h index 6ea9c561cfef6d..55532b7d045873 100644 --- a/include/drm/bridge/dw_hdmi_qp.h +++ b/include/drm/bridge/dw_hdmi_qp.h @@ -12,11 +12,17 @@ struct drm_encoder; struct dw_hdmi_qp; struct platform_device; +struct dw_hdmi_qp_link_cfg { + unsigned long long tmds_char_rate; + unsigned int bpc; +}; + struct dw_hdmi_qp_phy_ops { int (*init)(struct dw_hdmi_qp *hdmi, void *data); void (*disable)(struct dw_hdmi_qp *hdmi, void *data); enum drm_connector_status (*read_hpd)(struct dw_hdmi_qp *hdmi, void *data); void (*setup_hpd)(struct dw_hdmi_qp *hdmi, void *data); + const struct dw_hdmi_qp_link_cfg *(*get_link_cfg)(struct dw_hdmi_qp *hdmi, void *data); }; struct dw_hdmi_qp_plat_data { From 3684af9e8b7de8676245beab71f968181b514767 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 4 Feb 2026 01:11:56 +0200 Subject: [PATCH 149/218] drm/bridge: dw-hdmi-qp: Log resolution and refresh rate in atomic_enable() The debug entry in the HDMI branch of dw_hdmi_qp_bridge_atomic_enable() previously printed the literal string 'HDMI' as the mode field, giving no information about the actual display timing being configured. Extend it to include the active resolution and refresh rate by retrieving the CRTC mode from the incoming atomic state: dw_hdmi_qp_bridge_atomic_enable mode=1920x1080@50Hz fmt=RGB rate=185625000 bpc=10 This makes the log line self-contained and directly useful when debugging mode-setting issues, format negotiation, or TMDS rate mismatches without having to cross-reference a separate mode dump. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index 36fbd3831bc4d3..8de0da238a020d 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -857,6 +857,8 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, { struct dw_hdmi_qp *hdmi = bridge->driver_private; struct drm_connector_state *conn_state; + const struct drm_display_mode *mode; + struct drm_crtc_state *crtc_state; unsigned int op_mode; hdmi->curr_conn = drm_atomic_get_new_connector_for_encoder(state, @@ -869,9 +871,13 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, return; if (hdmi->curr_conn->display_info.is_hdmi) { - dev_dbg(hdmi->dev, "%s mode=HDMI %s rate=%llu bpc=%u\n", __func__, + crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc); + mode = &crtc_state->mode; + dev_dbg(hdmi->dev, "%s mode=%ux%u@%uHz fmt=%s rate=%llu bpc=%u\n", __func__, + mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode), drm_hdmi_connector_get_output_format_name(conn_state->hdmi.output_format), conn_state->hdmi.tmds_char_rate, conn_state->hdmi.output_bpc); + op_mode = 0; hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate; From 36b9e88cfabfb11e69aaf40c082a4448685393b4 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Mar 2026 22:42:11 +0200 Subject: [PATCH 150/218] drm/bridge: dw-hdmi-qp: Replace sentinel entries with ARRAY_SIZE() iteration The two static lookup tables for audio TMDS N and CTS values use all-zero sentinel entries as end-of-table markers, required to terminate the iteration loops. This is a rather fragile pattern, hence switch to ARRAY_SIZE()-bounded for loops and remove the now redundant 'End of table' comments along with the related rows. No functional change intended. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index 8de0da238a020d..e770195b104fee 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -103,9 +103,6 @@ static const struct dw_hdmi_audio_tmds_n { /* For 297 MHz+ HDMI spec have some other rule for setting N */ { .tmds = 297000000, .n_32k = 3073, .n_44k1 = 4704, .n_48k = 5120, }, { .tmds = 594000000, .n_32k = 3073, .n_44k1 = 9408, .n_48k = 10240,}, - - /* End of table */ - { .tmds = 0, .n_32k = 0, .n_44k1 = 0, .n_48k = 0, }, }; /* @@ -124,9 +121,6 @@ static const struct dw_hdmi_audio_tmds_cts { { .tmds = 54000000, .cts_32k = 54000, .cts_44k1 = 60000, .cts_48k = 54000, }, { .tmds = 74250000, .cts_32k = 74250, .cts_44k1 = 82500, .cts_48k = 74250, }, { .tmds = 148500000, .cts_32k = 148500, .cts_44k1 = 165000, .cts_48k = 148500, }, - - /* End of table */ - { .tmds = 0, .cts_32k = 0, .cts_44k1 = 0, .cts_48k = 0, }, }; struct dw_hdmi_qp_i2c { @@ -232,7 +226,7 @@ static int dw_hdmi_qp_match_tmds_n_table(struct dw_hdmi_qp *hdmi, const struct dw_hdmi_audio_tmds_n *tmds_n = NULL; int i; - for (i = 0; common_tmds_n_table[i].tmds != 0; i++) { + for (i = 0; i < ARRAY_SIZE(common_tmds_n_table); i++) { if (pixel_clk == common_tmds_n_table[i].tmds) { tmds_n = &common_tmds_n_table[i]; break; @@ -323,7 +317,7 @@ static unsigned int dw_hdmi_qp_find_cts(struct dw_hdmi_qp *hdmi, unsigned long p const struct dw_hdmi_audio_tmds_cts *tmds_cts = NULL; int i; - for (i = 0; common_tmds_cts_table[i].tmds != 0; i++) { + for (i = 0; i < ARRAY_SIZE(common_tmds_cts_table); i++) { if (pixel_clk == common_tmds_cts_table[i].tmds) { tmds_cts = &common_tmds_cts_table[i]; break; From 91beccd226f087058cb0708bbc601759864130b6 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:39:51 +0300 Subject: [PATCH 151/218] drm/bridge: dw-hdmi-qp: Add HDMI 2.1 FRL support Enable the DesignWare HDMI QP bridge to operate in HDMI 2.1 Fixed Rate Link (FRL) mode, in addition to the existing TMDS mode: - Expand dw_hdmi_qp_link_cfg to carry FRL rate/lane configuration (current, max, and min), allowing the PHY layer to communicate FRL operating parameters to the bridge - Implement the full FRL Link Training (FLT) state machine as a kernel work item, driving the SCDC-based handshake with the sink across all training states - Add an optional .set_frl_rate() PHY callback so the bridge can instruct the PHY to switch to a lower FRL rate during training fallback - Implement the .hdmi_frl_rate_valid() bridge callback so the DRM framework can correctly filter modes based on the hardware's FRL bandwidth capabilities Co-developed-by: Algea Cao Signed-off-by: Algea Cao Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 634 ++++++++++++++++++- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h | 3 + include/drm/bridge/dw_hdmi_qp.h | 12 +- 3 files changed, 627 insertions(+), 22 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index e770195b104fee..9e4cf46c214060 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -47,6 +47,22 @@ #define SCDC_MAX_SOURCE_VERSION 0x1 #define SCRAMB_POLL_DELAY_MS 3000 +/* + * Recommended N and Expected CTS Values in FRL Mode. + */ +static const struct dw_hdmi_qp_audio_frl_n { + unsigned int r_bit; + unsigned int n_32k; + unsigned int n_44k1; + unsigned int n_48k; +} common_frl_n_table[] = { + { .r_bit = 3, .n_32k = 4224, .n_44k1 = 5292, .n_48k = 5760, }, + { .r_bit = 6, .n_32k = 4032, .n_44k1 = 5292, .n_48k = 6048, }, + { .r_bit = 8, .n_32k = 4032, .n_44k1 = 3969, .n_48k = 6048, }, + { .r_bit = 10, .n_32k = 3456, .n_44k1 = 3969, .n_48k = 5184, }, + { .r_bit = 12, .n_32k = 3072, .n_44k1 = 3969, .n_48k = 4752, }, +}; + /* * Unless otherwise noted, entries in this table are 100% optimization. * Values can be obtained from dw_hdmi_qp_compute_n() but that function is @@ -168,10 +184,13 @@ struct dw_hdmi_qp { struct delayed_work scramb_work; bool scramb_enabled; + struct work_struct flt_work; + bool flt_no_timeout; + struct regmap *regm; int main_irq; - unsigned long tmds_char_rate; + unsigned long long tmds_char_rate; bool no_hpd; }; @@ -219,6 +238,49 @@ static void dw_hdmi_qp_set_cts_n(struct dw_hdmi_qp *hdmi, unsigned int cts, AUDPKT_ACR_CONTROL1); } +static int dw_hdmi_qp_match_frl_n_table(struct dw_hdmi_qp *hdmi, + unsigned long r_bit, + unsigned long freq) +{ + const struct dw_hdmi_qp_audio_frl_n *frl_n = NULL; + int i, n; + + for (i = 0; i < ARRAY_SIZE(common_frl_n_table); i++) { + if (r_bit == common_frl_n_table[i].r_bit) { + frl_n = &common_frl_n_table[i]; + break; + } + } + + if (!frl_n) { + dev_err(hdmi->dev, "Unexpected FRL Rbit: %lu Gbps\n", r_bit); + return 0; + } + + switch (freq) { + case 32000: + case 64000: + case 128000: + n = (freq / 32000) * frl_n->n_32k; + break; + case 44100: + case 88200: + case 176400: + n = (freq / 44100) * frl_n->n_44k1; + break; + case 48000: + case 96000: + case 192000: + n = (freq / 48000) * frl_n->n_48k; + break; + default: + dev_err(hdmi->dev, "Unexpected FRL freq: %lu Hz\n", freq); + n = 0; + } + + return n; +} + static int dw_hdmi_qp_match_tmds_n_table(struct dw_hdmi_qp *hdmi, unsigned long pixel_clk, unsigned long freq) @@ -300,10 +362,18 @@ static unsigned int dw_hdmi_qp_compute_n(struct dw_hdmi_qp *hdmi, static unsigned int dw_hdmi_qp_find_n(struct dw_hdmi_qp *hdmi, unsigned long pixel_clk, unsigned long sample_rate) { - int n = dw_hdmi_qp_match_tmds_n_table(hdmi, pixel_clk, sample_rate); + const struct dw_hdmi_qp_link_cfg *link_cfg; + int ret; + + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + if (link_cfg->frl_enabled) + return dw_hdmi_qp_match_frl_n_table(hdmi, + link_cfg->frl_rate_per_lane, + sample_rate); - if (n > 0) - return n; + ret = dw_hdmi_qp_match_tmds_n_table(hdmi, pixel_clk, sample_rate); + if (ret > 0) + return ret; dev_warn(hdmi->dev, "Rate %lu missing; compute N dynamically\n", pixel_clk); @@ -753,6 +823,451 @@ static struct i2c_adapter *dw_hdmi_qp_i2c_adapter(struct dw_hdmi_qp *hdmi) return adap; } +enum dw_hdmi_qp_frl_lts { + LTS1, /* Read EDID */ + LTS2, /* Prepare for FRL */ + LTS3, /* Training in progress */ + LTS4, /* Update FRL rate */ + LTSP, /* Training passed */ + LTSL, /* Legacy TMDS */ + LTSU, /* Undefined */ +}; + +/* + * Check sink version and FLT no-timeout mode. + */ +static int dw_hdmi_qp_frl_lts1(struct dw_hdmi_qp *hdmi) +{ + int ret; + u8 val; + + if (!hdmi->tmds_char_rate) { + dev_dbg(hdmi->dev, "lts1: hdmi disabled\n"); + return LTSL; + } + + dw_hdmi_qp_mod(hdmi, AVP_DATAPATH_VIDEO_SWDISABLE, + AVP_DATAPATH_VIDEO_SWDISABLE, GLOBAL_SWDISABLE); + + /* Reset AVP data path */ + dw_hdmi_qp_write(hdmi, AVP_DATAPATH_SWINIT_P, GLOBAL_SWRESET_REQUEST); + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_SINK_VERSION, &val); + if (ret) { + dev_err(hdmi->dev, "lts1: SCDC read failed\n"); + return LTSL; + } + + if (!val) { + dev_warn(hdmi->dev, "lts1: SCDC sink version is zero\n"); + return LTSL; + } + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_SOURCE_VERSION, 1); + if (ret) { + dev_err(hdmi->dev, "lts1: SCDC write failed\n"); + return LTSL; + } + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_SOURCE_TEST_CONFIG, &val); + if (ret) { + dev_err(hdmi->dev, "lts1: SCDC read failed\n"); + return LTSL; + } + + hdmi->flt_no_timeout = !!(val & SCDC_FLT_NO_TIMEOUT); + dev_dbg(hdmi->dev, "lts1: flt_no_timeout=%d\n", hdmi->flt_no_timeout); + + return LTS2; +} + +/* + * Check if sink is ready to training. Set FRL rate & max FFE level. + */ +static int dw_hdmi_qp_frl_lts2(struct dw_hdmi_qp *hdmi) +{ + const struct dw_hdmi_qp_link_cfg *link_cfg; + int ret, i; + u8 val; + + for (i = 0; i < 20; i++) { + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_STATUS_FLAGS_0, &val); + if (ret) { + dev_err(hdmi->dev, "lts2: SCDC read failed\n"); + return LTSL; + } + + if (val & SCDC_FLT_READY) { + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + + dev_dbg(hdmi->dev, "lts2: set rate=%ux%u\n", + link_cfg->frl_rate_per_lane, link_cfg->frl_lanes); + + ret = drm_scdc_set_frl(hdmi->curr_conn, link_cfg->frl_rate_per_lane, + link_cfg->frl_lanes, 0); + if (ret) + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_CONFIG_0, 0); + else + ret = -EIO; + + if (ret) { + dev_err(hdmi->dev, "lts2: SCDC write failed\n"); + return LTSL; + } + + return LTS3; + } + + msleep(20); + } + + dev_err(hdmi->dev, "lts2: sink flt not ready\n"); + return LTSL; +} + +/* + * Conduct link training for the specified FRL rate. + */ +static int dw_hdmi_qp_frl_lts3(struct dw_hdmi_qp *hdmi) +{ + u8 val; + int i, ret; + + /* Set 2s timeout */ + i = 4000; + + while (i-- > 0 || hdmi->flt_no_timeout) { + /* Poll FLT_update flag every 2 ms or less */ + usleep_range(400, 500); + + if (!hdmi->tmds_char_rate) { + dev_dbg(hdmi->dev, "lts3: hdmi disabled\n"); + return LTSL; + } + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_UPDATE_0, &val); + if (ret) { + dev_err(hdmi->dev, "lts3: SCDC read failed\n"); + return LTSL; + } + + if (val & SCDC_SOURCE_TEST_UPDATE) { + u8 test_cfg; + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_SOURCE_TEST_CONFIG, + &test_cfg); + if (ret) { + dev_err(hdmi->dev, "lts3: SCDC read failed\n"); + return LTSL; + } + + if (hdmi->flt_no_timeout && !(test_cfg & SCDC_FLT_NO_TIMEOUT)) { + dev_dbg(hdmi->dev, "lts3: exit test mode\n"); + hdmi->flt_no_timeout = false; + } else if (!hdmi->flt_no_timeout && (test_cfg & SCDC_FLT_NO_TIMEOUT)) { + dev_dbg(hdmi->dev, "lts3: enter test mode\n"); + hdmi->flt_no_timeout = true; + } + + /* Clear SCDC_SOURCE_TEST_UPDATE flag */ + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_SOURCE_TEST_UPDATE); + if (ret) { + dev_err(hdmi->dev, "lts3: SCDC write failed\n"); + return LTSL; + } + } + + if (val & SCDC_FLT_UPDATE) { + u8 ln0, ln1, ln2, ln3; + u32 flt_cfg; + + ret = drm_scdc_get_frl_ltp_request(hdmi->curr_conn, + &ln0, &ln1, &ln2, &ln3); + if (!ret) { + dev_err(hdmi->dev, "lts3: SCDC read failed\n"); + return LTSL; + } + + dev_dbg(hdmi->dev, "lts3: ln0=0x%x ln1=0x%x ln2=0x%x ln3=0x%x\n", + ln0, ln1, ln2, ln3); + + if (!ln0 && !ln1 && !ln2 && !ln3) { + dw_hdmi_qp_write(hdmi, 0, FLT_CONFIG1); + return LTSP; + } + + if (ln0 == 0xf && ln1 == 0xf && ln2 == 0xf && ln3 == 0xf) { + dev_dbg(hdmi->dev, "lts3: rate change request\n"); + return LTS4; + } + + if (ln0 == 0xe || ln1 == 0xe || ln2 == 0xe || ln3 == 0xe) { + dev_err(hdmi->dev, "lts3: ffe level update not expected\n"); + return LTSL; + } else { + flt_cfg = (ln3 << 16) | (ln2 << 12) | (ln1 << 8) | + (ln0 << 4) | 0xf; + + /* Support HFR1-10; send old LTP if ln0..3 == 0x3 */ + if (!hdmi->flt_no_timeout && flt_cfg == 0x3333f) + flt_cfg = dw_hdmi_qp_read(hdmi, FLT_CONFIG1); + + dw_hdmi_qp_write(hdmi, flt_cfg, FLT_CONFIG1); + } + + /* Clear FLT_update flag */ + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_FLT_UPDATE); + if (ret) { + dev_err(hdmi->dev, "lts3: SCDC write failed\n"); + return LTSL; + } + } + } + + dev_err(hdmi->dev, "lts3: timed out\n"); + return LTSL; +} + +/* + * Handle FRL rate change request from sink. + */ +static int dw_hdmi_qp_frl_lts4(struct dw_hdmi_qp *hdmi) +{ + const struct dw_hdmi_qp_link_cfg *link_cfg; + u8 try_rate_per_lane, try_lanes; + int ret; + + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + + /* Choose a reduced bandwidth FRL rate */ + ret = drm_scdc_calc_lower_frl(link_cfg->frl_rate_per_lane, link_cfg->frl_lanes, + &try_rate_per_lane, &try_lanes); + if (!ret) { + dev_err(hdmi->dev, "lts4: failed to compute lower frl\n"); + return LTSL; + } + + if (try_rate_per_lane < link_cfg->min_frl_rate_per_lane || + try_lanes < link_cfg->min_frl_lanes) { + dev_err(hdmi->dev, "lts4: unsupported %ux%u rate\n", + try_rate_per_lane, try_lanes); + return LTSL; + } + + dev_dbg(hdmi->dev, "lts4: switching to %ux%u\n", try_rate_per_lane, try_lanes); + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, SCDC_FLT_UPDATE); + if (ret) { + dev_err(hdmi->dev, "lts4: SCDC write failed\n"); + return LTSL; + } + + /* Disable phy */ + hdmi->phy.ops->disable(hdmi, hdmi->phy.data); + + ret = hdmi->phy.ops->set_frl_rate(hdmi, hdmi->phy.data, + try_rate_per_lane, try_lanes); + if (ret) { + dev_err(hdmi->dev, "lts4: failed to set phy link rate\n"); + return LTSL; + } + + /* Enable phy */ + hdmi->phy.ops->init(hdmi, hdmi->phy.data); + + /* Set new rate */ + ret = drm_scdc_set_frl(hdmi->curr_conn, try_rate_per_lane, try_lanes, 0); + if (ret) + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_FLT_UPDATE); + else + ret = -EIO; + + if (ret) { + dev_err(hdmi->dev, "lts4: SCDC write failed\n"); + return LTSL; + } + + return LTS3; +} + +/* + * Training passed, wait for further changes. + */ +static int dw_hdmi_qp_frl_ltsp(struct dw_hdmi_qp *hdmi) +{ + int i, ret; + u8 val; + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, SCDC_FLT_UPDATE); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC write failed\n"); + return LTSL; + } + + /* Set 2s timeout */ + i = 4000; + + while (i--) { + /* Poll Update Flags every 2 ms or less */ + usleep_range(400, 500); + + if (!hdmi->tmds_char_rate) { + dev_dbg(hdmi->dev, "ltsp: hdmi disabled\n"); + return LTSL; + } + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_UPDATE_0, &val); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC read failed\n"); + return LTSL; + } + + if (val & SCDC_FRL_START) { + dw_hdmi_qp_mod(hdmi, 0, AVP_DATAPATH_VIDEO_SWDISABLE, + GLOBAL_SWDISABLE); + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_FRL_START); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC write failed\n"); + return LTSL; + } + + dw_hdmi_qp_write(hdmi, PKTSCHED_GCP_CLEAR_AVMUTE, + PKTSCHED_PKT_CONTROL0); + dw_hdmi_qp_mod(hdmi, PKTSCHED_GCP_TX_EN, PKTSCHED_GCP_TX_EN, + PKTSCHED_PKT_EN); + + dev_dbg(hdmi->dev, "ltsp: flt success\n"); + break; + } + + if (val & SCDC_FLT_UPDATE) { + dw_hdmi_qp_mod(hdmi, AVP_DATAPATH_VIDEO_SWDISABLE, + AVP_DATAPATH_VIDEO_SWDISABLE, GLOBAL_SWDISABLE); + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_FLT_UPDATE); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC write failed\n"); + return LTSL; + } + + return LTS3; + } + } + + if (i < 0) { + dev_err(hdmi->dev, "ltsp: timed out\n"); + return LTSL; + } + + /* Ensure FLT_update flag is polled at least once every 250 ms. */ + i = 5; + + while (true) { + msleep(20); + + if (!hdmi->tmds_char_rate) { + dev_dbg(hdmi->dev, "ltsp: hdmi disabled\n"); + break; + } + + if (i) { + i--; + continue; + } + + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_UPDATE_0, &val); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC read failed\n"); + break; + } + + if (val & SCDC_FLT_UPDATE) { + dw_hdmi_qp_write(hdmi, PKTSCHED_GCP_SET_AVMUTE, + PKTSCHED_PKT_CONTROL0); + dw_hdmi_qp_mod(hdmi, PKTSCHED_GCP_TX_EN, PKTSCHED_GCP_TX_EN, + PKTSCHED_PKT_EN); + + msleep(50); + dw_hdmi_qp_mod(hdmi, AVP_DATAPATH_VIDEO_SWDISABLE, + AVP_DATAPATH_VIDEO_SWDISABLE, GLOBAL_SWDISABLE); + + ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, + SCDC_FLT_UPDATE); + if (ret) { + dev_err(hdmi->dev, "ltsp: SCDC write failed\n"); + break; + } + + return LTS2; + } + + i = 5; + } + + return LTSL; +} + +/* + * Exit frl mode, i.e. for training failures or hdmi disabled. + */ +static int dw_hdmi_qp_frl_ltsl(struct dw_hdmi_qp *hdmi) +{ + enum drm_connector_status status; + + status = hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data); + + dev_dbg(hdmi->dev, "ltsl: conn_stat=%d\n", status); + + if (status != connector_status_disconnected) { + drm_scdc_set_frl(hdmi->curr_conn, 0, 0, 0); + drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, SCDC_FLT_UPDATE); + } + + dw_hdmi_qp_mod(hdmi, 0, AVP_DATAPATH_VIDEO_SWDISABLE, GLOBAL_SWDISABLE); + + return LTSU; +} + +static void dw_hdmi_qp_flt_work(struct work_struct *work) +{ + struct dw_hdmi_qp *hdmi = container_of(work, struct dw_hdmi_qp, flt_work); + enum dw_hdmi_qp_frl_lts state = LTS1; + + while (true) { + switch (state) { + case LTS1: + state = dw_hdmi_qp_frl_lts1(hdmi); + break; + case LTS2: + state = dw_hdmi_qp_frl_lts2(hdmi); + break; + case LTS3: + state = dw_hdmi_qp_frl_lts3(hdmi); + break; + case LTS4: + state = dw_hdmi_qp_frl_lts4(hdmi); + break; + case LTSP: + state = dw_hdmi_qp_frl_ltsp(hdmi); + break; + case LTSL: + state = dw_hdmi_qp_frl_ltsl(hdmi); + break; + case LTSU: + return; + default: + dev_err(hdmi->dev, "unexpected flt state: %d\n", state); + return; + } + } +} + static bool dw_hdmi_qp_supports_scrambling(struct drm_display_info *display) { if (!display->is_hdmi) @@ -850,7 +1365,8 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_atomic_state *state) { struct dw_hdmi_qp *hdmi = bridge->driver_private; - struct drm_connector_state *conn_state; + const struct drm_connector_state *conn_state; + const struct dw_hdmi_qp_link_cfg *link_cfg; const struct drm_display_mode *mode; struct drm_crtc_state *crtc_state; unsigned int op_mode; @@ -864,6 +1380,8 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, if (WARN_ON(!conn_state)) return; + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + if (hdmi->curr_conn->display_info.is_hdmi) { crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc); mode = &crtc_state->mode; @@ -875,7 +1393,8 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, op_mode = 0; hdmi->tmds_char_rate = conn_state->hdmi.tmds_char_rate; - if (conn_state->hdmi.tmds_char_rate > HDMI14_MAX_TMDSCLK) + if (!link_cfg->frl_enabled && + conn_state->hdmi.tmds_char_rate > HDMI14_MAX_TMDSCLK) dw_hdmi_qp_enable_scramb(hdmi); } else { dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__); @@ -887,7 +1406,21 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, dw_hdmi_qp_mod(hdmi, HDCP2_BYPASS, HDCP2_BYPASS, HDCP2LOGIC_CONFIG0); dw_hdmi_qp_mod(hdmi, op_mode, OPMODE_DVI, LINK_CONFIG0); + dw_hdmi_qp_mod(hdmi, + link_cfg->frl_enabled && link_cfg->frl_lanes == 4 ? OPMODE_FRL_4LANES : 0, + OPMODE_FRL_4LANES, LINK_CONFIG0); + dw_hdmi_qp_mod(hdmi, link_cfg->frl_enabled, OPMODE_FRL, LINK_CONFIG0); + drm_atomic_helper_connector_hdmi_update_infoframes(hdmi->curr_conn, state); + + if (link_cfg->frl_enabled) { + /* Ensure phy output is stable before starting FLT */ + msleep(50); + schedule_work(&hdmi->flt_work); + } else { + dw_hdmi_qp_write(hdmi, PKTSCHED_GCP_CLEAR_AVMUTE, PKTSCHED_PKT_CONTROL0); + dw_hdmi_qp_mod(hdmi, PKTSCHED_GCP_TX_EN, PKTSCHED_GCP_TX_EN, PKTSCHED_PKT_EN); + } } static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge, @@ -897,7 +1430,11 @@ static void dw_hdmi_qp_bridge_atomic_disable(struct drm_bridge *bridge, hdmi->tmds_char_rate = 0; + dw_hdmi_qp_write(hdmi, PKTSCHED_GCP_SET_AVMUTE, PKTSCHED_PKT_CONTROL0); + msleep(50); + dw_hdmi_qp_disable_scramb(hdmi); + cancel_work_sync(&hdmi->flt_work); hdmi->curr_conn = NULL; hdmi->phy.ops->disable(hdmi, hdmi->phy.data); @@ -910,14 +1447,25 @@ static int dw_hdmi_qp_reset_crtc(struct dw_hdmi_qp *hdmi, u8 config; int ret; - ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_TMDS_CONFIG, &config); - if (ret < 0) { - dev_err(hdmi->dev, "Failed to read TMDS config: %d\n", ret); - return ret; - } + if (hdmi->scramb_enabled) { + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_TMDS_CONFIG, &config); + if (ret < 0) { + dev_err(hdmi->dev, "Failed to read TMDS config: %d\n", ret); + return ret; + } - if (!!(config & SCDC_SCRAMBLING_ENABLE) == hdmi->scramb_enabled) - return 0; + if (config & SCDC_SCRAMBLING_ENABLE) + return 0; + } else { + ret = drm_scdc_readb(hdmi->bridge.ddc, SCDC_CONFIG_1, &config); + if (ret < 0) { + dev_err(hdmi->dev, "Failed to read FRL config: %d\n", ret); + return ret; + } + + if (config & SCDC_FRL_RATE_MASK && work_busy(&hdmi->flt_work)) + return 0; + } drm_atomic_helper_connector_hdmi_hotplug(connector, connector_status_connected); @@ -937,8 +1485,10 @@ static int dw_hdmi_qp_bridge_detect_ctx(struct drm_bridge *bridge, struct drm_modeset_acquire_ctx *ctx) { struct dw_hdmi_qp *hdmi = bridge->driver_private; + const struct dw_hdmi_qp_link_cfg *link_cfg; enum drm_connector_status status; const struct drm_edid *drm_edid; + bool frl_active; int ret; if (hdmi->no_hpd) { @@ -951,10 +1501,14 @@ static int dw_hdmi_qp_bridge_detect_ctx(struct drm_bridge *bridge, status = hdmi->phy.ops->read_hpd(hdmi, hdmi->phy.data); - dev_dbg(hdmi->dev, "%s status=%d scramb=%d\n", __func__, - status, hdmi->scramb_enabled); + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + frl_active = link_cfg->frl_enabled && hdmi->tmds_char_rate; - if (status == connector_status_connected && hdmi->scramb_enabled) { + dev_dbg(hdmi->dev, "%s status=%d frl=%u scramb=%u\n", __func__, + status, frl_active, hdmi->scramb_enabled); + + if (status == connector_status_connected && + (frl_active || hdmi->scramb_enabled)) { ret = dw_hdmi_qp_reset_crtc(hdmi, connector, ctx); if (ret == -EDEADLK) return ret; @@ -999,12 +1553,47 @@ dw_hdmi_qp_bridge_tmds_char_rate_valid(const struct drm_bridge *bridge, return MODE_OK; } +static enum drm_mode_status +dw_hdmi_qp_bridge_frl_rate_valid(const struct drm_bridge *bridge, + const struct drm_display_mode *mode, + unsigned long long frl_data_rate, + unsigned long long frl_max_link_rate) +{ + struct dw_hdmi_qp *hdmi = bridge->driver_private; + const struct dw_hdmi_qp_link_cfg *link_cfg; + unsigned long long frl_bw; + + if (!hdmi->phy.ops->set_frl_rate) { + dev_dbg(hdmi->dev, "Unsupported FRL rate: %lld\n", frl_data_rate); + return MODE_CLOCK_HIGH; + } + + link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + + frl_bw = link_cfg->max_frl_rate_per_lane * + link_cfg->max_frl_lanes * 1000000000ULL; + if (frl_bw < frl_data_rate) { + dev_dbg(hdmi->dev, "Unsupported FRL data rate: req=%llu max=%llu\n", + frl_data_rate, frl_bw); + return MODE_CLOCK_HIGH; + } + + frl_bw = link_cfg->min_frl_rate_per_lane * + link_cfg->min_frl_lanes * 1000000000ULL; + if (frl_bw > frl_max_link_rate) { + dev_dbg(hdmi->dev, "Unsupported FRL link rate: req=%llu min=%llu\n", + frl_max_link_rate, frl_bw); + return MODE_CLOCK_LOW; + } + + return MODE_OK; +} + static int dw_hdmi_qp_bridge_clear_avi_infoframe(struct drm_bridge *bridge) { struct dw_hdmi_qp *hdmi = bridge->driver_private; - dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, - PKTSCHED_PKT_EN); + dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_TX_EN, PKTSCHED_PKT_EN); return 0; } @@ -1085,8 +1674,8 @@ static int dw_hdmi_qp_bridge_write_avi_infoframe(struct drm_bridge *bridge, dw_hdmi_qp_write_infoframe(hdmi, buffer, len, PKT_AVI_CONTENTS0); dw_hdmi_qp_mod(hdmi, 0, PKTSCHED_AVI_FIELDRATE, PKTSCHED_PKT_CONFIG1); - dw_hdmi_qp_mod(hdmi, PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, - PKTSCHED_AVI_TX_EN | PKTSCHED_GCP_TX_EN, PKTSCHED_PKT_EN); + dw_hdmi_qp_mod(hdmi, PKTSCHED_AVI_TX_EN, PKTSCHED_AVI_TX_EN, + PKTSCHED_PKT_EN); return 0; } @@ -1354,6 +1943,7 @@ static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = { .detect_ctx = dw_hdmi_qp_bridge_detect_ctx, .edid_read = dw_hdmi_qp_bridge_edid_read, .hdmi_tmds_char_rate_valid = dw_hdmi_qp_bridge_tmds_char_rate_valid, + .hdmi_frl_rate_valid = dw_hdmi_qp_bridge_frl_rate_valid, .hdmi_clear_avi_infoframe = dw_hdmi_qp_bridge_clear_avi_infoframe, .hdmi_write_avi_infoframe = dw_hdmi_qp_bridge_write_avi_infoframe, .hdmi_clear_hdmi_infoframe = dw_hdmi_qp_bridge_clear_hdmi_infoframe, @@ -1431,7 +2021,8 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev, int ret; if (!plat_data->phy_ops || !plat_data->phy_ops->init || - !plat_data->phy_ops->disable || !plat_data->phy_ops->read_hpd) { + !plat_data->phy_ops->disable || !plat_data->phy_ops->read_hpd || + !plat_data->phy_ops->get_link_cfg) { dev_err(dev, "Missing platform PHY ops\n"); return ERR_PTR(-ENODEV); } @@ -1442,6 +2033,7 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev, return ERR_CAST(hdmi); INIT_DELAYED_WORK(&hdmi->scramb_work, dw_hdmi_qp_scramb_work); + INIT_WORK(&hdmi->flt_work, dw_hdmi_qp_flt_work); hdmi->dev = dev; diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h index c07847e8d7dd09..dfb8cb6614ad7d 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.h @@ -23,6 +23,7 @@ #define GLOBAL_SWRESET_REQUEST 0x40 #define EARCRX_CMDC_SWINIT_P BIT(27) #define AVP_DATAPATH_PACKET_AUDIO_SWINIT_P BIT(10) +#define AVP_DATAPATH_SWINIT_P BIT(6) #define GLOBAL_SWDISABLE 0x44 #define CEC_SWDISABLE BIT(17) #define AVP_DATAPATH_PACKET_AUDIO_SWDISABLE BIT(10) @@ -216,6 +217,8 @@ #define PKTSCHED_ACR_TX_EN BIT(1) #define PKTSCHED_NULL_TX_EN BIT(0) #define PKTSCHED_PKT_CONTROL0 0xaac +#define PKTSCHED_GCP_CLEAR_AVMUTE BIT(1) +#define PKTSCHED_GCP_SET_AVMUTE BIT(0) #define PKTSCHED_PKT_SEND 0xab0 #define PKTSCHED_PKT_STATUS0 0xab4 #define PKTSCHED_PKT_STATUS1 0xab8 diff --git a/include/drm/bridge/dw_hdmi_qp.h b/include/drm/bridge/dw_hdmi_qp.h index 55532b7d045873..ea3c6c9fdd5cd1 100644 --- a/include/drm/bridge/dw_hdmi_qp.h +++ b/include/drm/bridge/dw_hdmi_qp.h @@ -1,12 +1,14 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd. - * Copyright (c) 2024 Collabora Ltd. + * Copyright (c) 2024-2026 Collabora Ltd. */ #ifndef __DW_HDMI_QP__ #define __DW_HDMI_QP__ +#include + struct device; struct drm_encoder; struct dw_hdmi_qp; @@ -14,6 +16,13 @@ struct platform_device; struct dw_hdmi_qp_link_cfg { unsigned long long tmds_char_rate; + bool frl_enabled; + u8 frl_rate_per_lane; + u8 frl_lanes; + u8 max_frl_rate_per_lane; + u8 max_frl_lanes; + u8 min_frl_rate_per_lane; + u8 min_frl_lanes; unsigned int bpc; }; @@ -23,6 +32,7 @@ struct dw_hdmi_qp_phy_ops { enum drm_connector_status (*read_hpd)(struct dw_hdmi_qp *hdmi, void *data); void (*setup_hpd)(struct dw_hdmi_qp *hdmi, void *data); const struct dw_hdmi_qp_link_cfg *(*get_link_cfg)(struct dw_hdmi_qp *hdmi, void *data); + int (*set_frl_rate)(struct dw_hdmi_qp *hdmi, void *data, u8 rate_per_lane, u8 lanes); }; struct dw_hdmi_qp_plat_data { From c5689f1098d183d71db770850e38dd90cdec8c2a Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 13:28:59 +0300 Subject: [PATCH 152/218] drm/bridge: dw-hdmi-qp: Add TxFFE level control support Extend the FRL link training state machine to handle sink-requested Transmitter Feed-Forward Equalization (TxFFE) level increases. During LTS3, a sink can signal that it needs a higher FFE level on the transmitter to improve signal integrity at a given FRL rate, instead of immediately requesting a rate downgrade. Handle this by incrementally stepping up the FFE level (up to the hardware maximum) via a new optional .set_ffe_level() PHY callback. Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 34 +++++++++++++++----- include/drm/bridge/dw_hdmi_qp.h | 2 ++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index 9e4cf46c214060..5582b661a10ebe 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -899,12 +899,13 @@ static int dw_hdmi_qp_frl_lts2(struct dw_hdmi_qp *hdmi) if (val & SCDC_FLT_READY) { link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); + val = hdmi->phy.ops->set_ffe_level ? link_cfg->max_ffe_level : 0; - dev_dbg(hdmi->dev, "lts2: set rate=%ux%u\n", - link_cfg->frl_rate_per_lane, link_cfg->frl_lanes); + dev_dbg(hdmi->dev, "lts2: set rate=%ux%u maxffe=%u\n", + link_cfg->frl_rate_per_lane, link_cfg->frl_lanes, val); ret = drm_scdc_set_frl(hdmi->curr_conn, link_cfg->frl_rate_per_lane, - link_cfg->frl_lanes, 0); + link_cfg->frl_lanes, val); if (ret) ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_CONFIG_0, 0); else @@ -930,7 +931,7 @@ static int dw_hdmi_qp_frl_lts2(struct dw_hdmi_qp *hdmi) */ static int dw_hdmi_qp_frl_lts3(struct dw_hdmi_qp *hdmi) { - u8 val; + u8 val, ffe_lv = 0; int i, ret; /* Set 2s timeout */ @@ -1003,8 +1004,22 @@ static int dw_hdmi_qp_frl_lts3(struct dw_hdmi_qp *hdmi) } if (ln0 == 0xe || ln1 == 0xe || ln2 == 0xe || ln3 == 0xe) { - dev_err(hdmi->dev, "lts3: ffe level update not expected\n"); - return LTSL; + if (!hdmi->phy.ops->set_ffe_level) { + dev_err(hdmi->dev, "lts3: ffe level update not expected\n"); + return LTSL; + } + + if (ffe_lv >= 3) { + dev_err(hdmi->dev, "lts3: ffe level limit reached\n"); + return LTSL; + } + + ffe_lv++; + dev_dbg(hdmi->dev, "lts3: ffe level up %d\n", ffe_lv); + + ret = hdmi->phy.ops->set_ffe_level(hdmi, hdmi->phy.data, ffe_lv); + if (ret) + return LTSL; } else { flt_cfg = (ln3 << 16) | (ln2 << 12) | (ln1 << 8) | (ln0 << 4) | 0xf; @@ -1036,7 +1051,7 @@ static int dw_hdmi_qp_frl_lts3(struct dw_hdmi_qp *hdmi) static int dw_hdmi_qp_frl_lts4(struct dw_hdmi_qp *hdmi) { const struct dw_hdmi_qp_link_cfg *link_cfg; - u8 try_rate_per_lane, try_lanes; + u8 try_rate_per_lane, try_lanes, max_ffe_level; int ret; link_cfg = hdmi->phy.ops->get_link_cfg(hdmi, hdmi->phy.data); @@ -1077,8 +1092,11 @@ static int dw_hdmi_qp_frl_lts4(struct dw_hdmi_qp *hdmi) /* Enable phy */ hdmi->phy.ops->init(hdmi, hdmi->phy.data); + max_ffe_level = hdmi->phy.ops->set_ffe_level ? link_cfg->max_ffe_level : 0; + /* Set new rate */ - ret = drm_scdc_set_frl(hdmi->curr_conn, try_rate_per_lane, try_lanes, 0); + ret = drm_scdc_set_frl(hdmi->curr_conn, try_rate_per_lane, try_lanes, + max_ffe_level); if (ret) ret = drm_scdc_writeb(hdmi->bridge.ddc, SCDC_UPDATE_0, SCDC_FLT_UPDATE); diff --git a/include/drm/bridge/dw_hdmi_qp.h b/include/drm/bridge/dw_hdmi_qp.h index ea3c6c9fdd5cd1..0d5f09ccb957c5 100644 --- a/include/drm/bridge/dw_hdmi_qp.h +++ b/include/drm/bridge/dw_hdmi_qp.h @@ -23,6 +23,7 @@ struct dw_hdmi_qp_link_cfg { u8 max_frl_lanes; u8 min_frl_rate_per_lane; u8 min_frl_lanes; + u8 max_ffe_level; unsigned int bpc; }; @@ -33,6 +34,7 @@ struct dw_hdmi_qp_phy_ops { void (*setup_hpd)(struct dw_hdmi_qp *hdmi, void *data); const struct dw_hdmi_qp_link_cfg *(*get_link_cfg)(struct dw_hdmi_qp *hdmi, void *data); int (*set_frl_rate)(struct dw_hdmi_qp *hdmi, void *data, u8 rate_per_lane, u8 lanes); + int (*set_ffe_level)(struct dw_hdmi_qp *hdmi, void *data, u8 ffe_level); }; struct dw_hdmi_qp_plat_data { From f7b29181610b52c4c7ae6e42faac6e8596e33c90 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:31:24 +0300 Subject: [PATCH 153/218] drm/rockchip: dw_hdmi_qp: Wire up FRL operating mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable HDMI 2.1 FRL mode in the Rockchip platform glue driver for the DesignWare HDMI QP controller: - Switch PHY mode selection to use phy_set_mode_ext() with PHY_HDMI_MODE_FRL or PHY_HDMI_MODE_TMDS depending on whether the required data rate exceeds HDMI 2.0's TMDS ceiling or the sink's own max TMDS clock - Negotiate the FRL rate/lane count by capping the sink's advertised capabilities against the hardware limits, and pass the result to the PHY via phy_configure() - Drive the frl_enable GPIO and set the HDMI 2.1 mode bit in the relevant GRF SoC control registers to reflect the active link mode - Implement .set_frl_rate() to allow the bridge's FLT state machine to reconfigure the PHY to a lower FRL rate during training fallback - Populate and validate the FRL rate/lane bounds in link_cfg at bind time, with per-SoC overrides in the config table (RK3588 is capped at 10 Gbps/lane × 4 lanes due to a known flicker issue at higher rates) Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 150 ++++++++++++++++-- 1 file changed, 136 insertions(+), 14 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index b0a95ea711708d..a5f135ceee866e 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (c) 2021-2022 Rockchip Electronics Co., Ltd. - * Copyright (c) 2024 Collabora Ltd. + * Copyright (c) 2024-2026 Collabora Ltd. * * Author: Algea Cao * Author: Cristian Ciocaltea @@ -63,13 +63,18 @@ #define RK3588_HDMI0_HPD_INT_CLR BIT(12) #define RK3588_HDMI1_HPD_INT_MSK BIT(15) #define RK3588_HDMI1_HPD_INT_CLR BIT(14) + #define RK3588_GRF_SOC_CON7 0x031c #define RK3588_HPD_HDMI0_IO_EN_MASK BIT(12) #define RK3588_HPD_HDMI1_IO_EN_MASK BIT(13) #define RK3588_GRF_SOC_STATUS1 0x0384 #define RK3588_HDMI0_LEVEL_INT BIT(16) #define RK3588_HDMI1_LEVEL_INT BIT(24) + #define RK3588_GRF_VO1_CON3 0x000c +#define RK3588_GRF_VO1_CON4 0x0010 +#define RK3588_HDMI21_MASK BIT(0) + #define RK3588_GRF_VO1_CON6 0x0018 #define RK3588_COLOR_DEPTH_MASK GENMASK(7, 4) #define RK3588_8BPC 0x0 @@ -81,12 +86,19 @@ #define RK3588_SDAIN_MASK BIT(10) #define RK3588_MODE_MASK BIT(11) #define RK3588_I2S_SEL_MASK BIT(13) + +#define RK3588_GRF_VO1_CON7 0x001c #define RK3588_GRF_VO1_CON9 0x0024 #define RK3588_HDMI0_GRANT_SEL BIT(10) #define RK3588_HDMI1_GRANT_SEL BIT(12) #define HOTPLUG_DEBOUNCE_MS 150 #define MAX_HDMI_PORT_NUM 2 +#define HDMI20_MAX_TMDS_RATE 600000000 +#define HDMI21_MAX_FRL_LANE_RATE 12 +#define HDMI21_MAX_FRL_LANE_NUM 4 +#define HDMI21_MIN_FRL_LANE_RATE 3 +#define HDMI21_MIN_FRL_LANE_NUM 3 struct rockchip_hdmi_qp { struct device *dev; @@ -120,16 +132,24 @@ static struct rockchip_hdmi_qp *to_rockchip_hdmi_qp(struct drm_encoder *encoder) static void dw_hdmi_qp_rockchip_encoder_enable(struct drm_encoder *encoder) { struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder); + const struct dw_hdmi_qp_link_cfg *lcfg = &hdmi->link_cfg; struct drm_crtc *crtc = encoder->crtc; + struct rockchip_crtc_state *rks; - /* Unconditionally switch to TMDS as FRL is not yet supported */ - gpiod_set_value_cansleep(hdmi->frl_enable_gpio, 0); + gpiod_set_value_cansleep(hdmi->frl_enable_gpio, lcfg->frl_enabled); if (!crtc || !crtc->state) return; + rks = to_rockchip_crtc_state(crtc->state); + if (hdmi->ctrl_ops->enc_init) - hdmi->ctrl_ops->enc_init(hdmi, to_rockchip_crtc_state(crtc->state)); + hdmi->ctrl_ops->enc_init(hdmi, rks); + + dev_dbg(hdmi->dev, "%s port=%d tmds=%llu frl=%ux%u bpc=%u/%u\n", + __func__, hdmi->port_id, lcfg->tmds_char_rate, + lcfg->frl_rate_per_lane, lcfg->frl_lanes, + rks->output_bpc, lcfg->bpc); } static int @@ -137,31 +157,72 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder, struct drm_crtc_state *crtc_state, struct drm_connector_state *conn_state) { - struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder); + const struct drm_display_info *info = &conn_state->connector->display_info; struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state); + struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder); struct dw_hdmi_qp_link_cfg *lcfg = &hdmi->link_cfg; union phy_configure_opts phy_cfg = {}; + enum phy_hdmi_mode mode; int ret; if (lcfg->tmds_char_rate == conn_state->hdmi.tmds_char_rate && s->output_bpc == conn_state->hdmi.output_bpc) return 0; - phy_cfg.hdmi.tmds_char_rate = conn_state->hdmi.tmds_char_rate; phy_cfg.hdmi.bpc = conn_state->hdmi.output_bpc; - ret = phy_configure(hdmi->phy, &phy_cfg); - if (!ret) { - hdmi->link_cfg.tmds_char_rate = conn_state->hdmi.tmds_char_rate; - hdmi->link_cfg.bpc = phy_cfg.hdmi.bpc; - s->output_mode = ROCKCHIP_OUT_MODE_AAAA; - s->output_type = DRM_MODE_CONNECTOR_HDMIA; - s->output_bpc = conn_state->hdmi.output_bpc; + if (conn_state->hdmi.tmds_char_rate > HDMI20_MAX_TMDS_RATE || + (info->max_tmds_clock && + conn_state->hdmi.tmds_char_rate > info->max_tmds_clock * 1000)) { + mode = PHY_HDMI_MODE_FRL; + + if (info->hdmi.max_frl_rate_per_lane > lcfg->max_frl_rate_per_lane) + phy_cfg.hdmi.frl.rate_per_lane = lcfg->max_frl_rate_per_lane; + else + phy_cfg.hdmi.frl.rate_per_lane = info->hdmi.max_frl_rate_per_lane; + + if (info->hdmi.max_lanes > lcfg->max_frl_lanes) + phy_cfg.hdmi.frl.lanes = lcfg->max_frl_lanes; + else + phy_cfg.hdmi.frl.lanes = info->hdmi.max_lanes; } else { + mode = PHY_HDMI_MODE_TMDS; + + phy_cfg.hdmi.tmds_char_rate = conn_state->hdmi.tmds_char_rate; + } + + ret = phy_set_mode_ext(hdmi->phy, PHY_MODE_HDMI, mode); + if (ret) { + dev_err(hdmi->dev, "Failed to switch phy mode: %d\n", ret); + return ret; + } + + ret = phy_configure(hdmi->phy, &phy_cfg); + if (ret) { dev_err(hdmi->dev, "Failed to configure phy: %d\n", ret); + return ret; } - return ret; + lcfg->tmds_char_rate = conn_state->hdmi.tmds_char_rate; + + if (mode == PHY_HDMI_MODE_FRL) { + lcfg->frl_enabled = true; + lcfg->frl_rate_per_lane = phy_cfg.hdmi.frl.rate_per_lane; + lcfg->frl_lanes = phy_cfg.hdmi.frl.lanes; + } else { + lcfg->frl_enabled = false; + lcfg->frl_rate_per_lane = 0; + lcfg->frl_lanes = 0; + } + + lcfg->bpc = phy_cfg.hdmi.bpc; + + s->output_mode = ROCKCHIP_OUT_MODE_AAAA; + s->output_type = DRM_MODE_CONNECTOR_HDMIA; + s->output_bpc = conn_state->hdmi.output_bpc; + s->frl_enabled = lcfg->frl_enabled; + + return 0; } static const struct @@ -220,12 +281,38 @@ dw_hdmi_qp_rk3588_get_link_cfg(struct dw_hdmi_qp *dw_hdmi, void *data) return &hdmi->link_cfg; } +static int dw_hdmi_qp_rk3588_set_frl_rate(struct dw_hdmi_qp *dw_hdmi, void *data, + u8 rate_per_lane, u8 lanes) +{ + struct rockchip_hdmi_qp *hdmi = (struct rockchip_hdmi_qp *)data; + union phy_configure_opts phy_cfg = {}; + int ret; + + if (!hdmi->link_cfg.frl_enabled || !rate_per_lane || !lanes) + return -EINVAL; + + phy_cfg.hdmi.frl.rate_per_lane = rate_per_lane; + phy_cfg.hdmi.frl.lanes = lanes; + phy_cfg.hdmi.bpc = hdmi->link_cfg.bpc; + + ret = phy_configure(hdmi->phy, &phy_cfg); + if (ret) { + dev_err(hdmi->dev, "Failed to set PHY FRL rate: %d\n", ret); + } else { + hdmi->link_cfg.frl_rate_per_lane = rate_per_lane; + hdmi->link_cfg.frl_lanes = lanes; + } + + return ret; +} + static const struct dw_hdmi_qp_phy_ops rk3588_hdmi_phy_ops = { .init = dw_hdmi_qp_rk3588_phy_init, .disable = dw_hdmi_qp_rk3588_phy_disable, .read_hpd = dw_hdmi_qp_rk3588_read_hpd, .setup_hpd = dw_hdmi_qp_rk3588_setup_hpd, .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, + .set_frl_rate = dw_hdmi_qp_rk3588_set_frl_rate, }; static enum drm_connector_status @@ -258,6 +345,7 @@ static const struct dw_hdmi_qp_phy_ops rk3576_hdmi_phy_ops = { .read_hpd = dw_hdmi_qp_rk3576_read_hpd, .setup_hpd = dw_hdmi_qp_rk3576_setup_hpd, .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, + .set_frl_rate = dw_hdmi_qp_rk3588_set_frl_rate, }; static void dw_hdmi_qp_rk3588_hpd_work(struct work_struct *work) @@ -397,6 +485,9 @@ static void dw_hdmi_qp_rk3576_enc_init(struct rockchip_hdmi_qp *hdmi, { u32 val; + val = FIELD_PREP_WM16(RK3576_HDMI_FRL_MOD, hdmi->link_cfg.frl_enabled); + regmap_write(hdmi->vo_regmap, RK3576_VO0_GRF_SOC_CON1, val); + if (state->output_bpc == 10) val = FIELD_PREP_WM16(RK3576_COLOR_DEPTH_MASK, RK3576_10BPC); else @@ -410,6 +501,11 @@ static void dw_hdmi_qp_rk3588_enc_init(struct rockchip_hdmi_qp *hdmi, { u32 val; + val = FIELD_PREP_WM16(RK3588_HDMI21_MASK, hdmi->link_cfg.frl_enabled); + regmap_write(hdmi->vo_regmap, + hdmi->port_id ? RK3588_GRF_VO1_CON7 : RK3588_GRF_VO1_CON4, + val); + if (state->output_bpc == 10) val = FIELD_PREP_WM16(RK3588_COLOR_DEPTH_MASK, RK3588_10BPC); else @@ -439,6 +535,10 @@ struct rockchip_hdmi_qp_cfg { unsigned int port_ids[MAX_HDMI_PORT_NUM]; const struct rockchip_hdmi_qp_ctrl_ops *ctrl_ops; const struct dw_hdmi_qp_phy_ops *phy_ops; + u8 max_frl_rate_per_lane; + u8 max_frl_lanes; + u8 min_frl_rate_per_lane; + u8 min_frl_lanes; }; static const struct rockchip_hdmi_qp_cfg rk3576_hdmi_cfg = { @@ -458,6 +558,9 @@ static const struct rockchip_hdmi_qp_cfg rk3588_hdmi_cfg = { }, .ctrl_ops = &rk3588_hdmi_ctrl_ops, .phy_ops = &rk3588_hdmi_phy_ops, + /* FIXME: Intermittent screen flicker if rate exceeds 40 Gbps */ + .max_frl_rate_per_lane = 10, + .max_frl_lanes = 4, }; static const struct of_device_id dw_hdmi_qp_rockchip_dt_ids[] = { @@ -478,6 +581,7 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, struct platform_device *pdev = to_platform_device(dev); struct dw_hdmi_qp_plat_data plat_data = {}; const struct rockchip_hdmi_qp_cfg *cfg; + struct dw_hdmi_qp_link_cfg *lcfg; struct drm_device *drm = data; struct drm_encoder *encoder; struct rockchip_hdmi_qp *hdmi; @@ -572,6 +676,24 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, if (IS_ERR(hdmi->phy)) return dev_err_probe(dev, PTR_ERR(hdmi->phy), "Failed to get phy\n"); + lcfg = &hdmi->link_cfg; + lcfg->max_frl_rate_per_lane = cfg->max_frl_rate_per_lane ?: HDMI21_MAX_FRL_LANE_RATE; + lcfg->max_frl_lanes = cfg->max_frl_lanes ?: HDMI21_MAX_FRL_LANE_NUM; + lcfg->min_frl_rate_per_lane = cfg->min_frl_rate_per_lane ?: HDMI21_MIN_FRL_LANE_RATE; + lcfg->min_frl_lanes = cfg->min_frl_lanes ?: HDMI21_MIN_FRL_LANE_NUM; + + if (lcfg->max_frl_rate_per_lane < lcfg->min_frl_rate_per_lane || + lcfg->max_frl_lanes < lcfg->min_frl_lanes || + lcfg->max_frl_rate_per_lane > HDMI21_MAX_FRL_LANE_RATE || + lcfg->max_frl_rate_per_lane < HDMI21_MIN_FRL_LANE_RATE || + lcfg->max_frl_lanes > HDMI21_MAX_FRL_LANE_NUM || + lcfg->max_frl_lanes < HDMI21_MIN_FRL_LANE_NUM || + lcfg->min_frl_rate_per_lane > HDMI21_MAX_FRL_LANE_RATE || + lcfg->min_frl_rate_per_lane < HDMI21_MIN_FRL_LANE_RATE || + lcfg->min_frl_lanes > HDMI21_MAX_FRL_LANE_NUM || + lcfg->min_frl_lanes < HDMI21_MIN_FRL_LANE_NUM) + return dev_err_probe(hdmi->dev, -EINVAL, "Invalid FRL config\n"); + cfg->ctrl_ops->io_init(hdmi); INIT_DELAYED_WORK(&hdmi->hpd_work, dw_hdmi_qp_rk3588_hpd_work); From d95fce3ab545b5e4e142e76958b4b3f8fd3b768e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:33:45 +0300 Subject: [PATCH 154/218] drm/rockchip: dw_hdmi_qp: Wire up TxFFE level adjustment Provide the .set_ffe_level() PHY callback for the Rockchip platform glue driver, enabling the bridge's FLT state machine to request FFE level adjustments from the PHY during FRL link training. The callback delegates to phy_configure() with the requested FFE level, using a dedicated flag (set_ffe_level) to distinguish it from a regular rate reconfiguration. A max_ffe_level field is added to the per-SoC config table (defaulting to level 3 in auto mode) and propagated into link_cfg at bind time, so the bridge knows the upper bound to advertise to the sink. Signed-off-by: Cristian Ciocaltea --- .../gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index a5f135ceee866e..863e9238f2373b 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -306,6 +306,26 @@ static int dw_hdmi_qp_rk3588_set_frl_rate(struct dw_hdmi_qp *dw_hdmi, void *data return ret; } +static int dw_hdmi_qp_rk3588_set_ffe_level(struct dw_hdmi_qp *dw_hdmi, void *data, + u8 ffe_level) +{ + struct rockchip_hdmi_qp *hdmi = (struct rockchip_hdmi_qp *)data; + union phy_configure_opts phy_cfg = {}; + int ret; + + if (!hdmi->link_cfg.frl_enabled) + return -EINVAL; + + phy_cfg.hdmi.frl.ffe_level = ffe_level; + phy_cfg.hdmi.frl.set_ffe_level = true; + + ret = phy_configure(hdmi->phy, &phy_cfg); + if (ret) + dev_err(hdmi->dev, "Failed to set PHY FFE level: %d\n", ret); + + return ret; +} + static const struct dw_hdmi_qp_phy_ops rk3588_hdmi_phy_ops = { .init = dw_hdmi_qp_rk3588_phy_init, .disable = dw_hdmi_qp_rk3588_phy_disable, @@ -313,6 +333,7 @@ static const struct dw_hdmi_qp_phy_ops rk3588_hdmi_phy_ops = { .setup_hpd = dw_hdmi_qp_rk3588_setup_hpd, .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, .set_frl_rate = dw_hdmi_qp_rk3588_set_frl_rate, + .set_ffe_level = dw_hdmi_qp_rk3588_set_ffe_level, }; static enum drm_connector_status @@ -346,6 +367,7 @@ static const struct dw_hdmi_qp_phy_ops rk3576_hdmi_phy_ops = { .setup_hpd = dw_hdmi_qp_rk3576_setup_hpd, .get_link_cfg = dw_hdmi_qp_rk3588_get_link_cfg, .set_frl_rate = dw_hdmi_qp_rk3588_set_frl_rate, + .set_ffe_level = dw_hdmi_qp_rk3588_set_ffe_level, }; static void dw_hdmi_qp_rk3588_hpd_work(struct work_struct *work) @@ -530,6 +552,14 @@ static const struct rockchip_hdmi_qp_ctrl_ops rk3588_hdmi_ctrl_ops = { .hardirq_callback = dw_hdmi_qp_rk3588_hardirq, }; +enum rockchip_hdmi_qp_ffe_cfg { + FFE_LEVEL_AUTO = 0, + FFE_LEVEL_0, + FFE_LEVEL_1, + FFE_LEVEL_2, + FFE_LEVEL_3, +}; + struct rockchip_hdmi_qp_cfg { unsigned int num_ports; unsigned int port_ids[MAX_HDMI_PORT_NUM]; @@ -539,6 +569,7 @@ struct rockchip_hdmi_qp_cfg { u8 max_frl_lanes; u8 min_frl_rate_per_lane; u8 min_frl_lanes; + enum rockchip_hdmi_qp_ffe_cfg max_ffe; }; static const struct rockchip_hdmi_qp_cfg rk3576_hdmi_cfg = { @@ -575,6 +606,14 @@ static const struct of_device_id dw_hdmi_qp_rockchip_dt_ids[] = { }; MODULE_DEVICE_TABLE(of, dw_hdmi_qp_rockchip_dt_ids); +static u8 dw_hdmi_qp_rockchip_ffe_cfg_to_level(enum rockchip_hdmi_qp_ffe_cfg ffe) +{ + if (ffe == FFE_LEVEL_AUTO) + ffe = FFE_LEVEL_3; + + return ffe - 1; +} + static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, void *data) { @@ -694,6 +733,8 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, lcfg->min_frl_lanes < HDMI21_MIN_FRL_LANE_NUM) return dev_err_probe(hdmi->dev, -EINVAL, "Invalid FRL config\n"); + lcfg->max_ffe_level = dw_hdmi_qp_rockchip_ffe_cfg_to_level(cfg->max_ffe); + cfg->ctrl_ops->io_init(hdmi); INIT_DELAYED_WORK(&hdmi->hpd_work, dw_hdmi_qp_rk3588_hpd_work); From e260e7ce27692d06fe0c56e2b08f13f90563acc9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:42:59 +0200 Subject: [PATCH 155/218] drm/rockchip: Select SND_SOC_HDMI_CODEC for ROCKCHIP_DW_HDMI_QP The HDMI QP bridge driver supports HDMI audio, but this requires SND_SOC_HDMI_CODEC, which needs to be selected. Make sure the select is done instead of relying on another config option enabling it for us. Reported-by: Michal Tomek Fixes: fd0141d1a8a2 ("drm/bridge: synopsys: Add audio support for dw-hdmi-qp") Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/Kconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/Kconfig b/drivers/gpu/drm/rockchip/Kconfig index 1479b8c4ed40ad..d12dce371cf0b3 100644 --- a/drivers/gpu/drm/rockchip/Kconfig +++ b/drivers/gpu/drm/rockchip/Kconfig @@ -20,7 +20,7 @@ config DRM_ROCKCHIP select DRM_INNO_HDMI if ROCKCHIP_INNO_HDMI select GENERIC_PHY if ROCKCHIP_DW_MIPI_DSI select GENERIC_PHY_MIPI_DPHY if ROCKCHIP_DW_MIPI_DSI - select SND_SOC_HDMI_CODEC if ROCKCHIP_CDN_DP && SND_SOC + select SND_SOC_HDMI_CODEC if SND_SOC && (ROCKCHIP_CDN_DP || ROCKCHIP_DW_HDMI_QP) help Choose this option if you have a Rockchip soc chipset. This driver provides kernel mode setting and buffer From 201298030500bce04cddda4b10dbc27e7f8c2867 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:57:35 +0200 Subject: [PATCH 156/218] drm/rockchip: Select SND_SOC_HDMI_CODEC for ROCKCHIP_DW_DP The DW DP bridge driver supports audio, but this requires SND_SOC_HDMI_CODEC, which needs to be selected. Make sure the select is done instead of relying on another config option enabling it for us. Reported-by: Michal Tomek Fixes: b6798038bbe3 ("drm/bridge: synopsys: dw-dp: Add audio support") Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/rockchip/Kconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/Kconfig b/drivers/gpu/drm/rockchip/Kconfig index d12dce371cf0b3..68e222350aeaec 100644 --- a/drivers/gpu/drm/rockchip/Kconfig +++ b/drivers/gpu/drm/rockchip/Kconfig @@ -20,7 +20,7 @@ config DRM_ROCKCHIP select DRM_INNO_HDMI if ROCKCHIP_INNO_HDMI select GENERIC_PHY if ROCKCHIP_DW_MIPI_DSI select GENERIC_PHY_MIPI_DPHY if ROCKCHIP_DW_MIPI_DSI - select SND_SOC_HDMI_CODEC if SND_SOC && (ROCKCHIP_CDN_DP || ROCKCHIP_DW_HDMI_QP) + select SND_SOC_HDMI_CODEC if SND_SOC && (ROCKCHIP_CDN_DP || ROCKCHIP_DW_HDMI_QP || ROCKCHIP_DW_DP) help Choose this option if you have a Rockchip soc chipset. This driver provides kernel mode setting and buffer From b69effe51bc4617cf2613e10275155d861049b75 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 16 Apr 2026 01:42:34 +0300 Subject: [PATCH 157/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3576 boards The following RK3576 boards expose a GPIO pin to control the voltage bias on the HDMI data lines: - rk3576-100ask-dshanpi-a1 - rk3576-armsom-sige5 - rk3576-evb1-v10 - rk3576-evb2-v10 - rk3576-nanopi-m5 - rk3576-roc-pc - rk3576-rock-4d The pin must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. Signed-off-by: Cristian Ciocaltea --- .../arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-evb2-v10.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-nanopi-m5.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-roc-pc.dts | 9 +++++++++ arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts | 9 +++++++++ 7 files changed, 63 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts b/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts index b19f9b6be6bf8a..a9e3beb3ee5ee6 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-100ask-dshanpi-a1.dts @@ -278,6 +278,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -738,6 +741,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + pcie { pcie_reset: pcie-reset { rockchip,pins = <1 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index b76b9c9fe0110b..f0afbd1d5449e4 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -324,6 +324,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -841,6 +844,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts index 7081c7cdd12641..f3ede9465d14a5 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts @@ -370,6 +370,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -897,6 +900,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { rtc_int: rtc-int { rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb2-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb2-v10.dts index 98d5d00d63b577..dfc756d5ec5e18 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb2-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb2-v10.dts @@ -375,6 +375,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -832,6 +835,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { rtc_int: rtc-int { rockchip,pins = <0 RK_PA5 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-nanopi-m5.dts b/arch/arm64/boot/dts/rockchip/rk3576-nanopi-m5.dts index 7406a4adf8105a..227fe6f3ec7c7d 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-nanopi-m5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-nanopi-m5.dts @@ -327,6 +327,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -813,6 +816,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PA5 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-roc-pc.dts b/arch/arm64/boot/dts/rockchip/rk3576-roc-pc.dts index d0ab1d1e0e1195..54de3a50571f0a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-roc-pc.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-roc-pc.dts @@ -283,6 +283,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -720,6 +723,12 @@ }; &pinctrl { + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { rtc_int_l: rtc-int-l { rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts index 43a8b2153a8b6a..6e5c891a85f9f0 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -356,6 +356,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -843,6 +846,12 @@ }; }; + hdmi { + hdmi_tx_on_h: hdmi-tx-on-h { + rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; From 46e2f55327675f54b48d516de5f39c13263a896e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 16 Apr 2026 01:34:12 +0300 Subject: [PATCH 158/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3576-luckfox-core3576 The board exposes the GPIO4_C6 pin to control the voltage bias on the HDMI data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. Additionally, remove the now unnecessary workaround of using vcc_5v0_hdmi as hdmi-pwr-supply solely to drive the GPIO into its default state. Also rename the hdmi_con_en pinctrl to hdmi_tx_on_h to match the schematic naming. Signed-off-by: Cristian Ciocaltea --- .../dts/rockchip/rk3576-luckfox-core3576.dtsi | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi index 749f0a54b478e4..4fc8496828f802 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi @@ -26,7 +26,6 @@ hdmi-con { compatible = "hdmi-connector"; - hdmi-pwr-supply = <&vcc_5v0_hdmi>; type = "a"; port { @@ -138,22 +137,6 @@ }; }; - vcc_5v0_hdmi: regulator-vcc-5v0-hdmi { - compatible = "regulator-fixed"; - enable-active-high; - gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_HIGH>; - pinctrl-names = "default"; - pinctrl-0 = <&hdmi_con_en>; - regulator-min-microvolt = <5000000>; - regulator-max-microvolt = <5000000>; - regulator-name = "vcc_5v0_hdmi"; - vin-supply = <&vcc_5v0_sys>; - - regulator-state-mem { - regulator-off-in-suspend; - }; - }; - vcc_5v0_host: regulator-vcc-5v0-host { compatible = "regulator-fixed"; enable-active-high; @@ -231,6 +214,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -655,7 +641,7 @@ &pinctrl { hdmi { - hdmi_con_en: hdmi-con-en { + hdmi_tx_on_h: hdmi-tx-on-h { rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; }; }; From 78dead5cce8343575324b353968ff78e867a1e14 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 16 Apr 2026 01:02:49 +0300 Subject: [PATCH 159/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3576-nanopi-r76s The board exposes the GPIO4_C6 pin to control the voltage bias on the HDMI data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. Additionally, drop the now unnecessary workaround of using vcc5v_hdmi_tx as hdmi-pwr-supply solely to drive the GPIO into its default state. Signed-off-by: Cristian Ciocaltea --- .../boot/dts/rockchip/rk3576-nanopi-r76s.dts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-nanopi-r76s.dts b/arch/arm64/boot/dts/rockchip/rk3576-nanopi-r76s.dts index 7ec27b05ff10e6..0a5cd5f6fd3349 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-nanopi-r76s.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-nanopi-r76s.dts @@ -70,7 +70,6 @@ hdmi-con { compatible = "hdmi-connector"; - hdmi-pwr-supply = <&vcc5v_hdmi_tx>; type = "a"; port { @@ -109,18 +108,6 @@ regulator-name = "vcc5v_dcin"; }; - vcc5v_hdmi_tx: regulator-vcc5v-hdmi-tx { - compatible = "regulator-fixed"; - enable-active-high; - gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_HIGH>; - pinctrl-names = "default"; - pinctrl-0 = <&hdmi_tx_on_h>; - regulator-min-microvolt = <5000000>; - regulator-max-microvolt = <5000000>; - regulator-name = "vcc5v_hdmi_tx"; - vin-supply = <&vcc5v0_sys_s5>; - }; - vcc5v0_device_s0: regulator-vcc5v0-device-s0 { compatible = "regulator-fixed"; regulator-always-on; @@ -252,6 +239,9 @@ }; &hdmi { + frl-enable-gpios = <&gpio4 RK_PC6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmi_txm0_pins &hdmi_tx_scl &hdmi_tx_sda &hdmi_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; From 574d368d4a15e49b9443913c8816fb1a7e5b57f9 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 15 Apr 2026 02:45:06 +0300 Subject: [PATCH 160/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588 boards The following RK3588 boards expose one or two GPIO pins to control the voltage bias on the HDMI0 and/or HDMI1 data lines: - rk3588-armsom-sige7 - rk3588-armsom-w3 - rk3588-coolpi-cm5-evb - rk3588-coolpi-cm5-genbook - rk3588-evb1-v10 - rk3588-evb2-v10 - rk3588-firefly-itx-3588j - rk3588-friendlyelec-cm3588-nas - rk3588-h96-max-v58 - rk3588-jaguar - rk3588-mnt-reform2 - rk3588-nanopc-t6 - rk3588-orangepi-5-max - rk3588-orangepi-5-plus - rk3588-orangepi-5-ultra - rk3588-roc-rt - rk3588-rock-5-itx - rk3588-rock-5b-5bp-5t - rk3588-tiger The pins must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 and/or hdmi1 nodes to their dedicated GPIO pin(s) via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. While at it, also ensure that pinctrl-names is present and ordered alphabetically within the hdmi nodes. Signed-off-by: Cristian Ciocaltea --- .../boot/dts/rockchip/rk3588-armsom-sige7.dts | 10 ++++++++++ .../boot/dts/rockchip/rk3588-armsom-w3.dts | 18 ++++++++++++++++++ .../dts/rockchip/rk3588-coolpi-cm5-evb.dts | 18 +++++++++++++++++- .../dts/rockchip/rk3588-coolpi-cm5-genbook.dts | 11 ++++++++++- .../boot/dts/rockchip/rk3588-evb1-v10.dts | 18 ++++++++++++++++++ .../boot/dts/rockchip/rk3588-evb2-v10.dts | 10 ++++++++++ .../dts/rockchip/rk3588-firefly-itx-3588j.dts | 10 ++++++++++ .../rk3588-friendlyelec-cm3588-nas.dts | 18 ++++++++++++++++++ .../boot/dts/rockchip/rk3588-h96-max-v58.dts | 10 ++++++++++ arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts | 10 +++++++++- .../boot/dts/rockchip/rk3588-mnt-reform2.dts | 10 ++++++++++ .../boot/dts/rockchip/rk3588-nanopc-t6.dtsi | 18 ++++++++++++++++++ .../dts/rockchip/rk3588-orangepi-5-max.dts | 18 ++++++++++++++++-- .../dts/rockchip/rk3588-orangepi-5-plus.dts | 18 ++++++++++++++++++ .../dts/rockchip/rk3588-orangepi-5-ultra.dts | 11 +++++++++-- arch/arm64/boot/dts/rockchip/rk3588-roc-rt.dts | 18 ++++++++++++++++++ .../boot/dts/rockchip/rk3588-rock-5-itx.dts | 10 +++++++++- .../dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 18 +++++++++++++++++- .../boot/dts/rockchip/rk3588-tiger-haikou.dts | 3 ++- arch/arm64/boot/dts/rockchip/rk3588-tiger.dtsi | 11 +++++++++-- 20 files changed, 256 insertions(+), 12 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-armsom-sige7.dts b/arch/arm64/boot/dts/rockchip/rk3588-armsom-sige7.dts index 39197ee198370b..c4854c0f22e26b 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-armsom-sige7.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-armsom-sige7.dts @@ -177,6 +177,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -347,6 +351,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-armsom-w3.dts b/arch/arm64/boot/dts/rockchip/rk3588-armsom-w3.dts index 6ad2759ddccafe..3bbafdd89ede81 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-armsom-w3.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-armsom-w3.dts @@ -162,6 +162,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -182,6 +186,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -330,6 +338,16 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-evb.dts b/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-evb.dts index 3d5c8b753208ab..66e3c20d7b4f13 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-evb.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-evb.dts @@ -125,6 +125,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -145,8 +149,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd &hdmim1_tx1_scl &hdmim1_tx1_sda + &hdmi1_tx_on_h>; pinctrl-names = "default"; - pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd &hdmim1_tx1_scl &hdmim1_tx1_sda>; status = "okay"; }; @@ -211,6 +217,16 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + lcd { lcdpwr_en: lcdpwr-en { rockchip,pins = <1 RK_PC4 RK_FUNC_GPIO &pcfg_pull_down>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-genbook.dts b/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-genbook.dts index 738637ecaf557f..e05e6b2d513673 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-genbook.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-coolpi-cm5-genbook.dts @@ -184,7 +184,10 @@ /* HDMI CEC is not used */ &hdmi0 { - pinctrl-0 = <&hdmim0_tx0_hpd &hdmim0_tx0_scl &hdmim0_tx0_sda>; + frl-enable-gpios = <&gpio4 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_hpd &hdmim0_tx0_scl &hdmim0_tx0_sda + &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -277,6 +280,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + lcd { lcdpwr_en: lcdpwr-en { rockchip,pins = <0 RK_PC4 RK_FUNC_GPIO &pcfg_pull_down>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts index 85adfadc6e4a9a..415bbe4df2f254 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -415,6 +415,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -431,6 +435,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -693,6 +701,16 @@ }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hdmirx { hdmirx_hpd: hdmirx-5v-detection { rockchip,pins = <2 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-evb2-v10.dts b/arch/arm64/boot/dts/rockchip/rk3588-evb2-v10.dts index 60ba6ac55b2399..b7d2cb4561484b 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb2-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb2-v10.dts @@ -143,6 +143,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -183,6 +187,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <3 RK_PC5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PD4 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-firefly-itx-3588j.dts b/arch/arm64/boot/dts/rockchip/rk3588-firefly-itx-3588j.dts index e086114c763487..e43afb0c53fbdb 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-firefly-itx-3588j.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-firefly-itx-3588j.dts @@ -322,6 +322,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -487,6 +491,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-friendlyelec-cm3588-nas.dts b/arch/arm64/boot/dts/rockchip/rk3588-friendlyelec-cm3588-nas.dts index 10a7d3691a26f8..0dce96ca8c288e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-friendlyelec-cm3588-nas.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-friendlyelec-cm3588-nas.dts @@ -331,6 +331,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -347,6 +351,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -520,6 +528,16 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hdmirx { hdmirx_hpd: hdmirx-5v-detection { rockchip,pins = <3 RK_PD4 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-h96-max-v58.dts b/arch/arm64/boot/dts/rockchip/rk3588-h96-max-v58.dts index 73d8ce4fde2b81..7c2a1e6bcff9b1 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-h96-max-v58.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-h96-max-v58.dts @@ -210,6 +210,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -339,6 +343,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts b/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts index 5f5d89a33a4ae7..05b524f682cf6d 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts @@ -278,8 +278,10 @@ &hdmi0 { /* No CEC on Jaguar */ + frl-enable-gpios = <&gpio0 RK_PD3 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_hpd &hdmim0_tx0_scl &hdmim0_tx0_sda + &hdmi0_tx_on_h>; pinctrl-names = "default"; - pinctrl-0 = <&hdmim0_tx0_hpd &hdmim0_tx0_scl &hdmim0_tx0_sda>; status = "okay"; }; @@ -571,6 +573,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + leds { led1_pin: led1-pin { rockchip,pins = <1 RK_PD4 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-mnt-reform2.dts b/arch/arm64/boot/dts/rockchip/rk3588-mnt-reform2.dts index 78a4e896f665b5..36a5977d079520 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-mnt-reform2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-mnt-reform2.dts @@ -148,6 +148,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -205,6 +209,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + pcie2 { pcie2_0_rst: pcie2-0-rst { rockchip,pins = <3 RK_PD1 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-nanopc-t6.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-nanopc-t6.dtsi index 84b6b53f016ab1..3b0903d0e3e2a6 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-nanopc-t6.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-nanopc-t6.dtsi @@ -361,6 +361,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -381,6 +385,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -682,6 +690,16 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hdmirx { hdmirx_hpd: hdmirx-5v-detection { rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-max.dts b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-max.dts index 8b1d35760c3bc9..762f9fd966a42b 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-max.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-max.dts @@ -35,6 +35,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -55,9 +59,10 @@ }; &hdmi1 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx1_cec &hdmim0_tx1_hpd - &hdmim1_tx1_scl &hdmim1_tx1_sda>; + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -102,6 +107,15 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; usb { usb_otg_pwren: usb-otg-pwren { diff --git a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts index 9950d1147e129d..7142938d297146 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts @@ -121,6 +121,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -141,6 +145,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -278,6 +286,16 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-ultra.dts b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-ultra.dts index f8c6c080e418db..2b693dfb434c4e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-ultra.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-ultra.dts @@ -25,9 +25,10 @@ }; &hdmi1 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx1_cec &hdmim0_tx1_hpd - &hdmim1_tx1_scl &hdmim1_tx1_sda>; + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -64,6 +65,12 @@ }; &pinctrl { + hdmi { + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + usb { usb_otg_pwren: usb-otg-pwren { rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-roc-rt.dts b/arch/arm64/boot/dts/rockchip/rk3588-roc-rt.dts index 2d6fed2a84a38d..c50217a7b5596b 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-roc-rt.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-roc-rt.dts @@ -325,6 +325,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB0 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -345,6 +349,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim2_tx1_cec &hdmim0_tx1_hpd + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -583,6 +591,16 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5-itx.dts b/arch/arm64/boot/dts/rockchip/rk3588-rock-5-itx.dts index f7dd01d6fa0ab3..d5c0b01987fc2e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5-itx.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5-itx.dts @@ -346,8 +346,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx1_cec &hdmim0_tx1_hpd - &hdmim1_tx1_scl &hdmim1_tx1_sda>; + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -696,6 +698,12 @@ }; &pinctrl { + hdmi { + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { rtc_int: rtc-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi index 611a23b5fb87cb..9c069737c76579 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi @@ -199,6 +199,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -219,8 +223,10 @@ }; &hdmi1 { + frl-enable-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx1_cec &hdmim0_tx1_hpd - &hdmim1_tx1_scl &hdmim1_tx1_sda>; + &hdmim1_tx1_scl &hdmim1_tx1_sda &hdmi1_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -527,6 +533,16 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + hdmi1_tx_on_h: hdmi1-tx-on-h { + rockchip,pins = <4 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-tiger-haikou.dts b/arch/arm64/boot/dts/rockchip/rk3588-tiger-haikou.dts index caa43d1abf1793..08b7d477d8dbc4 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-tiger-haikou.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-tiger-haikou.dts @@ -172,8 +172,9 @@ * While HDMI-CEC is present on the Q7 connector, it is not * connected on Haikou itself. */ + pinctrl-0 = <&hdmim0_tx0_hpd &hdmim1_tx0_scl &hdmim1_tx0_sda + &hdmi0_tx_on_h>; pinctrl-names = "default"; - pinctrl-0 = <&hdmim0_tx0_hpd &hdmim1_tx0_scl &hdmim1_tx0_sda>; status = "okay"; }; diff --git a/arch/arm64/boot/dts/rockchip/rk3588-tiger.dtsi b/arch/arm64/boot/dts/rockchip/rk3588-tiger.dtsi index a0e97481afb7d7..ea5c264d153719 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-tiger.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-tiger.dtsi @@ -148,9 +148,10 @@ }; &hdmi0 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio0 RK_PD3 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim1_tx0_cec &hdmim0_tx0_hpd &hdmim1_tx0_scl - &hdmim1_tx0_sda>; + &hdmim1_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; }; &i2c1 { @@ -349,6 +350,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + leds { module_led_pin: module-led-pin { rockchip,pins = <1 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; From 39bd9895913b6f0a5d259d36bc87a4e5d522c2a5 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sat, 11 Apr 2026 01:06:42 +0000 Subject: [PATCH 161/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588s boards The following RK3588s boards expose a GPIO pin to control the voltage bias on the HDMI0 data lines: - rk3588s-coolpi-4b - rk3588s-indiedroid-nova - rk3588s-nanopi-r6 - rk3588s-odroid-m2 - rk3588s-orangepi-5 - rk3588s-radxa-cm5-io - rk3588s-rock-5a - rk3588s-rock-5c The pin must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. While at it, also ensure that pinctrl-names is present and ordered alphabetically within the hdmi nodes. Signed-off-by: Cristian Ciocaltea --- arch/arm64/boot/dts/rockchip/rk3588s-coolpi-4b.dts | 10 ++++++++++ .../boot/dts/rockchip/rk3588s-indiedroid-nova.dts | 10 +++++++++- arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi | 10 ++++++++++ arch/arm64/boot/dts/rockchip/rk3588s-odroid-m2.dts | 10 ++++++++++ arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5.dtsi | 10 ++++++++++ .../arm64/boot/dts/rockchip/rk3588s-radxa-cm5-io.dts | 10 ++++++++++ arch/arm64/boot/dts/rockchip/rk3588s-rock-5a.dts | 12 ++++++++++-- arch/arm64/boot/dts/rockchip/rk3588s-rock-5c.dts | 12 ++++++++++-- 8 files changed, 79 insertions(+), 5 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-coolpi-4b.dts b/arch/arm64/boot/dts/rockchip/rk3588s-coolpi-4b.dts index 189444d2077979..16f19109fca615 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-coolpi-4b.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-coolpi-4b.dts @@ -251,6 +251,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -396,6 +400,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts b/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts index 174d299cc6bb94..f9e3c0134d5fbb 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts @@ -277,8 +277,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx0_scl>, <&hdmim0_tx0_sda>, - <&hdmim0_tx0_hpd>, <&hdmim0_tx0_cec>; + <&hdmim0_tx0_hpd>, <&hdmim0_tx0_cec>, + <&hdmi0_tx_on_h>; pinctrl-names = "default"; status = "okay"; }; @@ -517,6 +519,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi b/arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi index 1b6a59f7cabce6..b9c0a1a050a969 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588s-nanopi-r6.dtsi @@ -236,6 +236,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -404,6 +408,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { rtc_int: rtc-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-odroid-m2.dts b/arch/arm64/boot/dts/rockchip/rk3588s-odroid-m2.dts index a72063c5514010..4e98fa33492c6a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-odroid-m2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-odroid-m2.dts @@ -249,6 +249,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -438,6 +442,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + lcd { lcd_pwren: lcd-pwren { rockchip,pins = <4 RK_PA3 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5.dtsi b/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5.dtsi index dafad29f98544a..cd80cb15a405b5 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-5.dtsi @@ -181,6 +181,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -381,6 +385,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + hym8563 { hym8563_int: hym8563-int { rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-radxa-cm5-io.dts b/arch/arm64/boot/dts/rockchip/rk3588s-radxa-cm5-io.dts index f80d5a00a4bdd3..af4a9bc015e16d 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-radxa-cm5-io.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-radxa-cm5-io.dts @@ -120,6 +120,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -217,6 +221,12 @@ }; }; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + usb { vcc5v0_host_en: vcc5v0-host-en { rockchip,pins = <1 RK_PA0 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-rock-5a.dts b/arch/arm64/boot/dts/rockchip/rk3588s-rock-5a.dts index 0991f6a2119005..ffd26b43ae8c96 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-rock-5a.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-rock-5a.dts @@ -323,11 +323,13 @@ }; &hdmi0 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx0_cec &hdmim1_tx0_hpd &hdmim0_tx0_scl - &hdmim0_tx0_sda>; + &hdmim0_tx0_sda + &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -373,6 +375,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + leds { io_led: io-led { rockchip,pins = <3 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-rock-5c.dts b/arch/arm64/boot/dts/rockchip/rk3588s-rock-5c.dts index 7fe42f4ff82798..9d3d0791554d95 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-rock-5c.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-rock-5c.dts @@ -258,11 +258,13 @@ }; &hdmi0 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio4 RK_PB6 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx0_cec &hdmim1_tx0_hpd &hdmim0_tx0_scl - &hdmim0_tx0_sda>; + &hdmim0_tx0_sda + &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -461,6 +463,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + leds { led_pins: led-pins { rockchip,pins = <3 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>, From 55e7e419cd2e7ec1e89d1fedaaa6b80b71fd26b6 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 16 Apr 2026 22:32:06 +0300 Subject: [PATCH 162/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588s-gameforce-ace The board exposes the GPIO4_B3 pin to control the voltage bias on the HDMI0 data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. Additionally, drop the now unnecessary ddc-en-gpios property and the associated pinctrl-* entries from hdmi0-con, and rename the hdmi0_en pinmux to hdmi0_tx_on_h, in line with the naming commonly used in RK3588s-based board schematics. Signed-off-by: Cristian Ciocaltea --- arch/arm64/boot/dts/rockchip/rk3588s-gameforce-ace.dts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-gameforce-ace.dts b/arch/arm64/boot/dts/rockchip/rk3588s-gameforce-ace.dts index 89618394c0bfb4..b657d54c2c5903 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-gameforce-ace.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-gameforce-ace.dts @@ -302,9 +302,6 @@ hdmi0-con { compatible = "hdmi-connector"; - ddc-en-gpios = <&gpio4 RK_PB3 GPIO_ACTIVE_HIGH>; - pinctrl-0 = <&hdmi0_en>; - pinctrl-names = "default"; type = "d"; port { @@ -514,8 +511,9 @@ &hdmi0 { no-hpd; + frl-enable-gpios = <&gpio4 RK_PB3 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx0_cec>, <&hdmim0_tx0_scl>, - <&hdmim0_tx0_sda>; + <&hdmim0_tx0_sda>, <&hdmi0_tx_on_h>; pinctrl-names = "default"; status = "okay"; }; @@ -893,7 +891,7 @@ }; hdmi { - hdmi0_en: hdmi0-en { + hdmi0_tx_on_h: hdmi0-tx-on-h { rockchip,pins = <4 RK_PB3 RK_FUNC_GPIO &pcfg_pull_none>; }; From 1ddccd5bcbcd45844949766abf11eeea3eda3c56 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 16 Apr 2026 02:27:22 +0300 Subject: [PATCH 163/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588s-khadas-edge2 The board exposes the GPIO4_B1 pin to control the voltage bias on the HDMI0 data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. While at it, remove the duplicated &hdmi0_sound node. Signed-off-by: Cristian Ciocaltea --- .../boot/dts/rockchip/rk3588s-khadas-edge2.dts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-khadas-edge2.dts b/arch/arm64/boot/dts/rockchip/rk3588s-khadas-edge2.dts index 2c22abaf40a82a..5afbc593341ba4 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-khadas-edge2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-khadas-edge2.dts @@ -194,6 +194,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -217,10 +221,6 @@ status = "okay"; }; -&hdmi0_sound { - status = "okay"; -}; - &i2c0 { pinctrl-names = "default"; pinctrl-0 = <&i2c0m2_xfer>; @@ -282,6 +282,12 @@ }; &pinctrl { + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + vdd_sd { vdd_sd_en: vdd-sd-en { rockchip,pins = <1 RK_PB6 RK_FUNC_GPIO &pcfg_pull_up>; From ac830de15bca35c666cfb8c7e93544805d47bc3f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sat, 11 Apr 2026 00:37:23 +0000 Subject: [PATCH 164/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588s-orangepi-cm5-base The board exposes the GPIO4_B5 pin to control the voltage bias on the HDMI0 data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. While at it, rename the hdmi_frl_pin pinmux to hdmi0_tx_on_h, in line with the naming commonly used in RK3588s-bassed board schematics. Signed-off-by: Cristian Ciocaltea --- .../boot/dts/rockchip/rk3588s-orangepi-cm5-base.dts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-cm5-base.dts b/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-cm5-base.dts index 06120b2db690a9..20da0c2b3d92ea 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-cm5-base.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-orangepi-cm5-base.dts @@ -143,10 +143,11 @@ }; &hdmi0 { - pinctrl-names = "default"; + frl-enable-gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_LOW>; pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd &hdmim0_tx0_scl &hdmim0_tx0_sda - &hdmi_frl_pin>; + &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -245,8 +246,8 @@ }; hdmi { - hdmi_frl_pin: hdmi-frl-pin { - rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_up>; + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; }; }; From 8295c79d2ba06e53e21fd1cd3b82653e511230cb Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 15 Apr 2026 19:52:34 +0300 Subject: [PATCH 165/218] arm64: dts: rockchip: Add frl-enable-gpios to rk3588s-roc-pc The board exposes the GPIO4_B2 line to control the voltage bias on the HDMI0 data lines. It must be asserted when operating in HDMI 2.1 FRL mode and deasserted for HDMI 1.4/2.0 TMDS mode. Wire up the hdmi0 node to its dedicated GPIO via frl-enable-gpios to allow adjusting the bias when transitioning between TMDS and FRL modes. While at it, move hym8563 down to fix the ordering of &pinctrl entries. Signed-off-by: Cristian Ciocaltea --- arch/arm64/boot/dts/rockchip/rk3588s-roc-pc.dts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-roc-pc.dts b/arch/arm64/boot/dts/rockchip/rk3588s-roc-pc.dts index 7e179862da6e56..aa02cf510d6db5 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-roc-pc.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-roc-pc.dts @@ -224,6 +224,10 @@ }; &hdmi0 { + frl-enable-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&hdmim0_tx0_cec &hdmim0_tx0_hpd + &hdmim0_tx0_scl &hdmim0_tx0_sda &hdmi0_tx_on_h>; + pinctrl-names = "default"; status = "okay"; }; @@ -367,9 +371,9 @@ }; &pinctrl { - hym8563 { - hym8563_int: hym8563-int { - rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; + hdmi { + hdmi0_tx_on_h: hdmi0-tx-on-h { + rockchip,pins = <4 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; }; }; @@ -379,6 +383,12 @@ }; }; + hym8563 { + hym8563_int: hym8563-int { + rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + leds { led_pins: led-pins { rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>, From cd5426aa68930f96233755414c4e9c1c557aa8dd Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 27 Apr 2026 15:36:05 +0300 Subject: [PATCH 166/218] arm64: dts: rockchip: Drop unnecessary #{address,size}-cells from rk3588-jaguar Remove the unnecessary #address-cells and #size-cells properties from the usb_host0_xhci and usb_host1_xhci port nodes, as they each contain a single endpoint child with no reg property. This fixes the following dtc warnings: rk3588-jaguar.dts: Warning (avoid_unnecessary_addr_size): /usb@fc000000/port: unnecessary #address-cells/#size-cells [...] /usb@fc400000/port: unnecessary #address-cells/#size-cells [...] Signed-off-by: Cristian Ciocaltea --- arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts b/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts index 05b524f682cf6d..033db78aff676e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-jaguar.dts @@ -1142,9 +1142,6 @@ status = "okay"; port { - #address-cells = <1>; - #size-cells = <0>; - usb_host0_xhci_drd_sw: endpoint { remote-endpoint = <&usbc0_hs>; }; @@ -1157,9 +1154,6 @@ status = "okay"; port { - #address-cells = <1>; - #size-cells = <0>; - usb_host1_xhci_drd_sw: endpoint { remote-endpoint = <&usbc1_hs>; }; From 88dcbd4a1445cc56b0398ccdfa5d7c242518bd8a Mon Sep 17 00:00:00 2001 From: Andy Yan Date: Thu, 4 Jun 2026 18:50:24 +0800 Subject: [PATCH 167/218] usb: typec: tcpm: Fix VDM type for Enter Mode commands VDO() second parameter is VDM type (bit 15): 1 for SVDM, 0 for UVDM. Using 'vdo ? 2 : 1' corrupts SVID low bit when vdo is non-NULL (2 << 15 = BIT(16)). Enter Mode is always SVDM, hardcode to 1. Fixes: 8face9aa57c8 ("usb: typec: Add parameter for the VDO to typec_altmode_enter()") Signed-off-by: Andy Yan Reviewed-by: Heikki Krogerus Link: https://lore.kernel.org/r/20260604105059.18750-1-andyshrk@163.com Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index 0ab22eafaf35f0..2d6c5c63c5d75b 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -3099,7 +3099,7 @@ static int tcpm_altmode_enter(struct typec_altmode *altmode, u32 *vdo) if (svdm_version < 0) return svdm_version; - header = VDO(altmode->svid, vdo ? 2 : 1, svdm_version, CMD_ENTER_MODE); + header = VDO(altmode->svid, 1, svdm_version, CMD_ENTER_MODE); header |= VDO_OPOS(altmode->mode); return tcpm_queue_vdm_unlocked(port, header, vdo, vdo ? 1 : 0, TCPC_TX_SOP); @@ -3147,7 +3147,7 @@ static int tcpm_cable_altmode_enter(struct typec_altmode *altmode, enum typec_pl if (svdm_version < 0) return svdm_version; - header = VDO(altmode->svid, vdo ? 2 : 1, svdm_version, CMD_ENTER_MODE); + header = VDO(altmode->svid, 1, svdm_version, CMD_ENTER_MODE); header |= VDO_OPOS(altmode->mode); return tcpm_queue_vdm_unlocked(port, header, vdo, vdo ? 1 : 0, TCPC_TX_SOP_PRIME); From 23f93d5a2446d50346766c36617242a536abb1ef Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Tue, 19 May 2026 14:00:11 -0400 Subject: [PATCH 168/218] drm/bridge: dw-hdmi-qp: Return -EOPNOTSUPP in HDMI audio functions -EOPNOTSUPP is not logged as an error by the ASoC subsystem, but -ENODEV is. It also better represents the situation: The operation is currently not supported (because clocks are not enabled and tmds_char_rate is unavailable), but the hardware is present. Using -EOPNOTSUPP in the audio_prepare callback removes possible repeated warning log lines when HDMI is not connected. Returning -EOPNOTSUPP in the audio_enable callback is also needed as it avoids logging 0-valued ELD errors. When tmds_char_rate is available, the clocks are enabled and the functions will keep returning 0 as before. Signed-off-by: Detlev Casanova Fixes: fd0141d1a8a2a ("drm/bridge: synopsys: Add audio support for dw-hdmi-qp") --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index 5582b661a10ebe..c81de8285e13f9 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -543,8 +543,10 @@ static int dw_hdmi_qp_audio_enable(struct drm_bridge *bridge, { struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge); - if (hdmi->tmds_char_rate) - dw_hdmi_qp_mod(hdmi, 0, AVP_DATAPATH_PACKET_AUDIO_SWDISABLE, GLOBAL_SWDISABLE); + if (!hdmi->tmds_char_rate) + return -EOPNOTSUPP; + + dw_hdmi_qp_mod(hdmi, 0, AVP_DATAPATH_PACKET_AUDIO_SWDISABLE, GLOBAL_SWDISABLE); return 0; } @@ -558,7 +560,7 @@ static int dw_hdmi_qp_audio_prepare(struct drm_bridge *bridge, bool ref2stream = false; if (!hdmi->tmds_char_rate) - return -ENODEV; + return -EOPNOTSUPP; if (fmt->bit_clk_provider | fmt->frame_clk_provider) { dev_err(hdmi->dev, "unsupported clock settings\n"); From 1182bcabd01c246f1d00bf80e704a1b29e180d4a Mon Sep 17 00:00:00 2001 From: Frank Zhang Date: Tue, 12 May 2026 18:31:53 +0800 Subject: [PATCH 169/218] drm/bridge: dw-hdmi-qp: Guard clear_audio_infoframe when PHY is down The following panic was observed during system reboot: Kernel panic - not syncing: Asynchronous SError Interrupt CPU: 6 UID: 1000 PID: 2348 Comm: pipewire ... 7.0.5+ #4 PREEMPT(full) Call trace: ... regmap_update_bits_base+0x70/0xa8 dw_hdmi_qp_bridge_clear_audio_infoframe+0x3c/0x58 [dw_hdmi_qp] drm_bridge_connector_clear_audio_infoframe+0x2c/0x48 [drm_display_helper] ... dw_hdmi_qp_audio_disable+0x28/0xa8 [dw_hdmi_qp] drm_bridge_connector_audio_shutdown+0x38/0x68 [drm_display_helper] drm_connector_hdmi_audio_shutdown+0x28/0x40 [drm_display_helper] hdmi_codec_shutdown+0x60/0x90 [snd_soc_hdmi_codec] ... snd_pcm_release_substream+0xcc/0x120 [snd_pcm] snd_pcm_release+0x4c/0xc0 [snd_pcm] ... The root cause is pipewire tries to close the HDMI audio device after atomic_disable(), which sets tmds_char_rate to 0 and disables the PHY. In this case, dw_hdmi_qp_audio_disable() will call dw_hdmi_qp_bridge_clear_audio_infoframe(), accessing register without checking tmds_char_rate. Add a tmds_char_rate guard in dw_hdmi_qp_bridge_clear_audio_infoframe(). Decouple write_audio_infoframe from clear_audio_infoframe to avoid the redundant check in the write path. Add PKTSCHED_AMD_TX_EN to the clear mask to keep the enable/disable balance. Fixes: fd0141d1a8a2 ("drm/bridge: synopsys: Add audio support for dw-hdmi-qp") Cc: stable@vger.kernel.org Signed-off-by: Frank Zhang --- drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c index c81de8285e13f9..47de45d6225a46 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -1649,11 +1649,11 @@ static int dw_hdmi_qp_bridge_clear_audio_infoframe(struct drm_bridge *bridge) { struct dw_hdmi_qp *hdmi = bridge->driver_private; - dw_hdmi_qp_mod(hdmi, 0, - PKTSCHED_ACR_TX_EN | - PKTSCHED_AUDS_TX_EN | - PKTSCHED_AUDI_TX_EN, - PKTSCHED_PKT_EN); + if (hdmi->tmds_char_rate) + dw_hdmi_qp_mod(hdmi, 0, + PKTSCHED_ACR_TX_EN | PKTSCHED_AMD_TX_EN | + PKTSCHED_AUDS_TX_EN | PKTSCHED_AUDI_TX_EN, + PKTSCHED_PKT_EN); return 0; } @@ -1752,7 +1752,10 @@ static int dw_hdmi_qp_bridge_write_audio_infoframe(struct drm_bridge *bridge, { struct dw_hdmi_qp *hdmi = bridge->driver_private; - dw_hdmi_qp_bridge_clear_audio_infoframe(bridge); + dw_hdmi_qp_mod(hdmi, 0, + PKTSCHED_ACR_TX_EN | PKTSCHED_AMD_TX_EN | + PKTSCHED_AUDS_TX_EN | PKTSCHED_AUDI_TX_EN, + PKTSCHED_PKT_EN); /* * AUDI_CONTENTS0: { RSV, HB2, HB1, RSV } From edc649241c35db0403855720c0ce58db4ae595fc Mon Sep 17 00:00:00 2001 From: Anton Burticica Date: Tue, 23 Dec 2025 11:34:32 +0400 Subject: [PATCH 170/218] LIN-14: Use wiphy_dbg instead of bphy_err In environment with too many APs, the buffer of 64k is not enough to keep all BSS entries. Let's suppress the message (can be enabled via debugfs). --- drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c index 0b55d445895f20..c24e6e17163ddc 100644 --- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c +++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cfg80211.c @@ -3703,7 +3703,7 @@ brcmf_cfg80211_escan_handler(struct brcmf_if *ifp, list = (struct brcmf_scan_results *) cfg->escan_info.escan_buf; if (bi_length > BRCMF_ESCAN_BUF_SIZE - list->buflen) { - bphy_err(drvr, "Buffer is too small: ignoring\n"); + wiphy_dbg((drvr)->wiphy, "Buffer is too small: ignoring\n"); goto exit; } From 5e367a8e368f44b04b17e2ffe62c695d88e93b0c Mon Sep 17 00:00:00 2001 From: Pavel Zhovner Date: Mon, 29 Dec 2025 17:11:42 +0000 Subject: [PATCH 171/218] Hardcode always show single logo regardless of CPU count With our custom logo it becomes weird to duplicate it according to the number of CPU cores, so force only one copy by default Signed-off-by: Alexey Charkov --- drivers/video/fbdev/core/fb_logo.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/drivers/video/fbdev/core/fb_logo.c b/drivers/video/fbdev/core/fb_logo.c index 0bab8352b684a1..5568e23f8b0978 100644 --- a/drivers/video/fbdev/core/fb_logo.c +++ b/drivers/video/fbdev/core/fb_logo.c @@ -6,7 +6,15 @@ #include "fb_internal.h" bool fb_center_logo __read_mostly; -int fb_logo_count __read_mostly = -1; +/* + * fb_logo_count: number of boot logos to display + * -1 = show one logo per online CPU (default upstream behavior) + * 0 = disable logo + * >0 = show exactly this many logos + * + * Flipper: hardcode to 1 to always show single logo regardless of CPU count + */ +int fb_logo_count __read_mostly = 1; static inline unsigned int safe_shift(unsigned int d, int n) { From 9c6e1abb96387662d58b43d527ac0dcddcd4fe63 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 19:03:10 +0400 Subject: [PATCH 172/218] arm64: dts: rockchip: Fix Type-C SBU lines bias on ArmSoM Sige5 Remove extra pull from the GPIOs controlling the voltage bias of SBU lines on Sige5, as it makes DP AUX communication unreliable. The lines are supposed to be pulled up or down with a 10-105kOhm resistor, and the board schematic already includes a discrete 100kOhm resistor on each line, making the resulting pull-up too weak vs. spec. Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index f0afbd1d5449e4..fdf2a7d333aae4 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -885,10 +885,10 @@ rockchip,pins = <0 RK_PA5 RK_FUNC_GPIO &pcfg_pull_up>; }; usbc0_sbu1: usbc0-sbu1 { - rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_down>; + rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>; }; usbc0_sbu2: usbc0-sbu2 { - rockchip,pins = <2 RK_PA7 RK_FUNC_GPIO &pcfg_pull_down>; + rockchip,pins = <2 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>; }; }; From bd29f8d4e47d021f6ec96269048d8ab8a5a5b8de Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 12:30:40 +0400 Subject: [PATCH 173/218] WIP: Link up HUSB311 driver and DP on EVB1 Signed-off-by: Alexey Charkov --- .../boot/dts/rockchip/rk3576-evb1-v10.dts | 151 +++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts index f3ede9465d14a5..d1e243e2d277e8 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts @@ -11,6 +11,7 @@ #include #include #include +#include #include "rk3576.dtsi" / { @@ -334,6 +335,22 @@ status = "okay"; }; +&dp { + status = "okay"; +}; + +&dp0_in { + dp0_in_vp1: endpoint { + remote-endpoint = <&vp1_out_dp>; + }; +}; + +&dp0_out { + dp0_out_con: endpoint { + remote-endpoint = <&usbdp_phy_dp_in>; + }; +}; + &gmac0 { clock_in_out = "output"; phy-mode = "rgmii-rxid"; @@ -772,6 +789,58 @@ &i2c2 { status = "okay"; + usbc0: typec-portc@4e { + compatible = "hynetek,husb311", "richtek,rt1711h"; + reg = <0x4e>; + interrupt-parent = <&gpio0>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&usbc0_int>; + vbus-supply = <&vbus5v0_typec>; + + connector { + compatible = "usb-c-connector"; + label = "USB-C"; + data-role = "dual"; + power-role = "source"; + source-pdos = ; + + altmodes { + displayport { + svid = /bits/ 16 <0xff01>; + vdo = <0xffffffff>; + }; + }; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usbc0_hs: endpoint { + remote-endpoint = <&usb_drd0_hs_ep>; + }; + }; + + port@1 { + reg = <1>; + usbc0_ss: endpoint { + remote-endpoint = <&usbdp_phy_ss_out>; + }; + }; + + port@2 { + reg = <2>; + usbc0_sbu: endpoint { + remote-endpoint = <&usbdp_phy_dp_out>; + }; + }; + }; + }; + }; + hym8563: rtc@51 { compatible = "haoyu,hym8563"; reg = <0x51>; @@ -946,6 +1015,14 @@ usbc0_int: usbc0-int { rockchip,pins = <0 RK_PA5 RK_FUNC_GPIO &pcfg_pull_up>; }; + + usbc0_sbu1: usbc0-sbu1 { + rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + usbc0_sbu2: usbc0-sbu2 { + rockchip,pins = <2 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>; + }; }; wifi { @@ -1054,13 +1131,76 @@ }; &usbdp_phy { - rockchip,dp-lane-mux = <2 3>; + mode-switch; + orientation-switch; + pinctrl-names = "default"; + pinctrl-0 = <&usbc0_sbu1 &usbc0_sbu2>; + sbu1-dc-gpios = <&gpio2 RK_PA6 GPIO_ACTIVE_HIGH>; + sbu2-dc-gpios = <&gpio2 RK_PA7 GPIO_ACTIVE_HIGH>; status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + + port@0 { + reg = <0>; + + usbdp_phy_ss_out: endpoint { + remote-endpoint = <&usbc0_ss>; + }; + }; + + port@1 { + reg = <1>; + + usbdp_phy_ss_in: endpoint { + remote-endpoint = <&usb_drd0_ss_ep>; + }; + }; + + port@2 { + reg = <2>; + + usbdp_phy_dp_in: endpoint { + remote-endpoint = <&dp0_out_con>; + }; + }; + + port@3 { + reg = <3>; + + usbdp_phy_dp_out: endpoint { + remote-endpoint = <&usbc0_sbu>; + }; + }; + }; }; &usb_drd0_dwc3 { - dr_mode = "host"; + usb-role-switch; + dr_mode = "otg"; status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usb_drd0_hs_ep: endpoint { + remote-endpoint = <&usbc0_hs>; + }; + }; + + port@1 { + reg = <1>; + usb_drd0_ss_ep: endpoint { + remote-endpoint = <&usbdp_phy_ss_in>; + }; + }; + }; }; &usb_drd1_dwc3 { @@ -1082,3 +1222,10 @@ remote-endpoint = <&hdmi_in_vp0>; }; }; + +&vp1 { + vp1_out_dp: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp1>; + }; +}; From 506af2e1ccc5fcae7038068c64ec79d4bf6f0b34 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 26 Jan 2026 18:40:51 +0400 Subject: [PATCH 174/218] arm64: dts: rockchip: Add DP audio on RK3576 EVB1 Enable audio nodes for sound over Type-C DP AltMode on RK3576 EVB1 Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts index d1e243e2d277e8..b1fc17e72d28d9 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts @@ -351,6 +351,10 @@ }; }; +&dp0_sound { + status = "okay"; +}; + &gmac0 { clock_in_out = "output"; phy-mode = "rgmii-rxid"; @@ -1083,6 +1087,10 @@ status = "okay"; }; +&spdif_tx3 { + status = "okay"; +}; + &u2phy0 { status = "okay"; }; From 53f393fed40e06a4204fe6b06db1eb829ec525b3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 13:36:56 +0400 Subject: [PATCH 175/218] arm64: dts: rockchip: Add Ethernet support for Luckfox Omni3576 Include device tree nodes to enable Ethernet support on the Luckfox Omni3576 board. This board has its GMAC0+PHY0 on the pluggable SoM, with only magnetics and the RJ45 connector provided by the carrier board. For GMAC1 though the accompanying PHY1 is located on the carrier board, connected via RGMII pins exposed by the SoM. Link: https://wiki.luckfox.com/assets/files/Omni3576-3be654963438c9b39fd22672f449f807.pdf Signed-off-by: Alexey Charkov --- .../dts/rockchip/rk3576-luckfox-core3576.dtsi | 34 +++++++++++++++++++ .../dts/rockchip/rk3576-luckfox-omni3576.dts | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi index 4fc8496828f802..6ab15cf8716b31 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi @@ -208,6 +208,21 @@ cpu-supply = <&vdd_cpu_lit_s0>; }; +&gmac0 { + clock_in_out = "output"; + phy-mode = "rgmii-rxid"; + phy-handle = <&rgmii_phy0>; + pinctrl-names = "default"; + pinctrl-0 = <ð0m0_miim + ð0m0_tx_bus2 + ð0m0_rx_bus2 + ð0m0_rgmii_clk + ð0m0_rgmii_bus + ðm0_clk0_25m_out>; + tx_delay = <0x20>; + status = "okay"; +}; + &gpu { mali-supply = <&vdd_gpu_s0>; status = "okay"; @@ -631,6 +646,19 @@ }; }; +&mdio0 { + rgmii_phy0: ethernet-phy@0 { + compatible = "ethernet-phy-ieee802.3-c22"; + reg = <0x0>; + clocks = <&cru REFCLKO25M_GMAC0_OUT>; + pinctrl-names = "default"; + pinctrl-0 = <&gmac0_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio2 RK_PB3 GPIO_ACTIVE_LOW>; + }; +}; + &pcie0 { pinctrl-names = "default"; pinctrl-0 = <&pcie_reset>; @@ -640,6 +668,12 @@ }; &pinctrl { + gmac { + gmac0_rst: ethphy0-rst { + rockchip,pins = <2 RK_PB3 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + hdmi { hdmi_tx_on_h: hdmi-tx-on-h { rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-omni3576.dts b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-omni3576.dts index 6c75959adfe199..5ab0c7f3b54105 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-omni3576.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-omni3576.dts @@ -30,7 +30,41 @@ }; }; +&gmac1 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy1>; + phy-mode = "rgmii-rxid"; + pinctrl-names = "default"; + pinctrl-0 = <ð1m0_miim + ð1m0_tx_bus2 + ð1m0_rx_bus2 + ð1m0_rgmii_clk + ð1m0_rgmii_bus + ðm0_clk1_25m_out>; + tx_delay = <0x20>; + status = "okay"; +}; + +&mdio1 { + rgmii_phy1: ethernet-phy@0 { + compatible = "ethernet-phy-ieee802.3-c22"; + reg = <0x0>; + clocks = <&cru REFCLKO25M_GMAC1_OUT>; + pinctrl-names = "default"; + pinctrl-0 = <&gmac1_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_LOW>; + }; +}; + &pinctrl { + gmac { + gmac1_rst: ethphy1-rst { + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + leds { led_green_pin: led-green-pin { rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>; From e2e5fddacce766886b947325dc36edbd67a8cb0d Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 15:42:13 +0400 Subject: [PATCH 176/218] arm64: dts: rockchip: Drop incorrect eMMC regulators on Luckfox Core3576 Remove the supply regulators from the eMMC controller node on Luckfox Core3576, as they cause the eMMC to endlessly re-tune phase at startup, and are most likely wrong: 1. The vendor DTS doesn't define those regulators 2. The VCCQ regulator referenced here is actually used for the SD card, and it is unlikely that both can share the same regulator with an 1.8-3.3V range, whereas eMMC only expects 1.8V 3. Rockchip reference schematic, which this board broadly follows, drives the VCCQ supply of eMMC flash from VCC_1V8_S0 rather than VCCIO_SD_S0, where VCC_1V8_S0 is a fixed 1.8V load switch with VIN tied to VCC_1V8_S3 and EN tied to VCCA_1V8_S0 (a.k.a. PMIC PLDO1, not PLDO5) There is no published schematic for the Core3576 SoM unfortunately. Cc: stable@vger.kernel.org Fixes: d7ad90d22abe ("arm64: dts: rockchip: Add Luckfox Omni3576 Board support") Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi | 2 -- 1 file changed, 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi index 6ab15cf8716b31..861b8556831361 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-luckfox-core3576.dtsi @@ -732,8 +732,6 @@ no-sd; no-sdio; non-removable; - vmmc-supply = <&vcc_3v3_s3>; - vqmmc-supply = <&vccio_sd_s0>; status = "okay"; }; From bf36c30085810bb7a4f95ad5721955ad40f1179b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:31:15 +0400 Subject: [PATCH 177/218] dt-bindings: vendor-prefixes: Add Flipper FZCO Add a vendor prefix for Flipper FZCO, the company behind Flipper Zero, creating open and customizable multi-tool devices for tech enthusiasts. Link: https://flipper.net/ Signed-off-by: Alexey Charkov --- Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml index 28784d66ae7ba5..021032422a782b 100644 --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml @@ -598,6 +598,8 @@ patternProperties: description: Fitipower Integrated Technology Inc. "^flipkart,.*": description: Flipkart Inc. + "^flipper,.*": + description: Flipper FZCO "^focaltech,.*": description: FocalTech Systems Co.,Ltd "^forlinx,.*": From 29a21c426138906137a2e66a40c3d5ccb6f6bf7e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:38:30 +0400 Subject: [PATCH 178/218] dt-bindings: display: Add Flipper One display Document the display part used in the upcoming Flipper One handheld network multi-tool, connected over an SPI bus. Signed-off-by: Alexey Charkov --- .../bindings/display/flipper,one-display.yaml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Documentation/devicetree/bindings/display/flipper,one-display.yaml diff --git a/Documentation/devicetree/bindings/display/flipper,one-display.yaml b/Documentation/devicetree/bindings/display/flipper,one-display.yaml new file mode 100644 index 00000000000000..0c5f7c59975f37 --- /dev/null +++ b/Documentation/devicetree/bindings/display/flipper,one-display.yaml @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/display/flipper,one-display.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Flipper One display + +maintainers: + - Alexey Charkov + +description: + Flipper One is a handheld network multi-tool, which includes a custom + grayscale LCD panel driven by an MCU accepting image contents over a SPI bus. + The image data must consist of a full frame in each transmission, signalled + by the "active" GPIO line. One byte per pixel is used, with 0 representing + black and 255 white, lower two bits are discarded, and the expected frame + size is 258x144 pixels of which the leftmost 256x144 are visible. + + The MCU expects SPI mode 3 (CPOL=1, CPHA=1), up to 24MHz frequency. + +allOf: + - $ref: /schemas/spi/spi-peripheral-props.yaml# + +properties: + compatible: + enum: + - flipper,one-display + + reg: + maxItems: 1 + + spi-cpha: true + spi-cpol: true + + spi-max-frequency: + maximum: 24000000 + + active-gpios: + maxItems: 1 + description: Frame data is being transmitted when this line is asserted. + Deassert to signal the end of frame. + +required: + - compatible + - reg + - active-gpios + - spi-max-frequency + +unevaluatedProperties: false + +examples: + - | + #include + spi { + #address-cells = <1>; + #size-cells = <0>; + + display@0 { + compatible = "flipper,one-display"; + reg = <0>; + active-gpios = <&gpio4 6 GPIO_ACTIVE_LOW>; + spi-max-frequency = <20000000>; + spi-cpha; + spi-cpol; + spi-rx-bus-width = <0>; + }; + }; +... From a247742e3b542e8fce0ad6feb8629d6ccb960c02 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 12:30:56 +0400 Subject: [PATCH 179/218] drm/tiny: Add driver for the Flipper One display Flipper One has an SPI-attached custom grayscale display. Add a driver for it. Signed-off-by: Alexey Charkov --- MAINTAINERS | 4 + drivers/gpu/drm/tiny/Kconfig | 14 + drivers/gpu/drm/tiny/Makefile | 31 +- drivers/gpu/drm/tiny/flipper-one-display.c | 366 +++++++++++++++++++++ 4 files changed, 400 insertions(+), 15 deletions(-) create mode 100644 drivers/gpu/drm/tiny/flipper-one-display.c diff --git a/MAINTAINERS b/MAINTAINERS index 1ebbc6c8d6b52f..0f45d7644cfb28 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -7982,6 +7982,10 @@ F: include/linux/aperture.h F: include/linux/sysfb.h F: include/video/nomodeset.h +DRM DRIVER FOR THE FLIPPER ONE DISPLAY +M: Alexey Charkov +F: drivers/gpu/drm/tiny/flipper-one-display.c + DRM DRIVER FOR GENERIC EDP PANELS R: Douglas Anderson F: Documentation/devicetree/bindings/display/panel/panel-edp.yaml diff --git a/drivers/gpu/drm/tiny/Kconfig b/drivers/gpu/drm/tiny/Kconfig index f0e72d4b6a4709..4835e06c0724d2 100644 --- a/drivers/gpu/drm/tiny/Kconfig +++ b/drivers/gpu/drm/tiny/Kconfig @@ -98,6 +98,20 @@ config DRM_PIXPAPER If M is selected, the module will be built as pixpaper.ko. +config TINYDRM_FLIPPER_ONE_DISPLAY + tristate "DRM support for Flipper One display" + depends on DRM && SPI + select DRM_CLIENT_SELECTION + select DRM_GEM_DMA_HELPER + select DRM_KMS_HELPER + default y + help + DRM Driver for the Flipper One display, which uses an onboard MCU + to drive a 256x144 grayscale LCD based on a framebuffer written + over SPI (full frame at a time, 6 bpp padded to 8 bpp at 258x144). + + If M is selected the module will be called flipper_one_display. + config TINYDRM_HX8357D tristate "DRM support for HX8357D display panels" depends on DRM && SPI diff --git a/drivers/gpu/drm/tiny/Makefile b/drivers/gpu/drm/tiny/Makefile index 48d30bf6152f97..5df261a871f09d 100644 --- a/drivers/gpu/drm/tiny/Makefile +++ b/drivers/gpu/drm/tiny/Makefile @@ -1,17 +1,18 @@ # SPDX-License-Identifier: GPL-2.0-only -obj-$(CONFIG_DRM_APPLETBDRM) += appletbdrm.o -obj-$(CONFIG_DRM_ARCPGU) += arcpgu.o -obj-$(CONFIG_DRM_BOCHS) += bochs.o -obj-$(CONFIG_DRM_CIRRUS_QEMU) += cirrus-qemu.o -obj-$(CONFIG_DRM_GM12U320) += gm12u320.o -obj-$(CONFIG_DRM_PANEL_MIPI_DBI) += panel-mipi-dbi.o -obj-$(CONFIG_DRM_PIXPAPER) += pixpaper.o -obj-$(CONFIG_TINYDRM_HX8357D) += hx8357d.o -obj-$(CONFIG_TINYDRM_ILI9163) += ili9163.o -obj-$(CONFIG_TINYDRM_ILI9225) += ili9225.o -obj-$(CONFIG_TINYDRM_ILI9341) += ili9341.o -obj-$(CONFIG_TINYDRM_ILI9486) += ili9486.o -obj-$(CONFIG_TINYDRM_MI0283QT) += mi0283qt.o -obj-$(CONFIG_TINYDRM_REPAPER) += repaper.o -obj-$(CONFIG_TINYDRM_SHARP_MEMORY) += sharp-memory.o +obj-$(CONFIG_DRM_APPLETBDRM) += appletbdrm.o +obj-$(CONFIG_DRM_ARCPGU) += arcpgu.o +obj-$(CONFIG_DRM_BOCHS) += bochs.o +obj-$(CONFIG_DRM_CIRRUS_QEMU) += cirrus-qemu.o +obj-$(CONFIG_DRM_GM12U320) += gm12u320.o +obj-$(CONFIG_DRM_PANEL_MIPI_DBI) += panel-mipi-dbi.o +obj-$(CONFIG_DRM_PIXPAPER) += pixpaper.o +obj-$(CONFIG_TINYDRM_FLIPPER_ONE_DISPLAY) += flipper-one-display.o +obj-$(CONFIG_TINYDRM_HX8357D) += hx8357d.o +obj-$(CONFIG_TINYDRM_ILI9163) += ili9163.o +obj-$(CONFIG_TINYDRM_ILI9225) += ili9225.o +obj-$(CONFIG_TINYDRM_ILI9341) += ili9341.o +obj-$(CONFIG_TINYDRM_ILI9486) += ili9486.o +obj-$(CONFIG_TINYDRM_MI0283QT) += mi0283qt.o +obj-$(CONFIG_TINYDRM_REPAPER) += repaper.o +obj-$(CONFIG_TINYDRM_SHARP_MEMORY) += sharp-memory.o diff --git a/drivers/gpu/drm/tiny/flipper-one-display.c b/drivers/gpu/drm/tiny/flipper-one-display.c new file mode 100644 index 00000000000000..adcd9fa8f9138e --- /dev/null +++ b/drivers/gpu/drm/tiny/flipper-one-display.c @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct fo_device { + struct drm_device drm; + struct spi_device *spi; + + const struct drm_display_mode *mode; + + struct drm_crtc crtc; + struct drm_plane plane; + struct drm_encoder encoder; + struct drm_connector connector; + + struct gpio_desc *active_gpio; + + u32 pitch; + u32 tx_buffer_size; + u8 *tx_buffer; +}; + +static inline struct fo_device *drm_to_fo_device(struct drm_device *drm) +{ + return container_of(drm, struct fo_device, drm); +} + +DEFINE_DRM_GEM_DMA_FOPS(fo_fops); + +static const struct drm_driver fo_drm_driver = { + .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC, + .fops = &fo_fops, + DRM_GEM_DMA_DRIVER_OPS_VMAP, + DRM_FBDEV_DMA_DRIVER_OPS, + .name = "flipper_one_display", + .desc = "Flipper One LCD screen", + .major = 1, + .minor = 0, +}; + +static void fo_set_tx_buffer_data(struct fo_device *fo, + struct drm_plane_state *plane_state) +{ + struct drm_shadow_plane_state *s_plane_state = to_drm_shadow_plane_state(plane_state); + struct drm_framebuffer *fb = plane_state->fb; + struct drm_rect clip; + struct iosys_map *src = s_plane_state->data; + struct iosys_map dst; + + if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE)) + return; + + clip.x1 = 0; + clip.x2 = fb->width; + clip.y1 = 0; + clip.y2 = fb->height; + + iosys_map_set_vaddr(&dst, fo->tx_buffer); + drm_fb_xrgb8888_to_gray8(&dst, &fo->pitch, src, fb, &clip, &s_plane_state->fmtcnv_state); + drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE); +} + +static int fo_plane_atomic_check(struct drm_plane *plane, struct drm_atomic_state *state) +{ + struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane); + struct fo_device *fo; + struct drm_crtc_state *crtc_state; + + fo = container_of(plane, struct fo_device, plane); + crtc_state = drm_atomic_get_new_crtc_state(state, &fo->crtc); + + return drm_atomic_helper_check_plane_state(plane_state, crtc_state, + DRM_PLANE_NO_SCALING, + DRM_PLANE_NO_SCALING, + false, false); +} + +static void fo_plane_atomic_update(struct drm_plane *plane, struct drm_atomic_state *state) +{ + struct fo_device *fo = container_of(plane, struct fo_device, plane); + struct drm_plane_state *plane_state = plane->state; + + if (!fo->crtc.state->active) + return; + + /* Populate the transmit buffer with frame data */ + fo_set_tx_buffer_data(fo, plane_state); + + spi_write(fo->spi, fo->tx_buffer, fo->tx_buffer_size); +} + +static const struct drm_plane_helper_funcs fo_plane_helper_funcs = { + .prepare_fb = drm_gem_plane_helper_prepare_fb, + .atomic_check = fo_plane_atomic_check, + .atomic_update = fo_plane_atomic_update, + DRM_GEM_SHADOW_PLANE_HELPER_FUNCS, +}; + +static bool fo_format_mod_supported(struct drm_plane *plane, u32 format, u64 modifier) +{ + return modifier == DRM_FORMAT_MOD_LINEAR; +} + +static const struct drm_plane_funcs fo_plane_funcs = { + .update_plane = drm_atomic_helper_update_plane, + .disable_plane = drm_atomic_helper_disable_plane, + .destroy = drm_plane_cleanup, + DRM_GEM_SHADOW_PLANE_FUNCS, + .format_mod_supported = fo_format_mod_supported, +}; + +static enum drm_mode_status fo_crtc_mode_valid(struct drm_crtc *crtc, + const struct drm_display_mode *mode) +{ + struct fo_device *fo = drm_to_fo_device(crtc->dev); + + return drm_crtc_helper_mode_valid_fixed(crtc, mode, fo->mode); +} + +static int fo_crtc_check(struct drm_crtc *crtc, struct drm_atomic_state *state) +{ + struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc); + int ret; + + if (!crtc_state->enable) + goto out; + + ret = drm_atomic_helper_check_crtc_primary_plane(crtc_state); + if (ret) + return ret; + +out: + return drm_atomic_add_affected_planes(state, crtc); +} + +static void fo_begin_frame(struct drm_crtc *crtc, struct drm_atomic_state *state) +{ + struct fo_device *fo = drm_to_fo_device(crtc->dev); + + if (fo->active_gpio) + gpiod_set_value(fo->active_gpio, 1); +} + +static void fo_end_frame(struct drm_crtc *crtc, struct drm_atomic_state *state) +{ + struct fo_device *fo = drm_to_fo_device(crtc->dev); + + if (fo->active_gpio) + gpiod_set_value(fo->active_gpio, 0); +} + +static const struct drm_crtc_helper_funcs fo_crtc_helper_funcs = { + .mode_valid = fo_crtc_mode_valid, + .atomic_check = fo_crtc_check, + .atomic_begin = fo_begin_frame, + .atomic_flush = fo_end_frame, +}; + +static const struct drm_crtc_funcs fo_crtc_funcs = { + .reset = drm_atomic_helper_crtc_reset, + .destroy = drm_crtc_cleanup, + .set_config = drm_atomic_helper_set_config, + .page_flip = drm_atomic_helper_page_flip, + .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state, +}; + +static const struct drm_encoder_funcs fo_encoder_funcs = { + .destroy = drm_encoder_cleanup, +}; + +static int fo_connector_get_modes(struct drm_connector *connector) +{ + struct fo_device *fo = drm_to_fo_device(connector->dev); + + return drm_connector_helper_get_modes_fixed(connector, fo->mode); +} + +static const struct drm_connector_helper_funcs fo_connector_hfuncs = { + .get_modes = fo_connector_get_modes, +}; + +static const struct drm_connector_funcs fo_connector_funcs = { + .reset = drm_atomic_helper_connector_reset, + .fill_modes = drm_helper_probe_single_connector_modes, + .destroy = drm_connector_cleanup, + .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, + .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, +}; + +static const struct drm_mode_config_funcs fo_mode_config_funcs = { + .fb_create = drm_gem_fb_create, + .atomic_check = drm_atomic_helper_check, + .atomic_commit = drm_atomic_helper_commit, +}; + +static const struct drm_display_mode fo_display_mode = { + DRM_MODE_INIT(60, 256, 144, 53, 30), +}; + +static const struct spi_device_id fo_ids[] = { + {"flipper_one_display", (kernel_ulong_t)&fo_display_mode}, + {}, +}; +MODULE_DEVICE_TABLE(spi, fo_ids); + +static const struct of_device_id fo_of_match[] = { + {.compatible = "flipper,one-display", &fo_display_mode}, + {}, +}; +MODULE_DEVICE_TABLE(of, fo_of_match); + +static const u32 fo_formats[] = { + DRM_FORMAT_XRGB8888, +}; + +static int fo_pipe_init(struct drm_device *dev, struct fo_device *fo, + const u32 *formats, unsigned int format_count, + const u64 *format_modifiers) +{ + int ret; + struct drm_encoder *encoder = &fo->encoder; + struct drm_plane *plane = &fo->plane; + struct drm_crtc *crtc = &fo->crtc; + struct drm_connector *connector = &fo->connector; + + drm_plane_helper_add(plane, &fo_plane_helper_funcs); + ret = drm_universal_plane_init(dev, plane, 0, &fo_plane_funcs, + formats, format_count, + format_modifiers, + DRM_PLANE_TYPE_PRIMARY, NULL); + if (ret) + return ret; + + drm_crtc_helper_add(crtc, &fo_crtc_helper_funcs); + ret = drm_crtc_init_with_planes(dev, crtc, plane, NULL, + &fo_crtc_funcs, NULL); + if (ret) + return ret; + + encoder->possible_crtcs = drm_crtc_mask(crtc); + ret = drm_encoder_init(dev, encoder, &fo_encoder_funcs, + DRM_MODE_ENCODER_NONE, NULL); + if (ret) + return ret; + + ret = drm_connector_init(&fo->drm, &fo->connector, + &fo_connector_funcs, + DRM_MODE_CONNECTOR_SPI); + if (ret) + return ret; + + drm_connector_helper_add(&fo->connector, &fo_connector_hfuncs); + + return drm_connector_attach_encoder(connector, encoder); +} + +static int fo_probe(struct spi_device *spi) +{ + int ret; + struct device *dev; + struct fo_device *fo; + struct drm_device *drm; + + dev = &spi->dev; + spi->mode |= SPI_MODE_3; + + ret = spi_setup(spi); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to setup spi device\n"); + + if (!dev->coherent_dma_mask) { + ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32)); + if (ret) + return dev_err_probe(dev, ret, "Failed to set dma mask\n"); + } + + fo = devm_drm_dev_alloc(dev, &fo_drm_driver, struct fo_device, drm); + if (!fo) + return -ENOMEM; + + spi_set_drvdata(spi, fo); + + fo->spi = spi; + drm = &fo->drm; + ret = drmm_mode_config_init(drm); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize drm config\n"); + + fo->active_gpio = devm_gpiod_get_optional(dev, "active", GPIOD_OUT_LOW); + if (!fo->active_gpio) + dev_warn(dev, "Active gpio not defined\n"); + + drm->mode_config.funcs = &fo_mode_config_funcs; + fo->mode = spi_get_device_match_data(spi); + /* The controller expects 3-byte aligned input lines */ + fo->pitch = roundup(fo->mode->hdisplay, 3); + fo->tx_buffer_size = (fo->pitch) * (fo->mode->vdisplay); + + fo->tx_buffer = devm_kzalloc(dev, fo->tx_buffer_size, GFP_KERNEL); + if (!fo->tx_buffer) + return -ENOMEM; + + drm->mode_config.min_width = fo->mode->hdisplay; + drm->mode_config.max_width = fo->mode->hdisplay; + drm->mode_config.min_height = fo->mode->vdisplay; + drm->mode_config.max_height = fo->mode->vdisplay; + + ret = fo_pipe_init(drm, fo, fo_formats, ARRAY_SIZE(fo_formats), NULL); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize display pipeline.\n"); + + drm_mode_config_reset(drm); + + ret = drm_dev_register(drm, 0); + if (ret) + return dev_err_probe(dev, ret, "Failed to register drm device.\n"); + + drm_client_setup(drm, NULL); + + return 0; +} + +static void fo_remove(struct spi_device *spi) +{ + struct fo_device *fo = spi_get_drvdata(spi); + + drm_dev_unplug(&fo->drm); + drm_atomic_helper_shutdown(&fo->drm); +} + +static struct spi_driver fo_spi_driver = { + .driver = { + .name = "flipper_one_display", + .of_match_table = fo_of_match, + }, + .probe = fo_probe, + .remove = fo_remove, + .id_table = fo_ids, +}; +module_spi_driver(fo_spi_driver); + +MODULE_AUTHOR("Alexey Charkov "); +MODULE_DESCRIPTION("SPI Protocol driver for the Flipper One display"); +MODULE_LICENSE("GPL"); From d2cb2a730d416292081727f32ac8bca68baf7a8f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:40:17 +0400 Subject: [PATCH 180/218] dt-bindings: arm: rockchip: Add Flipper One Add the compatibles for the Flipper One, which is an upcoming network multi-tool device built around the Rockchip RK3576 SoC. These are revisions F0B0C1 and F0B1C2, which are pre-production engineering versions. Signed-off-by: Alexey Charkov --- Documentation/devicetree/bindings/arm/rockchip.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documentation/devicetree/bindings/arm/rockchip.yaml b/Documentation/devicetree/bindings/arm/rockchip.yaml index 1a9dde18626d00..d1330da8f17033 100644 --- a/Documentation/devicetree/bindings/arm/rockchip.yaml +++ b/Documentation/devicetree/bindings/arm/rockchip.yaml @@ -299,6 +299,16 @@ properties: - const: firefly,rk3568-roc-pc - const: rockchip,rk3568 + - description: Flipper One rev. F0B0C1 + items: + - const: flipper,one-rev-f0b0c1 + - const: rockchip,rk3576 + + - description: Flipper One rev. F0B1C2 + items: + - const: flipper,one-rev-f0b1c2 + - const: rockchip,rk3576 + - description: Forlinx FET3588-C SoM items: - enum: From 4564fe475b2324a90768362cbdfc242180392483 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:41:58 +0400 Subject: [PATCH 181/218] arm64: dts: rockchip: Add Flipper One Add a device tree for Flipper One, which is an upcoming handheld network multi-tool device for hardware enthusiasts. It is based on the Rockchip RK3576 SoC paired with an onboard MCU for low power tasks, has battery power with powerbank functionality, built-in custom grayscale display, controls and a wide array of connectivity options. These are revisions F0B0C1 and F0B1C2, which are pre-production engineering versions. Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/Makefile | 12 + .../boot/dts/rockchip/rk3576-armsom-sige5.dts | 1 + .../rk3576-flipper-one-i2c2-free.dtso | 51 + .../rk3576-flipper-one-rev-f0b0c1.dts | 61 + .../rk3576-flipper-one-rev-f0b1c2.dts | 95 + .../dts/rockchip/rk3576-flipper-one-sata.dtso | 16 + .../boot/dts/rockchip/rk3576-flipper-one.dtsi | 1764 +++++++++++++++++ 7 files changed, 2000 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-i2c2-free.dtso create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-sata.dtso create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index 761d82b4f4f2ac..83ac2a9b0e9726 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -170,6 +170,10 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5-v1.2-wifibt.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb2-v10.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b0c1.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b1c2.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-i2c2-free.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-sata.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-khadas-edge-2l.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-luckfox-omni3576.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-nanopi-m5.dtb @@ -299,6 +303,14 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtb rk3576-evb1-v10-pcie1-dtbs := rk3576-evb1-v10.dtb \ rk3576-evb1-v10-pcie1.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-i2c2-free.dtb +rk3576-flipper-one-i2c2-free-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-i2c2-free.dtbo + +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-sata.dtb +rk3576-flipper-one-sata-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-sata.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-edgeble-neu6a-wifi.dtb rk3588-edgeble-neu6a-wifi-dtbs := rk3588-edgeble-neu6a-io.dtb \ rk3588-edgeble-neu6a-wifi.dtbo diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index fdf2a7d333aae4..769842159b9bef 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -708,6 +708,7 @@ }; &i2c2 { + clock-frequency = <400000>; status = "okay"; usbc0: typec-portc@22 { diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-i2c2-free.dtso b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-i2c2-free.dtso new file mode 100644 index 00000000000000..fc5c56b3493ce4 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-i2c2-free.dtso @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay to disable CPU side access to all I2C2 peripherals + */ + +/dts-v1/; +/plugin/; + +&gpio_expander { + status = "disabled"; +}; + +&usbc0 { + status = "disabled"; +}; + +&ina4_1 { + status = "disabled"; +}; + +&ina4_2 { + status = "disabled"; +}; + +&ina4_3 { + status = "disabled"; +}; + +&ina4_4 { + status = "disabled"; +}; + +&ina_sys { + status = "disabled"; +}; + +&ina4_5 { + status = "disabled"; +}; + +&usbmux { + status = "disabled"; +}; + +&gauge { + status = "disabled"; +}; + +&charger { + status = "disabled"; +}; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts new file mode 100644 index 00000000000000..0b9648298be8b0 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Copyright (c) 2026 Flipper FZCO + */ + +/dts-v1/; + +#include "rk3576-flipper-one.dtsi" + +/ { + model = "Flipper One rev. F0B0C1"; + compatible = "flipper,one-rev-f0b0c1", "rockchip,rk3576"; +}; + +&charger { + compatible = "ti,bq25792"; +}; + +&gpio_expander { + compatible = "ti,tca6416"; + + vcc5v0-device-s0-hog { + gpios = <0xb GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Peripheral power supply"; + gpio-hog; + }; +}; + +&gpio_vcc5v0 { + vin-supply = <&vcc5v0_device_s0>; +}; + +&mcu { + pinctrl-names = "default"; + pinctrl-0 = <&audio_headset_int &cpu_int>; +}; + +&usbc0 { + interrupt-parent = <&gpio_expander>; + interrupts = <3 IRQ_TYPE_LEVEL_LOW>; +}; + +&usbmux { + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-0 = <&usb_mux_int>; + pinctrl-names = "default"; +}; + +&vcc5v0_device_s0 { + /* + * Controlled by the GPIO expander pin P13, but turning it off + * makes the expander I2C bus stuck and unrecoverable due to + * the USB MUX misbehavior on the same bus + */ + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; +}; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts new file mode 100644 index 00000000000000..0fe76d9b30eedb --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Copyright (c) 2026 Flipper FZCO + */ + +/dts-v1/; + +#include "rk3576-flipper-one.dtsi" + +/ { + model = "Flipper One rev. F0B1C2"; + compatible = "flipper,one-rev-f0b1c2", "rockchip,rk3576"; + + vcc3v3_wifi: regulator-vcc3v3-wifi { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vcc3v3_wifi"; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + gpios = <&gpio2 RK_PB5 GPIO_ACTIVE_HIGH>; + pinctrl-0 = <&wifi_pwr_en>; + pinctrl-names = "default"; + vin-supply = <&vcc_3v3_s3>; + }; + + vusb_typec_up: regulator-vcc5v2-vusb-typec-up { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vusb_typec_up"; + regulator-always-on; /* Remove once VBUS is wired up in the DT binding and driver */ + regulator-min-microvolt = <5200000>; + regulator-max-microvolt = <5200000>; + gpios = <&gpio_expander 0x7 GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_device_s0>; + }; +}; + +&charger { + compatible = "ti,bq25798", "ti,bq25792"; +}; + +&gpio_expander { + compatible = "nxp,pcal6416"; +}; + +&gpio_vcc5v0 { + vin-supply = <&vcc8v4_sys>; +}; + +&hub_2_0 { + vdd3v3-supply = <&vcc_3v3_s3>; +}; + +&hub_3_0 { + vdd3v3-supply = <&vcc_3v3_s3>; +}; + +&mcu { + pinctrl-names = "default"; + pinctrl-0 = <&cpu_int>; +}; + +&pinctrl { + wifi { + wifi_pwr_en: wifi-pwr-en { + rockchip,pins = <2 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; +}; + +&uart1 { + pinctrl-names = "default"; + pinctrl-0 = <&uart1m1_xfer>; + status = "okay"; +}; + +&usbc0 { + interrupt-parent = <&gpio0>; + interrupts = ; + pinctrl-0 = <&audio_headset_int>; + pinctrl-names = "default"; +}; + +&usbmux { + interrupt-parent = <&gpio_expander>; + interrupts = <4 IRQ_TYPE_LEVEL_LOW>; + vbus-supply = <&vusb_typec_up>; /* Wire it up in the DT binding and driver */ +}; + +&vcc5v0_device_s0 { + regulator-min-microvolt = <5200000>; + regulator-max-microvolt = <5200000>; + gpios = <&gpio_expander 0xb GPIO_ACTIVE_HIGH>; +}; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-sata.dtso b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-sata.dtso new file mode 100644 index 00000000000000..559d64ff50fa5f --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-sata.dtso @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay to switch the M.2 slot from PCIe mode to SATA (which shares the + * same pins). + */ + +/dts-v1/; +/plugin/; + +&sata0 { + status = "okay"; +}; + +&pcie0 { + status = "disabled"; +}; diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi new file mode 100644 index 00000000000000..a95f62f386d900 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi @@ -0,0 +1,1764 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * Copyright (c) 2026 Flipper FZCO + */ + +/dts-v1/; + +#include +#include +#include +#include +#include +#include +#include +#include "rk3576.dtsi" + +/ { + aliases { + ethernet0 = &gmac0; + ethernet1 = &gmac1; + }; + + battery: battery { + compatible = "simple-battery"; + charge-full-design-microamp-hours = <3100000>; + constant-charge-current-max-microamp = <3100000>; + constant-charge-voltage-max-microvolt = <8650000>; + device-chemistry = "lithium-ion-polymer"; + operating-range-celsius = <0 60>; + voltage-min-design-microvolt = <6000000>; + }; + + chosen: chosen { + stdout-path = "serial0:1500000n8"; + }; + + hdmi-con { + compatible = "hdmi-connector"; + type = "a"; + + port { + hdmi_con_in: endpoint { + remote-endpoint = <&hdmi_out_con>; + }; + }; + }; + + leds { + compatible = "gpio-leds"; + pinctrl-0 = <&led_cd2>, <&led_cd3>; + pinctrl-names = "default"; + + led-cd2 { + color = ; + function = LED_FUNCTION_HEARTBEAT; + gpios = <&gpio0 RK_PD2 GPIO_ACTIVE_HIGH>; + linux,default-trigger = "heartbeat"; + }; + + led-cd3 { + color = ; + function = LED_FUNCTION_STATUS; + gpios = <&gpio0 RK_PD3 GPIO_ACTIVE_HIGH>; + linux,default-trigger = "default-on"; + }; + }; + + vcc8v4_sys: regulator-vcc8v4-sys { + compatible = "regulator-fixed"; + regulator-name = "vcc8v4_sys"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <8400000>; + regulator-max-microvolt = <8400000>; + /* Powered by the charger's SYS output */ + }; + + vcc3v3_control: regulator-vcc3v3-control { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_control"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + vin-supply = <&vcc8v4_sys>; + }; + + gpio_vcc3v3: regulator-vcc3v3-extgpio { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "gpio_vcc3v3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + gpios = <&gpio_expander 0xe GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc3v3_control>; + }; + + vcc3v3_m2: regulator-vcc3v3-m2 { + compatible = "regulator-fixed"; + regulator-name = "vcc3v3_m2"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + enable-active-high; + gpios = <&gpio0 RK_PB1 GPIO_ACTIVE_HIGH>; + pinctrl-0 = <&m2_pwr_en>; + pinctrl-names = "default"; + startup-delay-us = <5000>; + vin-supply = <&vcc8v4_sys>; + }; + + vcc2v1_rtc: regulator-vcc2v1-rtc { + compatible = "regulator-fixed"; + regulator-name = "vcc2v1_rtc"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <2100000>; + regulator-max-microvolt = <2100000>; + /* Powered by the system battery */ + }; + + vcc5v0_device_s0: regulator-vcc5v0-device-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc5v0_device"; + vin-supply = <&vcc8v4_sys>; + }; + + gpio_vcc5v0: regulator-vcc5v0-extgpio { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "gpio_vcc5v0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + gpios = <&gpio_expander 0xd GPIO_ACTIVE_HIGH>; + }; + + vusb_typea_up: regulator-vcc5v0-vusb-typea-up { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vusb_typea_up"; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + gpios = <&gpio_expander 0xa GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_device_s0>; + }; + + vcc5v0_sys_s5: regulator-vcc5v0-sys-s5 { + compatible = "regulator-fixed"; + regulator-name = "vcc5v0_sys_s5"; + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <5000000>; + regulator-max-microvolt = <5000000>; + /* + * Controlled by the GPIO expander pin P14, but we + * can't let this pin to ever change state, as it makes + * the system lose power + */ + vin-supply = <&vcc8v4_sys>; + }; + + vcc_2v0_pldo_s3: regulator-vcc-2v0-pldo-s3 { + compatible = "regulator-fixed"; + regulator-name = "vcc_2v0_pldo_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <2000000>; + regulator-max-microvolt = <2000000>; + /* PMIC_EXT_EN_OUT */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vcc_1v1_nldo_s3: regulator-vcc-1v1-nldo-s3 { + compatible = "regulator-fixed"; + regulator-name = "vcc_1v1_nldo_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1100000>; + regulator-max-microvolt = <1100000>; + /* PMIC_EXT_EN_OUT */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vdd2l_0v9_ddr_s3: regulator-vdd2l-0v9-ddr-s3 { + compatible = "regulator-fixed"; + regulator-name = "vdd2l_0v9_ddr_s3"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <900000>; + regulator-max-microvolt = <900000>; + /* VCC_3V3_S3 */ + vin-supply = <&vcc5v0_sys_s5>; + }; + + vdd1v1_hub: regulator-vdd-1v1-hub { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vdd1v1_hub"; + regulator-min-microvolt = <1100000>; + regulator-max-microvolt = <1100000>; + gpios = <&gpio_expander 0x9 GPIO_ACTIVE_HIGH>; + vin-supply = <&vcc5v0_sys_s5>; + }; + + vcc_3v3_s0: regulator-vcc-3v3-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc_3v3_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_3v3_s3>; + }; + + vcc_1v8_s0: regulator-vcc-1v8-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc_1v8_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_1v8_s3>; + }; + + vccio_1v8_s0: regulator-vccio-1v8-s0 { + compatible = "regulator-fixed"; + regulator-name = "vccio_1v8_s0"; + regulator-boot-on; + regulator-always-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + /* VCCA_1V8_S0 */ + vin-supply = <&vcc_1v8_s3>; + }; + + vcc1v8_ufs_vccq2_s0: regulator-vcc1v8-ufs-vccq2-s0 { + compatible = "regulator-fixed"; + regulator-name = "vcc1v8_ufs_vccq2_s0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + vin-supply = <&vcc_1v8_s3>; + }; + + rfkill-onboard-wifi { + compatible = "rfkill-gpio"; + default-blocked; + radio-type = "wlan"; + label = "Onboard WiFi+BT"; + pinctrl-0 = <&wifi_pmu_en>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio1 RK_PD5 GPIO_ACTIVE_HIGH>; + }; + + rfkill-m2-wdisable1 { + compatible = "rfkill-gpio"; + radio-type = "wwan"; + label = "M.2 WWAN"; + pinctrl-0 = <&m2b_w_disable1>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio2 RK_PB4 GPIO_ACTIVE_HIGH>; + }; + + rfkill-m2-wdisable2 { + compatible = "rfkill-gpio"; + radio-type = "wwan"; + label = "M.2 WWAN (WDISABLE2)"; + pinctrl-0 = <&m2b_w_disable2>; + pinctrl-names = "default"; + shutdown-gpios = <&gpio4 RK_PA0 GPIO_ACTIVE_HIGH>; + }; + + sound { + compatible = "simple-audio-card"; + simple-audio-card,name = "On-board Analog NAU8822"; + simple-audio-card,bitclock-master = <&masterdai>; + simple-audio-card,format = "i2s"; + simple-audio-card,frame-master = <&masterdai>; + simple-audio-card,mclk-fs = <256>; + simple-audio-card,pin-switches = "Headphones", "Speaker", + "Headset Microphone", "Internal Microphone"; + simple-audio-card,routing = + "Headphones", "LHP", + "Headphones", "RHP", + "Speaker", "LSPK", + "Speaker", "RSPK", + "Line Out", "AUXOUT1", + "Line Out", "AUXOUT2", + "LMICP", "Internal Microphone", + "LMICN", "Internal Microphone", + "RMICP", "Headset Microphone"; + simple-audio-card,widgets = + "Headphones", "Headphones", + "Line Out", "Line Out", + "Speaker", "Speaker", + "Microphone", "Internal Microphone", + "Microphone", "Headset Microphone"; + + simple-audio-card,cpu { + sound-dai = <&sai2>; + }; + + masterdai: simple-audio-card,codec { + sound-dai = <&nau8822>; + system-clock-frequency = <12288000>; + }; + }; + + typea_up_con: usb-a-connector { + compatible = "usb-a-connector"; + data-role = "host"; + label = "USB-A (up)"; + power-role = "source"; + vbus-supply = <&vusb_typea_up>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + typea_up_con_hs: endpoint { + remote-endpoint = <&hub_2_0_ds2>; + }; + }; + + port@1 { + reg = <1>; + typea_up_con_ss: endpoint { + remote-endpoint = <&hub_3_0_ds2>; + }; + }; + }; + }; + + typec_up_con: usb-c-connector { + compatible = "usb-c-connector"; + data-role = "host"; + label = "USB-C (up)"; + power-role = "source"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + typec_up_con_hs: endpoint { + remote-endpoint = <&hub_2_0_ds3>; + }; + }; + + port@1 { + reg = <1>; + typec_up_con_ss: endpoint { + remote-endpoint = <&usb_mux_in>; + }; + }; + }; + }; +}; + +&cpu_l0 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l1 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l2 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_l3 { + cpu-supply = <&vdd_cpu_lit_s0>; +}; + +&cpu_b0 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b1 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b2 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&cpu_b3 { + cpu-supply = <&vdd_cpu_big_s0>; +}; + +&combphy0_ps { + status = "okay"; +}; + +&combphy1_psu { + status = "okay"; +}; + +&dp { + status = "okay"; +}; + +&dp0_in { + dp0_in_vp1: endpoint { + remote-endpoint = <&vp1_out_dp0>; + }; +}; + +&dp0_out { + dp0_out_con: endpoint { + remote-endpoint = <&usbdp_phy_dp_in>; + }; +}; + +&dp0_sound { + status = "okay"; +}; + +&gmac0 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy0>; + phy-mode = "rgmii-id"; + phy-supply = <&vccio_1v8_s0>; + pinctrl-names = "default"; + pinctrl-0 = <ð0m0_miim + ð0m0_tx_bus2 + ð0m0_rx_bus2 + ð0m0_rgmii_clk + ð0m0_rgmii_bus + ðm0_clk0_25m_out>; + status = "okay"; +}; + +&gmac1 { + clock_in_out = "output"; + phy-handle = <&rgmii_phy1>; + phy-mode = "rgmii-id"; + phy-supply = <&vccio_1v8_s0>; + pinctrl-names = "default"; + pinctrl-0 = <ð1m0_miim + ð1m0_tx_bus2 + ð1m0_rx_bus2 + ð1m0_rgmii_clk + ð1m0_rgmii_bus + ðm0_clk1_25m_out>; + status = "okay"; +}; + +&gpio2 { + m2-poweroff-hog { + gpios = ; + output-low; + line-name = "M.2 Full Card Power Off Disable"; + gpio-hog; + }; +}; + +&gpio3 { + m2-reset-hog { + gpios = ; + output-low; + line-name = "M.2 Reset Disable"; + gpio-hog; + }; +}; + +&gpu { + mali-supply = <&vdd_gpu_s0>; + status = "okay"; +}; + +&hdmi { + frl-enable-gpios = <&gpio0 RK_PC3 GPIO_ACTIVE_LOW>; + status = "okay"; +}; + +&hdmi_in { + hdmi_in_vp0: endpoint { + remote-endpoint = <&vp0_out_hdmi>; + }; +}; + +&hdmi_out { + hdmi_out_con: endpoint { + remote-endpoint = <&hdmi_con_in>; + }; +}; + +&hdmi_sound { + status = "okay"; +}; + +&hdptxphy { + status = "okay"; +}; + +&i2c0 { + clock-frequency = <400000>; + pinctrl-0 = <&i2c0m1_xfer &i2c0_sda_pullup>; + pinctrl-names = "default"; + status = "okay"; + + mcu: embedded-controller@69 { + compatible = "flipper,one-mcu"; + reg = <0x69>; + interrupt-parent = <&gpio2>; + interrupts = ; + wakeup-source; + }; +}; + +&i2c1 { + status = "okay"; + + rk806: pmic@23 { + compatible = "rockchip,rk806"; + reg = <0x23>; + interrupt-parent = <&gpio0>; + interrupts = ; + gpio-controller; + #gpio-cells = <2>; + pinctrl-names = "default"; + pinctrl-0 = <&pmic_pins>, <&rk806_dvs1_null>, + <&rk806_dvs2_null>, <&rk806_dvs3_null>; + system-power-controller; + + vcc1-supply = <&vcc5v0_sys_s5>; + vcc2-supply = <&vcc5v0_sys_s5>; + vcc3-supply = <&vcc5v0_sys_s5>; + vcc4-supply = <&vcc5v0_sys_s5>; + vcc5-supply = <&vcc5v0_sys_s5>; + vcc6-supply = <&vcc5v0_sys_s5>; + vcc7-supply = <&vcc5v0_sys_s5>; + vcc8-supply = <&vcc5v0_sys_s5>; + vcc9-supply = <&vcc5v0_sys_s5>; + vcc10-supply = <&vcc5v0_sys_s5>; + vcc11-supply = <&vcc_2v0_pldo_s3>; + vcc12-supply = <&vcc5v0_sys_s5>; + vcc13-supply = <&vcc_1v1_nldo_s3>; + vcc14-supply = <&vcc_1v1_nldo_s3>; + vcca-supply = <&vcc5v0_sys_s5>; + + rk806_dvs1_null: dvs1-null-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun0"; + }; + + rk806_dvs2_null: dvs2-null-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun0"; + }; + + rk806_dvs3_null: dvs3-null-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun0"; + }; + + rk806_dvs1_slp: dvs1-slp-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun1"; + }; + + rk806_dvs1_pwrdn: dvs1-pwrdn-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun2"; + }; + + rk806_dvs1_rst: dvs1-rst-pins { + pins = "gpio_pwrctrl1"; + function = "pin_fun3"; + }; + + rk806_dvs2_slp: dvs2-slp-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun1"; + }; + + rk806_dvs2_pwrdn: dvs2-pwrdn-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun2"; + }; + + rk806_dvs2_rst: dvs2-rst-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun3"; + }; + + rk806_dvs2_dvs: dvs2-dvs-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun4"; + }; + + rk806_dvs2_gpio: dvs2-gpio-pins { + pins = "gpio_pwrctrl2"; + function = "pin_fun5"; + }; + + rk806_dvs3_slp: dvs3-slp-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun1"; + }; + + rk806_dvs3_pwrdn: dvs3-pwrdn-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun2"; + }; + + rk806_dvs3_rst: dvs3-rst-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun3"; + }; + + rk806_dvs3_dvs: dvs3-dvs-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun4"; + }; + + rk806_dvs3_gpio: dvs3-gpio-pins { + pins = "gpio_pwrctrl3"; + function = "pin_fun5"; + }; + + regulators { + vdd_cpu_big_s0: dcdc-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_cpu_big_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_npu_s0: dcdc-reg2 { + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_npu_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_cpu_lit_s0: dcdc-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <950000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_cpu_lit_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + regulator-suspend-microvolt = <750000>; + }; + }; + + vcc_3v3_s3: dcdc-reg4 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-name = "vcc_3v3_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <3300000>; + }; + }; + + vdd_gpu_s0: dcdc-reg5 { + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <900000>; + regulator-ramp-delay = <12500>; + regulator-name = "vdd_gpu_s0"; + regulator-enable-ramp-delay = <400>; + + regulator-state-mem { + regulator-off-in-suspend; + regulator-suspend-microvolt = <850000>; + }; + }; + + vddq_ddr_s0: dcdc-reg6 { + regulator-always-on; + regulator-boot-on; + regulator-name = "vddq_ddr_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdd_logic_s0: dcdc-reg7 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <800000>; + regulator-name = "vdd_logic_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcc_1v8_s3: dcdc-reg8 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcc_1v8_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <1800000>; + }; + }; + + vdd2_ddr_s3: dcdc-reg9 { + regulator-always-on; + regulator-boot-on; + regulator-name = "vdd2_ddr_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + }; + }; + + vdd_ddr_s0: dcdc-reg10 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <1200000>; + regulator-name = "vdd_ddr_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca_1v8_s0: pldo-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca_1v8_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca1v8_pldo2_s0: pldo-reg2 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca1v8_pldo2_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_1v2_s0: pldo-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1200000>; + regulator-max-microvolt = <1200000>; + regulator-name = "vdda_1v2_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca_3v3_s0: pldo-reg4 { + regulator-enable-ramp-delay = <500>; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-ramp-delay = <12500>; + regulator-name = "vcca_3v3_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vccio_3v3_sd_s0: pldo-reg5 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <3300000>; + regulator-name = "vccio_3v3_sd_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vcca1v8_pldo6_s3: pldo-reg6 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + regulator-name = "vcca1v8_pldo6_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <1800000>; + }; + }; + + vdd_0v75_s3: nldo-reg1 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <550000>; + regulator-max-microvolt = <750000>; + regulator-name = "vdd_0v75_s3"; + + regulator-state-mem { + regulator-on-in-suspend; + regulator-suspend-microvolt = <750000>; + }; + }; + + vdda_ddr_pll_s0: nldo-reg2 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <850000>; + regulator-max-microvolt = <850000>; + regulator-name = "vdda_ddr_pll_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda0v75_hdmi_s0: nldo-reg3 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <837500>; + regulator-max-microvolt = <837500>; + regulator-name = "vdda0v75_hdmi_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_0v85_s0: nldo-reg4 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <850000>; + regulator-max-microvolt = <850000>; + regulator-name = "vdda_0v85_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + + vdda_0v75_s0: nldo-reg5 { + regulator-always-on; + regulator-boot-on; + regulator-min-microvolt = <750000>; + regulator-max-microvolt = <750000>; + regulator-name = "vdda_0v75_s0"; + + regulator-state-mem { + regulator-off-in-suspend; + }; + }; + }; + }; +}; + +&i2c2 { + clock-frequency = <400000>; + pinctrl-0 = <&i2c2m0_xfer_dr5>; + pinctrl-names = "default"; + status = "okay"; + + gpio_expander: gpio@20 { + reg = <0x20>; + gpio-controller; + #gpio-cells = <2>; + #interrupt-cells = <2>; + interrupt-controller; + interrupt-parent = <&gpio2>; + interrupts = ; + pinctrl-0 = <&expander_int>; + pinctrl-names = "default"; + vcc-supply = <&vcc3v3_control>; + + usb2-switch-hog { + gpios = <8 GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Main Type-C USB 2.0 pins switch to CPU"; + gpio-hog; + }; + + vcc5v0-sys-s5-hog { + gpios = <0xc GPIO_ACTIVE_HIGH>; + output-high; + line-name = "Main power supply to the system PMIC"; + gpio-hog; + }; + }; + + usbc0: typec-portc@22 { + compatible = "fcs,fusb302"; + reg = <0x22>; + vbus-supply = <&vusb_typec>; + + connector { + compatible = "usb-c-connector"; + label = "USB-C"; + data-role = "dual"; + op-sink-microwatt = <10000000>; + pd-revision = /bits/ 8 <0x3 0x1 0x1 0x8>; + power-role = "dual"; + self-powered; + sink-pdos = ; + source-pdos = ; + try-power-role = "sink"; + + altmodes { + displayport { + svid = /bits/ 16 <0xff01>; + vdo = <0xffffffff>; + }; + }; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usbc0_hs: endpoint { + remote-endpoint = <&usb_drd0_hs_ep>; + }; + }; + port@1 { + reg = <1>; + usbc0_ss: endpoint { + remote-endpoint = <&usbdp_phy_ss_out>; + }; + }; + port@2 { + reg = <2>; + usbc0_sbu: endpoint { + remote-endpoint = <&usbdp_phy0_dp_out>; + }; + }; + }; + }; + }; + + ina4_1: power-sensor@40 { + compatible = "ti,ina4230"; + reg = <0x40>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdd_0v75_s3"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@1 { + reg = <1>; + label = "vcc_3v3_control"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <4000000>; + }; + + input@2 { + reg = <2>; + label = "vdd0v85_ddr_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@3 { + reg = <3>; + label = "vcc_3v3_s3"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + }; + + ina4_2: power-sensor@41 { + compatible = "ti,ina4230"; + reg = <0x41>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vddq0v51_ddr_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@1 { + reg = <1>; + label = "vdd0v75_npu_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + + input@2 { + reg = <2>; + label = "vdd0v75_gpu_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + + input@3 { + reg = <3>; + label = "vdd0v75_logic_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + }; + + ina4_3: power-sensor@43 { + compatible = "ti,ina4230"; + reg = <0x43>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdd0v75_cpu_big_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <6500000>; + }; + + input@1 { + reg = <1>; + label = "vdda_1v2_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@2 { + reg = <2>; + label = "vcca_1v8_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@3 { + reg = <3>; + label = "vdd0v75_cpu_lit_s0"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + }; + + ina4_4: power-sensor@44 { + compatible = "ti,ina4230"; + reg = <0x44>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vdda_0v75_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@1 { + reg = <1>; + label = "vdda_0v85_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@2 { + reg = <2>; + label = "vdda0v75_hdmi_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@3 { + reg = <3>; + label = "vdda0v85_ddr_pll_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + }; + + ina_sys: power-sensor@45 { + compatible = "ti,ina219"; + reg = <0x45>; + #io-channel-cells = <1>; + label = "vcc8v4_sys"; + shunt-resistor = <4000>; + vs-supply = <&vcc3v3_control>; + }; + + ina4_5: power-sensor@46 { + compatible = "ti,ina4230"; + reg = <0x46>; + vs-supply = <&vcc3v3_control>; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0>; + label = "vcca_3v3_s0"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <500000>; + }; + + input@1 { + reg = <1>; + label = "vccio_3v3_sd_s0"; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@2 { + reg = <2>; + label = "vdd2_1v05_ddr_s3"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <2500000>; + }; + + input@3 { + reg = <3>; + label = "vcc_1v8_s3"; + shunt-resistor-micro-ohms = <20000>; + ti,maximum-expected-current-microamp = <3000000>; + }; + }; + + usbmux: usb-mux@47 { + compatible = "ti,hd3ss3220"; + reg = <0x47>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usb_mux_in: endpoint { + remote-endpoint = <&typec_up_con_ss>; + }; + }; + + port@1 { + reg = <1>; + usb_mux_out: endpoint { + remote-endpoint = <&hub_3_0_ds3>; + }; + }; + }; + }; + + gauge: fuel-gauge@55 { + compatible = "ti,bq28z610"; /* really bq28z620: tbc if we need a new compatible */ + reg = <0x55>; + monitored-battery = <&battery>; + }; + + charger: charger@6b { + reg = <0x6b>; + input-current-limit-microamp = <3300000>; + interrupt-parent = <&gpio_expander>; + interrupts = <2 IRQ_TYPE_LEVEL_LOW>; + monitored-battery = <&battery>; + power-supplies = <&usbc0>; + + regulators { + vusb_typec: vbus { + regulator-max-microamp = <3320000>; + regulator-max-microvolt = <22000000>; + regulator-min-microamp = <0>; + regulator-min-microvolt = <2800000>; + regulator-name = "vusb_typec"; + }; + }; + }; +}; + +&i2c5 { + pinctrl-0 = <&i2c5m3_xfer>; + pinctrl-names = "default"; + status = "okay"; + + /* MIPI CSI camera + * PDN GPIO: GPIO3 RK_PC6 <&cam_pdn> + * CLK: CAM_CLK2_OUT_M0 <&cam_clk2m0_clk2> + */ +}; + +&i2c6 { + pinctrl-0 = <&i2c6m3_xfer>; + pinctrl-names = "default"; + status = "okay"; + + nau8822: audio-codec@1a { + compatible = "nuvoton,nau8822"; + reg = <0x1a>; + + /* all supplies: VCCA_3V3_S0 */ + assigned-clocks = <&cru CLK_SAI2_MCLKOUT_TO_IO>; + assigned-clock-rates = <12288000>; + clock-names = "mclk"; + clocks = <&cru CLK_SAI2_MCLKOUT_TO_IO>; + pinctrl-names = "default"; + pinctrl-0 = <&sai2m0_mclk>; + vdda-supply = <&vcca_3v3_s0>; + vddb-supply = <&vcca_3v3_s0>; + vddc-supply = <&vcca_3v3_s0>; + vddspk-supply = <&vcca_3v3_s0>; + #sound-dai-cells = <0>; + nuvoton,spk-btl; + }; +}; + +&i2c8 { + pinctrl-0 = <&i2c8m2_xfer>; + pinctrl-names = "default"; + status = "okay"; + + hym8563: rtc@51 { + compatible = "haoyu,hym8563"; + reg = <0x51>; + clock-output-names = "hym8563"; + interrupt-parent = <&gpio0>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rtc_int &clk_32k_pins>; + wakeup-source; + #clock-cells = <0>; + }; +}; + +&mdio0 { + rgmii_phy0: ethernet-phy@1 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x1>; + clocks = <&cru REFCLKO25M_GMAC0_OUT>; + assigned-clocks = <&cru REFCLKO25M_GMAC0_OUT>; + assigned-clock-rates = <25000000>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rgmii_phy0_int &rgmii_phy0_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio1 RK_PB5 GPIO_ACTIVE_LOW>; + wakeup-source; + realtek,aldps-enable; + }; +}; + +&mdio1 { + rgmii_phy1: ethernet-phy@1 { + compatible = "ethernet-phy-id001c.c916"; + reg = <0x1>; + clocks = <&cru REFCLKO25M_GMAC1_OUT>; + assigned-clocks = <&cru REFCLKO25M_GMAC1_OUT>; + assigned-clock-rates = <25000000>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-names = "default"; + pinctrl-0 = <&rgmii_phy1_int &rgmii_phy1_rst>; + reset-assert-us = <20000>; + reset-deassert-us = <100000>; + reset-gpios = <&gpio1 RK_PB4 GPIO_ACTIVE_LOW>; + wakeup-source; + realtek,aldps-enable; + }; +}; + +&pcie0 { + pinctrl-names = "default"; + pinctrl-0 = <&pcie0m1_pins &pcie0_rst>; + reset-gpios = <&gpio1 RK_PC3 GPIO_ACTIVE_HIGH>; + vpcie3v3-supply = <&vcc3v3_m2>; + status = "okay"; +}; + +&pinctrl { + camera { + cam_pdn: cam-pdn { + rockchip,pins = <3 RK_PC6 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + display { + spi0m0_wo_miso: spi0m0-wo-miso { + rockchip,pins = + /* spi0m0_csn0 */ + <0 RK_PC6 11 &pcfg_pull_none>, + /* spi0_clk_m0 */ + <0 RK_PC7 11 &pcfg_pull_none>, + /* spi0_mosi_m0 */ + <0 RK_PD0 11 &pcfg_pull_none>; + }; + + spi0m0_gpiomiso: spi0m0-gpiomiso { + rockchip,pins = <0 RK_PD1 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + gpio-expander { + expander_int: expander-int { + rockchip,pins = <2 RK_PA7 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + hdmi { + hdmi_enable_frl: hdmi-enable-frl { + rockchip,pins = <0 RK_PC3 RK_FUNC_GPIO &pcfg_pull_down>; + }; + }; + + hym8563 { + rtc_int: rtc-int { + rockchip,pins = <0 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + i2c0 { + i2c0_sda_pullup: i2c0-sda-pullup { + rockchip,pins = <0 RK_PC5 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + i2c2 { + i2c2m0_xfer_dr5: i2c2m0-xfer-dr5 { + rockchip,pins = + /* i2c2_scl_m0 */ + <0 RK_PB7 9 &pcfg_pull_none_drv_level_5_smt>, + /* i2c2_sda_m0 */ + <0 RK_PC0 9 &pcfg_pull_none_drv_level_5_smt>; + }; + }; + + leds { + led_cd2: led-cd2 { + rockchip,pins = <0 RK_PD2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + led_cd3: led-cd3 { + rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + m2 { + m2_pwr_en: m2-pwren { + rockchip,pins = <0 RK_PB1 RK_FUNC_GPIO &pcfg_pull_down>; + }; + + m2b_w_disable1: m2-rfkill-wdisable1 { + rockchip,pins = <2 RK_PB4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + m2b_w_disable2: m2-rfkill-wdisable2 { + rockchip,pins = <4 RK_PA0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + }; + + mcu { + audio_headset_int: audio-headset-int { + rockchip,pins = <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + cpu_int: cpu-int { + rockchip,pins = <2 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + network { + rgmii_phy0_int: rgmii-phy0-int { + rockchip,pins = <1 RK_PC1 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + rgmii_phy0_rst: rgmii-phy0-rst { + rockchip,pins = <1 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + rgmii_phy1_int: rgmii-phy1-int { + rockchip,pins = <1 RK_PC2 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + rgmii_phy1_rst: rgmii-phy1-rst { + rockchip,pins = <1 RK_PB4 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + pcie0 { + pcie0_rst: pcie0-rst { + rockchip,pins = <1 RK_PC3 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + usb { + hub_reset: usb-hub-reset { + rockchip,pins = <0 RK_PC4 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + usb_mux_int: usb-mux-int { + rockchip,pins = <1 RK_PC0 RK_FUNC_GPIO &pcfg_pull_up>; + }; + + usbc0_sbu1: usbc0-sbu1 { + rockchip,pins = <4 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + usbc0_sbu2: usbc0-sbu2 { + rockchip,pins = <4 RK_PC5 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + + wifibt { + wifi_pmu_en: wifi-pmu-en { + rockchip,pins = <1 RK_PD5 RK_FUNC_GPIO &pcfg_pull_down>; + }; + + wifi_wgpio0: bt-wake-host { + rockchip,pins = <1 RK_PC6 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + wifi_wgpio1: wifi-wake-host { + rockchip,pins = <1 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; +}; + +&sai0 { + /* Serial audio for M.2 WWAN */ + pinctrl-names = "default"; + pinctrl-0 = <&sai0m2_lrck + &sai0m2_sclk + &sai0m2_sdi0 + &sai0m2_sdo0>; + status = "okay"; +}; + +&sai2 { + status = "okay"; +}; + +&sai6 { + status = "okay"; +}; + +&sdmmc { + bus-width = <4>; + cap-sd-highspeed; + cd-gpios = <&gpio0 RK_PA7 GPIO_ACTIVE_LOW>; + disable-wp; + no-sdio; + no-mmc; + sd-uhs-sdr104; + vmmc-supply = <&vcc_3v3_s3>; + vqmmc-supply = <&vccio_3v3_sd_s0>; + status = "okay"; +}; + +&saradc { + vref-supply = <&vcca_1v8_s0>; + status = "okay"; +}; + +&spdif_tx3 { + status = "okay"; +}; + +&spi0 { + pinctrl-0 = <&spi0m0_wo_miso &spi0m0_gpiomiso>; + status = "okay"; + + display@0 { + compatible = "flipper,one-display"; + reg = <0>; + active-gpios = <&gpio0 RK_PD1 GPIO_ACTIVE_LOW>; + spi-cpha; + spi-cpol; + spi-max-frequency = <20000000>; + spi-rx-bus-width = <0>; + }; +}; + +&u2phy0 { + status = "okay"; +}; + +&u2phy0_otg { + status = "okay"; +}; + +&u2phy1 { + status = "okay"; +}; + +&u2phy1_otg { + status = "okay"; +}; + +&uart0 { + status = "okay"; +}; + +&uart4 { + pinctrl-0 = <&uart4m1_xfer>; + pinctrl-names = "default"; + status = "okay"; + + /* MCU interconnect */ +}; + +&ufshc { + vcc-supply = <&vcc_3v3_s3>; + vccq2-supply = <&vcc1v8_ufs_vccq2_s0>; + status = "okay"; +}; + +&usbdp_phy { + mode-switch; + orientation-switch; + pinctrl-names = "default"; + pinctrl-0 = <&usbc0_sbu1 &usbc0_sbu2>; + sbu1-dc-gpios = <&gpio4 RK_PC4 GPIO_ACTIVE_HIGH>; + sbu2-dc-gpios = <&gpio4 RK_PC5 GPIO_ACTIVE_HIGH>; + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + + port@0 { + reg = <0>; + + usbdp_phy_ss_out: endpoint { + remote-endpoint = <&usbc0_ss>; + }; + }; + + port@1 { + reg = <1>; + + usbdp_phy_ss_in: endpoint { + remote-endpoint = <&usb_drd0_ss_ep>; + }; + }; + + port@2 { + reg = <2>; + + usbdp_phy_dp_in: endpoint { + remote-endpoint = <&dp0_out_con>; + }; + }; + + port@3 { + reg = <3>; + + usbdp_phy0_dp_out: endpoint { + remote-endpoint = <&usbc0_sbu>; + }; + }; + }; +}; + +&usb_drd0_dwc3 { + usb-role-switch; + dr_mode = "otg"; + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + usb_drd0_hs_ep: endpoint { + remote-endpoint = <&usbc0_hs>; + }; + }; + + port@1 { + reg = <1>; + usb_drd0_ss_ep: endpoint { + remote-endpoint = <&usbdp_phy_ss_in>; + }; + }; + }; +}; + +&usb_drd1_dwc3 { + #address-cells = <1>; + #size-cells = <0>; + dr_mode = "host"; + pinctrl-0 = <&hub_reset>; + pinctrl-names = "default"; + status = "okay"; + + /* 2.0 hub */ + hub_2_0: hub@1 { + compatible = "usb3431,6241"; + reg = <1>; + peer-hub = <&hub_3_0>; + reset-gpios = <&gpio0 RK_PC4 GPIO_ACTIVE_LOW>; + vdd1v1-supply = <&vdd1v1_hub>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@1 { + reg = <1>; + /* ext USB port 2.54mm */ + }; + + port@2 { + reg = <2>; + hub_2_0_ds2: endpoint { + remote-endpoint = <&typea_up_con_hs>; + }; + }; + + port@3 { + reg = <3>; + hub_2_0_ds3: endpoint { + remote-endpoint = <&typec_up_con_hs>; + }; + }; + + port@4 { + reg = <4>; + /* M.2 */ + }; + }; + }; + + /* 3.0 hub */ + hub_3_0: hub@2 { + compatible = "usb3431,6341"; + reg = <2>; + #address-cells = <1>; + #size-cells = <0>; + peer-hub = <&hub_2_0>; + vdd1v1-supply = <&vdd1v1_hub>; + + device@1 { + /* WiFi-BT module connection */ + compatible = "usbe8d,7961"; + reg = <1>; + #address-cells = <2>; + #size-cells = <0>; + + interface@0 { + /* BT part */ + compatible = "usbife8d,7961.config1.0"; + reg = <0 1>; + interrupt-parent = <&gpio1>; + interrupts = ; + interrupt-names = "wakeup"; + pinctrl-0 = <&wifi_wgpio0>; + pinctrl-names = "default"; + }; + + interface@0,3 { + /* WiFi part */ + compatible = "usbife8d,7961.config1.3"; + reg = <0 3>; + interrupt-parent = <&gpio1>; + interrupts = ; + pinctrl-0 = <&wifi_wgpio1>; + pinctrl-names = "default"; + }; + }; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@2 { + reg = <2>; + hub_3_0_ds2: endpoint { + remote-endpoint = <&typea_up_con_ss>; + }; + }; + + port@3 { + reg = <3>; + hub_3_0_ds3: endpoint { + remote-endpoint = <&usb_mux_out>; + }; + }; + + port@4 { + reg = <4>; + /* M.2 */ + }; + }; + }; +}; + +&vop { + status = "okay"; +}; + +&vop_mmu { + status = "okay"; +}; + +&vp0 { + vp0_out_hdmi: endpoint@ROCKCHIP_VOP2_EP_HDMI0 { + reg = ; + remote-endpoint = <&hdmi_in_vp0>; + }; +}; + +&vp1 { + vp1_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp1>; + }; +}; From 28fd0e957b178d6f03522a3637d3026a7fca2912 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 17:38:32 +0400 Subject: [PATCH 182/218] dt-bindings: mfd: ti,bq25703a: Expand to include BQ25792 TI BQ25792 is similar in operation to BQ25703A, but has a different register layout and different current/voltage capabilities. Expand the existing BQ25703A binding to include BQ25792, and move the voltage and current limits into per-variant conditional statements. Reviewed-by: Krzysztof Kozlowski Tested-by: Chris Morgan Signed-off-by: Alexey Charkov --- .../devicetree/bindings/mfd/ti,bq25703a.yaml | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/Documentation/devicetree/bindings/mfd/ti,bq25703a.yaml b/Documentation/devicetree/bindings/mfd/ti,bq25703a.yaml index ba14663c9266a5..cdce83f0580415 100644 --- a/Documentation/devicetree/bindings/mfd/ti,bq25703a.yaml +++ b/Documentation/devicetree/bindings/mfd/ti,bq25703a.yaml @@ -4,17 +4,16 @@ $id: http://devicetree.org/schemas/mfd/ti,bq25703a.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: BQ25703A Charger Manager/Buck/Boost Converter +title: BQ257xx Charger Manager/Buck/Boost Converter maintainers: - Chris Morgan -allOf: - - $ref: /schemas/power/supply/power-supply.yaml# - properties: compatible: - const: ti,bq25703a + enum: + - ti,bq25703a + - ti,bq25792 reg: const: 0x6b @@ -25,7 +24,6 @@ properties: powering the device. minimum: 50000 maximum: 6400000 - default: 3250000 interrupts: maxItems: 1 @@ -57,11 +55,11 @@ properties: minimum: 0 maximum: 6350000 regulator-min-microvolt: - minimum: 4480000 - maximum: 20800000 + minimum: 2800000 + maximum: 22000000 regulator-max-microvolt: - minimum: 4480000 - maximum: 20800000 + minimum: 2800000 + maximum: 22000000 enable-gpios: description: The BQ25703 may require both a register write and a GPIO @@ -74,6 +72,61 @@ properties: - regulator-min-microvolt - regulator-max-microvolt +allOf: + - $ref: /schemas/power/supply/power-supply.yaml# + - if: + properties: + compatible: + const: ti,bq25703a + then: + properties: + input-current-limit-microamp: + minimum: 50000 + maximum: 6400000 + default: 3250000 + regulators: + properties: + vbus: + properties: + regulator-min-microamp: + minimum: 0 + maximum: 6350000 + regulator-max-microamp: + minimum: 0 + maximum: 6350000 + regulator-min-microvolt: + minimum: 4480000 + maximum: 20800000 + regulator-max-microvolt: + minimum: 4480000 + maximum: 20800000 + - if: + properties: + compatible: + const: ti,bq25792 + then: + properties: + input-current-limit-microamp: + minimum: 100000 + maximum: 3300000 + default: 3000000 + regulators: + properties: + vbus: + properties: + regulator-min-microamp: + minimum: 0 + maximum: 3320000 + regulator-max-microamp: + minimum: 0 + maximum: 3320000 + regulator-min-microvolt: + minimum: 2800000 + maximum: 22000000 + regulator-max-microvolt: + minimum: 2800000 + maximum: 22000000 + unevaluatedProperties: false required: From aaf202653d621c8e3f46e9f3f7e32eddecaf26b6 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 17:50:37 +0400 Subject: [PATCH 183/218] regulator: bq257xx: Drop the regulator_dev from the driver data The field was not used anywhere in the driver, so just drop it. This helps further slim down the platform data structure. Acked-by: Mark Brown Tested-by: Chris Morgan Signed-off-by: Alexey Charkov --- drivers/regulator/bq257xx-regulator.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/regulator/bq257xx-regulator.c b/drivers/regulator/bq257xx-regulator.c index 09c466052c0487..913ca9186bf1e1 100644 --- a/drivers/regulator/bq257xx-regulator.c +++ b/drivers/regulator/bq257xx-regulator.c @@ -15,7 +15,6 @@ #include struct bq257xx_reg_data { - struct regulator_dev *bq257xx_reg; struct gpio_desc *otg_en_gpio; struct regulator_desc desc; }; @@ -145,6 +144,7 @@ static int bq257xx_regulator_probe(struct platform_device *pdev) struct bq257xx_reg_data *pdata; struct device_node *np = dev->of_node; struct regulator_config cfg = {}; + struct regulator_dev *rdev; device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent); @@ -164,9 +164,9 @@ static int bq257xx_regulator_probe(struct platform_device *pdev) if (!cfg.regmap) return -ENODEV; - pdata->bq257xx_reg = devm_regulator_register(dev, &pdata->desc, &cfg); - if (IS_ERR(pdata->bq257xx_reg)) { - return dev_err_probe(&pdev->dev, PTR_ERR(pdata->bq257xx_reg), + rdev = devm_regulator_register(dev, &pdata->desc, &cfg); + if (IS_ERR(rdev)) { + return dev_err_probe(&pdev->dev, PTR_ERR(rdev), "error registering bq257xx regulator"); } From 7f016849146c9dcf95249c78127161e01665f98e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 14:56:35 +0400 Subject: [PATCH 184/218] power: supply: bq257xx: Fix VSYSMIN clamping logic The minimal system voltage (VSYSMIN) is meant to protect the battery from dangerous over-discharge. When the device tree provides a value for the minimum design voltage of the battery, the user should not be allowed to set a lower VSYSMIN, as that would defeat the purpose of this protection. Flip the clamping logic when setting VSYSMIN to ensure that battery design voltage is respected. Cc: stable@vger.kernel.org Fixes: 1cc017b7f9c7 ("power: supply: bq257xx: Add support for BQ257XX charger") Tested-by: Chris Morgan Reviewed-by: Sebastian Reichel Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 02c7d8b61e82b6..7ca4ae6109027a 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -128,9 +128,8 @@ static int bq25703_get_min_vsys(struct bq257xx_chg *pdata, int *intval) * @vsys: voltage value to set in uV. * * This function takes a requested minimum system voltage value, clamps - * it between the minimum supported value by the charger and a user - * defined minimum system value, and then writes the value to the - * appropriate register. + * it between the user defined minimum system value and the maximum supported + * value by the charger, and then writes the value to the appropriate register. * * Return: Returns 0 on success or error if an error occurs. */ @@ -139,7 +138,7 @@ static int bq25703_set_min_vsys(struct bq257xx_chg *pdata, int vsys) unsigned int reg; int vsys_min = pdata->vsys_min; - vsys = clamp(vsys, BQ25703_MINVSYS_MIN_UV, vsys_min); + vsys = clamp(vsys, vsys_min, BQ25703_MINVSYS_MAX_UV); reg = ((vsys - BQ25703_MINVSYS_MIN_UV) / BQ25703_MINVSYS_STEP_UV); reg = FIELD_PREP(BQ25703_MINVSYS_MASK, reg); From faaad0c3195c773b864f0220659fcecd09c1dfa2 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:24:02 +0400 Subject: [PATCH 185/218] power: supply: bq257xx: Make the default current limit a per-chip attribute Add a field for the default current limit to the bq257xx_info structure and use it instead of the hardcoded value in the probe function. This prepares the driver for allowing different electrical constraints for different chip variants. Tested-by: Chris Morgan Reviewed-by: Sebastian Reichel Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 7ca4ae6109027a..39718223c3f9ce 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -18,6 +18,7 @@ struct bq257xx_chg; /** * struct bq257xx_chip_info - chip specific routines + * @default_iindpm_uA: default input current limit in microamps * @bq257xx_hw_init: init function for hw * @bq257xx_hw_shutdown: shutdown function for hw * @bq257xx_get_state: get and update state of hardware @@ -26,6 +27,7 @@ struct bq257xx_chg; * @bq257xx_set_iindpm: set maximum input current (in uA) */ struct bq257xx_chip_info { + int default_iindpm_uA; int (*bq257xx_hw_init)(struct bq257xx_chg *pdata); void (*bq257xx_hw_shutdown)(struct bq257xx_chg *pdata); int (*bq257xx_get_state)(struct bq257xx_chg *pdata); @@ -627,6 +629,7 @@ static const struct power_supply_desc bq257xx_power_supply_desc = { }; static const struct bq257xx_chip_info bq25703_chip_info = { + .default_iindpm_uA = BQ25703_IINDPM_DEFAULT_UA, .bq257xx_hw_init = &bq25703_hw_init, .bq257xx_hw_shutdown = &bq25703_hw_shutdown, .bq257xx_get_state = &bq25703_get_state, @@ -675,7 +678,7 @@ static int bq257xx_parse_dt(struct bq257xx_chg *pdata, "input-current-limit-microamp", &pdata->iindpm_max); if (ret) - pdata->iindpm_max = BQ25703_IINDPM_DEFAULT_UA; + pdata->iindpm_max = pdata->chip->default_iindpm_uA; return 0; } From 26d6b056b79bec49dbe4408a7312918305c47204 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:30:25 +0400 Subject: [PATCH 186/218] power: supply: bq257xx: Consistently use indirect get/set helpers Move the remaining get/set helper functions to indirect calls via the per-chip bq257xx_chip_info struct. This improves the consistency of the code and prepares the driver to support multiple chip variants with different register layouts and bit definitions. Tested-by: Chris Morgan Reviewed-by: Sebastian Reichel Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 39718223c3f9ce..0765673728e436 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -22,18 +22,30 @@ struct bq257xx_chg; * @bq257xx_hw_init: init function for hw * @bq257xx_hw_shutdown: shutdown function for hw * @bq257xx_get_state: get and update state of hardware + * @bq257xx_get_ichg: get maximum charge current (in uA) * @bq257xx_set_ichg: set maximum charge current (in uA) + * @bq257xx_get_vbatreg: get maximum charge voltage (in uV) * @bq257xx_set_vbatreg: set maximum charge voltage (in uV) + * @bq257xx_get_iindpm: get maximum input current (in uA) * @bq257xx_set_iindpm: set maximum input current (in uA) + * @bq257xx_get_cur: get battery current from ADC (in uA) + * @bq257xx_get_vbat: get battery voltage from ADC (in uV) + * @bq257xx_get_min_vsys: get minimum system voltage (in uV) */ struct bq257xx_chip_info { int default_iindpm_uA; int (*bq257xx_hw_init)(struct bq257xx_chg *pdata); void (*bq257xx_hw_shutdown)(struct bq257xx_chg *pdata); int (*bq257xx_get_state)(struct bq257xx_chg *pdata); + int (*bq257xx_get_ichg)(struct bq257xx_chg *pdata, int *intval); int (*bq257xx_set_ichg)(struct bq257xx_chg *pdata, int ichg); + int (*bq257xx_get_vbatreg)(struct bq257xx_chg *pdata, int *intval); int (*bq257xx_set_vbatreg)(struct bq257xx_chg *pdata, int vbatreg); + int (*bq257xx_get_iindpm)(struct bq257xx_chg *pdata, int *intval); int (*bq257xx_set_iindpm)(struct bq257xx_chg *pdata, int iindpm); + int (*bq257xx_get_cur)(struct bq257xx_chg *pdata, int *intval); + int (*bq257xx_get_vbat)(struct bq257xx_chg *pdata, int *intval); + int (*bq257xx_get_min_vsys)(struct bq257xx_chg *pdata, int *intval); }; /** @@ -490,22 +502,22 @@ static int bq257xx_get_charger_property(struct power_supply *psy, break; case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: - return bq25703_get_iindpm(pdata, &val->intval); + return pdata->chip->bq257xx_get_iindpm(pdata, &val->intval); case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: - return bq25703_get_chrg_volt(pdata, &val->intval); + return pdata->chip->bq257xx_get_vbatreg(pdata, &val->intval); case POWER_SUPPLY_PROP_CURRENT_NOW: - return bq25703_get_cur(pdata, &val->intval); + return pdata->chip->bq257xx_get_cur(pdata, &val->intval); case POWER_SUPPLY_PROP_VOLTAGE_NOW: - return bq25703_get_vbat(pdata, &val->intval); + return pdata->chip->bq257xx_get_vbat(pdata, &val->intval); case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: - return bq25703_get_ichg_cur(pdata, &val->intval); + return pdata->chip->bq257xx_get_ichg(pdata, &val->intval); case POWER_SUPPLY_PROP_VOLTAGE_MIN: - return bq25703_get_min_vsys(pdata, &val->intval); + return pdata->chip->bq257xx_get_min_vsys(pdata, &val->intval); case POWER_SUPPLY_PROP_USB_TYPE: val->intval = pdata->usb_type; @@ -633,9 +645,15 @@ static const struct bq257xx_chip_info bq25703_chip_info = { .bq257xx_hw_init = &bq25703_hw_init, .bq257xx_hw_shutdown = &bq25703_hw_shutdown, .bq257xx_get_state = &bq25703_get_state, + .bq257xx_get_ichg = &bq25703_get_ichg_cur, .bq257xx_set_ichg = &bq25703_set_ichg_cur, + .bq257xx_get_vbatreg = &bq25703_get_chrg_volt, .bq257xx_set_vbatreg = &bq25703_set_chrg_volt, + .bq257xx_get_iindpm = &bq25703_get_iindpm, .bq257xx_set_iindpm = &bq25703_set_iindpm, + .bq257xx_get_cur = &bq25703_get_cur, + .bq257xx_get_vbat = &bq25703_get_vbat, + .bq257xx_get_min_vsys = &bq25703_get_min_vsys, }; /** From 8f237d1ae9185d74a3368cfd2ccc94b3aea580d5 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:37:06 +0400 Subject: [PATCH 187/218] power: supply: bq257xx: Add fields for 'charging' and 'overvoltage' states The driver currently reports the 'charging' and 'overvoltage' states based on a logical expression in the get_charger_property() wrapper function. This doesn't scale well to other chip variants, which may have a different number and type of hardware reported conditions which fall into these broad power supply states. Move the logic for determining 'charging' and 'overvoltage' states into chip-specific accessors, which can be overridden by each variant as needed. This helps keep the get_charger_property() wrapper function chip-agnostic while allowing for new chip variants to be added bringing their own logic. Tested-by: Chris Morgan Reviewed-by: Sebastian Reichel Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 0765673728e436..9c082865e745bb 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -54,8 +54,10 @@ struct bq257xx_chip_info { * @bq: parent MFD device * @charger: power supply device * @online: charger input is present + * @charging: charger is actively charging the battery * @fast_charge: charger is in fast charge mode * @pre_charge: charger is in pre-charge mode + * @overvoltage: overvoltage fault detected * @ov_fault: charger reports over voltage fault * @batoc_fault: charger reports battery over current fault * @oc_fault: charger reports over current fault @@ -71,8 +73,10 @@ struct bq257xx_chg { struct bq257xx_device *bq; struct power_supply *charger; bool online; + bool charging; bool fast_charge; bool pre_charge; + bool overvoltage; bool ov_fault; bool batoc_fault; bool oc_fault; @@ -106,8 +110,10 @@ static int bq25703_get_state(struct bq257xx_chg *pdata) pdata->online = reg & BQ25703_STS_AC_STAT; pdata->fast_charge = reg & BQ25703_STS_IN_FCHRG; pdata->pre_charge = reg & BQ25703_STS_IN_PCHRG; + pdata->charging = pdata->fast_charge || pdata->pre_charge; pdata->ov_fault = reg & BQ25703_STS_FAULT_ACOV; pdata->batoc_fault = reg & BQ25703_STS_FAULT_BATOC; + pdata->overvoltage = pdata->ov_fault || pdata->batoc_fault; pdata->oc_fault = reg & BQ25703_STS_FAULT_ACOC; return 0; @@ -478,14 +484,14 @@ static int bq257xx_get_charger_property(struct power_supply *psy, case POWER_SUPPLY_PROP_STATUS: if (!pdata->online) val->intval = POWER_SUPPLY_STATUS_DISCHARGING; - else if (pdata->fast_charge || pdata->pre_charge) + else if (pdata->charging) val->intval = POWER_SUPPLY_STATUS_CHARGING; else val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; break; case POWER_SUPPLY_PROP_HEALTH: - if (pdata->ov_fault || pdata->batoc_fault) + if (pdata->overvoltage) val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; else if (pdata->oc_fault) val->intval = POWER_SUPPLY_HEALTH_OVERCURRENT; From f3f58fa9ffe0a28cfe190ce331df949a5c0a87f3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 23:56:44 +0400 Subject: [PATCH 188/218] mfd: bq257xx: Add BQ25792 support Add register definitions and a new 'type' enum to be passed via MFD private data to support the BQ25792, which is a newer variant of the BQ257xx family. BQ25792 shares similar logic of operation with the already supported BQ25703A but has a completely different register map and different electrical constraints. Tested-by: Chris Morgan Signed-off-by: Alexey Charkov --- drivers/mfd/bq257xx.c | 54 ++++- include/linux/mfd/bq257xx.h | 414 ++++++++++++++++++++++++++++++++++++ 2 files changed, 465 insertions(+), 3 deletions(-) diff --git a/drivers/mfd/bq257xx.c b/drivers/mfd/bq257xx.c index e9d49dac0a1670..054342c60b7318 100644 --- a/drivers/mfd/bq257xx.c +++ b/drivers/mfd/bq257xx.c @@ -39,6 +39,39 @@ static const struct regmap_config bq25703_regmap_config = { .val_format_endian = REGMAP_ENDIAN_LITTLE, }; +static const struct regmap_range bq25792_writeable_reg_ranges[] = { + regmap_reg_range(BQ25792_REG00_MIN_SYS_VOLTAGE, + BQ25792_REG18_NTC_CONTROL_1), + regmap_reg_range(BQ25792_REG28_CHARGER_MASK_0, + BQ25792_REG30_ADC_FUNCTION_DISABLE_1), +}; + +static const struct regmap_access_table bq25792_writeable_regs = { + .yes_ranges = bq25792_writeable_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(bq25792_writeable_reg_ranges), +}; + +static const struct regmap_range bq25792_volatile_reg_ranges[] = { + regmap_reg_range(BQ25792_REG19_ICO_CURRENT_LIMIT, + BQ25792_REG27_FAULT_FLAG_1), + regmap_reg_range(BQ25792_REG31_IBUS_ADC, + BQ25792_REG47_DPDM_DRIVER), +}; + +static const struct regmap_access_table bq25792_volatile_regs = { + .yes_ranges = bq25792_volatile_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(bq25792_volatile_reg_ranges), +}; + +static const struct regmap_config bq25792_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = BQ25792_REG48_PART_INFORMATION, + .cache_type = REGCACHE_MAPLE, + .wr_table = &bq25792_writeable_regs, + .volatile_table = &bq25792_volatile_regs, +}; + static const struct mfd_cell cells[] = { MFD_CELL_NAME("bq257xx-regulator"), MFD_CELL_NAME("bq257xx-charger"), @@ -46,6 +79,7 @@ static const struct mfd_cell cells[] = { static int bq257xx_probe(struct i2c_client *client) { + const struct regmap_config *rcfg; struct bq257xx_device *ddata; int ret; @@ -53,9 +87,21 @@ static int bq257xx_probe(struct i2c_client *client) if (!ddata) return -ENOMEM; + ddata->type = (uintptr_t)i2c_get_match_data(client); ddata->client = client; - ddata->regmap = devm_regmap_init_i2c(client, &bq25703_regmap_config); + switch (ddata->type) { + case BQ25703A: + rcfg = &bq25703_regmap_config; + break; + case BQ25792: + rcfg = &bq25792_regmap_config; + break; + default: + return dev_err_probe(&client->dev, -ENODEV, "Unsupported device type\n"); + } + + ddata->regmap = devm_regmap_init_i2c(client, rcfg); if (IS_ERR(ddata->regmap)) { return dev_err_probe(&client->dev, PTR_ERR(ddata->regmap), "Failed to allocate register map\n"); @@ -73,13 +119,15 @@ static int bq257xx_probe(struct i2c_client *client) } static const struct i2c_device_id bq257xx_i2c_ids[] = { - { "bq25703a" }, + { "bq25703a", BQ25703A }, + { "bq25792", BQ25792 }, {} }; MODULE_DEVICE_TABLE(i2c, bq257xx_i2c_ids); static const struct of_device_id bq257xx_of_match[] = { - { .compatible = "ti,bq25703a" }, + { .compatible = "ti,bq25703a", .data = (void *)BQ25703A }, + { .compatible = "ti,bq25792", .data = (void *)BQ25792 }, {} }; MODULE_DEVICE_TABLE(of, bq257xx_of_match); diff --git a/include/linux/mfd/bq257xx.h b/include/linux/mfd/bq257xx.h index 1d6ddc7fb09fcb..9b1752b0a18b13 100644 --- a/include/linux/mfd/bq257xx.h +++ b/include/linux/mfd/bq257xx.h @@ -98,7 +98,421 @@ #define BQ25703_EN_OTG_MASK BIT(12) +#define BQ25792_REG00_MIN_SYS_VOLTAGE 0x00 +#define BQ25792_REG01_CHARGE_VOLTAGE_LIMIT 0x01 +#define BQ25792_REG03_CHARGE_CURRENT_LIMIT 0x03 +#define BQ25792_REG05_INPUT_VOLTAGE_LIMIT 0x05 +#define BQ25792_REG06_INPUT_CURRENT_LIMIT 0x06 +#define BQ25792_REG08_PRECHARGE_CONTROL 0x08 +#define BQ25792_REG09_TERMINATION_CONTROL 0x09 +#define BQ25792_REG0A_RECHARGE_CONTROL 0x0a +#define BQ25792_REG0B_VOTG_REGULATION 0x0b +#define BQ25792_REG0D_IOTG_REGULATION 0x0d +#define BQ25792_REG0E_TIMER_CONTROL 0x0e +#define BQ25792_REG0F_CHARGER_CONTROL_0 0x0f +#define BQ25792_REG10_CHARGER_CONTROL_1 0x10 +#define BQ25792_REG11_CHARGER_CONTROL_2 0x11 +#define BQ25792_REG12_CHARGER_CONTROL_3 0x12 +#define BQ25792_REG13_CHARGER_CONTROL_4 0x13 +#define BQ25792_REG14_CHARGER_CONTROL_5 0x14 +/* REG15 reserved */ +#define BQ25792_REG16_TEMPERATURE_CONTROL 0x16 +#define BQ25792_REG17_NTC_CONTROL_0 0x17 +#define BQ25792_REG18_NTC_CONTROL_1 0x18 +#define BQ25792_REG19_ICO_CURRENT_LIMIT 0x19 +#define BQ25792_REG1B_CHARGER_STATUS_0 0x1b +#define BQ25792_REG1C_CHARGER_STATUS_1 0x1c +#define BQ25792_REG1D_CHARGER_STATUS_2 0x1d +#define BQ25792_REG1E_CHARGER_STATUS_3 0x1e +#define BQ25792_REG1F_CHARGER_STATUS_4 0x1f +#define BQ25792_REG20_FAULT_STATUS_0 0x20 +#define BQ25792_REG21_FAULT_STATUS_1 0x21 +#define BQ25792_REG22_CHARGER_FLAG_0 0x22 +#define BQ25792_REG23_CHARGER_FLAG_1 0x23 +#define BQ25792_REG24_CHARGER_FLAG_2 0x24 +#define BQ25792_REG25_CHARGER_FLAG_3 0x25 +#define BQ25792_REG26_FAULT_FLAG_0 0x26 +#define BQ25792_REG27_FAULT_FLAG_1 0x27 +#define BQ25792_REG28_CHARGER_MASK_0 0x28 +#define BQ25792_REG29_CHARGER_MASK_1 0x29 +#define BQ25792_REG2A_CHARGER_MASK_2 0x2a +#define BQ25792_REG2B_CHARGER_MASK_3 0x2b +#define BQ25792_REG2C_FAULT_MASK_0 0x2c +#define BQ25792_REG2D_FAULT_MASK_1 0x2d +#define BQ25792_REG2E_ADC_CONTROL 0x2e +#define BQ25792_REG2F_ADC_FUNCTION_DISABLE_0 0x2f +#define BQ25792_REG30_ADC_FUNCTION_DISABLE_1 0x30 +#define BQ25792_REG31_IBUS_ADC 0x31 +#define BQ25792_REG33_IBAT_ADC 0x33 +#define BQ25792_REG35_VBUS_ADC 0x35 +#define BQ25792_REG37_VAC1_ADC 0x37 +#define BQ25792_REG39_VAC2_ADC 0x39 +#define BQ25792_REG3B_VBAT_ADC 0x3b +#define BQ25792_REG3D_VSYS_ADC 0x3d +#define BQ25792_REG3F_TS_ADC 0x3f +#define BQ25792_REG41_TDIE_ADC 0x41 +#define BQ25792_REG43_DP_ADC 0x43 +#define BQ25792_REG45_DM_ADC 0x45 +#define BQ25792_REG47_DPDM_DRIVER 0x47 +#define BQ25792_REG48_PART_INFORMATION 0x48 + +/* Minimal System Voltage */ +#define BQ25792_REG00_VSYSMIN_MASK GENMASK(5, 0) + +#define BQ25792_MINVSYS_MIN_UV 2500000 +#define BQ25792_MINVSYS_STEP_UV 250000 +#define BQ25792_MINVSYS_MAX_UV 16000000 + +/* Charge Voltage Limit */ +#define BQ25792_REG01_VREG_MASK GENMASK(10, 0) + +#define BQ25792_VBATREG_MIN_UV 3000000 +#define BQ25792_VBATREG_STEP_UV 10000 +#define BQ25792_VBATREG_MAX_UV 18800000 + +/* Charge Current Limit */ +#define BQ25792_REG03_ICHG_MASK GENMASK(8, 0) + +#define BQ25792_ICHG_MIN_UA 50000 +#define BQ25792_ICHG_STEP_UA 10000 +#define BQ25792_ICHG_MAX_UA 5000000 + +/* Input Voltage Limit */ +#define BQ25792_REG05_VINDPM_MASK GENMASK(7, 0) + +/* Input Current Limit */ +#define BQ25792_REG06_IINDPM_MASK GENMASK(8, 0) +#define BQ25792_IINDPM_DEFAULT_UA 3000000 +#define BQ25792_IINDPM_STEP_UA 10000 +#define BQ25792_IINDPM_MIN_UA 100000 +#define BQ25792_IINDPM_MAX_UA 3300000 + +/* Precharge Control */ +#define BQ25792_REG08_VBAT_LOWV_MASK GENMASK(7, 6) +#define BQ25792_REG08_IPRECHG_MASK GENMASK(5, 0) + +/* Termination Control */ +#define BQ25792_REG09_REG_RST BIT(6) +#define BQ25792_REG09_ITERM_MASK GENMASK(4, 0) + +/* Re-charge Control */ +#define BQ25792_REG0A_CELL_MASK GENMASK(7, 6) +#define BQ25792_REG0A_TRECHG_MASK GENMASK(5, 4) +#define BQ25792_REG0A_VRECHG_MASK GENMASK(3, 0) + +/* VOTG regulation */ +#define BQ25792_REG0B_VOTG_MASK GENMASK(10, 0) + +#define BQ25792_OTG_VOLT_MIN_UV 2800000 +#define BQ25792_OTG_VOLT_STEP_UV 10000 +#define BQ25792_OTG_VOLT_MAX_UV 22000000 +#define BQ25792_OTG_VOLT_NUM_VOLT ((BQ25792_OTG_VOLT_MAX_UV \ + - BQ25792_OTG_VOLT_MIN_UV) \ + / BQ25792_OTG_VOLT_STEP_UV + 1) + +/* IOTG regulation */ +#define BQ25792_REG0D_PRECHG_TMR BIT(7) +#define BQ25792_REG0D_IOTG_MASK GENMASK(6, 0) + +#define BQ25792_OTG_CUR_MIN_UA 120000 +#define BQ25792_OTG_CUR_STEP_UA 40000 +#define BQ25792_OTG_CUR_MAX_UA 3320000 + +/* Timer Control */ +#define BQ25792_REG0E_TOPOFF_TMR_MASK GENMASK(7, 6) +#define BQ25792_REG0E_EN_TRICHG_TMR BIT(5) +#define BQ25792_REG0E_EN_PRECHG_TMR BIT(4) +#define BQ25792_REG0E_EN_CHG_TMR BIT(3) +#define BQ25792_REG0E_CHG_TMR_MASK GENMASK(2, 1) +#define BQ25792_REG0E_TMR2X_EN BIT(0) + +/* Charger Control 0 */ +#define BQ25792_REG0F_EN_AUTO_IBATDIS BIT(7) +#define BQ25792_REG0F_FORCE_IBATDIS BIT(6) +#define BQ25792_REG0F_EN_CHG BIT(5) +#define BQ25792_REG0F_EN_ICO BIT(4) +#define BQ25792_REG0F_FORCE_ICO BIT(3) +#define BQ25792_REG0F_EN_HIZ BIT(2) +#define BQ25792_REG0F_EN_TERM BIT(1) +/* bit0 reserved */ + +/* Charger Control 1 */ +#define BQ25792_REG10_VAC_OVP_MASK GENMASK(5, 4) +#define BQ25792_REG10_WD_RST BIT(3) +#define BQ25792_REG10_WATCHDOG_MASK GENMASK(2, 0) + +/* Charger Control 2 */ +#define BQ25792_REG11_FORCE_INDET BIT(7) +#define BQ25792_REG11_AUTO_INDET_EN BIT(6) +#define BQ25792_REG11_EN_12V BIT(5) +#define BQ25792_REG11_EN_9V BIT(4) +#define BQ25792_REG11_HVDCP_EN BIT(3) +#define BQ25792_REG11_SDRV_CTRL_MASK GENMASK(2, 1) +#define BQ25792_REG11_SDRV_DLY BIT(0) + +/* Charger Control 3 */ +#define BQ25792_REG12_DIS_ACDRV BIT(7) +#define BQ25792_REG12_EN_OTG BIT(6) +#define BQ25792_REG12_PFM_OTG_DIS BIT(5) +#define BQ25792_REG12_PFM_FWD_DIS BIT(4) +#define BQ25792_REG12_WKUP_DLY BIT(3) +#define BQ25792_REG12_DIS_LDO BIT(2) +#define BQ25792_REG12_DIS_OTG_OOA BIT(1) +#define BQ25792_REG12_DIS_FWD_OOA BIT(0) + +/* Charger Control 4 */ +#define BQ25792_REG13_EN_ACDRV2 BIT(7) +#define BQ25792_REG13_EN_ACDRV1 BIT(6) +#define BQ25792_REG13_PWM_FREQ BIT(5) +#define BQ25792_REG13_DIS_STAT BIT(4) +#define BQ25792_REG13_DIS_VSYS_SHORT BIT(3) +#define BQ25792_REG13_DIS_VOTG_UVP BIT(2) +#define BQ25792_REG13_FORCE_VINDPM_DET BIT(1) +#define BQ25792_REG13_EN_IBUS_OCP BIT(0) + +/* Charger Control 5 */ +#define BQ25792_REG14_SFET_PRESENT BIT(7) +/* bit6 reserved */ +#define BQ25792_REG14_EN_IBAT BIT(5) +#define BQ25792_REG14_IBAT_REG_MASK GENMASK(4, 3) +#define BQ25792_REG14_EN_IINDPM BIT(2) +#define BQ25792_REG14_EN_EXTILIM BIT(1) +#define BQ25792_REG14_EN_BATOC BIT(0) + +#define BQ25792_IBAT_3A FIELD_PREP(BQ25792_REG14_IBAT_REG_MASK, 0) +#define BQ25792_IBAT_4A FIELD_PREP(BQ25792_REG14_IBAT_REG_MASK, 1) +#define BQ25792_IBAT_5A FIELD_PREP(BQ25792_REG14_IBAT_REG_MASK, 2) +#define BQ25792_IBAT_UNLIM FIELD_PREP(BQ25792_REG14_IBAT_REG_MASK, 3) + +/* Temperature Control */ +#define BQ25792_REG16_TREG_MASK GENMASK(7, 6) +#define BQ25792_REG16_TSHUT_MASK GENMASK(5, 4) +#define BQ25792_REG16_VBUS_PD_EN BIT(3) +#define BQ25792_REG16_VAC1_PD_EN BIT(2) +#define BQ25792_REG16_VAC2_PD_EN BIT(1) + +/* NTC Control 0 */ +#define BQ25792_REG17_JEITA_VSET_MASK GENMASK(7, 5) +#define BQ25792_REG17_JEITA_ISETH_MASK GENMASK(4, 3) +#define BQ25792_REG17_JEITA_ISETC_MASK GENMASK(2, 1) + +/* NTC Control 1 */ +#define BQ25792_REG18_TS_COOL_MASK GENMASK(7, 6) +#define BQ25792_REG18_TS_WARM_MASK GENMASK(5, 4) +#define BQ25792_REG18_BHOT_MASK GENMASK(3, 2) +#define BQ25792_REG18_BCOLD BIT(1) +#define BQ25792_REG18_TS_IGNORE BIT(0) + +/* ICO Current Limit */ +#define BQ25792_REG19_ICO_ILIM_MASK GENMASK(8, 0) + +/* Charger Status 0 */ +#define BQ25792_REG1B_IINDPM_STAT BIT(7) +#define BQ25792_REG1B_VINDPM_STAT BIT(6) +#define BQ25792_REG1B_WD_STAT BIT(5) +#define BQ25792_REG1B_POORSRC_STAT BIT(4) +#define BQ25792_REG1B_PG_STAT BIT(3) +#define BQ25792_REG1B_AC2_PRESENT_STAT BIT(2) +#define BQ25792_REG1B_AC1_PRESENT_STAT BIT(1) +#define BQ25792_REG1B_VBUS_PRESENT_STAT BIT(0) + +/* Charger Status 1 */ +#define BQ25792_REG1C_CHG_STAT_MASK GENMASK(7, 5) +#define BQ25792_REG1C_VBUS_STAT_MASK GENMASK(4, 1) +#define BQ25792_REG1C_BC12_DONE_STAT BIT(0) + +/* Charger Status 2 */ +#define BQ25792_REG1D_ICO_STAT_MASK GENMASK(7, 6) +#define BQ25792_REG1D_TREG_STAT BIT(2) +#define BQ25792_REG1D_DPDM_STAT BIT(1) +#define BQ25792_REG1D_VBAT_PRESENT_STAT BIT(0) + +/* Charger Status 3 */ +#define BQ25792_REG1E_ACRB2_STAT BIT(7) +#define BQ25792_REG1E_ACRB1_STAT BIT(6) +#define BQ25792_REG1E_ADC_DONE_STAT BIT(5) +#define BQ25792_REG1E_VSYS_STAT BIT(4) +#define BQ25792_REG1E_CHG_TMR_STAT BIT(3) +#define BQ25792_REG1E_TRICHG_TMR_STAT BIT(2) +#define BQ25792_REG1E_PRECHG_TMR_STAT BIT(1) + +/* Charger Status 4 */ +#define BQ25792_REG1F_VBATOTG_LOW_STAT BIT(4) +#define BQ25792_REG1F_TS_COLD_STAT BIT(3) +#define BQ25792_REG1F_TS_COOL_STAT BIT(2) +#define BQ25792_REG1F_TS_WARM_STAT BIT(1) +#define BQ25792_REG1F_TS_HOT_STAT BIT(0) + +/* FAULT Status 0 */ +#define BQ25792_REG20_IBAT_REG_STAT BIT(7) +#define BQ25792_REG20_VBUS_OVP_STAT BIT(6) +#define BQ25792_REG20_VBAT_OVP_STAT BIT(5) +#define BQ25792_REG20_IBUS_OCP_STAT BIT(4) +#define BQ25792_REG20_IBAT_OCP_STAT BIT(3) +#define BQ25792_REG20_CONV_OCP_STAT BIT(2) +#define BQ25792_REG20_VAC2_OVP_STAT BIT(1) +#define BQ25792_REG20_VAC1_OVP_STAT BIT(0) + +#define BQ25792_REG20_OVERVOLTAGE_MASK (BQ25792_REG20_VBUS_OVP_STAT | \ + BQ25792_REG20_VBAT_OVP_STAT | \ + BQ25792_REG20_VAC2_OVP_STAT | \ + BQ25792_REG20_VAC1_OVP_STAT) +#define BQ25792_REG20_OVERCURRENT_MASK (BQ25792_REG20_IBUS_OCP_STAT | \ + BQ25792_REG20_IBAT_OCP_STAT | \ + BQ25792_REG20_CONV_OCP_STAT) + +/* FAULT Status 1 */ +#define BQ25792_REG21_VSYS_SHORT_STAT BIT(7) +#define BQ25792_REG21_VSYS_OVP_STAT BIT(6) +#define BQ25792_REG21_OTG_OVP_STAT BIT(5) +#define BQ25792_REG21_OTG_UVP_STAT BIT(4) +#define BQ25792_REG21_TSHUT_STAT BIT(2) + + +/* Charger Flag 0 */ +#define BQ25792_REG22_IINDPM_FLAG BIT(7) +#define BQ25792_REG22_VINDPM_FLAG BIT(6) +#define BQ25792_REG22_WD_FLAG BIT(5) +#define BQ25792_REG22_POORSRC_FLAG BIT(4) +#define BQ25792_REG22_PG_FLAG BIT(3) +#define BQ25792_REG22_AC2_PRESENT_FLAG BIT(2) +#define BQ25792_REG22_AC1_PRESENT_FLAG BIT(1) +#define BQ25792_REG22_VBUS_PRESENT_FLAG BIT(0) + +/* Charger Flag 1 */ +#define BQ25792_REG23_CHG_FLAG BIT(7) +#define BQ25792_REG23_ICO_FLAG BIT(6) +#define BQ25792_REG23_VBUS_FLAG BIT(4) +#define BQ25792_REG23_TREG_FLAG BIT(2) +#define BQ25792_REG23_VBAT_PRESENT_FLAG BIT(1) +#define BQ25792_REG23_BC12_DONE_FLAG BIT(0) + +/* Charger Flag 2 */ +#define BQ25792_REG24_DPDM_DONE_FLAG BIT(6) +#define BQ25792_REG24_ADC_DONE_FLAG BIT(5) +#define BQ25792_REG24_VSYS_FLAG BIT(4) +#define BQ25792_REG24_CHG_TMR_FLAG BIT(3) +#define BQ25792_REG24_TRICHG_TMR_FLAG BIT(2) +#define BQ25792_REG24_PRECHG_TMR_FLAG BIT(1) +#define BQ25792_REG24_TOPOFF_TMR_FLAG BIT(0) + +/* Charger Flag 3 */ +#define BQ25792_REG25_VBATOTG_LOW_FLAG BIT(4) +#define BQ25792_REG25_TS_COLD_FLAG BIT(3) +#define BQ25792_REG25_TS_COOL_FLAG BIT(2) +#define BQ25792_REG25_TS_WARM_FLAG BIT(1) +#define BQ25792_REG25_TS_HOT_FLAG BIT(0) + +/* FAULT Flag 0 */ +#define BQ25792_REG26_IBAT_REG_FLAG BIT(7) +#define BQ25792_REG26_VBUS_OVP_FLAG BIT(6) +#define BQ25792_REG26_VBAT_OVP_FLAG BIT(5) +#define BQ25792_REG26_IBUS_OCP_FLAG BIT(4) +#define BQ25792_REG26_IBAT_OCP_FLAG BIT(3) +#define BQ25792_REG26_CONV_OCP_FLAG BIT(2) +#define BQ25792_REG26_VAC2_OVP_FLAG BIT(1) +#define BQ25792_REG26_VAC1_OVP_FLAG BIT(0) + +/* FAULT Flag 1 */ +#define BQ25792_REG27_VSYS_SHORT_FLAG BIT(7) +#define BQ25792_REG27_VSYS_OVP_FLAG BIT(6) +#define BQ25792_REG27_OTG_OVP_FLAG BIT(5) +#define BQ25792_REG27_OTG_UVP_FLAG BIT(4) +#define BQ25792_REG27_TSHUT_FLAG BIT(2) + +/* Charger Mask 0 */ +#define BQ25792_REG28_IINDPM_MASK BIT(7) +#define BQ25792_REG28_VINDPM_MASK BIT(6) +#define BQ25792_REG28_WD_MASK BIT(5) +#define BQ25792_REG28_POORSRC_MASK BIT(4) +#define BQ25792_REG28_PG_MASK BIT(3) +#define BQ25792_REG28_AC2_PRESENT_MASK BIT(2) +#define BQ25792_REG28_AC1_PRESENT_MASK BIT(1) +#define BQ25792_REG28_VBUS_PRESENT_MASK BIT(0) + +/* Charger Mask 1 */ +#define BQ25792_REG29_CHG_MASK BIT(7) +#define BQ25792_REG29_ICO_MASK BIT(6) +#define BQ25792_REG29_VBUS_MASK BIT(4) +#define BQ25792_REG29_TREG_MASK BIT(2) +#define BQ25792_REG29_VBAT_PRESENT_MASK BIT(1) +#define BQ25792_REG29_BC12_DONE_MASK BIT(0) + +/* Charger Mask 2 */ +#define BQ25792_REG2A_DPDM_DONE_MASK BIT(6) +#define BQ25792_REG2A_ADC_DONE_MASK BIT(5) +#define BQ25792_REG2A_VSYS_MASK BIT(4) +#define BQ25792_REG2A_CHG_TMR_MASK BIT(3) +#define BQ25792_REG2A_TRICHG_TMR_MASK BIT(2) +#define BQ25792_REG2A_PRECHG_TMR_MASK BIT(1) +#define BQ25792_REG2A_TOPOFF_TMR_MASK BIT(0) + +/* Charger Mask 3 */ +#define BQ25792_REG2B_VBATOTG_LOW_MASK BIT(4) +#define BQ25792_REG2B_TS_COLD_MASK BIT(3) +#define BQ25792_REG2B_TS_COOL_MASK BIT(2) +#define BQ25792_REG2B_TS_WARM_MASK BIT(1) +#define BQ25792_REG2B_TS_HOT_MASK BIT(0) + +/* FAULT Mask 0 */ +#define BQ25792_REG2C_IBAT_REG_MASK BIT(7) +#define BQ25792_REG2C_VBUS_OVP_MASK BIT(6) +#define BQ25792_REG2C_VBAT_OVP_MASK BIT(5) +#define BQ25792_REG2C_IBUS_OCP_MASK BIT(4) +#define BQ25792_REG2C_IBAT_OCP_MASK BIT(3) +#define BQ25792_REG2C_CONV_OCP_MASK BIT(2) +#define BQ25792_REG2C_VAC2_OVP_MASK BIT(1) +#define BQ25792_REG2C_VAC1_OVP_MASK BIT(0) + +/* FAULT Mask 1 */ +#define BQ25792_REG2D_VSYS_SHORT_MASK BIT(7) +#define BQ25792_REG2D_VSYS_OVP_MASK BIT(6) +#define BQ25792_REG2D_OTG_OVP_MASK BIT(5) +#define BQ25792_REG2D_OTG_UVP_MASK BIT(4) +#define BQ25792_REG2D_TSHUT_MASK BIT(2) + +/* ADC Control */ +#define BQ25792_REG2E_ADC_EN BIT(7) +#define BQ25792_REG2E_ADC_RATE BIT(6) +#define BQ25792_REG2E_ADC_SAMPLE_MASK GENMASK(5, 4) +#define BQ25792_REG2E_ADC_AVG BIT(3) +#define BQ25792_REG2E_ADC_AVG_INIT BIT(2) + +/* ADC Function Disable 0 */ +#define BQ25792_REG2F_IBUS_ADC_DIS BIT(7) +#define BQ25792_REG2F_IBAT_ADC_DIS BIT(6) +#define BQ25792_REG2F_VBUS_ADC_DIS BIT(5) +#define BQ25792_REG2F_VBAT_ADC_DIS BIT(4) +#define BQ25792_REG2F_VSYS_ADC_DIS BIT(3) +#define BQ25792_REG2F_TS_ADC_DIS BIT(2) +#define BQ25792_REG2F_TDIE_ADC_DIS BIT(1) + +/* ADC Function Disable 1 */ +#define BQ25792_REG30_DP_ADC_DIS BIT(7) +#define BQ25792_REG30_DM_ADC_DIS BIT(6) +#define BQ25792_REG30_VAC2_ADC_DIS BIT(5) +#define BQ25792_REG30_VAC1_ADC_DIS BIT(4) + +/* 0x31-0x45: ADC result registers (16-bit, RO): single full-width field */ + +#define BQ25792_ADCVSYSVBAT_STEP_UV 1000 +#define BQ25792_ADCIBAT_STEP_UA 1000 + +/* DPDM Driver */ +#define BQ25792_REG47_DPLUS_DAC_MASK GENMASK(7, 5) +#define BQ25792_REG47_DMINUS_DAC_MASK GENMASK(4, 2) + +/* Part Information */ +#define BQ25792_REG48_PN_MASK GENMASK(5, 3) +#define BQ25792_REG48_DEV_REV_MASK GENMASK(2, 0) + +enum bq257xx_type { + BQ25703A = 1, + BQ25792, +}; + struct bq257xx_device { struct i2c_client *client; struct regmap *regmap; + enum bq257xx_type type; }; From e776c4d83addbb174626ced0601c3c73a0a70eb2 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:15:16 +0400 Subject: [PATCH 189/218] regulator: bq257xx: Add support for BQ25792 Add support for TI BQ25792, an integrated battery charger and buck/boost regulator. This enables VBUS output from the charger's boost converter for use in USB OTG applications, supporting 2.8-22V output at up to 3.32A with 10mV and 40mA resolution. Acked-by: Mark Brown Tested-by: Chris Morgan Signed-off-by: Alexey Charkov --- drivers/regulator/bq257xx-regulator.c | 98 ++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/drivers/regulator/bq257xx-regulator.c b/drivers/regulator/bq257xx-regulator.c index 913ca9186bf1e1..fec75b23cd40ff 100644 --- a/drivers/regulator/bq257xx-regulator.c +++ b/drivers/regulator/bq257xx-regulator.c @@ -31,6 +31,32 @@ static int bq25703_vbus_get_cur_limit(struct regulator_dev *rdev) return FIELD_GET(BQ25703_OTG_CUR_MASK, reg) * BQ25703_OTG_CUR_STEP_UA; } +static int bq25792_vbus_get_cur_limit(struct regulator_dev *rdev) +{ + struct regmap *regmap = rdev_get_regmap(rdev); + int ret; + unsigned int reg; + + ret = regmap_read(regmap, BQ25792_REG0D_IOTG_REGULATION, ®); + if (ret) + return ret; + return FIELD_GET(BQ25792_REG0D_IOTG_MASK, reg) * BQ25792_OTG_CUR_STEP_UA; +} + +static int bq25792_vbus_get_voltage_sel(struct regulator_dev *rdev) +{ + struct regmap *regmap = rdev_get_regmap(rdev); + __be16 reg; + int ret; + + ret = regmap_raw_read(regmap, BQ25792_REG0B_VOTG_REGULATION, + ®, sizeof(reg)); + if (ret) + return ret; + + return FIELD_GET(BQ25792_REG0B_VOTG_MASK, be16_to_cpu(reg)); +} + /* * Check if the minimum current and maximum current requested are * sane values, then set the register accordingly. @@ -54,6 +80,37 @@ static int bq25703_vbus_set_cur_limit(struct regulator_dev *rdev, FIELD_PREP(BQ25703_OTG_CUR_MASK, reg)); } +static int bq25792_vbus_set_cur_limit(struct regulator_dev *rdev, + int min_uA, int max_uA) +{ + struct regmap *regmap = rdev_get_regmap(rdev); + unsigned int reg; + + if ((min_uA > BQ25792_OTG_CUR_MAX_UA) || + (max_uA < BQ25792_OTG_CUR_MIN_UA)) + return -EINVAL; + + reg = (max_uA / BQ25792_OTG_CUR_STEP_UA); + + /* Catch rounding errors since our step is 40000uA. */ + if ((reg * BQ25792_OTG_CUR_STEP_UA) < min_uA) + return -EINVAL; + + return regmap_write(regmap, BQ25792_REG0D_IOTG_REGULATION, + FIELD_PREP(BQ25792_REG0D_IOTG_MASK, reg)); +} + +static int bq25792_vbus_set_voltage_sel(struct regulator_dev *rdev, + unsigned int sel) +{ + struct regmap *regmap = rdev_get_regmap(rdev); + __be16 reg; + + reg = cpu_to_be16(FIELD_PREP(BQ25792_REG0B_VOTG_MASK, sel)); + return regmap_raw_write(regmap, BQ25792_REG0B_VOTG_REGULATION, + ®, sizeof(reg)); +} + static int bq25703_vbus_enable(struct regulator_dev *rdev) { struct bq257xx_reg_data *pdata = rdev_get_drvdata(rdev); @@ -101,6 +158,34 @@ static const struct regulator_desc bq25703_vbus_desc = { .vsel_mask = BQ25703_OTG_VOLT_MASK, }; +static const struct regulator_ops bq25792_vbus_ops = { + /* No GPIO for enabling the OTG regulator */ + .enable = regulator_enable_regmap, + .disable = regulator_disable_regmap, + .is_enabled = regulator_is_enabled_regmap, + .list_voltage = regulator_list_voltage_linear, + .get_voltage_sel = bq25792_vbus_get_voltage_sel, + .set_voltage_sel = bq25792_vbus_set_voltage_sel, + .get_current_limit = bq25792_vbus_get_cur_limit, + .set_current_limit = bq25792_vbus_set_cur_limit, +}; + +static const struct regulator_desc bq25792_vbus_desc = { + .name = "vbus", + .of_match = of_match_ptr("vbus"), + .regulators_node = of_match_ptr("regulators"), + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .ops = &bq25792_vbus_ops, + .min_uV = BQ25792_OTG_VOLT_MIN_UV, + .uV_step = BQ25792_OTG_VOLT_STEP_UV, + .n_voltages = BQ25792_OTG_VOLT_NUM_VOLT, + .enable_mask = BQ25792_REG12_EN_OTG, + .enable_reg = BQ25792_REG12_CHARGER_CONTROL_3, + .enable_val = BQ25792_REG12_EN_OTG, + .disable_val = 0, +}; + /* Get optional GPIO for OTG regulator enable. */ static void bq257xx_reg_dt_parse_gpio(struct platform_device *pdev) { @@ -141,6 +226,7 @@ static void bq257xx_reg_dt_parse_gpio(struct platform_device *pdev) static int bq257xx_regulator_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; + struct bq257xx_device *bq = dev_get_drvdata(pdev->dev.parent); struct bq257xx_reg_data *pdata; struct device_node *np = dev->of_node; struct regulator_config cfg = {}; @@ -152,7 +238,17 @@ static int bq257xx_regulator_probe(struct platform_device *pdev) if (!pdata) return -ENOMEM; - pdata->desc = bq25703_vbus_desc; + switch (bq->type) { + case BQ25703A: + pdata->desc = bq25703_vbus_desc; + break; + case BQ25792: + pdata->desc = bq25792_vbus_desc; + break; + default: + return dev_err_probe(&pdev->dev, -EINVAL, + "Unsupported device type\n"); + } platform_set_drvdata(pdev, pdata); bq257xx_reg_dt_parse_gpio(pdev); From 55d23f72e814113731336bfbe8d1d676c5772c95 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:46:02 +0400 Subject: [PATCH 190/218] power: supply: bq257xx: Add support for BQ25792 Add support for TI BQ25792 integrated battery charger and buck-boost converter. It shares high-level logic of operation with the already supported BQ25703A, but has a different register map, bit definitions and some of the lower-level hardware states. Tested-by: Chris Morgan Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 528 ++++++++++++++++++++++++- include/linux/mfd/bq257xx.h | 20 +- 2 files changed, 543 insertions(+), 5 deletions(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 9c082865e745bb..7d02169248b17f 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -88,6 +89,53 @@ struct bq257xx_chg { u32 vsys_min; }; +/** + * bq25792_read16() - Read a 16-bit value from device register + * @pdata: driver platform data + * @reg: register address to read from + * @val: pointer to store the register value + * + * Read a 16-bit big-endian value from the BQ25792 device via regmap + * and convert to CPU byte order. + * + * Return: Returns 0 on success or error on failure to read. + */ +static int bq25792_read16(struct bq257xx_chg *pdata, unsigned int reg, u16 *val) +{ + __be16 regval; + int ret; + + ret = regmap_raw_read(pdata->bq->regmap, reg, ®val, sizeof(regval)); + if (ret) + return ret; + + *val = be16_to_cpu(regval); + return 0; +} + +/** + * bq25792_write16() - Write a 16-bit value to device register + * @pdata: driver platform data + * @reg: register address to write to + * @val: 16-bit value to write in CPU byte order + * + * Convert the value to big-endian and write a 16-bit value to the + * BQ25792 device via regmap. + * + * Return: Returns 0 on success or error on failure to write. + */ +static int bq25792_write16(struct bq257xx_chg *pdata, unsigned int reg, u16 val) +{ + __be16 regval = cpu_to_be16(val); + int ret; + + ret = regmap_raw_write(pdata->bq->regmap, reg, ®val, sizeof(regval)); + if (ret) + return ret; + + return 0; +} + /** * bq25703_get_state() - Get the current state of the device * @pdata: driver platform data @@ -119,6 +167,43 @@ static int bq25703_get_state(struct bq257xx_chg *pdata) return 0; } +/** + * bq25792_get_state() - Get the current state of the device + * @pdata: driver platform data + * + * Get the current state of the BQ25792 charger by reading status + * registers. Updates the online, charging, overvoltage, and fault + * status fields in the driver data structure. + * + * Return: Returns 0 on success or error on failure to read device. + */ +static int bq25792_get_state(struct bq257xx_chg *pdata) +{ + unsigned int reg; + int ret; + + ret = regmap_read(pdata->bq->regmap, BQ25792_REG1B_CHARGER_STATUS_0, ®); + if (ret) + return ret; + + pdata->online = reg & BQ25792_REG1B_PG_STAT; + + ret = regmap_read(pdata->bq->regmap, BQ25792_REG1C_CHARGER_STATUS_1, ®); + if (ret) + return ret; + + pdata->charging = reg & BQ25792_REG1C_CHG_STAT_MASK; + + ret = regmap_read(pdata->bq->regmap, BQ25792_REG20_FAULT_STATUS_0, ®); + if (ret) + return ret; + + pdata->overvoltage = reg & BQ25792_REG20_OVERVOLTAGE_MASK; + pdata->oc_fault = reg & BQ25792_REG20_OVERCURRENT_MASK; + + return 0; +} + /** * bq25703_get_min_vsys() - Get the minimum system voltage * @pdata: driver platform data @@ -142,6 +227,31 @@ static int bq25703_get_min_vsys(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_min_vsys() - Get the minimum system voltage + * @pdata: driver platform data + * @intval: pointer to store the minimum voltage value + * + * Read the current minimum system voltage setting from the device + * and return it in microvolts. + * + * Return: Returns 0 on success or error on failure to read. + */ +static int bq25792_get_min_vsys(struct bq257xx_chg *pdata, int *intval) +{ + unsigned int reg; + int ret; + + ret = regmap_read(pdata->bq->regmap, BQ25792_REG00_MIN_SYS_VOLTAGE, ®); + if (ret) + return ret; + + reg = FIELD_GET(BQ25792_REG00_VSYSMIN_MASK, reg); + *intval = (reg * BQ25792_MINVSYS_STEP_UV) + BQ25792_MINVSYS_MIN_UV; + + return ret; +} + /** * bq25703_set_min_vsys() - Set the minimum system voltage * @pdata: driver platform data @@ -166,6 +276,29 @@ static int bq25703_set_min_vsys(struct bq257xx_chg *pdata, int vsys) reg); } +/** + * bq25792_set_min_vsys() - Set the minimum system voltage + * @pdata: driver platform data + * @vsys: voltage value to set in uV + * + * Set the minimum system voltage by clamping the requested value + * between device limits and writing to the appropriate register. + * + * Return: Returns 0 on success or error on failure to write. + */ +static int bq25792_set_min_vsys(struct bq257xx_chg *pdata, int vsys) +{ + unsigned int reg; + int vsys_min = pdata->vsys_min; + + vsys = clamp(vsys, vsys_min, BQ25792_MINVSYS_MAX_UV); + reg = ((vsys - BQ25792_MINVSYS_MIN_UV) / BQ25792_MINVSYS_STEP_UV); + reg = FIELD_PREP(BQ25792_REG00_VSYSMIN_MASK, reg); + + return regmap_write(pdata->bq->regmap, + BQ25792_REG00_MIN_SYS_VOLTAGE, reg); +} + /** * bq25703_get_cur() - Get the reported current from the battery * @pdata: driver platform data @@ -195,6 +328,30 @@ static int bq25703_get_cur(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_cur() - Get the reported current from the battery + * @pdata: driver platform data + * @intval: pointer to store the battery current value + * + * Read the current ADC value from the device representing the battery + * charge or discharge current and return it in microamps. + * + * Return: Returns 0 on success or error on failure to read. + */ +static int bq25792_get_cur(struct bq257xx_chg *pdata, int *intval) +{ + u16 reg; + int ret; + + ret = bq25792_read16(pdata, BQ25792_REG33_IBAT_ADC, ®); + if (ret < 0) + return ret; + + *intval = (s16)reg * BQ25792_ADCIBAT_STEP_UA; + + return ret; +} + /** * bq25703_get_ichg_cur() - Get the maximum reported charge current * @pdata: driver platform data @@ -218,6 +375,30 @@ static int bq25703_get_ichg_cur(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_ichg_cur() - Get the maximum reported charge current + * @pdata: driver platform data + * @intval: pointer to store the maximum charge current value + * + * Read the programmed maximum charge current limit from the device. + * + * Return: Returns 0 on success or error on failure to read value. + */ +static int bq25792_get_ichg_cur(struct bq257xx_chg *pdata, int *intval) +{ + u16 reg; + int ret; + + ret = bq25792_read16(pdata, BQ25792_REG03_CHARGE_CURRENT_LIMIT, ®); + if (ret) + return ret; + + *intval = FIELD_GET(BQ25792_REG03_ICHG_MASK, reg) * + BQ25792_ICHG_STEP_UA; + + return ret; +} + /** * bq25703_set_ichg_cur() - Set the maximum charge current * @pdata: driver platform data @@ -242,6 +423,28 @@ static int bq25703_set_ichg_cur(struct bq257xx_chg *pdata, int ichg) reg); } +/** + * bq25792_set_ichg_cur() - Set the maximum charge current + * @pdata: driver platform data + * @ichg: current value to set in uA + * + * Set the maximum charge current by clamping the requested value + * between device limits and writing to the appropriate register. + * + * Return: Returns 0 on success or error on failure to write. + */ +static int bq25792_set_ichg_cur(struct bq257xx_chg *pdata, int ichg) +{ + int ichg_max = pdata->ichg_max; + u16 reg; + + ichg = clamp(ichg, BQ25792_ICHG_MIN_UA, ichg_max); + reg = FIELD_PREP(BQ25792_REG03_ICHG_MASK, + (ichg / BQ25792_ICHG_STEP_UA)); + + return bq25792_write16(pdata, BQ25792_REG03_CHARGE_CURRENT_LIMIT, reg); +} + /** * bq25703_get_chrg_volt() - Get the maximum set charge voltage * @pdata: driver platform data @@ -265,6 +468,30 @@ static int bq25703_get_chrg_volt(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_chrg_volt() - Get the maximum set charge voltage + * @pdata: driver platform data + * @intval: pointer to store the maximum charge voltage value + * + * Read the current charge voltage limit from the device. + * + * Return: Returns 0 on success or error on failure to read value. + */ +static int bq25792_get_chrg_volt(struct bq257xx_chg *pdata, int *intval) +{ + u16 reg; + int ret; + + ret = bq25792_read16(pdata, BQ25792_REG01_CHARGE_VOLTAGE_LIMIT, ®); + if (ret) + return ret; + + *intval = FIELD_GET(BQ25792_REG01_VREG_MASK, reg) * + BQ25792_VBATREG_STEP_UV; + + return ret; +} + /** * bq25703_set_chrg_volt() - Set the maximum charge voltage * @pdata: driver platform data @@ -291,6 +518,29 @@ static int bq25703_set_chrg_volt(struct bq257xx_chg *pdata, int vbat) reg); } +/** + * bq25792_set_chrg_volt() - Set the maximum charge voltage + * @pdata: driver platform data + * @vbat: voltage value to set in uV + * + * Set the maximum charge voltage by clamping the requested value + * between device limits and writing to the appropriate register. + * + * Return: Returns 0 on success or error on failure to write. + */ +static int bq25792_set_chrg_volt(struct bq257xx_chg *pdata, int vbat) +{ + int vbat_max = pdata->vbat_max; + u16 reg; + + vbat = clamp(vbat, BQ25792_VBATREG_MIN_UV, vbat_max); + + reg = FIELD_PREP(BQ25792_REG01_VREG_MASK, + (vbat / BQ25792_VBATREG_STEP_UV)); + + return bq25792_write16(pdata, BQ25792_REG01_CHARGE_VOLTAGE_LIMIT, reg); +} + /** * bq25703_get_iindpm() - Get the maximum set input current * @pdata: driver platform data @@ -319,6 +569,30 @@ static int bq25703_get_iindpm(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_iindpm() - Get the maximum set input current + * @pdata: driver platform data + * @intval: pointer to store the maximum input current value + * + * Read the current input current limit from the device. + * + * Return: Returns 0 on success or error on failure to read value. + */ +static int bq25792_get_iindpm(struct bq257xx_chg *pdata, int *intval) +{ + u16 reg; + int ret; + + ret = bq25792_read16(pdata, BQ25792_REG06_INPUT_CURRENT_LIMIT, ®); + if (ret) + return ret; + + reg = FIELD_GET(BQ25792_REG06_IINDPM_MASK, reg); + *intval = reg * BQ25792_IINDPM_STEP_UA; + + return ret; +} + /** * bq25703_set_iindpm() - Set the maximum input current * @pdata: driver platform data @@ -344,6 +618,29 @@ static int bq25703_set_iindpm(struct bq257xx_chg *pdata, int iindpm) FIELD_PREP(BQ25703_IINDPM_MASK, reg)); } +/** + * bq25792_set_iindpm() - Set the maximum input current + * @pdata: driver platform data + * @iindpm: current value in uA + * + * Set the maximum input current by clamping the requested value + * between device limits and writing to the appropriate register. + * + * Return: Returns 0 on success or error on failure to write. + */ +static int bq25792_set_iindpm(struct bq257xx_chg *pdata, int iindpm) +{ + u16 reg; + int iindpm_max = pdata->iindpm_max; + + iindpm = clamp(iindpm, BQ25792_IINDPM_MIN_UA, iindpm_max); + + reg = iindpm / BQ25792_IINDPM_STEP_UA; + + return bq25792_write16(pdata, BQ25792_REG06_INPUT_CURRENT_LIMIT, + FIELD_PREP(BQ25792_REG06_IINDPM_MASK, reg)); +} + /** * bq25703_get_vbat() - Get the reported voltage from the battery * @pdata: driver platform data @@ -368,6 +665,30 @@ static int bq25703_get_vbat(struct bq257xx_chg *pdata, int *intval) return ret; } +/** + * bq25792_get_vbat() - Get the reported voltage from the battery + * @pdata: driver platform data + * @intval: pointer to store the battery voltage value + * + * Read the current ADC value representing the battery voltage + * and return it in microvolts. + * + * Return: Returns 0 on success or error on failure to read value. + */ +static int bq25792_get_vbat(struct bq257xx_chg *pdata, int *intval) +{ + u16 reg; + int ret; + + ret = bq25792_read16(pdata, BQ25792_REG3B_VBAT_ADC, ®); + if (ret) + return ret; + + *intval = reg * BQ25792_ADCVSYSVBAT_STEP_UV; + + return ret; +} + /** * bq25703_hw_init() - Set all the required registers to init the charger * @pdata: driver platform data @@ -434,6 +755,108 @@ static int bq25703_hw_init(struct bq257xx_chg *pdata) return ret; } +/** + * bq25792_hw_init() - Initialize BQ25792 hardware + * @pdata: driver platform data + * + * Initialize the BQ25792 by disabling the watchdog, enabling discharge + * current sensing with 5A limit, and configuring input current regulation. + * Set the charge current, charge voltage, minimum system voltage, and + * input current limit from platform data. Enable and configure the ADC + * to measure all available channels. + * + * Return: Returns 0 on success or error code on error. + */ +static int bq25792_hw_init(struct bq257xx_chg *pdata) +{ + struct regmap *regmap = pdata->bq->regmap; + int ret = 0; + u8 reg; + + /* Disable watchdog (TODO: make it work instead) */ + ret = regmap_write(regmap, BQ25792_REG10_CHARGER_CONTROL_1, 0); + if (ret) + return ret; + + /* + * Enable battery discharge current sensing, 5A discharge current + * limit, input current regulation and ship FET functions + */ + ret = regmap_write(regmap, BQ25792_REG14_CHARGER_CONTROL_5, + BQ25792_REG14_SFET_PRESENT | + BQ25792_REG14_EN_IBAT | + BQ25792_IBAT_5A | + BQ25792_REG14_EN_IINDPM); + if (ret) + return ret; + + if (pdata->vbat_max < 5000000) { + /* 1S batteries */ + reg = FIELD_PREP(BQ25792_REG0A_CELL_MASK, BQ25792_CELL_1S); + } else if (pdata->vbat_max < 10000000) { + /* 2S batteries */ + reg = FIELD_PREP(BQ25792_REG0A_CELL_MASK, BQ25792_CELL_2S); + } else if (pdata->vbat_max < 14000000) { + /* 3S batteries */ + reg = FIELD_PREP(BQ25792_REG0A_CELL_MASK, BQ25792_CELL_3S); + } else { + /* 4S batteries */ + reg = FIELD_PREP(BQ25792_REG0A_CELL_MASK, BQ25792_CELL_4S); + } + + /* Recharge voltage detection deglitch time (default 1024ms) */ + reg |= FIELD_PREP(BQ25792_REG0A_TRECHG_MASK, BQ25792_TRECHG_1024MS); + + /* Recharge voltage offset: 5% of the set charge voltage */ + reg |= FIELD_PREP(BQ25792_REG0A_VRECHG_MASK, + (pdata->vbat_max / 20 - BQ25792_VRECHG_MIN_UV) / BQ25792_VRECHG_STEP_UV); + + ret = regmap_write(regmap, BQ25792_REG0A_RECHARGE_CONTROL, reg); + if (ret) + return ret; + + ret = pdata->chip->bq257xx_set_ichg(pdata, pdata->ichg_max); + if (ret) + return ret; + + ret = pdata->chip->bq257xx_set_vbatreg(pdata, pdata->vbat_max); + if (ret) + return ret; + + ret = bq25792_set_min_vsys(pdata, pdata->vsys_min); + if (ret) + return ret; + + ret = pdata->chip->bq257xx_set_iindpm(pdata, pdata->iindpm_max); + if (ret) + return ret; + + /* Enable the Input Current Optimizer (the rest is at POR value) */ + ret = regmap_write(regmap, BQ25792_REG0F_CHARGER_CONTROL_0, + BQ25792_REG0F_EN_AUTO_IBATDIS | + BQ25792_REG0F_EN_CHG | + BQ25792_REG0F_EN_ICO | + BQ25792_REG0F_EN_TERM); + if (ret) + return ret; + + /* Enable the ADC. */ + ret = regmap_write(regmap, BQ25792_REG2E_ADC_CONTROL, BQ25792_REG2E_ADC_EN); + if (ret) + return ret; + + /* Clear per-channel ADC disable bits - enable all channels */ + ret = regmap_write(regmap, BQ25792_REG2F_ADC_FUNCTION_DISABLE_0, 0); + if (ret) + return ret; + + ret = regmap_write(regmap, BQ25792_REG30_ADC_FUNCTION_DISABLE_1, 0); + if (ret) + return ret; + + return ret; +} + /** * bq25703_hw_shutdown() - Set registers for shutdown * @pdata: driver platform data @@ -446,6 +869,30 @@ static void bq25703_hw_shutdown(struct bq257xx_chg *pdata) BQ25703_EN_LWPWR, BQ25703_EN_LWPWR); } +/** + * bq25792_hw_shutdown() - Shutdown BQ25792 hardware + * @pdata: driver platform data + * + * Perform hardware shutdown for the BQ25792. Currently a no-op + * as the device does not require special shutdown configuration. + */ +static void bq25792_hw_shutdown(struct bq257xx_chg *pdata) +{ + /* Nothing to do here */ +} + +/** + * bq257xx_set_charger_property() - Set a power supply property + * @psy: power supply device + * @prop: power supply property to set + * @val: value to set for the property + * + * Handle requests to set power supply properties such as input current + * limit, constant charge voltage, and constant charge current. Routes + * the request to the chip-specific implementation. + * + * Return: Returns 0 on success or -EINVAL if property is not supported. + */ static int bq257xx_set_charger_property(struct power_supply *psy, enum power_supply_property prop, const union power_supply_propval *val) @@ -469,6 +916,19 @@ static int bq257xx_set_charger_property(struct power_supply *psy, return -EINVAL; } +/** + * bq257xx_get_charger_property() - Get a power supply property + * @psy: power supply device + * @psp: power supply property to get + * @val: pointer to store the property value + * + * Handle requests to get power supply properties, including status, + * health, manufacturer, online state, and various voltage/current + * measurements. Reads current device state and routes chip-specific + * property requests to appropriate handlers. + * + * Return: Returns 0 on success or -EINVAL if property is not supported. + */ static int bq257xx_get_charger_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) @@ -550,6 +1010,17 @@ static enum power_supply_property bq257xx_power_supply_props[] = { POWER_SUPPLY_PROP_USB_TYPE, }; +/** + * bq257xx_property_is_writeable() - Check if a property is writeable + * @psy: power supply device + * @prop: power supply property to check + * + * Determines which power supply properties can be written to. Only + * charge current limit, charge voltage limit, and input current + * limit are writeable. + * + * Return: Returns 1 if property is writeable, 0 otherwise. + */ static int bq257xx_property_is_writeable(struct power_supply *psy, enum power_supply_property prop) { @@ -622,6 +1093,17 @@ static void bq257xx_external_power_changed(struct power_supply *psy) power_supply_changed(psy); } +/** + * bq257xx_irq_handler_thread() - Handle charger interrupt + * @irq: interrupt number + * @private: pointer to driver private data + * + * Thread handler for charger interrupts. Triggers re-evaluation of + * external power status and updates power supply state in response + * to charger events. + * + * Return: Returns IRQ_HANDLED if interrupt was processed. + */ static irqreturn_t bq257xx_irq_handler_thread(int irq, void *private) { struct bq257xx_chg *pdata = private; @@ -662,6 +1144,22 @@ static const struct bq257xx_chip_info bq25703_chip_info = { .bq257xx_get_min_vsys = &bq25703_get_min_vsys, }; +static const struct bq257xx_chip_info bq25792_chip_info = { + .default_iindpm_uA = BQ25792_IINDPM_DEFAULT_UA, + .bq257xx_hw_init = &bq25792_hw_init, + .bq257xx_hw_shutdown = &bq25792_hw_shutdown, + .bq257xx_get_state = &bq25792_get_state, + .bq257xx_get_ichg = &bq25792_get_ichg_cur, + .bq257xx_set_ichg = &bq25792_set_ichg_cur, + .bq257xx_get_vbatreg = &bq25792_get_chrg_volt, + .bq257xx_set_vbatreg = &bq25792_set_chrg_volt, + .bq257xx_get_iindpm = &bq25792_get_iindpm, + .bq257xx_set_iindpm = &bq25792_set_iindpm, + .bq257xx_get_cur = &bq25792_get_cur, + .bq257xx_get_vbat = &bq25792_get_vbat, + .bq257xx_get_min_vsys = &bq25792_get_min_vsys, +}; + /** * bq257xx_parse_dt() - Parse the device tree for required properties * @pdata: driver platform data @@ -707,6 +1205,17 @@ static int bq257xx_parse_dt(struct bq257xx_chg *pdata, return 0; } +/** + * bq257xx_charger_probe() - Probe routine for charger platform device + * @pdev: platform device + * + * Probe the charger device, allocate driver data structure, select the + * appropriate chip-specific function pointers, register the power supply, + * parse device tree properties for battery limits, initialize hardware, + * and set up the interrupt handler if available. + * + * Return: Returns 0 on success or error code on failure. + */ static int bq257xx_charger_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -722,7 +1231,17 @@ static int bq257xx_charger_probe(struct platform_device *pdev) return -ENOMEM; pdata->bq = bq; - pdata->chip = &bq25703_chip_info; + + switch (bq->type) { + case BQ25703A: + pdata->chip = &bq25703_chip_info; + break; + case BQ25792: + pdata->chip = &bq25792_chip_info; + break; + default: + return dev_err_probe(dev, -EINVAL, "Unknown chip type\n"); + } platform_set_drvdata(pdev, pdata); @@ -760,6 +1279,13 @@ static int bq257xx_charger_probe(struct platform_device *pdev) return ret; } +/** + * bq257xx_charger_shutdown() - Shutdown routine for charger platform device + * @pdev: platform device + * + * Called during system shutdown to perform charger cleanup, including + * disabling watchdog timers or other chip-specific shutdown procedures. + */ static void bq257xx_charger_shutdown(struct platform_device *pdev) { struct bq257xx_chg *pdata = platform_get_drvdata(pdev); diff --git a/include/linux/mfd/bq257xx.h b/include/linux/mfd/bq257xx.h index 9b1752b0a18b13..379ef4ee8291b5 100644 --- a/include/linux/mfd/bq257xx.h +++ b/include/linux/mfd/bq257xx.h @@ -200,6 +200,20 @@ #define BQ25792_REG0A_TRECHG_MASK GENMASK(5, 4) #define BQ25792_REG0A_VRECHG_MASK GENMASK(3, 0) +#define BQ25792_CELL_1S 0 +#define BQ25792_CELL_2S 1 +#define BQ25792_CELL_3S 2 +#define BQ25792_CELL_4S 3 + +#define BQ25792_TRECHG_64MS 0 +#define BQ25792_TRECHG_256MS 1 +#define BQ25792_TRECHG_1024MS 2 +#define BQ25792_TRECHG_2048MS 3 + +#define BQ25792_VRECHG_MIN_UV 50000 +#define BQ25792_VRECHG_STEP_UV 50000 +#define BQ25792_VRECHG_MAX_UV 800000 + /* VOTG regulation */ #define BQ25792_REG0B_VOTG_MASK GENMASK(10, 0) @@ -353,12 +367,10 @@ #define BQ25792_REG20_VAC2_OVP_STAT BIT(1) #define BQ25792_REG20_VAC1_OVP_STAT BIT(0) -#define BQ25792_REG20_OVERVOLTAGE_MASK (BQ25792_REG20_VBUS_OVP_STAT | \ - BQ25792_REG20_VBAT_OVP_STAT | \ +#define BQ25792_REG20_OVERVOLTAGE_MASK (BQ25792_REG20_VBAT_OVP_STAT | \ BQ25792_REG20_VAC2_OVP_STAT | \ BQ25792_REG20_VAC1_OVP_STAT) -#define BQ25792_REG20_OVERCURRENT_MASK (BQ25792_REG20_IBUS_OCP_STAT | \ - BQ25792_REG20_IBAT_OCP_STAT | \ +#define BQ25792_REG20_OVERCURRENT_MASK (BQ25792_REG20_IBAT_OCP_STAT | \ BQ25792_REG20_CONV_OCP_STAT) /* FAULT Status 1 */ From bc764696bb94e23b76bdda88f92863a8af4533c7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 19 Feb 2026 17:29:09 +0400 Subject: [PATCH 191/218] dt-bindings: hwmon: Add TI INA4230 4-channel I2C power monitor Add TI INA4230, which is a 48V 4-channel 16-bit I2C-based current/voltage/power/energy monitor with alert function. Link: https://www.ti.com/product/INA4230 Reviewed-by: Krzysztof Kozlowski Signed-off-by: Alexey Charkov --- .../devicetree/bindings/hwmon/ti,ina4230.yaml | 134 ++++++++++++++++++ MAINTAINERS | 6 + 2 files changed, 140 insertions(+) create mode 100644 Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml diff --git a/Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml b/Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml new file mode 100644 index 00000000000000..f33e52a12657fe --- /dev/null +++ b/Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/hwmon/ti,ina4230.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Texas Instruments INA4230 quad-channel power monitors + +maintainers: + - Alexey Charkov + +description: | + The INA4230 is a 48V quad-channel 16-bit current, voltage, power and energy + monitor with an I2C interface. + + Datasheet: + https://www.ti.com/product/INA4230 + +properties: + compatible: + enum: + - ti,ina4230 + + reg: + maxItems: 1 + + "#address-cells": + description: Required only if a child node is present. + const: 1 + + "#size-cells": + description: Required only if a child node is present. + const: 0 + + vs-supply: + description: phandle to the regulator that provides the VS supply typically + in range from 1.7 V to 5.5 V. + + ti,alert-polarity-active-high: + description: Alert pin is asserted based on the value of Alert polarity Bit + of the CONFIG2 register. Default value is 0, for which the alert pin + toggles from high to low during faults. When this property is set, the + corresponding register bit is set to 1, and the alert pin toggles from + low to high during faults. + $ref: /schemas/types.yaml#/definitions/flag + +patternProperties: + "^input@[0-3]$": + description: The node contains optional child nodes for four channels. + Each child node describes the information of input source. Input channels + default to enabled in the chip. Unless channels are explicitly disabled + in device-tree, input channels will be enabled. + type: object + additionalProperties: false + properties: + reg: + description: Must be 0, 1, 2 or 3, corresponding to the IN1, IN2, IN3 + or IN4 ports of the INA4230, respectively. + enum: [ 0, 1, 2, 3 ] + + label: + description: name of the input source + + shunt-resistor-micro-ohms: + description: shunt resistor value in micro-Ohm + + ti,maximum-expected-current-microamp: + description: | + This value indicates the maximum current in microamps that you can + expect to measure with ina4230 in your circuit. + + This value will be used to calculate the Current_LSB to maximize the + available precision while ensuring your expected maximum current fits + within the chip's ADC range. It will also enable built-in shunt gain + to increase ADC granularity by a factor of 4 if the provided maximum + current / shunt resistance combination does not produce more than + 20.48 mV drop at the shunt. + minimum: 32768 + maximum: 4294967295 + default: 32768000 + + required: + - reg + +required: + - compatible + - reg + +allOf: + - $ref: hwmon-common.yaml# + +unevaluatedProperties: false + +examples: + - | + i2c { + #address-cells = <1>; + #size-cells = <0>; + + power-sensor@44 { + compatible = "ti,ina4230"; + reg = <0x44>; + vs-supply = <&vdd_3v0>; + ti,alert-polarity-active-high; + #address-cells = <1>; + #size-cells = <0>; + + input@0 { + reg = <0x0>; + /* + * Input channels are enabled by default in the device and so + * to disable, must be explicitly disabled in device-tree. + */ + status = "disabled"; + }; + + input@1 { + reg = <0x1>; + shunt-resistor-micro-ohms = <50000>; + ti,maximum-expected-current-microamp = <300000>; + }; + + input@2 { + reg = <0x2>; + label = "VDD_5V"; + shunt-resistor-micro-ohms = <10000>; + ti,maximum-expected-current-microamp = <5000000>; + }; + + input@3 { + reg = <0x3>; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 0f45d7644cfb28..c68bdd60fc8fde 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12588,6 +12588,12 @@ S: Maintained F: Documentation/hwmon/ina233.rst F: drivers/hwmon/pmbus/ina233.c +INA4230 HWMON DRIVER +M: Alexey Charkov +L: linux-hwmon@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml + INDEX OF FURTHER KERNEL DOCUMENTATION M: Carlos Bilbao S: Maintained From fb907c248b5b6fef197b1ceadc98586688d2917c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 20 Feb 2026 16:48:29 +0400 Subject: [PATCH 192/218] hwmon: Add support for TI INA4230 power monitor Add a driver for the TI INA4230, a 4-channel power monitor with I2C interface. The driver supports voltage, current, power and energy measurements, but skips the alert functionality in this initial implementation. Signed-off-by: Alexey Charkov --- MAINTAINERS | 1 + drivers/hwmon/Kconfig | 11 + drivers/hwmon/Makefile | 1 + drivers/hwmon/ina4230.c | 1032 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 1045 insertions(+) create mode 100644 drivers/hwmon/ina4230.c diff --git a/MAINTAINERS b/MAINTAINERS index c68bdd60fc8fde..2670e60c73f24d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12593,6 +12593,7 @@ M: Alexey Charkov L: linux-hwmon@vger.kernel.org S: Maintained F: Documentation/devicetree/bindings/hwmon/ti,ina4230.yaml +F: drivers/hwmon/ina4230.c INDEX OF FURTHER KERNEL DOCUMENTATION M: Carlos Bilbao diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 14e4cea48acc47..79b4bd232e9c34 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -2307,6 +2307,17 @@ config SENSORS_INA3221 This driver can also be built as a module. If so, the module will be called ina3221. +config SENSORS_INA4230 + tristate "Texas Instruments INA4230 Quad Current/Voltage Monitor" + depends on I2C + select REGMAP_I2C + help + If you say yes here you get support for the TI INA4230 Quad + Current/Voltage Monitor. + + This driver can also be built as a module. If so, the module + will be called ina4230. + config SENSORS_SPD5118 tristate "SPD5118 Compliant Temperature Sensors" depends on I2C diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 982ee2c6f9deb6..673ecf989fe09b 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -104,6 +104,7 @@ obj-$(CONFIG_SENSORS_INA209) += ina209.o obj-$(CONFIG_SENSORS_INA2XX) += ina2xx.o obj-$(CONFIG_SENSORS_INA238) += ina238.o obj-$(CONFIG_SENSORS_INA3221) += ina3221.o +obj-$(CONFIG_SENSORS_INA4230) += ina4230.o obj-$(CONFIG_SENSORS_INTEL_M10_BMC_HWMON) += intel-m10-bmc-hwmon.o obj-$(CONFIG_SENSORS_ISL28022) += isl28022.o obj-$(CONFIG_SENSORS_IT87) += it87.o diff --git a/drivers/hwmon/ina4230.c b/drivers/hwmon/ina4230.c new file mode 100644 index 00000000000000..b5233c004089aa --- /dev/null +++ b/drivers/hwmon/ina4230.c @@ -0,0 +1,1032 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * INA4230 Quad Current/Voltage Monitor + * + * Based on INA3221 driver by Texas Instruments Incorporated - https://www.ti.com/ + * Adapted for INA4230 by Alexey Charkov + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define INA4230_DRIVER_NAME "ina4230" + +#define INA4230_SHUNT_VOLTAGE_CH1 0x00 +#define INA4230_BUS_VOLTAGE_CH1 0x01 +#define INA4230_CURRENT_CH1 0x02 +#define INA4230_POWER_CH1 0x03 +#define INA4230_ENERGY_CH1 0x04 +#define INA4230_CALIBRATION_CH1 0x05 +#define INA4230_ALERT_LIMIT1 0x06 +#define INA4230_ALERT_CONFIG1 0x07 +#define INA4230_SHUNT_VOLTAGE_CH2 0x08 +#define INA4230_BUS_VOLTAGE_CH2 0x09 +#define INA4230_CURRENT_CH2 0x0A +#define INA4230_POWER_CH2 0x0B +#define INA4230_ENERGY_CH2 0x0C +#define INA4230_CALIBRATION_CH2 0x0D +#define INA4230_ALERT_LIMIT2 0x0E +#define INA4230_ALERT_CONFIG2 0x0F +#define INA4230_SHUNT_VOLTAGE_CH3 0x10 +#define INA4230_BUS_VOLTAGE_CH3 0x11 +#define INA4230_CURRENT_CH3 0x12 +#define INA4230_POWER_CH3 0x13 +#define INA4230_ENERGY_CH3 0x14 +#define INA4230_CALIBRATION_CH3 0x15 +#define INA4230_ALERT_LIMIT3 0x16 +#define INA4230_ALERT_CONFIG3 0x17 +#define INA4230_SHUNT_VOLTAGE_CH4 0x18 +#define INA4230_BUS_VOLTAGE_CH4 0x19 +#define INA4230_CURRENT_CH4 0x1A +#define INA4230_POWER_CH4 0x1B +#define INA4230_ENERGY_CH4 0x1C +#define INA4230_CALIBRATION_CH4 0x1D +#define INA4230_ALERT_LIMIT4 0x1E +#define INA4230_ALERT_CONFIG4 0x1F +#define INA4230_CONFIG1 0x20 +#define INA4230_CONFIG2 0x21 +#define INA4230_FLAGS 0x22 +#define INA4230_MANUFACTURER_ID 0x7E + +#define INA4230_CALIBRATION_MASK GENMASK(14, 0) + +#define INA4230_ALERT_CHANNEL_MASK GENMASK(4, 3) +#define INA4230_ALERT_MASK GENMASK(2, 0) +/* Shunt voltage over limit */ +#define INA4230_ALERT_MASK_SOL 0x1 +/* Shunt voltage under limit */ +#define INA4230_ALERT_MASK_SUL 0x2 +/* Bus voltage over limit */ +#define INA4230_ALERT_MASK_BOL 0x3 +/* Bus voltage under limit */ +#define INA4230_ALERT_MASK_BUL 0x4 +/* Power over limit */ +#define INA4230_ALERT_MASK_POL 0x5 + +#define INA4230_CONFIG1_ACTIVE_CHANNEL_MASK GENMASK(15, 12) +#define INA4230_CONFIG1_AVG_MASK GENMASK(11, 9) +#define INA4230_CONFIG1_VBUSCT_MASK GENMASK(8, 6) +#define INA4230_CONFIG1_VSHCT_MASK GENMASK(5, 3) +#define INA4230_CONFIG1_MODE_MASK GENMASK(2, 0) +#define INA4230_MODE_POWERDOWN 0 +#define INA4230_MODE_SHUNT_SINGLE 1 +#define INA4230_MODE_BUS_SINGLE 2 +#define INA4230_MODE_BUS_SHUNT_SINGLE 3 +#define INA4230_MODE_POWERDOWN1 4 +#define INA4230_MODE_SHUNT_CONTINUOUS 5 +#define INA4230_MODE_BUS_CONTINUOUS 6 +#define INA4230_MODE_BUS_SHUNT_CONTINUOUS 7 + +#define INA4230_CONFIG2_RST BIT(15) +#define INA4230_CONFIG2_ACC_RST_MASK GENMASK(11, 8) +#define INA4230_CONFIG2_CNVR_MASK BIT(7) +#define INA4230_CONFIG2_ENOF_MASK BIT(6) +#define INA4230_CONFIG2_ALERT_LATCH BIT(5) +#define INA4230_CONFIG2_ALERT_POL BIT(4) +#define INA4230_CONFIG2_RANGE_MASK GENMASK(3, 0) +#define INA4230_CONFIG2_RANGE_CH(x) \ + FIELD_PREP(INA4230_CONFIG2_RANGE_MASK, BIT((x))) + +#define INA4230_FLAGS_LIMIT4_ALERT BIT(15) +#define INA4230_FLAGS_LIMIT3_ALERT BIT(14) +#define INA4230_FLAGS_LIMIT2_ALERT BIT(13) +#define INA4230_FLAGS_LIMIT1_ALERT BIT(12) +#define INA4230_FLAGS_ENERGY_OVERFLOW_CH4 BIT(11) +#define INA4230_FLAGS_ENERGY_OVERFLOW_CH3 BIT(10) +#define INA4230_FLAGS_ENERGY_OVERFLOW_CH2 BIT(9) +#define INA4230_FLAGS_ENERGY_OVERFLOW_CH1 BIT(8) +#define INA4230_FLAGS_CVRF BIT(7) +#define INA4230_FLAGS_MATH_OVERFLOW BIT(6) + +#define INA4230_RSHUNT_DEFAULT 10000 +#define INA4230_CONFIG_DEFAULT \ + (FIELD_PREP(INA4230_CONFIG1_ACTIVE_CHANNEL_MASK, 0xF) | \ + FIELD_PREP(INA4230_CONFIG1_AVG_MASK, 0x1) | \ + FIELD_PREP(INA4230_CONFIG1_VBUSCT_MASK, 0x4) | \ + FIELD_PREP(INA4230_CONFIG1_VSHCT_MASK, 0x4) | \ + FIELD_PREP(INA4230_CONFIG1_MODE_MASK, 0x7)) +#define INA4230_CONFIG_CHx_EN(x) \ + FIELD_PREP(INA4230_CONFIG1_ACTIVE_CHANNEL_MASK, BIT((x))) + +enum ina4230_fields { + /* Alert configuration settings: channel masks */ + F_ALERT1_CH, F_ALERT2_CH, F_ALERT3_CH, F_ALERT4_CH, + /* Alert configuration settings: alert masks */ + F_ALERT1_TYPE, F_ALERT2_TYPE, F_ALERT3_TYPE, F_ALERT4_TYPE, + /* Configuration registers */ + F_CH_EN, F_AVG, F_VBUSCT, F_VSHCT, F_MODE, + F_RST, F_ACC_RST, F_CNV_ALERT, F_ENOF, F_ALERT_LATCH, F_ALERT_POL, F_RANGE, + /* Status flags */ + F_LIMIT1_ALERT, F_LIMIT2_ALERT, F_LIMIT3_ALERT, F_LIMIT4_ALERT, + F_ENERGY_OVERFLOW_CH1, F_ENERGY_OVERFLOW_CH2, F_ENERGY_OVERFLOW_CH3, F_ENERGY_OVERFLOW_CH4, + F_CVRF, F_MATH_OVERFLOW, + /* sentinel */ + F_MAX_FIELDS +}; + +static const struct reg_field ina4230_reg_fields[] = { + [F_ALERT1_CH] = REG_FIELD(INA4230_ALERT_CONFIG1, 3, 4), + [F_ALERT2_CH] = REG_FIELD(INA4230_ALERT_CONFIG2, 3, 4), + [F_ALERT3_CH] = REG_FIELD(INA4230_ALERT_CONFIG3, 3, 4), + [F_ALERT4_CH] = REG_FIELD(INA4230_ALERT_CONFIG4, 3, 4), + + [F_ALERT1_TYPE] = REG_FIELD(INA4230_ALERT_CONFIG1, 0, 2), + [F_ALERT2_TYPE] = REG_FIELD(INA4230_ALERT_CONFIG2, 0, 2), + [F_ALERT3_TYPE] = REG_FIELD(INA4230_ALERT_CONFIG3, 0, 2), + [F_ALERT4_TYPE] = REG_FIELD(INA4230_ALERT_CONFIG4, 0, 2), + + [F_CH_EN] = REG_FIELD(INA4230_CONFIG1, 12, 15), + [F_AVG] = REG_FIELD(INA4230_CONFIG1, 9, 11), + [F_VBUSCT] = REG_FIELD(INA4230_CONFIG1, 6, 8), + [F_VSHCT] = REG_FIELD(INA4230_CONFIG1, 3, 5), + [F_MODE] = REG_FIELD(INA4230_CONFIG1, 0, 2), + [F_RST] = REG_FIELD(INA4230_CONFIG2, 15, 15), + [F_ACC_RST] = REG_FIELD(INA4230_CONFIG2, 8, 11), + [F_CNV_ALERT] = REG_FIELD(INA4230_CONFIG2, 7, 7), + [F_ENOF] = REG_FIELD(INA4230_CONFIG2, 6, 6), + [F_ALERT_LATCH] = REG_FIELD(INA4230_CONFIG2, 5, 5), + [F_ALERT_POL] = REG_FIELD(INA4230_CONFIG2, 4, 4), + [F_RANGE] = REG_FIELD(INA4230_CONFIG2, 0, 3), + + [F_LIMIT1_ALERT] = REG_FIELD(INA4230_FLAGS, 12, 12), + [F_LIMIT2_ALERT] = REG_FIELD(INA4230_FLAGS, 13, 13), + [F_LIMIT3_ALERT] = REG_FIELD(INA4230_FLAGS, 14, 14), + [F_LIMIT4_ALERT] = REG_FIELD(INA4230_FLAGS, 15, 15), + [F_ENERGY_OVERFLOW_CH1] = REG_FIELD(INA4230_FLAGS, 8, 8), + [F_ENERGY_OVERFLOW_CH2] = REG_FIELD(INA4230_FLAGS, 9, 9), + [F_ENERGY_OVERFLOW_CH3] = REG_FIELD(INA4230_FLAGS, 10, 10), + [F_ENERGY_OVERFLOW_CH4] = REG_FIELD(INA4230_FLAGS, 11, 11), + [F_CVRF] = REG_FIELD(INA4230_FLAGS, 7, 7), + [F_MATH_OVERFLOW] = REG_FIELD(INA4230_FLAGS, 6, 6), +}; + +enum ina4230_channels { + INA4230_CHANNEL1, + INA4230_CHANNEL2, + INA4230_CHANNEL3, + INA4230_CHANNEL4, + INA4230_NUM_CHANNELS +}; + +/** + * struct ina4230_input - channel input source specific information + * @label: label of channel input source + * @shunt_resistor: shunt resistor value of channel input source + * @shunt_gain: gain of shunt voltage for current calculation + * @max_expected_current: maximum expected current in micro-Ampere for ADC + * calibration + * @current_lsb_uA: current LSB in micro-Amperes + * @disconnected: connection status of channel input source + */ +struct ina4230_input { + const char *label; + int shunt_resistor; + int shunt_gain; + int max_expected_current; + int current_lsb_uA; + bool disconnected; +}; + +/** + * struct ina4230_data - device specific information + * @pm_dev: Device pointer for pm runtime + * @regmap: Register map of the device + * @fields: Register fields of the device + * @inputs: Array of channel input source specific structures + * @reg_config1: cached value of CONFIG1 register + * @reg_config2: cached value of CONFIG2 register + * @alert_active_high: flag indicating alert polarity is active high + */ +struct ina4230_data { + struct device *pm_dev; + struct regmap *regmap; + struct regmap_field *fields[F_MAX_FIELDS]; + struct ina4230_input inputs[INA4230_NUM_CHANNELS]; + unsigned int reg_config1; + unsigned int reg_config2; + bool alert_active_high; +}; + +static inline bool ina4230_is_enabled(struct ina4230_data *ina, int channel) +{ + return pm_runtime_active(ina->pm_dev) && + !ina->inputs[channel].disconnected && + ina->reg_config1 & INA4230_CONFIG_CHx_EN(channel); +} + +/* Lookup table for Bus and Shunt conversion times in usec */ +static const u16 ina4230_conv_time[] = { + 140, 204, 332, 588, 1100, 2116, 4156, 8244, +}; + +/* Lookup table for number of samples used in averaging mode */ +static const int ina4230_avg_samples[] = { + 1, 4, 16, 64, 128, 256, 512, 1024, +}; + +/* Converting update_interval in msec to conversion time in usec */ +static inline u32 ina4230_interval_ms_to_conv_time(u16 config, int interval) +{ + u32 channels = hweight16(config & INA4230_CONFIG1_ACTIVE_CHANNEL_MASK); + u32 samples_idx = FIELD_GET(INA4230_CONFIG1_AVG_MASK, config); + u32 samples = ina4230_avg_samples[samples_idx]; + + /* Bisect the result to Bus and Shunt conversion times */ + return DIV_ROUND_CLOSEST(interval * 1000 / 2, channels * samples); +} + +/* Converting CONFIG register value to update_interval in usec */ +static inline u32 ina4230_reg_to_interval_us(u16 config) +{ + u32 channels = hweight16(config & INA4230_CONFIG1_ACTIVE_CHANNEL_MASK); + u32 vbus_ct_idx = FIELD_GET(INA4230_CONFIG1_VBUSCT_MASK, config); + u32 vsh_ct_idx = FIELD_GET(INA4230_CONFIG1_VSHCT_MASK, config); + u32 vbus_ct = ina4230_conv_time[vbus_ct_idx]; + u32 vsh_ct = ina4230_conv_time[vsh_ct_idx]; + + /* Calculate total conversion time */ + return channels * (vbus_ct + vsh_ct); +} + +static const u8 ina4230_calibration_reg[] = { + INA4230_CALIBRATION_CH1, + INA4230_CALIBRATION_CH2, + INA4230_CALIBRATION_CH3, + INA4230_CALIBRATION_CH4, +}; + +static int ina4230_set_calibration(struct ina4230_data *ina, int channel) +{ + struct ina4230_input *input = &ina->inputs[channel]; + u8 reg = ina4230_calibration_reg[channel]; + int shunt_range_uV, ret; + u32 calibration; + u64 n, d; + + shunt_range_uV = mult_frac(input->max_expected_current, + input->shunt_resistor, + 1000000); + input->shunt_gain = shunt_range_uV > 20480 ? 1 : 4; + ina->reg_config2 &= ~INA4230_CONFIG2_RANGE_CH(channel); + if (input->shunt_gain == 4) + ina->reg_config2 |= INA4230_CONFIG2_RANGE_CH(channel); + + ret = regmap_write(ina->regmap, INA4230_CONFIG2, ina->reg_config2); + if (ret) + return ret; + + input->current_lsb_uA = DIV_ROUND_UP(input->max_expected_current, 32768); + n = 5120000000ULL; + d = (u64)input->current_lsb_uA * input->shunt_resistor * input->shunt_gain; + /* Ensure rounding to the closest integer */ + n += d / 2; + n = div64_u64(n, d); + if (n > INA4230_CALIBRATION_MASK) { + dev_err(ina->pm_dev, + "Shunt %duOhm too low for expected current %duA, cannot calibrate channel %d\n", + input->shunt_resistor, input->max_expected_current, channel + 1); + return -ERANGE; + } + + calibration = n & INA4230_CALIBRATION_MASK; + + return regmap_write(ina->regmap, reg, calibration); +} + +static const u8 ina4230_in_reg[] = { + INA4230_BUS_VOLTAGE_CH1, + INA4230_BUS_VOLTAGE_CH2, + INA4230_BUS_VOLTAGE_CH3, + INA4230_BUS_VOLTAGE_CH4, + INA4230_SHUNT_VOLTAGE_CH1, + INA4230_SHUNT_VOLTAGE_CH2, + INA4230_SHUNT_VOLTAGE_CH3, + INA4230_SHUNT_VOLTAGE_CH4, +}; + +static const u8 ina4230_curr_reg[][INA4230_NUM_CHANNELS] = { + [hwmon_curr_input] = { INA4230_CURRENT_CH1, INA4230_CURRENT_CH2, + INA4230_CURRENT_CH3, INA4230_CURRENT_CH4 }, +}; + +static const u8 ina4230_power_reg[] = { + INA4230_POWER_CH1, INA4230_POWER_CH2, INA4230_POWER_CH3, INA4230_POWER_CH4 +}; + +static const u8 ina4230_energy_reg[] = { + INA4230_ENERGY_CH1, INA4230_ENERGY_CH2, + INA4230_ENERGY_CH3, INA4230_ENERGY_CH4 +}; + +static int ina4230_read_chip(struct device *dev, u32 attr, long *val) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + int regval; + + switch (attr) { + case hwmon_chip_samples: + regval = FIELD_GET(INA4230_CONFIG1_AVG_MASK, ina->reg_config1); + *val = ina4230_avg_samples[regval]; + return 0; + case hwmon_chip_update_interval: + /* Return in msec */ + *val = ina4230_reg_to_interval_us(ina->reg_config1); + *val = DIV_ROUND_CLOSEST(*val, 1000); + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_read_in(struct device *dev, u32 attr, int channel, long *val) +{ + const bool is_shunt = channel > INA4230_CHANNEL4; + struct ina4230_data *ina = dev_get_drvdata(dev); + u8 reg = ina4230_in_reg[channel]; + int regval, ret; + + /* + * Translate shunt channel index to sensor channel index + */ + channel %= INA4230_NUM_CHANNELS; + + switch (attr) { + case hwmon_in_input: + if (!ina4230_is_enabled(ina, channel)) + return -ENODATA; + + ret = regmap_read(ina->regmap, reg, ®val); + if (ret) + return ret; + + /* + * Scale of shunt voltage (uV): LSB is 2.5uV or 625nV + * depending on gain setting + * Scale of bus voltage (mV): LSB is 1.6mV + */ + if (is_shunt) + *val = mult_frac((long)(int16_t)regval, + 2500 / ina->inputs[channel].shunt_gain, + 1000000); + else + *val = mult_frac((long)(int16_t)regval, + 1600, + 1000); + return 0; + case hwmon_in_enable: + *val = ina4230_is_enabled(ina, channel); + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_read_power(struct device *dev, u32 attr, int channel, long *val) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + u8 reg = ina4230_power_reg[channel]; + int regval, ret; + + switch (attr) { + case hwmon_power_input: + if (!ina4230_is_enabled(ina, channel)) + return -ENODATA; + + ret = regmap_read(ina->regmap, reg, ®val); + if (ret) + return ret; + + *val = (int16_t)regval * + (long)ina->inputs[channel].current_lsb_uA * 32; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_read_energy(struct device *dev, u32 attr, int channel, long *val) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + u8 reg = ina4230_energy_reg[channel]; + int ret; + __be32 regval; + + switch (attr) { + case hwmon_energy_input: + if (!ina4230_is_enabled(ina, channel)) + return -ENODATA; + + ret = regmap_noinc_read(ina->regmap, reg, ®val, sizeof(regval)); + if (ret) + return ret; + + *val = be32_to_cpu(regval) * + (long)ina->inputs[channel].current_lsb_uA * 32; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_read_curr(struct device *dev, u32 attr, + int channel, long *val) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + u8 reg = ina4230_curr_reg[attr][channel]; + int regval, ret; + + switch (attr) { + case hwmon_curr_input: + if (!ina4230_is_enabled(ina, channel)) + return -ENODATA; + + ret = regmap_read(ina->regmap, reg, ®val); + if (ret) + return ret; + + *val = (int16_t)regval * + (long)ina->inputs[channel].current_lsb_uA / 1000; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_write_chip(struct device *dev, u32 attr, long val) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + int idx; + u32 tmp; + + switch (attr) { + case hwmon_chip_samples: + idx = find_closest(val, ina4230_avg_samples, + ARRAY_SIZE(ina4230_avg_samples)); + + FIELD_MODIFY(INA4230_CONFIG1_AVG_MASK, &ina->reg_config1, idx); + return regmap_write(ina->regmap, INA4230_CONFIG1, ina->reg_config1); + case hwmon_chip_update_interval: + tmp = ina4230_interval_ms_to_conv_time(ina->reg_config1, val); + idx = find_closest(tmp, ina4230_conv_time, + ARRAY_SIZE(ina4230_conv_time)); + + FIELD_MODIFY(INA4230_CONFIG1_VBUSCT_MASK, &ina->reg_config1, idx); + FIELD_MODIFY(INA4230_CONFIG1_VSHCT_MASK, &ina->reg_config1, idx); + return regmap_write(ina->regmap, INA4230_CONFIG1, ina->reg_config1); + default: + return -EOPNOTSUPP; + } +} + +static int ina4230_write_enable(struct device *dev, int channel, bool enable) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + u16 config, mask = INA4230_CONFIG_CHx_EN(channel); + u16 config_old = ina->reg_config1 & mask; + u32 tmp; + int ret; + + config = enable ? mask : 0; + + /* Bypass if enable status is not being changed */ + if (config_old == config) + return 0; + + /* For enabling routine, increase refcount and resume() at first */ + if (enable) { + ret = pm_runtime_resume_and_get(ina->pm_dev); + if (ret < 0) { + dev_err(dev, "Failed to get PM runtime\n"); + return ret; + } + } + + /* Enable or disable the channel */ + tmp = (ina->reg_config1 & ~mask) | (config & mask); + ret = regmap_write(ina->regmap, INA4230_CONFIG1, tmp); + if (ret) + goto fail; + + /* Cache the latest config register value */ + ina->reg_config1 = tmp; + + /* For disabling routine, decrease refcount or suspend() at last */ + if (!enable) + pm_runtime_put_sync(ina->pm_dev); + + return 0; + +fail: + if (enable) { + dev_err(dev, "Failed to enable channel %d: error %d\n", + channel, ret); + pm_runtime_put_sync(ina->pm_dev); + } + + return ret; +} + +static int ina4230_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + int ret; + + switch (type) { + case hwmon_chip: + ret = ina4230_read_chip(dev, attr, val); + break; + case hwmon_in: + /* 0-align channel ID */ + ret = ina4230_read_in(dev, attr, channel - 1, val); + break; + case hwmon_curr: + ret = ina4230_read_curr(dev, attr, channel, val); + break; + case hwmon_power: + ret = ina4230_read_power(dev, attr, channel, val); + break; + case hwmon_energy: + ret = ina4230_read_energy(dev, attr, channel, val); + break; + default: + ret = -EOPNOTSUPP; + break; + } + return ret; +} + +static int ina4230_write(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long val) +{ + int ret; + + switch (type) { + case hwmon_chip: + ret = ina4230_write_chip(dev, attr, val); + break; + case hwmon_in: + /* 0-align channel ID */ + ret = ina4230_write_enable(dev, channel - 1, val); + break; + default: + ret = -EOPNOTSUPP; + break; + } + return ret; +} + +static int ina4230_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + int index = channel - 1; + + *str = ina->inputs[index].label; + + return 0; +} + +static umode_t ina4230_is_visible(const void *drvdata, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct ina4230_data *ina = drvdata; + const struct ina4230_input *input = NULL; + + switch (type) { + case hwmon_chip: + switch (attr) { + case hwmon_chip_samples: + case hwmon_chip_update_interval: + return 0644; + default: + return 0; + } + case hwmon_in: + /* Ignore in0_ */ + if (channel == 0) + return 0; + + switch (attr) { + case hwmon_in_label: + if (channel - 1 <= INA4230_CHANNEL4) + input = &ina->inputs[channel - 1]; + /* Hide label node if label is not provided */ + return (input && input->label) ? 0444 : 0; + case hwmon_in_input: + return 0444; + case hwmon_in_enable: + return 0644; + default: + return 0; + } + case hwmon_curr: + switch (attr) { + case hwmon_curr_input: + return 0444; + default: + return 0; + } + case hwmon_power: + switch (attr) { + case hwmon_power_input: + return 0444; + default: + return 0; + } + case hwmon_energy: + switch (attr) { + case hwmon_energy_input: + return 0444; + default: + return 0; + } + default: + return 0; + } +} + +static const struct hwmon_channel_info * const ina4230_info[] = { + HWMON_CHANNEL_INFO(chip, + HWMON_C_SAMPLES, + HWMON_C_UPDATE_INTERVAL), + HWMON_CHANNEL_INFO(in, + /* 0: dummy, skipped in is_visible */ + HWMON_I_INPUT, + /* 1-4: input voltage Channels */ + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_INPUT | HWMON_I_LABEL, + /* 5-8: shunt voltage Channels */ + HWMON_I_INPUT, + HWMON_I_INPUT, + HWMON_I_INPUT, + HWMON_I_INPUT), + HWMON_CHANNEL_INFO(curr, + /* 1-4: current channels*/ + HWMON_C_INPUT, + HWMON_C_INPUT, + HWMON_C_INPUT, + HWMON_C_INPUT), + HWMON_CHANNEL_INFO(power, + /* 1-4: power channels*/ + HWMON_P_INPUT, + HWMON_P_INPUT, + HWMON_P_INPUT, + HWMON_P_INPUT), + HWMON_CHANNEL_INFO(energy, + /* 1-4: energy channels*/ + HWMON_E_INPUT, + HWMON_E_INPUT, + HWMON_E_INPUT, + HWMON_E_INPUT), + NULL +}; + +static const struct hwmon_ops ina4230_hwmon_ops = { + .is_visible = ina4230_is_visible, + .read_string = ina4230_read_string, + .read = ina4230_read, + .write = ina4230_write, +}; + +static const struct hwmon_chip_info ina4230_chip_info = { + .ops = &ina4230_hwmon_ops, + .info = ina4230_info, +}; + +/* Extra attribute groups */ +static ssize_t ina4230_shunt_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute *sd_attr = to_sensor_dev_attr(attr); + struct ina4230_data *ina = dev_get_drvdata(dev); + unsigned int channel = sd_attr->index; + struct ina4230_input *input = &ina->inputs[channel]; + + return sysfs_emit(buf, "%d\n", input->shunt_resistor); +} + +static ssize_t ina4230_shunt_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute *sd_attr = to_sensor_dev_attr(attr); + struct ina4230_data *ina = dev_get_drvdata(dev); + unsigned int channel = sd_attr->index; + struct ina4230_input *input = &ina->inputs[channel]; + int val; + int ret; + + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + + val = clamp_val(val, 1, INT_MAX); + + input->shunt_resistor = val; + ret = ina4230_set_calibration(ina, channel); + if (ret) + return ret; + + return count; +} + +/* shunt resistance */ +static SENSOR_DEVICE_ATTR_RW(shunt1_resistor, ina4230_shunt, INA4230_CHANNEL1); +static SENSOR_DEVICE_ATTR_RW(shunt2_resistor, ina4230_shunt, INA4230_CHANNEL2); +static SENSOR_DEVICE_ATTR_RW(shunt3_resistor, ina4230_shunt, INA4230_CHANNEL3); +static SENSOR_DEVICE_ATTR_RW(shunt4_resistor, ina4230_shunt, INA4230_CHANNEL4); + +static struct attribute *ina4230_attrs[] = { + &sensor_dev_attr_shunt1_resistor.dev_attr.attr, + &sensor_dev_attr_shunt2_resistor.dev_attr.attr, + &sensor_dev_attr_shunt3_resistor.dev_attr.attr, + &sensor_dev_attr_shunt4_resistor.dev_attr.attr, + NULL, +}; +ATTRIBUTE_GROUPS(ina4230); + +static const struct regmap_range ina4230_vol_ranges[] = { + regmap_reg_range(INA4230_SHUNT_VOLTAGE_CH1, INA4230_ENERGY_CH1), + regmap_reg_range(INA4230_SHUNT_VOLTAGE_CH2, INA4230_ENERGY_CH2), + regmap_reg_range(INA4230_SHUNT_VOLTAGE_CH3, INA4230_ENERGY_CH3), + regmap_reg_range(INA4230_SHUNT_VOLTAGE_CH4, INA4230_ENERGY_CH4), + regmap_reg_range(INA4230_FLAGS, INA4230_FLAGS), +}; + +static const struct regmap_access_table ina4230_volatile_table = { + .yes_ranges = ina4230_vol_ranges, + .n_yes_ranges = ARRAY_SIZE(ina4230_vol_ranges), +}; + +static const struct regmap_config ina4230_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + + .cache_type = REGCACHE_MAPLE, + .volatile_table = &ina4230_volatile_table, +}; + +static int ina4230_probe_child_from_dt(struct device *dev, + struct device_node *child, + struct ina4230_data *ina) +{ + struct ina4230_input *input; + u32 val; + int ret; + + ret = of_property_read_u32(child, "reg", &val); + if (ret) + return dev_err_probe(dev, ret, + "missing reg property of %pOFn\n", child); + else if (val > INA4230_CHANNEL4) + return dev_err_probe(dev, -EINVAL, + "invalid reg %d of %pOFn\n", val, child); + + input = &ina->inputs[val]; + + /* Log the disconnected channel input */ + if (!of_device_is_available(child)) { + input->disconnected = true; + return 0; + } + + /* Save the connected input label if available */ + of_property_read_string(child, "label", &input->label); + + /* Overwrite default shunt resistor value optionally */ + if (!of_property_read_u32(child, "shunt-resistor-micro-ohms", &val)) { + if (val < 1 || val > INT_MAX) + return dev_err_probe(dev, -EINVAL, + "invalid shunt resistor value %u of %pOFn\n", + val, child); + + input->shunt_resistor = val; + } + + /* Save the expected maxcurrent */ + if (!of_property_read_u32(child, "ti,maximum-expected-current-microamp", &val)) { + if (val < 32768 || val > INT_MAX) + return dev_err_probe(dev, -EINVAL, + "invalid max current value %u of %pOFn\n", + val, child); + + input->max_expected_current = val; + } + + return 0; +} + +static int ina4230_probe_from_dt(struct device *dev, struct ina4230_data *ina) +{ + const struct device_node *np = dev->of_node; + int ret; + + /* Compatible with non-DT platforms */ + if (!np) + return 0; + + ina->alert_active_high = of_property_read_bool(np, "ti,alert-polarity-active-high"); + + for_each_child_of_node_scoped(np, child) { + ret = ina4230_probe_child_from_dt(dev, child, ina); + if (ret) + return ret; + } + + ret = devm_regulator_get_enable_optional(dev, "vs"); + if (ret && ret != -ENODEV) + return dev_err_probe(dev, ret, "Failed to get regulator\n"); + + return 0; +} + +static int ina4230_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct ina4230_data *ina; + struct device *hwmon_dev; + int i, ret; + + ina = devm_kzalloc(dev, sizeof(*ina), GFP_KERNEL); + if (!ina) + return -ENOMEM; + + ina->regmap = devm_regmap_init_i2c(client, &ina4230_regmap_config); + if (IS_ERR(ina->regmap)) + return PTR_ERR(ina->regmap); + + ret = devm_regmap_field_bulk_alloc(dev, ina->regmap, ina->fields, + ina4230_reg_fields, + ARRAY_SIZE(ina4230_reg_fields)); + if (ret) + return ret; + + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + ina->inputs[i].shunt_resistor = INA4230_RSHUNT_DEFAULT; + /* Default for 1mA LSB current measurements */ + ina->inputs[i].max_expected_current = 32768000; + } + + ret = ina4230_probe_from_dt(dev, ina); + if (ret) + return dev_err_probe(dev, ret, + "Unable to probe from device tree\n"); + + /* The driver will be reset, so use reset value */ + ina->reg_config1 = INA4230_CONFIG_DEFAULT; + ina->reg_config2 = 0; + + if (ina->alert_active_high) + FIELD_MODIFY(INA4230_CONFIG2_ALERT_POL, &ina->reg_config2, 1); + + /* Disable channels if their inputs are disconnected */ + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + if (ina->inputs[i].disconnected) + ina->reg_config1 &= ~INA4230_CONFIG_CHx_EN(i); + } + + ina->pm_dev = dev; + dev_set_drvdata(dev, ina); + + /* Enable PM runtime -- status is suspended by default */ + pm_runtime_enable(ina->pm_dev); + + /* Initialize (resume) the device */ + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + if (ina->inputs[i].disconnected) + continue; + + /* Match the refcount with number of enabled channels */ + ret = pm_runtime_get_sync(ina->pm_dev); + if (ret < 0) + goto fail; + } + + /* Set calibration values after device resume/reset */ + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + if (!ina->inputs[i].disconnected) { + ret = ina4230_set_calibration(ina, i); + if (ret) + goto fail; + } + } + + hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name, ina, + &ina4230_chip_info, + ina4230_groups); + if (IS_ERR(hwmon_dev)) { + ret = dev_err_probe(dev, PTR_ERR(hwmon_dev), + "Unable to register hwmon device\n"); + goto fail; + } + + return 0; + +fail: + pm_runtime_disable(ina->pm_dev); + pm_runtime_set_suspended(ina->pm_dev); + /* pm_runtime_put_noidle() for connected channels to balance get_sync */ + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + if (!ina->inputs[i].disconnected) + pm_runtime_put_noidle(ina->pm_dev); + } + + return ret; +} + +static void ina4230_remove(struct i2c_client *client) +{ + struct ina4230_data *ina = dev_get_drvdata(&client->dev); + int i; + + pm_runtime_disable(ina->pm_dev); + pm_runtime_set_suspended(ina->pm_dev); + + /* pm_runtime_put_noidle() for connected channels to balance get_sync */ + for (i = 0; i < INA4230_NUM_CHANNELS; i++) { + if (!ina->inputs[i].disconnected) + pm_runtime_put_noidle(ina->pm_dev); + } +} + +static int ina4230_suspend(struct device *dev) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + int ret; + + /* Save config register value and enable cache-only */ + ret = regmap_read(ina->regmap, INA4230_CONFIG1, &ina->reg_config1); + if (ret) + return ret; + + regcache_cache_only(ina->regmap, true); + regcache_mark_dirty(ina->regmap); + + return 0; +} + +static int ina4230_resume(struct device *dev) +{ + struct ina4230_data *ina = dev_get_drvdata(dev); + int ret; + + regcache_cache_only(ina->regmap, false); + + /* Software reset the chip */ + ret = regmap_field_write(ina->fields[F_RST], true); + if (ret) { + dev_err(dev, "Unable to reset device\n"); + return ret; + } + + /* Restore cached register values to hardware */ + ret = regcache_sync(ina->regmap); + if (ret) + return ret; + + return 0; +} + +static DEFINE_RUNTIME_DEV_PM_OPS(ina4230_pm, ina4230_suspend, ina4230_resume, + NULL); + +static const struct of_device_id ina4230_of_match_table[] = { + { .compatible = "ti,ina4230", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, ina4230_of_match_table); + +static const struct i2c_device_id ina4230_ids[] = { + { "ina4230" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(i2c, ina4230_ids); + +static struct i2c_driver ina4230_i2c_driver = { + .probe = ina4230_probe, + .remove = ina4230_remove, + .driver = { + .name = INA4230_DRIVER_NAME, + .of_match_table = ina4230_of_match_table, + .pm = pm_ptr(&ina4230_pm), + }, + .id_table = ina4230_ids, +}; +module_i2c_driver(ina4230_i2c_driver); + +MODULE_AUTHOR("Alexey Charkov "); +MODULE_DESCRIPTION("Texas Instruments INA4230 HWMon Driver"); +MODULE_LICENSE("GPL"); From b1efabc6290b7b96ef82fcb8841af5dc509f6fef Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 13 Mar 2026 13:02:31 +0400 Subject: [PATCH 193/218] usb: typec: tcpci: add DRM DP HPD bridge support Add support to use TCPCI based USB-C connectors with the DP AltMode helper code on devicetree based platforms. Signed-off-by: Alexey Charkov --- drivers/usb/typec/tcpm/Kconfig | 2 ++ drivers/usb/typec/tcpm/tcpci.c | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/drivers/usb/typec/tcpm/Kconfig b/drivers/usb/typec/tcpm/Kconfig index 00baa7503d4527..53abde8ebef93a 100644 --- a/drivers/usb/typec/tcpm/Kconfig +++ b/drivers/usb/typec/tcpm/Kconfig @@ -13,7 +13,9 @@ if TYPEC_TCPM config TYPEC_TCPCI tristate "Type-C Port Controller Interface driver" + depends on DRM || DRM=n depends on I2C + select DRM_AUX_HPD_BRIDGE if DRM_BRIDGE && OF select REGMAP_I2C help Type-C Port Controller driver for TCPCI-compliant controller. diff --git a/drivers/usb/typec/tcpm/tcpci.c b/drivers/usb/typec/tcpm/tcpci.c index 0148b8f50412b0..e6cccbd377f7db 100644 --- a/drivers/usb/typec/tcpm/tcpci.c +++ b/drivers/usb/typec/tcpm/tcpci.c @@ -5,6 +5,7 @@ * USB Type-C Port Controller Interface. */ +#include #include #include #include @@ -837,6 +838,7 @@ static int tcpci_parse_config(struct tcpci *tcpci) struct tcpci *tcpci_register_port(struct device *dev, struct tcpci_data *data) { + struct auxiliary_device *bridge_dev; struct tcpci *tcpci; int err; @@ -889,12 +891,23 @@ struct tcpci *tcpci_register_port(struct device *dev, struct tcpci_data *data) if (err < 0) return ERR_PTR(err); + bridge_dev = devm_drm_dp_hpd_bridge_alloc(tcpci->dev, to_of_node(tcpci->tcpc.fwnode)); + if (IS_ERR(bridge_dev)) + return ERR_CAST(bridge_dev); + tcpci->port = tcpm_register_port(tcpci->dev, &tcpci->tcpc); if (IS_ERR(tcpci->port)) { fwnode_handle_put(tcpci->tcpc.fwnode); return ERR_CAST(tcpci->port); } + err = devm_drm_dp_hpd_bridge_add(tcpci->dev, bridge_dev); + if (err < 0) { + tcpm_unregister_port(tcpci->port); + fwnode_handle_put(tcpci->tcpc.fwnode); + return ERR_PTR(err); + } + return tcpci; } EXPORT_SYMBOL_GPL(tcpci_register_port); From ae300441c03593d8834f5cba029476a385cce3dc Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:49:48 +0400 Subject: [PATCH 194/218] WIP: mfd: Add support for Flipper One's MCU Signed-off-by: Alexey Charkov --- drivers/mfd/Kconfig | 15 +++ drivers/mfd/Makefile | 1 + drivers/mfd/flipper-one-mcu.c | 179 ++++++++++++++++++++++++++++ include/linux/mfd/flipper-one-mcu.h | 74 ++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 drivers/mfd/flipper-one-mcu.c create mode 100644 include/linux/mfd/flipper-one-mcu.h diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 80b4e82c4937fd..83d6969faf4a55 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -539,6 +539,21 @@ config MFD_EXYNOS_LPASS SoCs (e.g. Exynos5433). Choose Y here only if you build for such Samsung SoC. +config MFD_FLIPPER_ONE + tristate "Flipper One MCU support" + depends on I2C && OF + depends on ARCH_ROCKCHIP || COMPILE_TEST + select MFD_CORE + select REGMAP_I2C + select REGMAP_IRQ + help + This adds support for the MCU handling human-machine interface, + power controls and USB Type-C functions on the Flipper One + networking multitool. + This driver provides common support for accessing the device. + Additional drivers must be enabled in order to use the + functionality of the device. + config MFD_GATEWORKS_GSC tristate "Gateworks System Controller" depends on I2C && OF diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index ebadbaea9e4a09..3ff94e56e16583 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -21,6 +21,7 @@ obj-$(CONFIG_MFD_CS42L43_I2C) += cs42l43-i2c.o obj-$(CONFIG_MFD_CS42L43_SDW) += cs42l43-sdw.o obj-$(CONFIG_MFD_ENE_KB3930) += ene-kb3930.o obj-$(CONFIG_MFD_EXYNOS_LPASS) += exynos-lpass.o +obj-$(CONFIG_MFD_FLIPPER_ONE) += flipper-one-mcu.o obj-$(CONFIG_MFD_GATEWORKS_GSC) += gateworks-gsc.o obj-$(CONFIG_MFD_MACSMC) += macsmc.o diff --git a/drivers/mfd/flipper-one-mcu.c b/drivers/mfd/flipper-one-mcu.c new file mode 100644 index 00000000000000..ed3df0a1956ec2 --- /dev/null +++ b/drivers/mfd/flipper-one-mcu.c @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Flipper One MCU interconnect driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include +#include + +static const struct regmap_range fomcu_writeable_reg_ranges[] = { + regmap_reg_range(FOMCU_REG_INTMSK_INPUT, + FOMCU_REG_INPUT_BTNS - 1), + regmap_reg_range(FOMCU_REG_LEDS_BR_LINK, + FOMCU_REG_LEDS_COLOR_LINK4), + regmap_reg_range(FOMCU_REG_HAPTIC, FOMCU_REG_HAPTIC), + regmap_reg_range(FOMCU_REG_UCSI_CONTROL, + FOMCU_REG_UCSI_CONTROL + 6), + regmap_reg_range(FOMCU_REG_UCSI_MESSAGE_OUT, FOMCU_REG_MAX), +}; + +static const struct regmap_access_table fomcu_writeable_regs = { + .yes_ranges = fomcu_writeable_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(fomcu_writeable_reg_ranges), +}; + +static const struct regmap_range fomcu_nonvolatile_reg_ranges[] = { + regmap_reg_range(FOMCU_REG_VERSION, FOMCU_REG_VERSION + 1), + regmap_reg_range(FOMCU_REG_INTMSK_INPUT, FOMCU_REG_INPUT_BTNS - 1), +}; + +static const struct regmap_access_table fomcu_volatile_regs = { + .no_ranges = fomcu_nonvolatile_reg_ranges, + .n_no_ranges = ARRAY_SIZE(fomcu_nonvolatile_reg_ranges), +}; + +static const struct regmap_range fomcu_precious_reg_ranges[] = { + regmap_reg_range(FOMCU_REG_INTSTS_INPUT, + FOMCU_REG_INTMSK_INPUT - 1), +}; + +static const struct regmap_access_table fomcu_precious_regs = { + .yes_ranges = fomcu_precious_reg_ranges, + .n_yes_ranges = ARRAY_SIZE(fomcu_precious_reg_ranges), +}; + +static const struct regmap_config fomcu_regmap_config = { + .name = "flipper-one-mcu", + .reg_bits = 16, + .reg_stride = 2, + .val_bits = 16, + .val_format_endian = REGMAP_ENDIAN_LITTLE, + .max_register = FOMCU_REG_MAX, + .wr_table = &fomcu_writeable_regs, + .volatile_table = &fomcu_volatile_regs, + .precious_table = &fomcu_precious_regs, +}; + +#define CAT(a, b) CAT_I(a, b) +#define CAT_I(a, b) a##b +#define FOMCU_IRQ_REG(subsys, bit) \ + REGMAP_IRQ_REG(CAT(FOMCU_INT_, CAT(subsys, CAT(_, bit))), \ + CAT(FOMCU_INTOFF_, subsys), \ + CAT(FOMCU_INTSTS_, CAT(subsys, CAT(_, bit)))) + +static const struct regmap_irq fomcu_irqs[] = { + FOMCU_IRQ_REG(INPUT, BTN), + FOMCU_IRQ_REG(INPUT, TOUCH), + FOMCU_IRQ_REG(INPUT, HEADSET), + FOMCU_IRQ_REG(UCSI, EVENT), +}; + +static unsigned int irq_input_offsets[] = { FOMCU_INTOFF_INPUT }; +static unsigned int irq_ucsi_offsets[] = { FOMCU_INTOFF_UCSI }; + +static const struct regmap_irq_sub_irq_map fomcu_sub_irqs[] = { + REGMAP_IRQ_MAIN_REG_OFFSET(irq_input_offsets), + REGMAP_IRQ_MAIN_REG_OFFSET(irq_ucsi_offsets), +}; + +static const struct regmap_irq_chip fomcu_irq_chip = { + .name = "fomcu-irq", + .irqs = fomcu_irqs, + .num_irqs = ARRAY_SIZE(fomcu_irqs), + .main_status = FOMCU_REG_INTSTS, + .status_base = FOMCU_REG_INTSTS_INPUT, + .mask_base = FOMCU_REG_INTMSK_INPUT, + .sub_reg_offsets = &fomcu_sub_irqs[0], + .num_main_regs = 1, + .num_regs = ARRAY_SIZE(fomcu_sub_irqs), +}; + +static const struct resource fo_input_irqs[] = { + DEFINE_RES_IRQ_NAMED(FOMCU_INT_INPUT_BTN, "flipper-one-input-btn"), + DEFINE_RES_IRQ_NAMED(FOMCU_INT_INPUT_TOUCH, "flipper-one-input-touch"), + DEFINE_RES_IRQ_NAMED(FOMCU_INT_INPUT_HEADSET, "flipper-one-input-headset"), +}; + +static const struct resource fo_ucsi_irqs[] = { + DEFINE_RES_IRQ_NAMED(FOMCU_INT_UCSI_EVENT, "flipper-one-ucsi"), +}; + +static const struct mfd_cell cells[] = { + MFD_CELL_NAME("flipper-one-haptic"), + MFD_CELL_RES("flipper-one-input", fo_input_irqs), + MFD_CELL_NAME("flipper-one-leds"), + MFD_CELL_NAME("flipper-one-power"), + MFD_CELL_NAME("flipper-one-regulators"), + MFD_CELL_NAME("flipper-one-thermal"), + MFD_CELL_OF("flipper-one-typec", fo_ucsi_irqs, NULL, 0, 0, + "flipper,one-typec"), +}; + +static int fomcu_probe(struct i2c_client *client) +{ + struct regmap_irq_chip_data *irq_data; + struct fomcu_device *ddata; + int ret; + + ddata = devm_kzalloc(&client->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->client = client; + + ddata->regmap = devm_regmap_init_i2c(client, &fomcu_regmap_config); + if (IS_ERR(ddata->regmap)) { + return dev_err_probe(&client->dev, PTR_ERR(ddata->regmap), + "Failed to allocate register map\n"); + } + + i2c_set_clientdata(client, ddata); + + ret = devm_regmap_add_irq_chip(&client->dev, ddata->regmap, + client->irq, IRQF_ONESHOT, 0, + &fomcu_irq_chip, &irq_data); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to add IRQ chip\n"); + + ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_AUTO, + cells, ARRAY_SIZE(cells), NULL, 0, + regmap_irq_get_domain(irq_data)); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to register child devices\n"); + + return ret; +} + +static const struct i2c_device_id fomcu_i2c_ids[] = { + { "flipper-one-mcu" }, + {} +}; +MODULE_DEVICE_TABLE(i2c, fomcu_i2c_ids); + +static const struct of_device_id fomcu_of_match[] = { + { .compatible = "flipper,one-mcu" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, fomcu_of_match); + +static struct i2c_driver fomcu_driver = { + .driver = { + .name = "flipper-one-mcu", + .of_match_table = fomcu_of_match, + }, + .probe = fomcu_probe, + .id_table = fomcu_i2c_ids, +}; +module_i2c_driver(fomcu_driver); + +MODULE_DESCRIPTION("Flipper One MCU driver"); +MODULE_AUTHOR("Alexey Charkov "); +MODULE_LICENSE("GPL"); diff --git a/include/linux/mfd/flipper-one-mcu.h b/include/linux/mfd/flipper-one-mcu.h new file mode 100644 index 00000000000000..1082d3085155a5 --- /dev/null +++ b/include/linux/mfd/flipper-one-mcu.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Register definitions for the Flipper One MCU interconnect + * Copyright (C) 2026 Flipper FZCO + */ + +#ifndef __LINUX_MFD_FLIPPER_ONE_MCU_H +#define __LINUX_MFD_FLIPPER_ONE_MCU_H + +#include +#include + +enum fomcu_interrupts { + FOMCU_INT_INPUT_BTN, + FOMCU_INT_INPUT_TOUCH, + FOMCU_INT_INPUT_HEADSET, + FOMCU_INT_UCSI_EVENT, +}; + +#define FOMCU_REG_INTSTS 0x0000 + +#define FOMCU_REG_VERSION 0x0080 + +#define FOMCU_REG_INTSTS_INPUT 0x0100 +#define FOMCU_INTOFF_INPUT 0x0 +#define FOMCU_INTSTS_INPUT_BTN BIT(0) +#define FOMCU_INTSTS_INPUT_TOUCH BIT(1) +#define FOMCU_INTSTS_INPUT_HEADSET BIT(2) + +#define FOMCU_REG_INTSTS_UCSI 0x0102 +#define FOMCU_INTOFF_UCSI 0x2 +#define FOMCU_INTSTS_UCSI_EVENT BIT(0) + +#define FOMCU_REG_INTMSK_INPUT 0x0180 +#define FOMCU_REG_INTMSK_UCSI 0x0182 + +#define FOMCU_REG_INPUT_BTNS 0x0200 +#define FOMCU_REG_INPUT_TOUCH_X 0x0202 +#define FOMCU_REG_INPUT_TOUCH_Y 0x0204 +#define FOMCU_REG_INPUT_TOUCH_Z 0x0206 +#define FOMCU_REG_INPUT_HEADSET 0x0208 + +#define FOMCU_REG_LEDS_BR_LINK 0x0300 +#define FOMCU_REG_LEDS_BR_POWER 0x0302 +#define FOMCU_REG_LEDS_BR_WATT 0x0304 + +/* RGB565 values per each LED */ +#define FOMCU_REG_LEDS_COLOR_LINK1 0x0310 +#define FOMCU_REG_LEDS_COLOR_LINK2 0x0312 +#define FOMCU_REG_LEDS_COLOR_LINK3 0x0314 +#define FOMCU_REG_LEDS_COLOR_LINK4 0x0316 + +#define FOMCU_REG_HAPTIC 0x0400 +#define FOMCU_HAPTIC_PLAY BIT(15) +#define FOMCU_HAPTIC_EFFECT GENMASK(14, 8) +#define FOMCU_HAPTIC_DURATION GENMASK(7, 0) + +#define FOMCU_REG_UCSI 0x0500 +#define FOMCU_REG_UCSI_VERSION (FOMCU_REG_UCSI + 0x00) +#define FOMCU_REG_UCSI_CCI (FOMCU_REG_UCSI + 0x04) +#define FOMCU_REG_UCSI_CONTROL (FOMCU_REG_UCSI + 0x08) +#define FOMCU_REG_UCSI_MESSAGE_IN 0x0600 +#define FOMCU_REG_UCSI_MESSAGE_OUT 0x0700 +#define FOMCU_UCSI_MESSAGE_LEN 256 + +#define FOMCU_REG_MAX (FOMCU_REG_UCSI_MESSAGE_OUT + \ + FOMCU_UCSI_MESSAGE_LEN - 1) + +struct fomcu_device { + struct i2c_client *client; + struct regmap *regmap; +}; + +#endif /* __LINUX_MFD_FLIPPER_ONE_MCU_H */ From c1d868fc37dcfe93cc4741818a0893b9101c4ac9 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:50:54 +0400 Subject: [PATCH 195/218] Input: add support for Flipper One buttons and touchpad Flipper One is a network multi-tool device with a small screen, buttons, and a touchpad. Its physical inputs are handled by an onboard MCU, which then exposes them along with a hardware interrupt line over an I2C interconnect interface. Add support for the MCU provided touchpad and button inputs. Signed-off-by: Alexey Charkov --- drivers/input/misc/Kconfig | 11 + drivers/input/misc/Makefile | 1 + drivers/input/misc/flipper-one-input.c | 268 +++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 drivers/input/misc/flipper-one-input.c diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index 94a753fcb64fa5..b6817b25b4c7eb 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -178,6 +178,17 @@ config INPUT_E3X0_BUTTON To compile this driver as a module, choose M here: the module will be called e3x0_button. +config INPUT_FLIPPER_ONE + tristate "Flipper One input support" + depends on MFD_FLIPPER_ONE + help + Say Y here to enable support for human-machine interfacing + functions on the Flipper One networking multitool, including its + builtin buttons, touchpad, haptic feedback and headset inputs. + + To compile this driver as a module, choose M here: the + module will be called flipper-one-input. + config INPUT_PCSPKR tristate "PC Speaker support" depends on PCSPKR_PLATFORM diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 415fc4e2918be8..704b6cc9b41c1c 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -36,6 +36,7 @@ obj-$(CONFIG_INPUT_DA9052_ONKEY) += da9052_onkey.o obj-$(CONFIG_INPUT_DA9055_ONKEY) += da9055_onkey.o obj-$(CONFIG_INPUT_DA9063_ONKEY) += da9063_onkey.o obj-$(CONFIG_INPUT_E3X0_BUTTON) += e3x0-button.o +obj-$(CONFIG_INPUT_FLIPPER_ONE) += flipper-one-input.o obj-$(CONFIG_INPUT_DRV260X_HAPTICS) += drv260x.o obj-$(CONFIG_INPUT_DRV2665_HAPTICS) += drv2665.o obj-$(CONFIG_INPUT_DRV2667_HAPTICS) += drv2667.o diff --git a/drivers/input/misc/flipper-one-input.c b/drivers/input/misc/flipper-one-input.c new file mode 100644 index 00000000000000..eb0cbeb9c529f0 --- /dev/null +++ b/drivers/input/misc/flipper-one-input.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Flipper One builtin buttons and touchpad driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define FO_BTN_VIEW BIT(0) +#define FO_BTN_ESCAPE BIT(1) +#define FO_BTN_POWER BIT(2) +#define FO_BTN_EDIT BIT(3) +#define FO_BTN_RUN BIT(4) +#define FO_BTN_APPSELECT BIT(5) +#define FO_BTN_BACK BIT(6) +#define FO_BTN_DOWN BIT(7) +#define FO_BTN_RIGHT BIT(8) +#define FO_BTN_CENTER BIT(9) +#define FO_BTN_LEFT BIT(10) +#define FO_BTN_UP BIT(11) +#define FO_BTN_PTT BIT(12) + +#define FO_HS_HPPRESENT BIT(0) +#define FO_HS_MICPRESENT BIT(1) +#define FO_HS_BTN_A BIT(2) +#define FO_HS_BTN_B BIT(3) +#define FO_HS_BTN_C BIT(4) +#define FO_HS_BTN_D BIT(5) + +struct fo_input { + struct input_dev *idev_btn; + struct input_dev *idev_touch; + struct input_dev *idev_headset; + struct fomcu_device *fomcu; +}; + +struct fo_irq { + const char *name; + irqreturn_t (*handler)(int, void *); +}; + +static irqreturn_t fo_input_btn_handler(int irq, void *data) +{ + struct fo_input *input = data; + struct regmap *regmap = input->fomcu->regmap; + struct input_dev *idev = input->idev_btn; + struct device *parent = idev->dev.parent; + unsigned int reg; + int err; + + err = regmap_read(regmap, FOMCU_REG_INPUT_BTNS, ®); + if (err) { + dev_err(parent, "Failed to read button states: %d\n", err); + return IRQ_NONE; + } + + input_report_key(idev, KEY_ENTER, FO_BTN_CENTER & reg); + input_report_key(idev, KEY_UP, FO_BTN_UP & reg); + input_report_key(idev, KEY_DOWN, FO_BTN_DOWN & reg); + input_report_key(idev, KEY_LEFT, FO_BTN_LEFT & reg); + input_report_key(idev, KEY_RIGHT, FO_BTN_RIGHT & reg); + input_report_key(idev, KEY_TAB, FO_BTN_APPSELECT & reg); + input_report_key(idev, KEY_BACKSPACE, FO_BTN_BACK & reg); + input_report_key(idev, KEY_A, FO_BTN_PTT & reg); + input_report_key(idev, KEY_Z, FO_BTN_ESCAPE & reg); + input_report_key(idev, KEY_X, FO_BTN_VIEW & reg); + input_report_key(idev, KEY_C, FO_BTN_POWER & reg); + input_report_key(idev, KEY_V, FO_BTN_EDIT & reg); + input_report_key(idev, KEY_B, FO_BTN_RUN & reg); + input_sync(idev); + + return IRQ_HANDLED; +} + +static irqreturn_t fo_input_touch_handler(int irq, void *data) +{ + struct fo_input *input = data; + struct regmap *regmap = input->fomcu->regmap; + struct input_dev *idev = input->idev_touch; + struct device *parent = idev->dev.parent; + uint16_t buf[3]; + int err; + + err = regmap_bulk_read(regmap, FOMCU_REG_INPUT_TOUCH_X, &buf, ARRAY_SIZE(buf)); + if (err) { + dev_err(parent, "Failed to read touch inputs: %d\n", err); + return IRQ_NONE; + } + + input_report_key(idev, BTN_TOUCH, !!buf[2]); + input_report_key(idev, BTN_TOOL_FINGER, !!buf[2]); + input_report_abs(idev, ABS_X, buf[0]); + input_report_abs(idev, ABS_Y, buf[1]); + input_report_abs(idev, ABS_PRESSURE, buf[2]); + input_sync(idev); + + return IRQ_HANDLED; +} + +static irqreturn_t fo_input_headset_handler(int irq, void *data) +{ + struct fo_input *input = data; + struct regmap *regmap = input->fomcu->regmap; + struct input_dev *idev = input->idev_headset; + struct device *parent = idev->dev.parent; + unsigned int reg; + int err; + + err = regmap_read(regmap, FOMCU_REG_INPUT_HEADSET, ®); + if (err) { + dev_err(parent, "Failed to read headset states: %d\n", err); + return IRQ_NONE; + } + + input_report_switch(idev, SW_HEADPHONE_INSERT, reg & FO_HS_HPPRESENT); + input_report_switch(idev, SW_MICROPHONE_INSERT, reg & FO_HS_MICPRESENT); + input_report_key(idev, KEY_PLAYPAUSE, reg & FO_HS_BTN_A); + input_report_key(idev, KEY_VOLUMEUP, reg & FO_HS_BTN_B); + input_report_key(idev, KEY_VOLUMEDOWN, reg & FO_HS_BTN_C); + input_report_key(idev, KEY_VOICECOMMAND, reg & FO_HS_BTN_D); + input_sync(idev); + + return IRQ_HANDLED; +} + +static const struct fo_irq fo_irqs[] = { + { .name = "flipper-one-input-btn", .handler = fo_input_btn_handler }, + { .name = "flipper-one-input-touch", .handler = fo_input_touch_handler }, + { .name = "flipper-one-input-headset", .handler = fo_input_headset_handler }, +}; + +static int fo_input_probe(struct platform_device *pdev) +{ + struct fomcu_device *fomcu = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct fo_input *input; + struct input_dev *idev_btn, *idev_touch, *idev_headset; + int irq, err, i; + + input = devm_kzalloc(dev, sizeof(*input), GFP_KERNEL); + if (!input) + return -ENOMEM; + + input->fomcu = fomcu; + + idev_btn = devm_input_allocate_device(dev); + if (!idev_btn) { + dev_err(dev, "Failed to allocate buttons input device\n"); + return -ENOMEM; + } + input->idev_btn = idev_btn; + + idev_btn->name = "Flipper One Buttons"; + idev_btn->phys = "flipper-one-input/input0"; + idev_btn->id.bustype = BUS_I2C; + + idev_touch = devm_input_allocate_device(dev); + if (!idev_touch) { + dev_err(dev, "Failed to allocate touch input device\n"); + return -ENOMEM; + } + input->idev_touch = idev_touch; + + idev_touch->name = "Flipper One Touchpad"; + idev_touch->phys = "flipper-one-input/input1"; + idev_touch->id.bustype = BUS_I2C; + + idev_headset = devm_input_allocate_device(dev); + if (!idev_headset) { + dev_err(dev, "Failed to allocate headset input device\n"); + return -ENOMEM; + } + input->idev_headset = idev_headset; + + idev_headset->name = "Flipper One Headset"; + idev_headset->phys = "flipper-one-input/input2"; + idev_headset->id.bustype = BUS_I2C; + + /* Buttons */ + input_set_capability(idev_btn, EV_KEY, KEY_ENTER); /* D-pad center */ + input_set_capability(idev_btn, EV_KEY, KEY_UP); /* D-pad up */ + input_set_capability(idev_btn, EV_KEY, KEY_DOWN); /* D-pad down */ + input_set_capability(idev_btn, EV_KEY, KEY_LEFT); /* D-pad left */ + input_set_capability(idev_btn, EV_KEY, KEY_RIGHT); /* D-pad right */ + input_set_capability(idev_btn, EV_KEY, KEY_TAB); /* App switcher */ + input_set_capability(idev_btn, EV_KEY, KEY_BACKSPACE); /* Back */ + input_set_capability(idev_btn, EV_KEY, KEY_A); /* PTT */ + input_set_capability(idev_btn, EV_KEY, KEY_Z); /* Escape */ + input_set_capability(idev_btn, EV_KEY, KEY_X); /* View */ + input_set_capability(idev_btn, EV_KEY, KEY_C); /* Power */ + input_set_capability(idev_btn, EV_KEY, KEY_V); /* Edit */ + input_set_capability(idev_btn, EV_KEY, KEY_B); /* Run */ + + /* Touchpad */ + input_set_capability(idev_touch, EV_KEY, BTN_TOUCH); + input_set_capability(idev_touch, EV_KEY, BTN_TOOL_FINGER); + input_set_abs_params(idev_touch, ABS_X, 0, 1024, 0, 0); + input_set_abs_params(idev_touch, ABS_Y, 0, 800, 0, 0); + input_set_abs_params(idev_touch, ABS_PRESSURE, 0, 12288, 0, 0); + __set_bit(INPUT_PROP_POINTER, idev_touch->propbit); + + /* Headset */ + input_set_capability(idev_headset, EV_SW, SW_HEADPHONE_INSERT); + input_set_capability(idev_headset, EV_SW, SW_MICROPHONE_INSERT); + input_set_capability(idev_headset, EV_KEY, KEY_PLAYPAUSE); + input_set_capability(idev_headset, EV_KEY, KEY_VOLUMEUP); + input_set_capability(idev_headset, EV_KEY, KEY_VOLUMEDOWN); + input_set_capability(idev_headset, EV_KEY, KEY_VOICECOMMAND); + + device_set_wakeup_capable(dev, true); + device_wakeup_enable(dev); + + for (i = 0; i < ARRAY_SIZE(fo_irqs); i++) { + irq = platform_get_irq_byname(pdev, fo_irqs[i].name); + if (irq < 0) + return dev_err_probe(dev, irq, "Failed to get IRQ %s\n", + fo_irqs[i].name); + + err = devm_request_threaded_irq(dev, irq, NULL, fo_irqs[i].handler, + IRQF_ONESHOT | IRQF_NO_SUSPEND, + fo_irqs[i].name, input); + if (err) + return dev_err_probe(dev, err, "Failed to request IRQ %s\n", + fo_irqs[i].name); + } + + err = input_register_device(idev_btn); + if (err) + return dev_err_probe(dev, err, "Failed to register buttons input device\n"); + + err = input_register_device(idev_touch); + if (err) + return dev_err_probe(dev, err, "Failed to register touch input device\n"); + + err = input_register_device(idev_headset); + if (err) + return dev_err_probe(dev, err, "Failed to register headset input device\n"); + + return 0; +} + +static const struct platform_device_id fo_input_id_table[] = { + { "flipper-one-input", }, + { } +}; +MODULE_DEVICE_TABLE(platform, fo_input_id_table); + +static struct platform_driver fo_input_driver = { + .driver = { + .name = "flipper-one-input", + }, + .probe = fo_input_probe, + .id_table = fo_input_id_table, +}; +module_platform_driver(fo_input_driver); + +MODULE_DESCRIPTION("Flipper One buttons and touchpad driver"); +MODULE_AUTHOR("Alexey Charkov "); +MODULE_LICENSE("GPL"); From cac91285cccd4cd3a5e36fe7150ca107a0738b6f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Apr 2026 14:15:26 +0400 Subject: [PATCH 196/218] leds: rgb: Add support for Flipper One LEDs The Flipper One networking multitool has a number of RGB LEDs on its front panel, four of which (the "link" LEDs) can be controlled by the user. They are driven by the Flipper One MCU, which exposes them as registers over the I2C interconnect bus (single RGB565 value per LED). Add a driver to expose them to the user. Signed-off-by: Alexey Charkov --- drivers/leds/rgb/Kconfig | 11 +++ drivers/leds/rgb/Makefile | 1 + drivers/leds/rgb/leds-flipper-one.c | 133 ++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 drivers/leds/rgb/leds-flipper-one.c diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig index 28ef4c487367cf..3e83dea254a72e 100644 --- a/drivers/leds/rgb/Kconfig +++ b/drivers/leds/rgb/Kconfig @@ -14,6 +14,17 @@ config LEDS_GROUP_MULTICOLOR To compile this driver as a module, choose M here: the module will be called leds-group-multicolor. +config LEDS_FLIPPER_ONE + tristate "LED support for Flipper One" + depends on MFD_FLIPPER_ONE + help + This option enables support for the four RGB link LEDs on the + Flipper One networking multitool. Each LED is driven via a packed + RGB565 value written to the Flipper One MCU over I2C. + + To compile this driver as a module, choose M here: the module + will be called leds-flipper-one. + config LEDS_KTD202X tristate "LED support for KTD202x Chips" depends on I2C diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile index be45991f63f50d..234682f96ba2fb 100644 --- a/drivers/leds/rgb/Makefile +++ b/drivers/leds/rgb/Makefile @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_LEDS_GROUP_MULTICOLOR) += leds-group-multicolor.o +obj-$(CONFIG_LEDS_FLIPPER_ONE) += leds-flipper-one.o obj-$(CONFIG_LEDS_KTD202X) += leds-ktd202x.o obj-$(CONFIG_LEDS_LP5812) += leds-lp5812.o obj-$(CONFIG_LEDS_NCP5623) += leds-ncp5623.o diff --git a/drivers/leds/rgb/leds-flipper-one.c b/drivers/leds/rgb/leds-flipper-one.c new file mode 100644 index 00000000000000..837513486c61bd --- /dev/null +++ b/drivers/leds/rgb/leds-flipper-one.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Flipper One LED driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include + +#define FOLED_NUM_LEDS 4 +#define FOLED_NUM_COLORS 3 + +/* RGB565 component shifts and max values */ +#define FOLED_R_SHIFT 11 +#define FOLED_G_SHIFT 5 +#define FOLED_B_SHIFT 0 +#define FOLED_R_MAX 31 +#define FOLED_G_MAX 63 +#define FOLED_B_MAX 31 + +struct foled_device; + +struct foled_led { + struct foled_device *foled; + struct led_classdev_mc mc_cdev; + struct mc_subled subleds[FOLED_NUM_COLORS]; + unsigned int reg; +}; + +struct foled_device { + struct regmap *regmap; + struct foled_led leds[FOLED_NUM_LEDS]; +}; + +static const unsigned int foled_regs[FOLED_NUM_LEDS] = { + FOMCU_REG_LEDS_COLOR_LINK1, + FOMCU_REG_LEDS_COLOR_LINK2, + FOMCU_REG_LEDS_COLOR_LINK3, + FOMCU_REG_LEDS_COLOR_LINK4, +}; + +static const char * const foled_led_names[FOLED_NUM_LEDS] = { + "flipper-one:rgb:link", + "flipper-one:rgb:wifi", + "flipper-one:rgb:eth1", + "flipper-one:rgb:eth0", +}; + +static int foled_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); + struct foled_led *led = container_of(mc_cdev, struct foled_led, mc_cdev); + u16 r, g, b, rgb565; + + led_mc_calc_color_components(mc_cdev, brightness); + + r = (u16)mc_cdev->subled_info[0].brightness * FOLED_R_MAX / LED_FULL; + g = (u16)mc_cdev->subled_info[1].brightness * FOLED_G_MAX / LED_FULL; + b = (u16)mc_cdev->subled_info[2].brightness * FOLED_B_MAX / LED_FULL; + + rgb565 = (r << FOLED_R_SHIFT) | (g << FOLED_G_SHIFT) | (b << FOLED_B_SHIFT); + + return regmap_write(led->foled->regmap, led->reg, rgb565); +} + +static int foled_probe(struct platform_device *pdev) +{ + struct foled_device *foled; + struct foled_led *led; + struct led_classdev *cdev; + int i, ret; + + foled = devm_kzalloc(&pdev->dev, sizeof(*foled), GFP_KERNEL); + if (!foled) + return -ENOMEM; + + foled->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!foled->regmap) + return dev_err_probe(&pdev->dev, -ENODEV, + "Failed to get parent regmap\n"); + + for (i = 0; i < FOLED_NUM_LEDS; i++) { + led = &foled->leds[i]; + led->foled = foled; + led->reg = foled_regs[i]; + + led->subleds[0].color_index = LED_COLOR_ID_RED; + led->subleds[1].color_index = LED_COLOR_ID_GREEN; + led->subleds[2].color_index = LED_COLOR_ID_BLUE; + + led->mc_cdev.subled_info = led->subleds; + led->mc_cdev.num_colors = FOLED_NUM_COLORS; + + cdev = &led->mc_cdev.led_cdev; + cdev->name = foled_led_names[i]; + cdev->max_brightness = LED_FULL; + cdev->brightness_set_blocking = foled_brightness_set; + cdev->flags = LED_CORE_SUSPENDRESUME; + + ret = devm_led_classdev_multicolor_register(&pdev->dev, + &led->mc_cdev); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Failed to register LED %d\n", i + 1); + } + + platform_set_drvdata(pdev, foled); + return 0; +} + +static const struct platform_device_id foled_id_table[] = { + { "flipper-one-leds" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(platform, foled_id_table); + +static struct platform_driver foled_driver = { + .probe = foled_probe, + .driver = { + .name = "flipper-one-leds", + }, + .id_table = foled_id_table, +}; +module_platform_driver(foled_driver); + +MODULE_DESCRIPTION("Flipper One LED driver"); +MODULE_AUTHOR("Alexey Charkov "); +MODULE_LICENSE("GPL"); From ee54e0bb87f60a428106291c638ecdb244b2e4e1 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 2 Apr 2026 12:59:52 +0400 Subject: [PATCH 197/218] ASoC: dt-bindings: nau8822: Add supply regulators NAU8822 has 4 power supply pins: VDDA, VDDB, VDDC and VDDSPK, which need to be online and stable before communication with the device is attempted. List them (as optional) so that device tree users can ensure correct power sequencing. Signed-off-by: Alexey Charkov --- .../devicetree/bindings/sound/nuvoton,nau8822.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/nuvoton,nau8822.yaml b/Documentation/devicetree/bindings/sound/nuvoton,nau8822.yaml index cb8182bbc491fc..0a8e40a140c3a9 100644 --- a/Documentation/devicetree/bindings/sound/nuvoton,nau8822.yaml +++ b/Documentation/devicetree/bindings/sound/nuvoton,nau8822.yaml @@ -30,6 +30,20 @@ properties: clock-names: const: mclk + vdda-supply: + description: Analog power supply + + vddb-supply: + description: Digital buffer (input/output) supply + + vddc-supply: + description: Digital core supply + + vddspk-supply: + description: + Speaker supply (power supply pin for RSPKOUT, LSPKOUT, AUXOUT2 and + AUXTOUT1 output drivers) + nuvoton,spk-btl: description: If set, configure the two loudspeaker outputs as a Bridge Tied Load output From 2f10ed8b651d97fc1fe1ec8e248bbac8e44678a3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 2 Apr 2026 13:03:59 +0400 Subject: [PATCH 198/218] ASoC: codecs: nau8822: add support for supply regulators NAU8822 has four power supply pins: VDDA, VDDB, VDDC, and VDDSPK, which need to be online and stable before communication with the device is attempted. Request and enable these regulators at init time, if provided. Also wait for 100 us after powering up the supply regulators before attempting to access the device registers, as recommended by the datasheet. This helps avoid -ENXIO errors when the codec is probed before the regulators are ready. Signed-off-by: Alexey Charkov --- sound/soc/codecs/nau8822.c | 46 +++++++++++++++++++++++++++++++++++--- sound/soc/codecs/nau8822.h | 3 +++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/sound/soc/codecs/nau8822.c b/sound/soc/codecs/nau8822.c index a11759f85eaca1..cd4a7de47939b6 100644 --- a/sound/soc/codecs/nau8822.c +++ b/sound/soc/codecs/nau8822.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -108,6 +109,10 @@ static const struct reg_default nau8822_reg_defaults[] = { { NAU8822_REG_OUTPUT_TIEOFF, 0x0000 }, }; +static const char * const nau8822_supply_names[NAU8822_NUM_SUPPLIES] = { + "vdda", "vddb", "vddc", "vddspk", +}; + static bool nau8822_readable_reg(struct device *dev, unsigned int reg) { switch (reg) { @@ -1056,6 +1061,7 @@ static int nau8822_suspend(struct snd_soc_component *component) struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(component); snd_soc_dapm_force_bias_level(dapm, SND_SOC_BIAS_OFF); + regulator_bulk_disable(NAU8822_NUM_SUPPLIES, nau8822->supplies); regcache_mark_dirty(nau8822->regmap); @@ -1066,6 +1072,15 @@ static int nau8822_resume(struct snd_soc_component *component) { struct nau8822 *nau8822 = snd_soc_component_get_drvdata(component); struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(component); + int ret = regulator_bulk_enable(NAU8822_NUM_SUPPLIES, nau8822->supplies); + + if (ret) { + dev_err(component->dev, + "Failed to enable regulators: %d\n", ret); + return ret; + } + + fsleep(100); regcache_sync(nau8822->regmap); @@ -1153,7 +1168,7 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) { struct device *dev = &i2c->dev; struct nau8822 *nau8822 = dev_get_platdata(dev); - int ret; + int ret, i; if (!nau8822) { nau8822 = devm_kzalloc(dev, sizeof(*nau8822), GFP_KERNEL); @@ -1167,6 +1182,13 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) return dev_err_probe(&i2c->dev, PTR_ERR(nau8822->mclk), "Error getting mclk\n"); + for (i = 0; i < NAU8822_NUM_SUPPLIES; i++) + nau8822->supplies[i].supply = nau8822_supply_names[i]; + + ret = devm_regulator_bulk_get(dev, NAU8822_NUM_SUPPLIES, nau8822->supplies); + if (ret) + return dev_err_probe(dev, ret, "Failed to get regulators\n"); + nau8822->regmap = devm_regmap_init_i2c(i2c, &nau8822_regmap_config); if (IS_ERR(nau8822->regmap)) { ret = PTR_ERR(nau8822->regmap); @@ -1175,21 +1197,38 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) } nau8822->dev = dev; + ret = regulator_bulk_enable(NAU8822_NUM_SUPPLIES, nau8822->supplies); + if (ret) + return dev_err_probe(dev, ret, "Failed to enable regulators\n"); + + fsleep(100); + /* Reset the codec */ ret = regmap_write(nau8822->regmap, NAU8822_REG_RESET, 0x00); if (ret != 0) { dev_err(&i2c->dev, "Failed to issue reset: %d\n", ret); - return ret; + goto err_reg; } ret = devm_snd_soc_register_component(dev, &soc_component_dev_nau8822, &nau8822_dai, 1); if (ret != 0) { dev_err(&i2c->dev, "Failed to register CODEC: %d\n", ret); - return ret; + goto err_reg; } return 0; + +err_reg: + regulator_bulk_disable(NAU8822_NUM_SUPPLIES, nau8822->supplies); + return ret; +} + +static void nau8822_i2c_remove(struct i2c_client *i2c) +{ + struct nau8822 *nau8822 = i2c_get_clientdata(i2c); + + regulator_bulk_disable(NAU8822_NUM_SUPPLIES, nau8822->supplies); } static const struct i2c_device_id nau8822_i2c_id[] = { @@ -1212,6 +1251,7 @@ static struct i2c_driver nau8822_i2c_driver = { .of_match_table = of_match_ptr(nau8822_of_match), }, .probe = nau8822_i2c_probe, + .remove = nau8822_i2c_remove, .id_table = nau8822_i2c_id, }; module_i2c_driver(nau8822_i2c_driver); diff --git a/sound/soc/codecs/nau8822.h b/sound/soc/codecs/nau8822.h index 13fe0a091e9ed4..24799c7b5931b8 100644 --- a/sound/soc/codecs/nau8822.h +++ b/sound/soc/codecs/nau8822.h @@ -211,6 +211,8 @@ struct nau8822_pll { int freq_out; }; +#define NAU8822_NUM_SUPPLIES 4 + /* Codec Private Data */ struct nau8822 { struct device *dev; @@ -219,6 +221,7 @@ struct nau8822 { struct nau8822_pll pll; int sysclk; int div_id; + struct regulator_bulk_data supplies[NAU8822_NUM_SUPPLIES]; }; #endif /* __NAU8822_H__ */ From 9008e6aacad565bbccd34b449a7a3f1ed91687ac Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:50:45 +0400 Subject: [PATCH 199/218] arm64: dts: rockchip: rk3576: assign dclk_vp{0,1}_src to VPLL Reparent dclk_vp{0,1}_src from GPLL to VPLL on Rockchip RK3576. VPLL is a programmable PLL with no other consumers, allowing the CCF to synthesize accurate pixel clocks for the two display outputs with arbitrary modes, as long as only one of them is active at a time (or using the HDMI PHY as the clock source, which works for HDMI modes up to 4K@60Hz). This gives much greater flexibility for display modes as compared to the boot-time default of using GPLL, which is effectively fixed at 1188 MHz due to a large number of system components depending on it and making runtime rate changes unrealistic. Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index f98d7803dc3236..239196b137aef0 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -1379,6 +1379,8 @@ "dclk_vp1", "dclk_vp2", "pll_hdmiphy0"; + assigned-clocks = <&cru DCLK_VP0_SRC>, <&cru DCLK_VP1_SRC>; + assigned-clock-parents = <&cru PLL_VPLL>, <&cru PLL_VPLL>; iommus = <&vop_mmu>; power-domains = <&power RK3576_PD_VOP>; rockchip,grf = <&sys_grf>; From fdefd9cd65e6e9217b3e72b98b1231d5ed7b4500 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Apr 2026 17:04:11 +0400 Subject: [PATCH 200/218] clk: rockchip: Fix fractional PLL calculation for RK3588 RK3588/RK3576 have several fractional PLLs using a delta-sigma adjustment factor (k) for the fractional component of the PLL output frequency. According to the TRM, the k is a two's complement 16-bit value, not an unsiged integer (see for example the TRM description of CRU_GPLL_CON2, and also a related U-boot fix at [1]). Adjust the code to properly handle negative k values. While at it, also fix the denominator to use the correct value of 65536, not 65535 for the full range of PLL ratios. Cc: stable@vger.kernel.org Fixes: 8f6594494b1c ("clk: rockchip: add pll type for RK3588") Link: https://lore.kernel.org/all/20231012101828.27195-1-zhangqing@rock-chips.com/ [1] Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk-pll.c | 10 +++++----- drivers/clk/rockchip/clk.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/clk/rockchip/clk-pll.c b/drivers/clk/rockchip/clk-pll.c index 6b853800cb6bc6..23a1d4ee9ccb97 100644 --- a/drivers/clk/rockchip/clk-pll.c +++ b/drivers/clk/rockchip/clk-pll.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include "clk.h" @@ -897,7 +898,7 @@ static void rockchip_rk3588_pll_get_params(struct rockchip_clk_pll *pll, rate->s = ((pllcon >> RK3588_PLLCON1_S_SHIFT) & RK3588_PLLCON1_S_MASK); pllcon = readl_relaxed(pll->reg_base + RK3588_PLLCON(2)); - rate->k = ((pllcon >> RK3588_PLLCON2_K_SHIFT) & RK3588_PLLCON2_K_MASK); + rate->k = (s16)((pllcon >> RK3588_PLLCON2_K_SHIFT) & RK3588_PLLCON2_K_MASK); } static unsigned long rockchip_rk3588_pll_recalc_rate(struct clk_hw *hw, unsigned long prate) @@ -913,11 +914,10 @@ static unsigned long rockchip_rk3588_pll_recalc_rate(struct clk_hw *hw, unsigned if (cur.k) { /* fractional mode */ - u64 frac_rate64 = prate * cur.k; + s64 frac_rate64 = prate * cur.k; - postdiv = cur.p * 65535; - do_div(frac_rate64, postdiv); - rate64 += frac_rate64; + postdiv = cur.p * 65536ULL; + rate64 += div_s64(frac_rate64, postdiv); } rate64 = rate64 >> cur.s; diff --git a/drivers/clk/rockchip/clk.h b/drivers/clk/rockchip/clk.h index cf0f5f11c34b5a..5136bf4b5f69c8 100644 --- a/drivers/clk/rockchip/clk.h +++ b/drivers/clk/rockchip/clk.h @@ -638,7 +638,7 @@ struct rockchip_pll_rate_table { unsigned int m; unsigned int p; unsigned int s; - unsigned int k; + int k; }; }; }; From dda2c72c32a14619d20f9e8486fea5a3a5c14f25 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Apr 2026 13:51:32 +0400 Subject: [PATCH 201/218] clk: rockchip: rk3576: add PLL rates for weird display clocks Add precomputed PLL parameters for the display clocks found out in the wild which can't be cleanly derived from the existing PLL rates. These have been computed in a semi-bruteforce way to solve for low overall error while preferring smaller integer dividers and multipliers, and avoiding the fractional delta-sigma K component where possible. Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk-rk3576.c | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/drivers/clk/rockchip/clk-rk3576.c b/drivers/clk/rockchip/clk-rk3576.c index 2557358e0b9d87..48272314613ae2 100644 --- a/drivers/clk/rockchip/clk-rk3576.c +++ b/drivers/clk/rockchip/clk-rk3576.c @@ -68,13 +68,16 @@ static struct rockchip_pll_rate_table rk3576_pll_rates[] = { RK3588_PLL_RATE(1536000000, 2, 256, 1, 0), RK3588_PLL_RATE(1512000000, 2, 252, 1, 0), RK3588_PLL_RATE(1488000000, 2, 248, 1, 0), + RK3588_PLL_RATE(1478740000, 1, 123, 1, 14964), RK3588_PLL_RATE(1464000000, 2, 244, 1, 0), RK3588_PLL_RATE(1440000000, 2, 240, 1, 0), RK3588_PLL_RATE(1416000000, 2, 236, 1, 0), RK3588_PLL_RATE(1392000000, 2, 232, 1, 0), RK3588_PLL_RATE(1320000000, 2, 220, 1, 0), + RK3588_PLL_RATE(1275830000, 1, 851, 4, -29273), RK3588_PLL_RATE(1200000000, 2, 200, 1, 0), RK3588_PLL_RATE(1188000000, 2, 198, 1, 0), + RK3588_PLL_RATE(1186813000, 1, 791, 4, 13675), RK3588_PLL_RATE(1100000000, 3, 550, 2, 0), RK3588_PLL_RATE(1008000000, 2, 336, 2, 0), RK3588_PLL_RATE(1000000000, 3, 500, 2, 0), @@ -87,11 +90,43 @@ static struct rockchip_pll_rate_table rk3576_pll_rates[] = { RK3588_PLL_RATE(786000000, 1, 131, 2, 0), RK3588_PLL_RATE(785560000, 3, 392, 2, 51117), RK3588_PLL_RATE(722534400, 8, 963, 2, 24850), + RK3588_PLL_RATE(645000000, 1, 215, 3, 0), RK3588_PLL_RATE(600000000, 2, 200, 2, 0), RK3588_PLL_RATE(594000000, 2, 198, 2, 0), + RK3588_PLL_RATE(593410000, 1, 791, 5, 13981), + RK3588_PLL_RATE(593407000, 7, 173, 0, 5049), + RK3588_PLL_RATE(533250000, 4, 355, 2, 32767), + RK3588_PLL_RATE(497750000, 3, 124, 1, 28672), + RK3588_PLL_RATE(488400000, 5, 407, 2, 0), RK3588_PLL_RATE(408000000, 2, 272, 3, 0), + RK3588_PLL_RATE(368881000, 5, 307, 2, 26269), RK3588_PLL_RATE(312000000, 2, 208, 3, 0), + RK3588_PLL_RATE(304250000, 4, 811, 4, 10923), + RK3588_PLL_RATE(296703000, 5, 989, 4, 655), + RK3588_PLL_RATE(296700000, 5, 989, 4, 0), + RK3588_PLL_RATE(285500000, 3, 571, 4, 0), + RK3588_PLL_RATE(277250000, 3, 69, 1, 20480), + RK3588_PLL_RATE(262750000, 4, 701, 4, 10923), + RK3588_PLL_RATE(248880000, 1, 166, 4, -5243), + RK3588_PLL_RATE(245500000, 3, 491, 4, 0), + RK3588_PLL_RATE(241700000, 1, 81, 3, -28399), + RK3588_PLL_RATE(241500000, 1, 161, 4, 0), + RK3588_PLL_RATE(237600000, 5, 99, 1, 0), RK3588_PLL_RATE(216000000, 2, 288, 4, 0), + RK3588_PLL_RATE(193250000, 3, 773, 5, 0), + RK3588_PLL_RATE(177500000, 3, 355, 4, 0), + RK3588_PLL_RATE(174250000, 3, 87, 2, 8192), + RK3588_PLL_RATE(167000000, 3, 167, 3, 0), + RK3588_PLL_RATE(163240000, 5, 272, 3, 4369), + RK3588_PLL_RATE(148360000, 5, 989, 5, 4369), + RK3588_PLL_RATE(148352000, 1, 396, 6, -25865), + RK3588_PLL_RATE(147180000, 5, 981, 5, 13107), + RK3588_PLL_RATE(146250000, 1, 195, 5, 0), + RK3588_PLL_RATE(119000000, 3, 119, 3, 0), + RK3588_PLL_RATE(116460000, 1, 156, 5, -23593), + RK3588_PLL_RATE(108108000, 3, 865, 6, -8913), + RK3588_PLL_RATE(100700000, 2, 537, 6, 4369), + RK3588_PLL_RATE(100680000, 5, 671, 5, 13107), RK3588_PLL_RATE(96000000, 2, 256, 5, 0), { /* sentinel */ }, }; From fffd4f98e976c3ee745647dfa96a26d665f350ac Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:46:19 +0400 Subject: [PATCH 202/218] clk: rockchip: rk3576: allow dclk_vp{0,1}_src to propagate rate to parent PLL dclk_vp{0,1}_src muxes feed the display clock for Video Ports 0 and 1. With CLK_SET_RATE_NO_REPARENT the mux is locked to its current parent, but without CLK_SET_RATE_PARENT rate requests stop at the integer divider and never reach the parent PLL, making it impossible to achieve certain pixel clock frequencies. Add CLK_SET_RATE_PARENT so that when dclk_vp{0,1}_src is reparented to a programmable PLL (e.g. VPLL via assigned-clock-parents), the CCF divider can ask the PLL to retune, utilizing its fractional capabilities to obtain the exact pixel clock. This flag relies on reparenting the VP0-1 source clock to VPLL at DT level to ensure no consumer calls clk_set_rate on dclk_vp{0,1} while its parent is set to the boot-time default of GPLL, which may skew clocks for other consumers. Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk-rk3576.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/clk/rockchip/clk-rk3576.c b/drivers/clk/rockchip/clk-rk3576.c index 48272314613ae2..39b8dbd0f2af9a 100644 --- a/drivers/clk/rockchip/clk-rk3576.c +++ b/drivers/clk/rockchip/clk-rk3576.c @@ -1137,10 +1137,10 @@ static struct rockchip_clk_branch rk3576_clk_branches[] __initdata = { RK3576_CLKGATE_CON(61), 8, GFLAGS), GATE(ACLK_VOP, "aclk_vop", "aclk_vop_root", 0, RK3576_CLKGATE_CON(61), 9, GFLAGS), - COMPOSITE(DCLK_VP0_SRC, "dclk_vp0_src", gpll_cpll_vpll_bpll_lpll_p, CLK_SET_RATE_NO_REPARENT, + COMPOSITE(DCLK_VP0_SRC, "dclk_vp0_src", gpll_cpll_vpll_bpll_lpll_p, CLK_SET_RATE_NO_REPARENT | CLK_SET_RATE_PARENT, RK3576_CLKSEL_CON(145), 8, 3, MFLAGS, 0, 8, DFLAGS, RK3576_CLKGATE_CON(61), 10, GFLAGS), - COMPOSITE(DCLK_VP1_SRC, "dclk_vp1_src", gpll_cpll_vpll_bpll_lpll_p, CLK_SET_RATE_NO_REPARENT, + COMPOSITE(DCLK_VP1_SRC, "dclk_vp1_src", gpll_cpll_vpll_bpll_lpll_p, CLK_SET_RATE_NO_REPARENT | CLK_SET_RATE_PARENT, RK3576_CLKSEL_CON(146), 8, 3, MFLAGS, 0, 8, DFLAGS, RK3576_CLKGATE_CON(61), 11, GFLAGS), COMPOSITE(DCLK_VP2_SRC, "dclk_vp2_src", gpll_cpll_vpll_bpll_lpll_p, CLK_SET_RATE_NO_REPARENT, From eefb842601fa173098bf2b466a530b85c37d1cd7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 20 Apr 2026 21:08:31 +0400 Subject: [PATCH 203/218] arm64: dts: rockchip: Add overlay for 4K DP and 2.5K HDMI on RK3576 Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/Makefile | 13 +++ .../dts/rockchip/rk3576-dp-4k-hdmi-2.5k.dtso | 83 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-dp-4k-hdmi-2.5k.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index 83ac2a9b0e9726..ff84b2fe5100d4 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -167,6 +167,7 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-wolfvision-pf5-io-expander.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-100ask-dshanpi-a1.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5-v1.2-wifibt.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-dp-4k-hdmi-2.5k.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb2-v10.dtb @@ -295,14 +296,26 @@ rk3568-wolfvision-pf5-vz-2-uhd-dtbs := rk3568-wolfvision-pf5.dtb \ rk3568-wolfvision-pf5-display-vz.dtbo \ rk3568-wolfvision-pf5-io-expander.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5-dp-4k-hdmi-2.5k.dtb +rk3576-armsom-sige5-dp-4k-hdmi-2.5k-dtbs := rk3576-armsom-sige5.dtb \ + rk3576-dp-4k-hdmi-2.5k.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5-v1.2-wifibt.dtb rk3576-armsom-sige5-v1.2-wifibt-dtbs := rk3576-armsom-sige5.dtb \ rk3576-armsom-sige5-v1.2-wifibt.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-dp-4k-hdmi-2.5k.dtb +rk3576-evb1-v10-dp-4k-hdmi-2.5k-dtbs := rk3576-evb1-v10.dtb \ + rk3576-dp-4k-hdmi-2.5k.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtb rk3576-evb1-v10-pcie1-dtbs := rk3576-evb1-v10.dtb \ rk3576-evb1-v10-pcie1.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-dp-4k-hdmi-2.5k.dtb +rk3576-flipper-one-dp-4k-hdmi-2.5k-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-dp-4k-hdmi-2.5k.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-i2c2-free.dtb rk3576-flipper-one-i2c2-free-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ rk3576-flipper-one-i2c2-free.dtbo diff --git a/arch/arm64/boot/dts/rockchip/rk3576-dp-4k-hdmi-2.5k.dtso b/arch/arm64/boot/dts/rockchip/rk3576-dp-4k-hdmi-2.5k.dtso new file mode 100644 index 00000000000000..b172c97004ce15 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-dp-4k-hdmi-2.5k.dtso @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay to switch from the default HDMI 4k and DP 2.5k mode to DP 4K and + * HDMI 2.5k mode. Should be applicable for any boards having both HDMI and DP + */ + +#include + +/dts-v1/; +/plugin/; + +&dp { + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + dp0_in: port@0 { + reg = <0>; + }; + }; +}; + +&dp0_in { + dp0_in_vp0: endpoint { + remote-endpoint = <&vp0_out_dp0>; + }; +}; + +&hdmi { + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + hdmi_in: port@0 { + reg = <0>; + }; + }; +}; + +&hdmi_in { + hdmi_in_vp1: endpoint { + remote-endpoint = <&vp1_out_hdmi>; + }; +}; + +&vop { + status = "okay"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + vp0: port@0 { + #address-cells = <1>; + #size-cells = <0>; + reg = <0>; + }; + + vp1: port@1 { + #address-cells = <1>; + #size-cells = <0>; + reg = <1>; + }; + }; +}; + +&vp0 { + vp0_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp0>; + }; +}; + +&vp1 { + vp1_out_hdmi: endpoint@ROCKCHIP_VOP2_EP_HDMI0 { + reg = ; + remote-endpoint = <&hdmi_in_vp1>; + }; +}; From 6bed8f721d241e90ed3f1b9c49cdda23f3dee395 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 19 May 2026 17:29:49 +0400 Subject: [PATCH 204/218] Bluetooth: btusb: Enable Mediatek modules to work on USB 3.0 busses MT7921U and possibly other related chips expose a different configuration on USB 3.0 vs. USB 2.0. Current driver logic only works with the USB 2.0 configuration, while connecting the module to a USB 3.0-only bus results in enumeration and scanning succeeding but connections silently failing. Add a special case for Mediatek modules on SuperSpeed busses to select the right bulk OUT endpoint (second not first) to make them work. Signed-off-by: Alexey Charkov --- drivers/bluetooth/btusb.c | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/drivers/bluetooth/btusb.c b/drivers/bluetooth/btusb.c index 830fefb342c6b1..437e4bec454a1c 100644 --- a/drivers/bluetooth/btusb.c +++ b/drivers/bluetooth/btusb.c @@ -4090,6 +4090,49 @@ static int btusb_probe(struct usb_interface *intf, return -ENODEV; } + /* + * MediaTek MT7921U (and likely related MTK combo chips), when + * attached over the SuperSpeed lanes, present a HCI interface with + * *two* bulk OUT endpoints: the lower-numbered one is a vendor / + * diagnostic pipe that silently consumes data without ever raising + * a transfer-complete event, and the higher-numbered one is the + * actual ACL OUT. usb_find_common_endpoints() returns the first + * match, which sinks all outbound HCI traffic and leaves the device + * able to scan but unable to complete any connection. + * + * Most MT7921U deployments wire the Bluetooth function to the USB + * 2.0 differential pair and enumerate at High-Speed, where the + * descriptor only exposes a single bulk OUT and this code path is + * a no-op. + */ + if ((id->driver_info & BTUSB_MEDIATEK) && + interface_to_usbdev(intf)->speed >= USB_SPEED_SUPER) { + struct usb_host_interface *alt = intf->cur_altsetting; + struct usb_endpoint_descriptor *ep, *second_bulk_out = NULL; + int i, bulk_out_count = 0; + + for (i = 0; i < alt->desc.bNumEndpoints; i++) { + ep = &alt->endpoint[i].desc; + if (!usb_endpoint_is_bulk_out(ep)) + continue; + if (++bulk_out_count == 2) + second_bulk_out = ep; + } + + if (bulk_out_count == 2 && second_bulk_out && + second_bulk_out != data->bulk_tx_ep) { + dev_info(&intf->dev, + "btusb: MTK: SuperSpeed: using bEP 0x%02x as ACL OUT (overrides diag bEP 0x%02x)\n", + second_bulk_out->bEndpointAddress, + data->bulk_tx_ep->bEndpointAddress); + data->bulk_tx_ep = second_bulk_out; + } else if (bulk_out_count > 2) { + dev_warn(&intf->dev, + "btusb: MTK: SuperSpeed: unexpected bulk OUT count %d, leaving default selection\n", + bulk_out_count); + } + } + if (id->driver_info & BTUSB_AMP) { data->cmdreq_type = USB_TYPE_CLASS | 0x01; data->cmdreq = 0x2b; From 4996f2dfa176be3e2a8f7514bdb96062a4d31185 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 1 Jun 2026 15:22:06 +0400 Subject: [PATCH 205/218] drm/rockchip: vop2: honor TV margins from CRTC state for overscan compensation Replace the hard-coded percent values with pixel margins carried in struct rockchip_crtc_state, sourced from the standard DRM "left/right/top/bottom margin" connector properties (struct drm_connector_tv_margins) to pave way for HDMI overscan compensation support. Signed-off-by: Alexey Charkov --- drivers/gpu/drm/rockchip/rockchip_drm_drv.h | 2 ++ drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 17 +++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_drv.h b/drivers/gpu/drm/rockchip/rockchip_drm_drv.h index 4ea8878e10a86d..926608da21a7bc 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_drv.h +++ b/drivers/gpu/drm/rockchip/rockchip_drm_drv.h @@ -10,6 +10,7 @@ #define _ROCKCHIP_DRM_DRV_H #include +#include #include #include @@ -54,6 +55,7 @@ struct rockchip_crtc_state { u32 bus_flags; int color_space; bool frl_enabled; + struct drm_connector_tv_margins tv_margins; }; #define to_rockchip_crtc_state(s) \ container_of(s, struct rockchip_crtc_state, base) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 9c802946c25022..62c0b940ef3112 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c @@ -1577,30 +1577,35 @@ static void vop2_post_config(struct drm_crtc *crtc) { struct vop2_video_port *vp = to_vop2_video_port(crtc); struct vop2 *vop2 = vp->vop2; + struct rockchip_crtc_state *vcstate = to_rockchip_crtc_state(crtc->state); struct drm_display_mode *mode = &crtc->state->adjusted_mode; + const struct drm_connector_tv_margins *m = &vcstate->tv_margins; u64 bgcolor = crtc->state->background_color; u16 vtotal = mode->crtc_vtotal; u16 hdisplay = mode->crtc_hdisplay; u16 hact_st = mode->crtc_htotal - mode->crtc_hsync_start; u16 vdisplay = mode->crtc_vdisplay; u16 vact_st = mode->crtc_vtotal - mode->crtc_vsync_start; - u32 left_margin = 100, right_margin = 100; - u32 top_margin = 100, bottom_margin = 100; - u16 hsize = hdisplay * (left_margin + right_margin) / 200; - u16 vsize = vdisplay * (top_margin + bottom_margin) / 200; + u16 hsize = hdisplay; + u16 vsize = vdisplay; u16 hact_end, vact_end; u32 val; vop2->ops->setup_bg_dly(vp); + if (m->left + m->right < hdisplay) + hsize = hdisplay - m->left - m->right; + if (m->top + m->bottom < vdisplay) + vsize = vdisplay - m->top - m->bottom; + vsize = rounddown(vsize, 2); hsize = rounddown(hsize, 2); - hact_st += hdisplay * (100 - left_margin) / 200; + hact_st += m->left; hact_end = hact_st + hsize; val = hact_st << 16; val |= hact_end; vop2_vp_write(vp, RK3568_VP_POST_DSP_HACT_INFO, val); - vact_st += vdisplay * (100 - top_margin) / 200; + vact_st += m->top; vact_end = vact_st + vsize; val = vact_st << 16; val |= vact_end; From f2531e9d8a9d975e79d60662dec3b9890705dd3e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 2 Jun 2026 16:58:06 +0400 Subject: [PATCH 206/218] drm/rockchip: dw_hdmi_qp: expose "overscan" property Expose the "overscan" connector property as recognized by KWin and the likes to compensate for TV overscan cropping. The CRTC will use the margin values derived from this overscan percentage in its post-composition scaler to add appropriate blank margins on all sides of the output image so that the TV doesn't eat up visible content. Signed-off-by: Alexey Charkov --- drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index 863e9238f2373b..4947b368bea040 100644 --- a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c @@ -158,13 +158,21 @@ dw_hdmi_qp_rockchip_encoder_atomic_check(struct drm_encoder *encoder, struct drm_connector_state *conn_state) { const struct drm_display_info *info = &conn_state->connector->display_info; + const struct drm_display_mode *adj_mode = &crtc_state->adjusted_mode; struct rockchip_crtc_state *s = to_rockchip_crtc_state(crtc_state); struct rockchip_hdmi_qp *hdmi = to_rockchip_hdmi_qp(encoder); struct dw_hdmi_qp_link_cfg *lcfg = &hdmi->link_cfg; union phy_configure_opts phy_cfg = {}; + unsigned int overscan; enum phy_hdmi_mode mode; int ret; + overscan = min(conn_state->tv.overscan, 100u); + s->tv_margins.left = adj_mode->hdisplay * overscan / 200; + s->tv_margins.right = s->tv_margins.left; + s->tv_margins.top = adj_mode->vdisplay * overscan / 200; + s->tv_margins.bottom = s->tv_margins.top; + if (lcfg->tmds_char_rate == conn_state->hdmi.tmds_char_rate && s->output_bpc == conn_state->hdmi.output_bpc) return 0; @@ -768,6 +776,14 @@ static int dw_hdmi_qp_rockchip_bind(struct device *dev, struct device *master, return dev_err_probe(dev, PTR_ERR(hdmi->connector), "Failed to init bridge connector\n"); + ret = drm_mode_create_tv_properties_legacy(drm, 0, NULL); + if (ret) + return dev_err_probe(dev, ret, + "Failed to create TV connector properties\n"); + + drm_object_attach_property(&hdmi->connector->base, + drm->mode_config.tv_overscan_property, 0); + ret = drm_connector_attach_encoder(hdmi->connector, encoder); if (ret) return dev_err_probe(dev, ret, "Failed to attach connector\n"); From 3c73f9ca0cb4d04feda4385551472040a154532c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 10 Apr 2026 00:53:11 +0400 Subject: [PATCH 207/218] ASoC: codecs: nau8822: add support for speaker gain boost setting The speaker amplifier can be supplied by a higher voltage than the rest of the codec, namely up to 5V. When the speaker supply voltage is above 3.6V, the speaker gain boost setting should be enabled to prevent distortion on speaker and AUX pins. Enable gain boost setting based on the reading of the VDDSPK supply regulator's voltage. Signed-off-by: Alexey Charkov --- sound/soc/codecs/nau8822.c | 19 ++++++++++++++++++- sound/soc/codecs/nau8822.h | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/sound/soc/codecs/nau8822.c b/sound/soc/codecs/nau8822.c index cd4a7de47939b6..535e43f0c26a11 100644 --- a/sound/soc/codecs/nau8822.c +++ b/sound/soc/codecs/nau8822.c @@ -10,6 +10,7 @@ // // Based on WM8974.c +#include "linux/regulator/consumer.h" #include #include #include @@ -1168,7 +1169,7 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) { struct device *dev = &i2c->dev; struct nau8822 *nau8822 = dev_get_platdata(dev); - int ret, i; + int ret, i, vddspk; if (!nau8822) { nau8822 = devm_kzalloc(dev, sizeof(*nau8822), GFP_KERNEL); @@ -1189,6 +1190,11 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) if (ret) return dev_err_probe(dev, ret, "Failed to get regulators\n"); + vddspk = regulator_get_voltage(nau8822->supplies[SUPPLY_VDDSPK].consumer); + if (vddspk < 0 && vddspk != -ENODEV) + return dev_err_probe(dev, vddspk, + "Failed to get VDDSPK voltage\n"); + nau8822->regmap = devm_regmap_init_i2c(i2c, &nau8822_regmap_config); if (IS_ERR(nau8822->regmap)) { ret = PTR_ERR(nau8822->regmap); @@ -1210,6 +1216,17 @@ static int nau8822_i2c_probe(struct i2c_client *i2c) goto err_reg; } + if (vddspk > 3600000) { + ret = regmap_update_bits(nau8822->regmap, + NAU8822_REG_OUTPUT_CONTROL, + NAU8822_SPKBST | + NAU8822_AUX2BST | + NAU8822_AUX1BST, 0x7); + if (ret != 0) + return dev_err_probe(dev, ret, + "Failed to update gain boost control\n"); + } + ret = devm_snd_soc_register_component(dev, &soc_component_dev_nau8822, &nau8822_dai, 1); if (ret != 0) { diff --git a/sound/soc/codecs/nau8822.h b/sound/soc/codecs/nau8822.h index 24799c7b5931b8..cefc2c3feca28d 100644 --- a/sound/soc/codecs/nau8822.h +++ b/sound/soc/codecs/nau8822.h @@ -196,6 +196,15 @@ #define NAU8822_RAUXSMUT 0x01 +/* NAU8822_REG_OUTPUT_CONTROL (0x31) */ +#define NAU8822_AOUTIMP (1 << 0) +#define NAU8822_TSEN (1 << 1) +#define NAU8822_SPKBST (1 << 2) +#define NAU8822_AUX2BST (1 << 3) +#define NAU8822_AUX1BST (1 << 4) +#define NAU8822_RDACLMX (1 << 5) +#define NAU8822_LDACLMX (1 << 6) + /* System Clock Source */ enum { NAU8822_CLK_MCLK, @@ -211,7 +220,13 @@ struct nau8822_pll { int freq_out; }; -#define NAU8822_NUM_SUPPLIES 4 +enum { + SUPPLY_VDDA = 0, + SUPPLY_VDDB, + SUPPLY_VDDC, + SUPPLY_VDDSPK, + NAU8822_NUM_SUPPLIES +}; /* Codec Private Data */ struct nau8822 { From 1f8449dfbb0ca4787c5b8af11f5cd224ab2695f3 Mon Sep 17 00:00:00 2001 From: Simon Wright Date: Wed, 10 Jun 2026 23:54:26 +1200 Subject: [PATCH 208/218] media: rkvdec: prime VDPU383 deblock state on RK3576 power-up The VDPU383 video decoder on the RK3576 comes out of power-up with its internal H.264 deblocking-context state uninitialised. Decoding then intermittently produces wrong pixels at the horizontal deblock edges (luma rows 4, 12 and 13 within each 16-row macroblock row), which propagate through the following P-frames. The corruption is non-deterministic and its rate varies between boards (a few percent up to ~40%); it affects H.264 only - HEVC and VP9 on the same IP are fine. The Rockchip vendor stack (BSP/MPP) avoids this by running a one-shot priming decode of a small canned bitstream at every decoder power-up (rk3576_workaround_init / rk3576_workaround_run); the mainline driver omits it. Port that priming sequence: allocate the descriptor buffer once at probe and run the priming decode on every pm_runtime resume, which catches every power-up. It is scoped to the VDPU383 variant and is best-effort - a failure is logged and decoding continues. With the priming in place, an H.264 stream that reproduced the race decodes bit-exact against avdec_h264 across 64 consecutive runs, versus the reproduced baseline race without it. The priming runs once per power-up and does not affect steady-state throughput. No devicetree change is required: the "link" register bank the priming sequence uses is already mapped. Signed-off-by: Simon Wright Signed-off-by: Alexey Charkov --- .../media/platform/rockchip/rkvdec/Makefile | 3 +- .../rkvdec/rkvdec-rk3576-workaround.c | 163 ++++++++++++++++++ .../media/platform/rockchip/rkvdec/rkvdec.c | 39 ++++- .../media/platform/rockchip/rkvdec/rkvdec.h | 8 + 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 drivers/media/platform/rockchip/rkvdec/rkvdec-rk3576-workaround.c diff --git a/drivers/media/platform/rockchip/rkvdec/Makefile b/drivers/media/platform/rockchip/rkvdec/Makefile index e629d571e4d893..057ed4da10fee6 100644 --- a/drivers/media/platform/rockchip/rkvdec/Makefile +++ b/drivers/media/platform/rockchip/rkvdec/Makefile @@ -12,4 +12,5 @@ rockchip-vdec-y += \ rkvdec-vdpu381-hevc.o \ rkvdec-vdpu383-h264.o \ rkvdec-vdpu383-hevc.o \ - rkvdec-vp9.o + rkvdec-vp9.o \ + rkvdec-rk3576-workaround.o diff --git a/drivers/media/platform/rockchip/rkvdec/rkvdec-rk3576-workaround.c b/drivers/media/platform/rockchip/rkvdec/rkvdec-rk3576-workaround.c new file mode 100644 index 00000000000000..1c8fd1bc4a9b48 --- /dev/null +++ b/drivers/media/platform/rockchip/rkvdec/rkvdec-rk3576-workaround.c @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Rockchip RK3576 (VDPU383) power-up priming workaround. + * + * The VDPU383 decoder on the RK3576 comes out of power-up with its internal + * deblocking-context state uninitialised. Without priming, H.264 decode + * intermittently produces wrong pixels at the horizontal deblock edges (luma + * rows 4, 12 and 13 within each 16-row macroblock row), propagating through + * subsequent P-frames. The failure is non-deterministic and its rate varies + * between boards. + * + * The Rockchip BSP avoids this by running a one-shot priming decode of a small + * canned bitstream at every decoder power-up (rk3576_workaround_init / + * rk3576_workaround_run). This ports that sequence: the priming buffer is + * allocated once at probe and the priming decode is run on every pm_runtime + * resume (which catches every power-up). It is best-effort - on failure a + * warning is logged and decoding continues (degraded for H.264). + * + * The descriptor layout and the link-bank register kick sequence are + * transcribed from the BSP rk3576_workaround_{init,run}; the 64-byte header is + * an opaque IP-init blob taken verbatim from the BSP. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "rkvdec.h" + +/* Opaque 64-byte IP-init header blob, verbatim from the BSP .rodata. */ +static const u8 rkvdec_rk3576_warmup_hdr[64] = { + 0x00, 0x00, 0x01, 0x65, 0x88, 0x82, 0x0b, 0x01, + 0x2f, 0x08, 0xc5, 0x00, 0x01, 0x51, 0x78, 0xe0, + 0x00, 0x24, 0xf7, 0x1c, 0x00, 0x04, 0xcc, 0xeb, + 0x89, 0xd7, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x26, 0x00, 0x10, 0x04, 0x08, 0x00, 0x08, + 0x80, 0x01, 0x00, 0x00, 0x00, 0x40, 0x01, 0xd8, + 0x07, 0x7c, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +/* "rk" recognition markers the IP looks for in the priming descriptor. */ +#define RK3576_WARMUP_SENTINEL_HI 0x76543210u +#define RK3576_WARMUP_SENTINEL_HI_PLUS2 0x76543212u +/* BSP allocates 8 KiB; the descriptor lives at +0x1000. */ +#define RK3576_WARMUP_BUF_SIZE 0x2000u + +/* Lay out the priming descriptor in the (zeroed) DMA buffer. Offsets and + * constants are transcribed verbatim from the BSP rk3576_workaround_init. + */ +static void rkvdec_rk3576_warmup_populate(void *buf, dma_addr_t iova) +{ + u8 *b8 = buf; + u32 *b32 = buf; + u32 iova_lo = lower_32_bits(iova); + + memcpy(&b8[0], &rkvdec_rk3576_warmup_hdr[0], 32); + memcpy(&b8[64], &rkvdec_rk3576_warmup_hdr[32], 16); + memcpy(&b8[80], &rkvdec_rk3576_warmup_hdr[48], 8); + memcpy(&b8[88], &rkvdec_rk3576_warmup_hdr[56], 4); + + b32[4128 / 4] = 0x00000001u; + b32[4148 / 4] = 0x0000ffffu; + b32[4160 / 4] = 0x00000101u; + b32[4176 / 4] = 0xffffffffu; + b32[4180 / 4] = 0x3ff3ffffu; + + b32[4352 / 4] = RK3576_WARMUP_SENTINEL_HI; + b32[4356 / 4] = 0x00000000u; + b32[4360 / 4] = 0x00000020u; + b32[4364 / 4] = 0x000000a8u; + b32[4368 / 4] = 0x00000002u; + b32[4372 / 4] = 0x00000002u; + b32[4376 / 4] = 0x00000040u; + + b32[4608 / 4] = iova_lo; + b32[4612 / 4] = iova_lo + 0x140u; + b32[4620 / 4] = iova_lo + 0x040u; + b32[4656 / 4] = iova_lo + 0x240u; + b32[4660 / 4] = 0x000000c0u; + b32[4688 / 4] = iova_lo + 0x340u; + b32[4692 / 4] = 0x00000200u; + b32[4768 / 4] = iova_lo + 0x540u; + b32[4960 / 4] = iova_lo + 0xb40u; + b32[4100 / 4] = RK3576_WARMUP_SENTINEL_HI_PLUS2; + b32[4096 / 4] = iova_lo + 0x540u; + b32[4104 / 4] = iova_lo + 0x1400u; + b32[4108 / 4] = iova_lo + 0x1020u; + b32[4112 / 4] = iova_lo + 0x1100u; + b32[4116 / 4] = iova_lo + 0x1200u; + + wmb(); /* descriptor must reach RAM before the HW reads it */ +} + +/* + * Allocate + populate the priming buffer (once, at probe). Device-managed, so + * it is freed automatically on device teardown. Returns 0 or negative errno. + */ +int rkvdec_rk3576_warmup_alloc(struct device *dev, void **out_cpu, + dma_addr_t *out_dma) +{ + void *cpu; + dma_addr_t dma; + + cpu = dmam_alloc_coherent(dev, RK3576_WARMUP_BUF_SIZE, &dma, GFP_KERNEL); + if (!cpu) + return -ENOMEM; + + rkvdec_rk3576_warmup_populate(cpu, dma); + *out_cpu = cpu; + *out_dma = dma; + return 0; +} + +/* + * Run the priming decode through the link bank. Clocks and the power domain + * must be on (true in pm_runtime resume). The priming decode raises no + * completion IRQ - only the status register - so it is polled (~20 ms budget). + * Returns 0 on clean completion, -EIO on HW error status, -ETIMEDOUT on no + * completion. + */ +int rkvdec_rk3576_warmup_run(void __iomem *link_base, dma_addr_t buf_iova) +{ + u32 status = 0; + int i; + + /* Kick sequence, verbatim from the BSP rk3576_workaround_run. */ + writel(0x8000u, link_base + 0x58); /* ip_en: BIT(15) only */ + writel(0x7ffffu, link_base + 0x54); /* ip watchdog */ + writel(0x10001u, link_base + 0x00); /* ccu/init */ + writel(lower_32_bits(buf_iova) + 0x1000u, + link_base + 0x04); /* cfg_addr -> descriptor */ + writel(0x1u, link_base + 0x08); /* link_mode = 1 */ + writel(0x1u, link_base + 0x18); /* link_en = 1 */ + wmb(); /* commit config before cfg_done kicks the HW */ + writel(0x1u, link_base + 0x0c); /* cfg_done */ + + for (i = 0; i < 200; i++) { + usleep_range(100, 150); + status = readl(link_base + 0x4c); + if (status) + break; + } + + /* Teardown: clear irq + status, zero the config registers. */ + writel(0xffff0000u, link_base + 0x48); + writel(0xffff0000u, link_base + 0x4c); + writel(0x0u, link_base + 0x00); + writel(0x0u, link_base + 0x08); + writel(0x0u, link_base + 0x18); + wmb(); + writel(0x0u, link_base + 0x58); + + if (i >= 200) + return -ETIMEDOUT; + if (status & 0x3fe) /* err_mask */ + return -EIO; + return 0; +} diff --git a/drivers/media/platform/rockchip/rkvdec/rkvdec.c b/drivers/media/platform/rockchip/rkvdec/rkvdec.c index 1d1e9bfef8e96e..9a9f607076e042 100644 --- a/drivers/media/platform/rockchip/rkvdec/rkvdec.c +++ b/drivers/media/platform/rockchip/rkvdec/rkvdec.c @@ -1817,6 +1817,24 @@ static int rkvdec_probe(struct platform_device *pdev) vb2_dma_contig_set_max_seg_size(&pdev->dev, DMA_BIT_MASK(32)); + /* + * RK3576/VDPU383 needs a power-up priming decode (see + * rkvdec-rk3576-workaround.c). Allocate the priming buffer once here; + * it is run on every pm_runtime resume. Best-effort. + */ + if (rkvdec->variant == &vdpu383_variant) { + ret = rkvdec_rk3576_warmup_alloc(&pdev->dev, + &rkvdec->rk3576_warmup_cpu, + &rkvdec->rk3576_warmup_dma); + if (ret) + dev_warn(&pdev->dev, + "rk3576 warmup alloc failed: %d (H.264 may corrupt)\n", + ret); + else + dev_info(&pdev->dev, + "RK3576 H.264 deblock-priming workaround enabled\n"); + } + irq = platform_get_irq(pdev, 0); if (irq <= 0) return -ENXIO; @@ -1881,8 +1899,27 @@ static void rkvdec_remove(struct platform_device *pdev) static int rkvdec_runtime_resume(struct device *dev) { struct rkvdec_dev *rkvdec = dev_get_drvdata(dev); + int ret; + + ret = clk_bulk_prepare_enable(rkvdec->num_clocks, rkvdec->clocks); + if (ret) + return ret; + + /* + * RK3576/VDPU383: prime internal deblock-context state after every + * power-up (clocks + power domain are up here). Without it H.264 + * decode races and corrupts horizontal deblock edges. Best-effort. + */ + if (rkvdec->variant == &vdpu383_variant && rkvdec->rk3576_warmup_cpu) { + ret = rkvdec_rk3576_warmup_run(rkvdec->link, + rkvdec->rk3576_warmup_dma); + if (ret) + dev_warn_ratelimited(dev, + "rk3576 warmup on resume failed: %d\n", + ret); + } - return clk_bulk_prepare_enable(rkvdec->num_clocks, rkvdec->clocks); + return 0; } static int rkvdec_runtime_suspend(struct device *dev) diff --git a/drivers/media/platform/rockchip/rkvdec/rkvdec.h b/drivers/media/platform/rockchip/rkvdec/rkvdec.h index a24be6638b6b7a..78452155677ce9 100644 --- a/drivers/media/platform/rockchip/rkvdec/rkvdec.h +++ b/drivers/media/platform/rockchip/rkvdec/rkvdec.h @@ -136,6 +136,11 @@ struct rkvdec_dev { struct clk *axi_clk; void __iomem *regs; void __iomem *link; + /* RK3576/VDPU383 power-up priming buffer; see + * rkvdec-rk3576-workaround.c + */ + void *rk3576_warmup_cpu; + dma_addr_t rk3576_warmup_dma; struct mutex vdev_lock; /* serializes ioctls */ struct delayed_work watchdog_work; struct gen_pool *sram_pool; @@ -180,6 +185,9 @@ void rkvdec_run_preamble(struct rkvdec_ctx *ctx, struct rkvdec_run *run); void rkvdec_run_postamble(struct rkvdec_ctx *ctx, struct rkvdec_run *run); void rkvdec_memcpy_toio(void __iomem *dst, void *src, size_t len); void rkvdec_schedule_watchdog(struct rkvdec_dev *rkvdec, u32 timeout_threshold); +int rkvdec_rk3576_warmup_alloc(struct device *dev, void **out_cpu, + dma_addr_t *out_dma); +int rkvdec_rk3576_warmup_run(void __iomem *link_base, dma_addr_t buf_iova); void rkvdec_quirks_disable_qos(struct rkvdec_ctx *ctx); From c05b7d9f6a967b68dc9a6b20c2b6729922c7e341 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 15:02:56 +0400 Subject: [PATCH 209/218] Input: add support for Flipper One haptic LRA Flipper One has a haptic LRA (linear resonant actuator) exposed via its MCU interconnect interface. Add a driver to expose the haptic actuator as a force feedback device. Signed-off-by: Alexey Charkov --- drivers/input/misc/Kconfig | 14 ++- drivers/input/misc/Makefile | 1 + drivers/input/misc/flipper-one-haptic.c | 160 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 drivers/input/misc/flipper-one-haptic.c diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig index b6817b25b4c7eb..d6897d1c13d6fa 100644 --- a/drivers/input/misc/Kconfig +++ b/drivers/input/misc/Kconfig @@ -184,11 +184,23 @@ config INPUT_FLIPPER_ONE help Say Y here to enable support for human-machine interfacing functions on the Flipper One networking multitool, including its - builtin buttons, touchpad, haptic feedback and headset inputs. + builtin buttons, touchpad and headset inputs. To compile this driver as a module, choose M here: the module will be called flipper-one-input. +config INPUT_FLIPPER_ONE_HAPTIC + tristate "Flipper One haptic feedback support" + depends on MFD_FLIPPER_ONE + help + Say Y here to enable support for the haptic feedback actuator + on the Flipper One networking multitool. Effects are played from + the controller's built-in waveform library through the force + feedback interface. + + To compile this driver as a module, choose M here: the + module will be called flipper-one-haptic. + config INPUT_PCSPKR tristate "PC Speaker support" depends on PCSPKR_PLATFORM diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile index 704b6cc9b41c1c..01777dc5cae8ef 100644 --- a/drivers/input/misc/Makefile +++ b/drivers/input/misc/Makefile @@ -37,6 +37,7 @@ obj-$(CONFIG_INPUT_DA9055_ONKEY) += da9055_onkey.o obj-$(CONFIG_INPUT_DA9063_ONKEY) += da9063_onkey.o obj-$(CONFIG_INPUT_E3X0_BUTTON) += e3x0-button.o obj-$(CONFIG_INPUT_FLIPPER_ONE) += flipper-one-input.o +obj-$(CONFIG_INPUT_FLIPPER_ONE_HAPTIC) += flipper-one-haptic.o obj-$(CONFIG_INPUT_DRV260X_HAPTICS) += drv260x.o obj-$(CONFIG_INPUT_DRV2665_HAPTICS) += drv2665.o obj-$(CONFIG_INPUT_DRV2667_HAPTICS) += drv2667.o diff --git a/drivers/input/misc/flipper-one-haptic.c b/drivers/input/misc/flipper-one-haptic.c new file mode 100644 index 00000000000000..046b0b5aff6555 --- /dev/null +++ b/drivers/input/misc/flipper-one-haptic.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Flipper One haptic feedback driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* Highest effect index in the controller's built-in waveform library */ +#define FO_HAPTIC_EFFECT_MAX 123 + +/* Number of effects userspace may keep uploaded at once */ +#define FO_HAPTIC_MAX_EFFECTS 16 + +struct fo_haptic { + struct fomcu_device *fomcu; + struct device *dev; + struct work_struct play_work; + u16 effect_val[FO_HAPTIC_MAX_EFFECTS]; + u16 play_val; +}; + +static void fo_haptic_play_work(struct work_struct *work) +{ + struct fo_haptic *haptic = container_of(work, struct fo_haptic, + play_work); + int err; + + err = regmap_write(haptic->fomcu->regmap, FOMCU_REG_HAPTIC, + READ_ONCE(haptic->play_val)); + if (err) + dev_err(haptic->dev, "Failed to write haptic register: %d\n", + err); +} + +static int fo_haptic_upload(struct input_dev *idev, struct ff_effect *effect, + struct ff_effect *old) +{ + struct fo_haptic *haptic = input_get_drvdata(idev); + unsigned int duration; + s16 id; + + if (effect->type != FF_PERIODIC || + effect->u.periodic.waveform != FF_CUSTOM) + return -EINVAL; + + /* custom_data holds a single s16: the library effect index */ + if (effect->u.periodic.custom_len != 1) + return -EINVAL; + + if (copy_from_user(&id, effect->u.periodic.custom_data, sizeof(id))) + return -EFAULT; + + if (id < 0 || id > FO_HAPTIC_EFFECT_MAX) + return -EINVAL; + + /* + * Durations 0 and 1 are reserved to mean "play the full + * library waveform", other values are in milliseconds + */ + duration = min_t(unsigned int, effect->replay.length, + FIELD_MAX(FOMCU_HAPTIC_DURATION)); + + haptic->effect_val[effect->id] = FOMCU_HAPTIC_PLAY | + FIELD_PREP(FOMCU_HAPTIC_EFFECT, id) | + FIELD_PREP(FOMCU_HAPTIC_DURATION, duration); + + return 0; +} + +static int fo_haptic_playback(struct input_dev *idev, int effect_id, int value) +{ + struct fo_haptic *haptic = input_get_drvdata(idev); + + WRITE_ONCE(haptic->play_val, + value ? haptic->effect_val[effect_id] : 0); + schedule_work(&haptic->play_work); + + return 0; +} + +static void fo_haptic_close(struct input_dev *idev) +{ + struct fo_haptic *haptic = input_get_drvdata(idev); + + cancel_work_sync(&haptic->play_work); + regmap_write(haptic->fomcu->regmap, FOMCU_REG_HAPTIC, 0); +} + +static int fo_haptic_probe(struct platform_device *pdev) +{ + struct fomcu_device *fomcu = dev_get_drvdata(pdev->dev.parent); + struct device *dev = &pdev->dev; + struct fo_haptic *haptic; + struct input_dev *idev; + int err; + + haptic = devm_kzalloc(dev, sizeof(*haptic), GFP_KERNEL); + if (!haptic) + return -ENOMEM; + + haptic->fomcu = fomcu; + haptic->dev = dev; + INIT_WORK(&haptic->play_work, fo_haptic_play_work); + + idev = devm_input_allocate_device(dev); + if (!idev) + return -ENOMEM; + + idev->name = "Flipper One Haptic"; + idev->phys = "flipper-one-haptic/input0"; + idev->id.bustype = BUS_I2C; + idev->close = fo_haptic_close; + input_set_drvdata(idev, haptic); + + input_set_capability(idev, EV_FF, FF_PERIODIC); + input_set_capability(idev, EV_FF, FF_CUSTOM); + + err = input_ff_create(idev, FO_HAPTIC_MAX_EFFECTS); + if (err) + return dev_err_probe(dev, err, "Failed to create FF device\n"); + + idev->ff->upload = fo_haptic_upload; + idev->ff->playback = fo_haptic_playback; + + err = input_register_device(idev); + if (err) + return dev_err_probe(dev, err, + "Failed to register input device\n"); + + return 0; +} + +static const struct platform_device_id fo_haptic_id_table[] = { + { "flipper-one-haptic", }, + { } +}; +MODULE_DEVICE_TABLE(platform, fo_haptic_id_table); + +static struct platform_driver fo_haptic_driver = { + .driver = { + .name = "flipper-one-haptic", + }, + .probe = fo_haptic_probe, + .id_table = fo_haptic_id_table, +}; +module_platform_driver(fo_haptic_driver); + +MODULE_DESCRIPTION("Flipper One haptic feedback driver"); +MODULE_AUTHOR("Alexey Charkov "); +MODULE_LICENSE("GPL"); From 5f8b21274ff4edbdb1e61c130b0d067b5876b20a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 18:29:58 +0400 Subject: [PATCH 210/218] usb: typec: ucsi: Add support for Flipper One MCU UCSI controller Flipper One is a handheld multi-tool device with an integrated MCU that exposes USB Type-C controller functionality among other things, following the UCSI specification. Add a driver for it. Signed-off-by: Alexey Charkov --- drivers/usb/typec/ucsi/Kconfig | 11 ++ drivers/usb/typec/ucsi/Makefile | 1 + drivers/usb/typec/ucsi/ucsi_flipper_one.c | 175 ++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 drivers/usb/typec/ucsi/ucsi_flipper_one.c diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig index 87dd992a4b9e9a..b99207da41b873 100644 --- a/drivers/usb/typec/ucsi/Kconfig +++ b/drivers/usb/typec/ucsi/Kconfig @@ -104,4 +104,15 @@ config UCSI_HUAWEI_GAOKUN To compile the driver as a module, choose M here: the module will be called ucsi_huawei_gaokun. +config UCSI_FLIPPER_ONE + tristate "UCSI Interface Driver for Flipper One MCU" + depends on MFD_FLIPPER_ONE + help + This driver enables UCSI support for the Type-C controller exposed + by the Flipper One MCU, which presents the UCSI data structure as a + register block in the shared MCU I2C register map. + + To compile the driver as a module, choose M here: the module will be + called ucsi_flipper_one. + endif diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile index c7e38bf01350de..34f582036ecf6e 100644 --- a/drivers/usb/typec/ucsi/Makefile +++ b/drivers/usb/typec/ucsi/Makefile @@ -28,3 +28,4 @@ obj-$(CONFIG_UCSI_PMIC_GLINK) += ucsi_glink.o obj-$(CONFIG_CROS_EC_UCSI) += cros_ec_ucsi.o obj-$(CONFIG_UCSI_LENOVO_YOGA_C630) += ucsi_yoga_c630.o obj-$(CONFIG_UCSI_HUAWEI_GAOKUN) += ucsi_huawei_gaokun.o +obj-$(CONFIG_UCSI_FLIPPER_ONE) += ucsi_flipper_one.o diff --git a/drivers/usb/typec/ucsi/ucsi_flipper_one.c b/drivers/usb/typec/ucsi/ucsi_flipper_one.c new file mode 100644 index 00000000000000..5a7301b8fd75a0 --- /dev/null +++ b/drivers/usb/typec/ucsi/ucsi_flipper_one.c @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * UCSI driver for the Flipper One MCU Type-C controller + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include + +#include "ucsi.h" + +struct flipper_one_ucsi { + struct device *dev; + struct regmap *regmap; + struct ucsi *ucsi; + int irq; +}; + +/* + * Read @len bytes from the UCSI register at MCU address @reg. + * + * regmap_raw_read() only accepts lengths that are a multiple of the register + * width, but UCSI messages can have odd lengths, so round @len up into a bounce + * buffer (the largest possible read is one message window) + */ +static int flipper_one_ucsi_read(struct ucsi *ucsi, unsigned int reg, + void *val, size_t len) +{ + struct flipper_one_ucsi *fo = ucsi_get_drvdata(ucsi); + u8 buf[FOMCU_UCSI_MESSAGE_LEN]; + size_t aligned = round_up(len, regmap_get_val_bytes(fo->regmap)); + int ret; + + if (aligned > sizeof(buf)) + return -EINVAL; + + ret = regmap_raw_read(fo->regmap, reg, buf, aligned); + if (ret) + return ret; + + memcpy(val, buf, len); + return 0; +} + +static int flipper_one_ucsi_read_version(struct ucsi *ucsi, u16 *version) +{ + return flipper_one_ucsi_read(ucsi, FOMCU_REG_UCSI_VERSION, version, + sizeof(*version)); +} + +static int flipper_one_ucsi_read_cci(struct ucsi *ucsi, u32 *cci) +{ + return flipper_one_ucsi_read(ucsi, FOMCU_REG_UCSI_CCI, cci, + sizeof(*cci)); +} + +static int flipper_one_ucsi_read_message_in(struct ucsi *ucsi, void *val, + size_t len) +{ + return flipper_one_ucsi_read(ucsi, FOMCU_REG_UCSI_MESSAGE_IN, val, len); +} + +static int flipper_one_ucsi_async_control(struct ucsi *ucsi, u64 command) +{ + struct flipper_one_ucsi *fo = ucsi_get_drvdata(ucsi); + + return regmap_raw_write(fo->regmap, FOMCU_REG_UCSI_CONTROL, + &command, sizeof(command)); +} + +static const struct ucsi_operations flipper_one_ucsi_ops = { + .read_version = flipper_one_ucsi_read_version, + .read_cci = flipper_one_ucsi_read_cci, + .poll_cci = flipper_one_ucsi_read_cci, + .read_message_in = flipper_one_ucsi_read_message_in, + .sync_control = ucsi_sync_control_common, + .async_control = flipper_one_ucsi_async_control, +}; + +static irqreturn_t flipper_one_ucsi_irq(int irq, void *data) +{ + struct flipper_one_ucsi *fo = data; + u32 cci; + int ret; + + ret = flipper_one_ucsi_read_cci(fo->ucsi, &cci); + if (ret) + return IRQ_NONE; + + ucsi_notify_common(fo->ucsi, cci); + + return IRQ_HANDLED; +} + +static int flipper_one_ucsi_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct flipper_one_ucsi *fo; + int ret; + + fo = devm_kzalloc(dev, sizeof(*fo), GFP_KERNEL); + if (!fo) + return -ENOMEM; + + fo->dev = dev; + + fo->regmap = dev_get_regmap(dev->parent, NULL); + if (!fo->regmap) + return dev_err_probe(dev, -ENODEV, "Failed to get MCU regmap\n"); + + fo->irq = platform_get_irq(pdev, 0); + if (fo->irq < 0) + return fo->irq; + + fo->ucsi = ucsi_create(dev, &flipper_one_ucsi_ops); + if (IS_ERR(fo->ucsi)) + return PTR_ERR(fo->ucsi); + + ucsi_set_drvdata(fo->ucsi, fo); + platform_set_drvdata(pdev, fo); + + ret = request_threaded_irq(fo->irq, NULL, flipper_one_ucsi_irq, + IRQF_ONESHOT, dev_name(dev), fo); + if (ret) { + dev_err_probe(dev, ret, "Failed to request IRQ\n"); + goto err_destroy; + } + + ret = ucsi_register(fo->ucsi); + if (ret) { + dev_err_probe(dev, ret, "Failed to register UCSI\n"); + goto err_free_irq; + } + + return 0; + +err_free_irq: + free_irq(fo->irq, fo); +err_destroy: + ucsi_destroy(fo->ucsi); + return ret; +} + +static void flipper_one_ucsi_remove(struct platform_device *pdev) +{ + struct flipper_one_ucsi *fo = platform_get_drvdata(pdev); + + ucsi_unregister(fo->ucsi); + free_irq(fo->irq, fo); + ucsi_destroy(fo->ucsi); +} + +static const struct platform_device_id flipper_one_ucsi_ids[] = { + { "flipper-one-typec" }, + {} +}; +MODULE_DEVICE_TABLE(platform, flipper_one_ucsi_ids); + +static struct platform_driver flipper_one_ucsi_driver = { + .driver = { + .name = "flipper-one-typec", + }, + .probe = flipper_one_ucsi_probe, + .remove = flipper_one_ucsi_remove, + .id_table = flipper_one_ucsi_ids, +}; +module_platform_driver(flipper_one_ucsi_driver); + +MODULE_DESCRIPTION("UCSI driver for Flipper One MCU Type-C controller"); +MODULE_AUTHOR("Alexey Charkov "); +MODULE_LICENSE("GPL"); From dbad655dadf018d1503803fb3db6586a9e8728d0 Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Fri, 22 May 2026 09:11:57 -0700 Subject: [PATCH 211/218] phy: rockchip: inno-csidphy: add support for RK3576 The RK3576 has two Innosilicon MIPI CSI D-PHY instances that use the same register layout as the RK3588 variant (THS-settle at offset 0x160, calibration at 0x168, GRF at offset 0x0). Add the compatible string and driver data for these PHY instances. Signed-off-by: Jason Devers --- drivers/phy/rockchip/phy-rockchip-inno-csidphy.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c b/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c index c79fb53d8ee5c5..c88ca713d76dc6 100644 --- a/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c +++ b/drivers/phy/rockchip/phy-rockchip-inno-csidphy.c @@ -403,6 +403,17 @@ static const struct dphy_drv_data rk3568_mipidphy_drv_data = { .resets_num = ARRAY_SIZE(rk3368_reset_names), }; +static const struct dphy_drv_data rk3576_mipidphy_drv_data = { + .pwrctl_offset = -1, + .ths_settle_offset = RK3568_CSIDPHY_CLK_WR_THS_SETTLE, + .calib_offset = RK3568_CSIDPHY_CLK_CALIB_EN, + .hsfreq_ranges = rk1808_mipidphy_hsfreq_ranges, + .num_hsfreq_ranges = ARRAY_SIZE(rk1808_mipidphy_hsfreq_ranges), + .grf_regs = rk3588_grf_dphy_regs, + .resets = rk3588_reset_names, + .resets_num = ARRAY_SIZE(rk3588_reset_names), +}; + static const struct dphy_drv_data rk3588_mipidphy_drv_data = { .pwrctl_offset = -1, .ths_settle_offset = RK3568_CSIDPHY_CLK_WR_THS_SETTLE, @@ -435,6 +446,10 @@ static const struct of_device_id rockchip_inno_csidphy_match_id[] = { .compatible = "rockchip,rk3568-csi-dphy", .data = &rk3568_mipidphy_drv_data, }, + { + .compatible = "rockchip,rk3576-csi-dphy", + .data = &rk3576_mipidphy_drv_data, + }, { .compatible = "rockchip,rk3588-csi-dphy", .data = &rk3588_mipidphy_drv_data, From 700e8baaa04ef755e3c2516c3ab8ea17d4989b0d Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Fri, 22 May 2026 09:12:02 -0700 Subject: [PATCH 212/218] media: synopsys: dw-mipi-csi2rx: add support for RK3576 The RK3576 uses the same Synopsys DesignWare MIPI CSI-2 Host controller IP as the RK3568. Add the compatible string using the existing RK3568 driver data. Signed-off-by: Jason Devers --- drivers/media/platform/synopsys/dw-mipi-csi2rx.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/media/platform/synopsys/dw-mipi-csi2rx.c b/drivers/media/platform/synopsys/dw-mipi-csi2rx.c index 02eb4a6cafadea..57dabb88ca16c7 100644 --- a/drivers/media/platform/synopsys/dw-mipi-csi2rx.c +++ b/drivers/media/platform/synopsys/dw-mipi-csi2rx.c @@ -839,6 +839,10 @@ static const struct of_device_id dw_mipi_csi2rx_of_match[] = { .compatible = "fsl,imx93-mipi-csi2", .data = &imx93_drvdata, }, + { + .compatible = "rockchip,rk3576-mipi-csi2", + .data = &rk3568_drvdata, + }, { .compatible = "rockchip,rk3568-mipi-csi2", .data = &rk3568_drvdata, From 00e6b62fed11de69a8d06c3b8ee88521d238c6e1 Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Fri, 22 May 2026 09:12:08 -0700 Subject: [PATCH 213/218] media: rockchip: rkcif: add support for RK3576 VICAP The RK3576 Video Capture (VICAP) unit features five MIPI CSI-2 capture interfaces (compared to six on RK3588). The register layout is identical to the RK3588 variant. Add the compatible string and match data with mipi_num set to 5. Signed-off-by: Jason Devers --- .../rockchip/rkcif/rkcif-capture-mipi.c | 81 +++++++++++++++++++ .../rockchip/rkcif/rkcif-capture-mipi.h | 1 + .../media/platform/rockchip/rkcif/rkcif-dev.c | 18 +++++ 3 files changed, 100 insertions(+) diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c index aa70d3e9db047f..b255a744312bd7 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c +++ b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.c @@ -531,6 +531,87 @@ rkcif_rk3588_mipi_ctrl0(struct rkcif_stream *stream, return ctrl0; } +const struct rkcif_mipi_match_data rkcif_rk3576_vicap_mipi_match_data = { + .mipi_num = 5, + .mipi_ctrl0 = rkcif_rk3588_mipi_ctrl0, + .regs = { + [RKCIF_MIPI_CTRL] = 0x20, + [RKCIF_MIPI_INTEN] = 0x74, + [RKCIF_MIPI_INTSTAT] = 0x78, + }, + .regs_id = { + [RKCIF_ID0] = { + [RKCIF_MIPI_CTRL0] = 0x00, + [RKCIF_MIPI_CTRL1] = 0x04, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x24, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x2c, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x34, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x28, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x30, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x8c, + }, + [RKCIF_ID1] = { + [RKCIF_MIPI_CTRL0] = 0x08, + [RKCIF_MIPI_CTRL1] = 0x0c, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x38, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x40, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x48, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x3c, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x44, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x90, + }, + [RKCIF_ID2] = { + [RKCIF_MIPI_CTRL0] = 0x10, + [RKCIF_MIPI_CTRL1] = 0x14, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x4c, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x54, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x5c, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x50, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x58, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x94, + }, + [RKCIF_ID3] = { + [RKCIF_MIPI_CTRL0] = 0x18, + [RKCIF_MIPI_CTRL1] = 0x1c, + [RKCIF_MIPI_FRAME0_ADDR_Y] = 0x60, + [RKCIF_MIPI_FRAME0_ADDR_UV] = 0x68, + [RKCIF_MIPI_FRAME0_VLW_Y] = 0x70, + [RKCIF_MIPI_FRAME0_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_ADDR_Y] = 0x64, + [RKCIF_MIPI_FRAME1_ADDR_UV] = 0x6c, + [RKCIF_MIPI_FRAME1_VLW_Y] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_FRAME1_VLW_UV] = RKCIF_REGISTER_NOTSUPPORTED, + [RKCIF_MIPI_CROP_START] = 0x98, + }, + }, + .blocks = { + { + .offset = 0x100, + }, + { + .offset = 0x200, + }, + { + .offset = 0x300, + }, + { + .offset = 0x400, + }, + { + .offset = 0x500, + }, + }, +}; + const struct rkcif_mipi_match_data rkcif_rk3588_vicap_mipi_match_data = { .mipi_num = 6, .mipi_ctrl0 = rkcif_rk3588_mipi_ctrl0, diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h index 7edaca44f653ca..4e17cbc04b76e9 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h +++ b/drivers/media/platform/rockchip/rkcif/rkcif-capture-mipi.h @@ -13,6 +13,7 @@ #include "rkcif-common.h" extern const struct rkcif_mipi_match_data rkcif_rk3568_vicap_mipi_match_data; +extern const struct rkcif_mipi_match_data rkcif_rk3576_vicap_mipi_match_data; extern const struct rkcif_mipi_match_data rkcif_rk3588_vicap_mipi_match_data; int rkcif_mipi_register(struct rkcif_device *rkcif); diff --git a/drivers/media/platform/rockchip/rkcif/rkcif-dev.c b/drivers/media/platform/rockchip/rkcif/rkcif-dev.c index 86575d398f807f..cfd6115c815dec 100644 --- a/drivers/media/platform/rockchip/rkcif/rkcif-dev.c +++ b/drivers/media/platform/rockchip/rkcif/rkcif-dev.c @@ -53,6 +53,20 @@ static const struct rkcif_match_data rk3568_vicap_match_data = { .mipi = &rkcif_rk3568_vicap_mipi_match_data, }; +static const char *const rk3576_vicap_clks[] = { + "aclk", + "hclk", + "dclk", + "iclk_host0", + "iclk_host1", +}; + +static const struct rkcif_match_data rk3576_vicap_match_data = { + .clks = rk3576_vicap_clks, + .clks_num = ARRAY_SIZE(rk3576_vicap_clks), + .mipi = &rkcif_rk3576_vicap_mipi_match_data, +}; + static const char *const rk3588_vicap_clks[] = { "aclk", "hclk", @@ -76,6 +90,10 @@ static const struct of_device_id rkcif_plat_of_match[] = { .compatible = "rockchip,rk3568-vicap", .data = &rk3568_vicap_match_data, }, + { + .compatible = "rockchip,rk3576-vicap", + .data = &rk3576_vicap_match_data, + }, { .compatible = "rockchip,rk3588-vicap", .data = &rk3588_vicap_match_data, From bfe96963ca2d30d6343f0110453a77ff479a92ea Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Fri, 22 May 2026 09:12:15 -0700 Subject: [PATCH 214/218] arm64: dts: rockchip: add camera pipeline nodes to rk3576 Add device tree nodes for the RK3576 camera capture pipeline: - Two Innosilicon CSI D-PHY instances (csi_dphy0, csi_dphy1) - Two MIPI D-PHY GRF syscon nodes (mipidphy0_grf, mipidphy1_grf) - Five MIPI CSI-2 receiver nodes (csi0 through csi4) - Video Capture (VICAP) unit with five MIPI input ports - VICAP IOMMU The CSI hosts are connected to the VICAP ports and wired to the Innosilicon D-PHY instances. All nodes are disabled by default and must be enabled by board device trees that have camera connectors. Signed-off-by: Jason Devers --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 265 +++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index 239196b137aef0..0d79247fe360b2 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -988,11 +988,21 @@ reg = <0x0 0x26038000 0x0 0x1000>; }; + mipidphy0_grf: syscon@2603a000 { + compatible = "rockchip,rk3576-csidphy-grf", "syscon"; + reg = <0x0 0x2603a000 0x0 0x2000>; + }; + ioc_grf: syscon@26040000 { compatible = "rockchip,rk3576-ioc-grf", "syscon", "simple-mfd"; reg = <0x0 0x26040000 0x0 0xc000>; }; + mipidphy1_grf: syscon@2604c000 { + compatible = "rockchip,rk3576-csidphy-grf", "syscon"; + reg = <0x0 0x2604c000 0x0 0x2000>; + }; + cru: clock-controller@27200000 { compatible = "rockchip,rk3576-cru"; reg = <0x0 0x27200000 0x0 0x50000>; @@ -1355,6 +1365,235 @@ #iommu-cells = <0>; }; + vicap: video-capture@27c10000 { + compatible = "rockchip,rk3576-vicap"; + reg = <0x0 0x27c10000 0x0 0x800>; + interrupts = ; + clocks = <&cru ACLK_VICAP>, <&cru HCLK_VICAP>, + <&cru DCLK_VICAP>, <&cru ICLK_CSIHOST0>, + <&cru ICLK_CSIHOST01>; + clock-names = "aclk", "hclk", "dclk", "iclk_host0", "iclk_host1"; + iommus = <&vicap_mmu>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_A_VICAP>, <&cru SRST_H_VICAP>, + <&cru SRST_D_VICAP>, <&cru SRST_VICAP_I0CLK>, + <&cru SRST_VICAP_I1CLK>, <&cru SRST_VICAP_I2CLK>, + <&cru SRST_VICAP_I3CLK>, <&cru SRST_VICAP_I4CLK>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + vicap_mipi0: port@1 { + reg = <1>; + + vicap_mipi0_input: endpoint { + remote-endpoint = <&csi0_output>; + }; + }; + + vicap_mipi1: port@2 { + reg = <2>; + + vicap_mipi1_input: endpoint { + remote-endpoint = <&csi1_output>; + }; + }; + + vicap_mipi2: port@3 { + reg = <3>; + + vicap_mipi2_input: endpoint { + remote-endpoint = <&csi2_output>; + }; + }; + + vicap_mipi3: port@4 { + reg = <4>; + + vicap_mipi3_input: endpoint { + remote-endpoint = <&csi3_output>; + }; + }; + + vicap_mipi4: port@5 { + reg = <5>; + + vicap_mipi4_input: endpoint { + remote-endpoint = <&csi4_output>; + }; + }; + }; + }; + + vicap_mmu: iommu@27c10800 { + compatible = "rockchip,rk3576-iommu", "rockchip,rk3568-iommu"; + reg = <0x0 0x27c10800 0x0 0x100>; + interrupts = ; + clocks = <&cru ACLK_VICAP>, <&cru HCLK_VICAP>; + clock-names = "aclk", "iface"; + power-domains = <&power RK3576_PD_VI>; + rockchip,disable-mmu-reset; + #iommu-cells = <0>; + status = "disabled"; + }; + + csi0: csi@27c80000 { + compatible = "rockchip,rk3576-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0x27c80000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_0>, <&cru ICLK_CSIHOST0>; + clock-names = "per", "pixel"; + phys = <&csi_dphy0>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_0>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi0_in: port@0 { + reg = <0>; + }; + + csi0_out: port@1 { + reg = <1>; + + csi0_output: endpoint { + remote-endpoint = <&vicap_mipi0_input>; + }; + }; + }; + }; + + csi1: csi@27c90000 { + compatible = "rockchip,rk3576-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0x27c90000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_1>; + clock-names = "per"; + phys = <&csi_dphy0>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_1>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi1_in: port@0 { + reg = <0>; + }; + + csi1_out: port@1 { + reg = <1>; + + csi1_output: endpoint { + remote-endpoint = <&vicap_mipi1_input>; + }; + }; + }; + }; + + csi2: csi@27ca0000 { + compatible = "rockchip,rk3576-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0x27ca0000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_2>; + clock-names = "per"; + phys = <&csi_dphy1>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_2>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi2_in: port@0 { + reg = <0>; + }; + + csi2_out: port@1 { + reg = <1>; + + csi2_output: endpoint { + remote-endpoint = <&vicap_mipi2_input>; + }; + }; + }; + }; + + csi3: csi@27cb0000 { + compatible = "rockchip,rk3576-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0x27cb0000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_3>; + clock-names = "per"; + phys = <&csi_dphy1>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_3>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi3_in: port@0 { + reg = <0>; + }; + + csi3_out: port@1 { + reg = <1>; + + csi3_output: endpoint { + remote-endpoint = <&vicap_mipi3_input>; + }; + }; + }; + }; + + csi4: csi@27cc0000 { + compatible = "rockchip,rk3576-mipi-csi2", "rockchip,rk3568-mipi-csi2"; + reg = <0x0 0x27cc0000 0x0 0x10000>; + interrupts = , + ; + interrupt-names = "err1", "err2"; + clocks = <&cru PCLK_CSI_HOST_4>; + clock-names = "per"; + phys = <&csi_dphy1>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSI_HOST_4>; + status = "disabled"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + csi4_in: port@0 { + reg = <0>; + }; + + csi4_out: port@1 { + reg = <1>; + + csi4_output: endpoint { + remote-endpoint = <&vicap_mipi4_input>; + }; + }; + }; + }; + vop: vop@27d00000 { compatible = "rockchip,rk3576-vop"; reg = <0x0 0x27d00000 0x0 0x3000>, <0x0 0x27d05000 0x0 0x1000>; @@ -2974,6 +3213,19 @@ status = "disabled"; }; + csi_dphy0: phy@2b030000 { + compatible = "rockchip,rk3576-csi-dphy"; + reg = <0x0 0x2b030000 0x0 0x8000>; + clocks = <&cru PCLK_CSIDPHY>; + clock-names = "pclk"; + #phy-cells = <0>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSIPHY>, <&cru SRST_SCAN_CSIPHY>; + reset-names = "apb", "phy"; + rockchip,grf = <&mipidphy0_grf>; + status = "disabled"; + }; + combphy0_ps: phy@2b050000 { compatible = "rockchip,rk3576-naneng-combphy"; reg = <0x0 0x2b050000 0x0 0x100>; @@ -3010,6 +3262,19 @@ status = "disabled"; }; + csi_dphy1: phy@2b070000 { + compatible = "rockchip,rk3576-csi-dphy"; + reg = <0x0 0x2b070000 0x0 0x8000>; + clocks = <&cru PCLK_CSIDPHY1>; + clock-names = "pclk"; + #phy-cells = <0>; + power-domains = <&power RK3576_PD_VI>; + resets = <&cru SRST_P_CSIDPHY1>, <&cru SRST_SCAN_CSIDPHY1>; + reset-names = "apb", "phy"; + rockchip,grf = <&mipidphy1_grf>; + status = "disabled"; + }; + usbdp_phy: phy@2b010000 { compatible = "rockchip,rk3576-usbdp-phy"; reg = <0x0 0x2b010000 0x0 0x10000>; From d80cd24ce618cb597fdc27b51bf180491d1d8ae7 Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Sat, 23 May 2026 13:40:01 -0700 Subject: [PATCH 215/218] dt-bindings: phy: rockchip-inno-csi-dphy: add rk3576 support Add rockchip,rk3576-csi-dphy to the compatible enum. The RK3576 has two Innosilicon CSI D-PHY instances and follows the same two-reset pattern (apb, phy) as the existing rk3588-csi-dphy entry. Signed-off-by: Jason Devers --- .../devicetree/bindings/phy/rockchip-inno-csi-dphy.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/phy/rockchip-inno-csi-dphy.yaml b/Documentation/devicetree/bindings/phy/rockchip-inno-csi-dphy.yaml index 03950b3cad08c1..243863d62d8312 100644 --- a/Documentation/devicetree/bindings/phy/rockchip-inno-csi-dphy.yaml +++ b/Documentation/devicetree/bindings/phy/rockchip-inno-csi-dphy.yaml @@ -21,6 +21,7 @@ properties: - rockchip,rk3326-csi-dphy - rockchip,rk3368-csi-dphy - rockchip,rk3568-csi-dphy + - rockchip,rk3576-csi-dphy - rockchip,rk3588-csi-dphy reg: From 47691ff31859a9ac7c23076811cb10ba3e34ee96 Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Sat, 23 May 2026 13:40:08 -0700 Subject: [PATCH 216/218] dt-bindings: media: rockchip,rk3568-mipi-csi2: add rk3576 support Add rockchip,rk3576-mipi-csi2 as a compatible that falls back to rockchip,rk3568-mipi-csi2, following the same pattern as the existing rk3588-mipi-csi2 entry. Signed-off-by: Jason Devers --- .../media/rockchip,rk3568-mipi-csi2.yaml | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml b/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml index aa9a41927410bb..cd8b1673d1d64e 100644 --- a/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml +++ b/Documentation/devicetree/bindings/media/rockchip,rk3568-mipi-csi2.yaml @@ -20,6 +20,7 @@ properties: - const: fsl,imx93-mipi-csi2 - items: - enum: + - rockchip,rk3576-mipi-csi2 - rockchip,rk3588-mipi-csi2 - const: rockchip,rk3568-mipi-csi2 - const: rockchip,rk3568-mipi-csi2 @@ -117,12 +118,39 @@ allOf: minItems: 2 interrupt-names: minItems: 2 + required: + - resets + + - if: + properties: + compatible: + contains: + const: rockchip,rk3568-mipi-csi2 + not: + properties: + compatible: + contains: + const: rockchip,rk3576-mipi-csi2 + then: + properties: clocks: maxItems: 1 clock-names: maxItems: 1 - required: - - resets + + - if: + properties: + compatible: + contains: + const: rockchip,rk3576-mipi-csi2 + then: + properties: + clocks: + minItems: 1 + maxItems: 2 + clock-names: + minItems: 1 + maxItems: 2 - if: properties: From cac60fadeda40d65c4c7bc7c075dd92df9231e88 Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Mon, 1 Jun 2026 10:30:58 -0700 Subject: [PATCH 217/218] dt-bindings: media: rockchip,rk3588-vicap: add rk3576 support The RK3576 VICAP uses the same register layout and clock setup as the RK3588 one, with two differences: it has five MIPI CSI-2 input ports instead of six and no DVP parallel port. Extend the existing RK3588 binding rather than adding a separate file: add the rockchip,rk3576-vicap compatible and an allOf/if-then block that drops the DVP port@0 and the sixth MIPI port@6, and caps resets at eight, when that compatible is used. Signed-off-by: Jason Devers --- .../bindings/media/rockchip,rk3588-vicap.yaml | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml b/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml index 0911a6b07d1819..735cffe1ad2262 100644 --- a/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml +++ b/Documentation/devicetree/bindings/media/rockchip,rk3588-vicap.yaml @@ -4,21 +4,23 @@ $id: http://devicetree.org/schemas/media/rockchip,rk3588-vicap.yaml# $schema: http://devicetree.org/meta-schemas/core.yaml# -title: Rockchip RK3588 Video Capture (VICAP) +title: Rockchip RK3576 and RK3588 Video Capture (VICAP) maintainers: - Michael Riesch description: - The Rockchip RK3588 Video Capture (VICAP) block features a digital video - port (DVP, a parallel video interface) and six MIPI CSI-2 ports. It receives - the data from camera sensors, video decoders, or other companion ICs and - transfers it into system main memory by AXI bus and/or passes it the image - signal processing (ISP) blocks. + The Rockchip Video Capture (VICAP) block receives data from camera sensors, + video decoders, or other companion ICs and transfers it into system main + memory by AXI bus and/or passes it to the image signal processing (ISP) + blocks. On RK3588 it features a digital video port (DVP, a parallel video + interface) and six MIPI CSI-2 ports. RK3576 has no DVP and five MIPI CSI-2 + ports. properties: compatible: enum: + - rockchip,rk3576-vicap - rockchip,rk3588-vicap reg: @@ -47,6 +49,7 @@ properties: maxItems: 1 resets: + minItems: 8 maxItems: 9 rockchip,grf: @@ -63,7 +66,8 @@ properties: port@0: $ref: /schemas/graph.yaml#/$defs/port-base unevaluatedProperties: false - description: The digital video port (DVP, a parallel video interface). + description: + The digital video port (DVP, a parallel video interface). RK3588 only. properties: endpoint: @@ -124,7 +128,8 @@ properties: port@6: $ref: /schemas/graph.yaml#/properties/port - description: Port connected to the MIPI CSI-2 receiver 5 output. + description: + Port connected to the MIPI CSI-2 receiver 5 output. RK3588 only. properties: endpoint: @@ -138,6 +143,25 @@ required: - clocks - ports +allOf: + - if: + properties: + compatible: + contains: + const: rockchip,rk3576-vicap + then: + properties: + resets: + maxItems: 8 + ports: + properties: + port@0: false + port@6: false + else: + properties: + resets: + minItems: 9 + additionalProperties: false examples: From b3f915a715fca2750fc20fed13d05b3db4c5349a Mon Sep 17 00:00:00 2001 From: Jason Devers Date: Mon, 1 Jun 2026 10:57:05 -0700 Subject: [PATCH 218/218] dt-bindings: soc: rockchip: grf: add rk3576 CSI D-PHY GRF The RK3576 has two GRF blocks for its Innosilicon CSI D-PHY instances, analogous to the rk3588-csidphy-grf. Document the compatible so the rk3576 camera pipeline nodes validate. Signed-off-by: Jason Devers --- Documentation/devicetree/bindings/soc/rockchip/grf.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/soc/rockchip/grf.yaml b/Documentation/devicetree/bindings/soc/rockchip/grf.yaml index 2cc43742b8e3b8..1dc4f0844517d7 100644 --- a/Documentation/devicetree/bindings/soc/rockchip/grf.yaml +++ b/Documentation/devicetree/bindings/soc/rockchip/grf.yaml @@ -32,6 +32,7 @@ properties: - rockchip,rk3568-usb2phy-grf - rockchip,rk3576-bigcore-grf - rockchip,rk3576-cci-grf + - rockchip,rk3576-csidphy-grf - rockchip,rk3576-dcphy-grf - rockchip,rk3576-gpu-grf - rockchip,rk3576-hdptxphy-grf