From 108fea69219cb221f0dbc3b7c2ce95d2560a067e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Mar 2026 02:55:07 +0200 Subject: [PATCH 001/202] [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 21541be094c47e..36ee6e027af528 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2093,6 +2093,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 6336d35fb8bb170a02f1b3cbe5f0df6f6710c41f Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 4 Apr 2025 20:04:49 +0200 Subject: [PATCH 002/202] [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 89eec20a2064ce..70298f766cdb03 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 b8ee20e4d7f4021267a6f96f8335a280d918ee72 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 1 Jul 2025 22:58:37 +0200 Subject: [PATCH 003/202] [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 3319f6a2b0c9b4..2c58deca65ef5a 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 e0177f0baf666777ed5a649db66e0321a84a91de Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:09:35 +0200 Subject: [PATCH 004/202] 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 66132eec37d9221052cecac48dd1fd784ead5b73 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:13:50 +0200 Subject: [PATCH 005/202] 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 3a18db9630fcde3f06f69eff5e28c6affcab4a5d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 18:09:57 +0200 Subject: [PATCH 006/202] 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 2991bf6a6d06fa954dc30058fe4d93180eaf39a4 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:35:43 +0100 Subject: [PATCH 007/202] 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 8969b56f3063e7..00ce4793dfb969 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"; @@ -608,6 +617,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 99fe4649ae59c5218ab39ad416f17dae948f681c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:39:11 +0100 Subject: [PATCH 008/202] 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 00ce4793dfb969..c9cfdfe310633a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -564,7 +564,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 2b594be4e92c7e6b92b7355ee6fac9bac6e9db83 Mon Sep 17 00:00:00 2001 From: "Carsten Haitzler (Rasterman)" Date: Tue, 6 Feb 2024 10:12:54 +0000 Subject: [PATCH 009/202] 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 13aaf63ad09369..2bb9a0fa406bd4 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 @@ -608,8 +608,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 c38e8bb1856d78262b4dc4fa6380fffaa26ab20f Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 25 Jul 2024 18:12:24 +0200 Subject: [PATCH 010/202] 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 6633c85a56fedfbdf3ed52e56442def9c0914dfb Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:40 -0500 Subject: [PATCH 011/202] 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 d49a0638160c2132daa54206ddb9ead0fb226edb Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:41 -0500 Subject: [PATCH 012/202] 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 a160077a507f23..afb69904552692 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_commit *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 6f756ebf69c34c59b2ab72997669c2feed1580e8 Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:42 -0500 Subject: [PATCH 013/202] 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 fc1fdbfd316223..07d43083f39cde 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1651,6 +1651,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 79d43710401944e5e87f36fb8ad98893744bbcde Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 8 Nov 2024 17:35:44 +0100 Subject: [PATCH 014/202] 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 c9cfdfe310633a..bbf9a6497d6baf 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>; @@ -496,6 +542,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"; @@ -536,6 +598,10 @@ }; }; +&mipidcphy0 { + status = "okay"; +}; + &pcie2x1l0 { pinctrl-names = "default"; pinctrl-0 = <&pcie2_0_rst>, <&pcie2_0_wake>, <&pcie2_0_clkreq>, <&wifi_host_wake_irq>; @@ -658,6 +724,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>; @@ -686,6 +758,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>; @@ -1498,3 +1578,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 21aecff1b16d5a2b398f975729cda12ec7cf5746 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 14 Nov 2024 17:36:18 +0100 Subject: [PATCH 015/202] 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 afb69904552692..df9eaeea1e41fc 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 9f159a7a41734803e8138f635321c98a11e4ec4c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 29 Jul 2025 18:21:03 +0200 Subject: [PATCH 016/202] 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 bbf9a6497d6baf..7e51cbc4651631 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588-evb1-v10.dts @@ -505,24 +505,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>; }; }; }; @@ -1513,14 +1513,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>; }; }; }; @@ -1545,9 +1545,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 ba737b13cf027eec24c37da355e674098e2b7531 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 15 Mar 2025 09:34:02 +0100 Subject: [PATCH 017/202] 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 2bb9a0fa406bd4..680481f94fc80b 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 @@ -310,6 +310,10 @@ }; }; +&i2c3 { + status = "okay"; +}; + &i2c4 { pinctrl-names = "default"; pinctrl-0 = <&i2c4m1_xfer>; From a821c999f7c0609465d4ccaff0383c329887407c Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Tue, 10 Jun 2025 16:07:09 +0200 Subject: [PATCH 018/202] 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 7d8a533f24aead..229f6f97eef9b8 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; @@ -205,6 +210,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; @@ -224,6 +230,7 @@ struct rockchip_usb2phy_port { struct notifier_block event_nb; enum usb_otg_state state; enum usb_dr_mode mode; + bool typec_vbus_det; }; /** @@ -511,6 +518,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 */ @@ -551,8 +565,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); @@ -680,8 +693,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; @@ -1287,6 +1309,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) @@ -1308,6 +1407,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) { @@ -2135,6 +2236,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 }, @@ -2172,6 +2275,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 d37f42d943b28ff64833a575dbcc0b132371a8d1 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:24 +0200 Subject: [PATCH 019/202] 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 87aa3394c06215f06eecb0308ae601f2b790ee75 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:25 +0200 Subject: [PATCH 020/202] 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 d86f972a14d802c0dc7ff2d8450cb5280ffc00d8 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:26 +0200 Subject: [PATCH 021/202] 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 272af1012ab03b..a163f255ee6f91 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; @@ -685,6 +742,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>; @@ -779,10 +855,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 1adce42b25826826e864899da4dc812965670fab Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 3 Jul 2025 01:21:14 +0200 Subject: [PATCH 022/202] 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 b65d0f5fa1a014..d56ca494b0638c 100644 --- a/drivers/net/phy/realtek/realtek_main.c +++ b/drivers/net/phy/realtek/realtek_main.c @@ -323,6 +323,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"); @@ -968,8 +970,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) @@ -2772,7 +2776,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 a428062de5f93e5fb07083865da6b9b16ec69b20 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 8 Jul 2025 19:36:31 +0200 Subject: [PATCH 023/202] 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 a163f255ee6f91..b338c63dbcbb37 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 185a58724f921f19d1251e838390e88eb6058f16 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 21 Nov 2025 19:27:00 +0100 Subject: [PATCH 024/202] 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 00c50d55b24243d728d9e215e54b5cca512e29d0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 22 Dec 2025 20:25:56 +0100 Subject: [PATCH 025/202] 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 2c6d7d31219c46494588e80c1da60087ded8cab3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:06:36 +0200 Subject: [PATCH 026/202] 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 f08a45097dc56081e84262cbd5d72057136684fd Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:21:02 +0200 Subject: [PATCH 027/202] 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 0cd3a10ecea08b486a61f0cc359e2e398902a9d5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:27:04 +0200 Subject: [PATCH 028/202] 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 eb83f07129aaf618d8c48a9431514905b0980a06 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:30:11 +0200 Subject: [PATCH 029/202] 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 1e12f45583d29b93506e62bc5aed0c009e481962 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:38:54 +0200 Subject: [PATCH 030/202] 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 bae2442fc028cc47c467dd8bc011185180f02f75 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:49:34 +0200 Subject: [PATCH 031/202] 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 cfb497efdab390e7f93c1b075c8c3ddbfde2ff60 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 24 Nov 2025 20:06:39 +0100 Subject: [PATCH 032/202] [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 c33d4c17ebe245796761c6fed6c2193dd6193867 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 22 Aug 2025 18:49:35 +0200 Subject: [PATCH 033/202] 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 | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml b/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml index 8b7059d5b1826f..89efaf005a7b0d 100644 --- a/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml +++ b/Documentation/devicetree/bindings/phy/phy-rockchip-usbdp.yaml @@ -110,10 +110,34 @@ properties: port: $ref: /schemas/graph.yaml#/properties/port + deprecated: true description: 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 Auxiliary, which should either point to + the SBU port of a USB-C connector or a DisplayPort connector input port. + required: - compatible - reg From b0b8d3e925348cbc2b186a53980329ecce0a1e3a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 16:28:38 +0200 Subject: [PATCH 034/202] 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 Closes: https://lore.kernel.org/linux-phy/20260612163835.8D5471F000E9@smtp.kernel.org/ 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 f68de14366dbae..8a3ad19b1ae2c3 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -999,15 +999,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); @@ -1016,6 +1015,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 734d5ff26981246debbeeec7b40f5ed68e3b43da Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 01:22:02 +0100 Subject: [PATCH 035/202] 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 8a3ad19b1ae2c3..957df7457f4b68 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1007,7 +1007,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 e17be5b3969b90a009a8dcc4ef4f494716a94d24 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 13:45:06 +0200 Subject: [PATCH 036/202] 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 Closes: https://lore.kernel.org/linux-phy/20260612164107.C7DB21F000E9@smtp.kernel.org/ 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 957df7457f4b68..8485e62e23f354 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -426,8 +426,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 a298bbd72039ecd69dabca7c2e548de1bd3bffb1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 19 Jun 2026 21:42:03 +0200 Subject: [PATCH 037/202] phy: rockchip: usbdp: Handle missing clock-names DT property gracefully The rk_udphy_clk_init() function would currently try to do a strncmp for a NULL pointer, if DT specifies 'clocks' property, but no 'clock-names' property. Fix this by making sure the clock has an id string set. Note that DT binding requires setting clock-names, so this is only a problem when booting a non-compliant device tree. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Closes: https://sashiko.dev/#/message/20260619154349.071321F000E9%40smtp.kernel.org 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 8485e62e23f354..22d070c2f8503e 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -431,6 +431,9 @@ static int rk_udphy_clk_init(struct rk_udphy *udphy, struct device *dev) /* used for configure phy reference clock frequency */ for (i = 0; i < udphy->num_clks; i++) { + if (!udphy->clks[i].id) + continue; + if (!strncmp(udphy->clks[i].id, "refclk", 6)) { udphy->refclk = udphy->clks[i].clk; break; From 9c0eea3731b08fa7888d53cc4895f9c0a3bd18bc Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 15:51:07 +0200 Subject: [PATCH 038/202] phy: rockchip: usbdp: Drop seamless DP takeover Right now the DRM drivers do not support seamless DP takeover and I'm I'm not aware of any bootloader implementing this feature either. In any case this feature would be limited to boards using the USBDP PHY for a DP or eDP connection instead of the more commonly USB-C connector. With USB-C's DP AltMode a seamless DP takeover requires handing over the state of the TCPM state machine from the bootloader to the kernel. This in turn requires a huge amount of work to keep the state machine implementations synchronized. It's very unlikely we will see somebody implementing that in the foreseeable future. As the current code is obviously buggy and untested, let's simply drop support for seamless DP takeover. It can be re-implemented cleanly once somebody adds all missing bits. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Closes: https://lore.kernel.org/linux-phy/20260612164107.C7DB21F000E9@smtp.kernel.org/ Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 31 ----------------------- 1 file changed, 31 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 22d070c2f8503e..00f2881c5d23ce 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -451,11 +451,6 @@ static int rk_udphy_reset_assert_all(struct rk_udphy *udphy) return reset_control_bulk_assert(udphy->num_rsts, udphy->rsts); } -static int rk_udphy_reset_deassert_all(struct rk_udphy *udphy) -{ - return reset_control_bulk_deassert(udphy->num_rsts, udphy->rsts); -} - static int rk_udphy_reset_deassert(struct rk_udphy *udphy, char *name) { struct reset_control_bulk_data *list = udphy->rsts; @@ -923,28 +918,6 @@ static int rk_udphy_parse_lane_mux_data(struct rk_udphy *udphy) return 0; } -static int rk_udphy_get_initial_status(struct rk_udphy *udphy) -{ - int ret; - u32 value; - - ret = clk_bulk_prepare_enable(udphy->num_clks, udphy->clks); - if (ret) { - dev_err(udphy->dev, "failed to enable clk\n"); - return ret; - } - - rk_udphy_reset_deassert_all(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); - - return 0; -} - static int rk_udphy_parse_dt(struct rk_udphy *udphy) { struct device *dev = udphy->dev; @@ -1494,10 +1467,6 @@ static int rk_udphy_probe(struct platform_device *pdev) if (ret) return ret; - ret = rk_udphy_get_initial_status(udphy); - if (ret) - return ret; - mutex_init(&udphy->mutex); platform_set_drvdata(pdev, udphy); From 476fc7eedb7b39a44cc9868ef7671d602b5315d7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Feb 2026 19:26:12 +0100 Subject: [PATCH 039/202] 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 00f2881c5d23ce..c5861fb03ca326 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -986,8 +986,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 dd6fa24ab20a63071fa95d1ef52e3ae13952621c Mon Sep 17 00:00:00 2001 From: Frank Wang Date: Fri, 16 Jan 2026 20:28:08 +0100 Subject: [PATCH 040/202] 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 c5861fb03ca326..c7dc3177250dcb 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -349,7 +349,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[] = { @@ -376,7 +377,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[] = { @@ -411,8 +412,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 40168d96b3cbf797d23d2905e36e4cec9bae12a5 Mon Sep 17 00:00:00 2001 From: William Wu Date: Fri, 16 Jan 2026 20:31:20 +0100 Subject: [PATCH 041/202] 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 c7dc3177250dcb..ecad58e7b18adb 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -412,7 +412,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 7d8457dbee5ab1419d764a1c91a68ff804015ff9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 21:32:59 +0100 Subject: [PATCH 042/202] 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 ecad58e7b18adb..b2875bb70c5afd 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -616,6 +616,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) { @@ -646,7 +655,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, @@ -1360,10 +1369,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 f63e503263fcf6c97bf7aca43bda8f5db8531517 Mon Sep 17 00:00:00 2001 From: Zhang Yubing Date: Thu, 29 Jan 2026 13:04:47 +0100 Subject: [PATCH 043/202] 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 b2875bb70c5afd..96a5dd60e597b4 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -192,6 +192,7 @@ struct rk_udphy { int id; bool dp_in_use; + int dp_lanes; /* PHY const config */ const struct rk_udphy_cfg *cfgs; @@ -534,6 +535,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) @@ -541,18 +549,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: @@ -565,28 +573,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; @@ -656,6 +642,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, @@ -894,7 +881,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"); @@ -920,9 +907,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; @@ -1049,18 +1038,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); @@ -1340,6 +1328,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: @@ -1356,6 +1345,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; } @@ -1500,7 +1490,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 284b05fbdc97d0d4fd1b379ce993bceeae619e85 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 18:05:39 +0200 Subject: [PATCH 044/202] 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 Closes: https://lore.kernel.org/linux-phy/20260612165546.98E1F1F000E9@smtp.kernel.org/ 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 96a5dd60e597b4..04b62ccd02cae4 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1098,6 +1098,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 ae2fb53a7ab4838b5215a4aac57607dab986e029 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 22:45:18 +0100 Subject: [PATCH 045/202] 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 04b62ccd02cae4..becc55b9b697df 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -544,7 +544,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; @@ -573,7 +573,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; @@ -1048,9 +1048,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); @@ -1068,7 +1068,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 2f3757efbe3cffa0bfcfbf42779ffd29e676a737 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:15:23 +0100 Subject: [PATCH 046/202] 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 becc55b9b697df..a8279e2388e196 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 @@ -74,7 +75,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 @@ -103,8 +103,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 c45725db62a4a95b378513f7c00f4562c4fb0649 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:24:20 +0100 Subject: [PATCH 047/202] 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 a8279e2388e196..d554b191d91921 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -547,30 +547,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 b545c30e46f116a467eea2bc52565bf05c212251 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:06:48 +0100 Subject: [PATCH 048/202] 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 | 17 +++++++++++++++++ 2 files changed, 19 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 d554b191d91921..22dd05af85251f 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 @@ -1413,6 +1414,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; @@ -1467,6 +1469,21 @@ 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. The + * aux bridge itself will be registered using port 0, endpoint 0, which + * is fine as that is the actual superspeed data connection shared by + * USB3 and DP based on the mux config. + */ + 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 c87d435cd3ad785c3086aaf130511bc23a01886b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:53:21 +0100 Subject: [PATCH 049/202] 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 22dd05af85251f..fc34eb380bc875 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -127,7 +127,6 @@ struct rk_udphy_grf_cfg { struct rk_udphy_vogrf_cfg { /* vo-grf */ - struct rk_udphy_grf_reg hpd_trigger; u32 dp_lane_reg; }; @@ -185,14 +184,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 */ @@ -576,19 +572,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) @@ -999,29 +982,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); @@ -1253,8 +1213,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, @@ -1308,6 +1266,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) { @@ -1339,22 +1305,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; @@ -1509,20 +1460,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" }; @@ -1546,7 +1483,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, }, }, @@ -1587,11 +1523,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, }, }, @@ -1627,7 +1561,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 357c63a876042e33c1b176f54966f5b47304576d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 11:25:21 +0200 Subject: [PATCH 050/202] 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 fc34eb380bc875..29f358e57c0229 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -170,7 +170,7 @@ struct rk_udphy { /* PHY status management */ bool flip; - bool mode_change; + bool phy_needs_reinit; u8 mode; u8 status; @@ -577,7 +577,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; } @@ -950,15 +950,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 831617c8dff705feb25073ec09dcf3392bed1025 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:25:32 +0200 Subject: [PATCH 051/202] 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 29f358e57c0229..386f57c44f8d65 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -619,6 +619,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); @@ -630,7 +631,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 522e24cd676f523495a70bccee4a97ba8c8b0ed0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:45:50 +0200 Subject: [PATCH 052/202] 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 386f57c44f8d65..7866ecf6876950 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -581,6 +581,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) { @@ -588,10 +624,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); @@ -601,18 +633,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, @@ -1268,7 +1296,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 @@ -1280,38 +1307,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 3c6663507a2fa7098863e9f25d7117e1b7b3fc4c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 19 Jun 2026 23:02:03 +0200 Subject: [PATCH 053/202] phy: rockchip: usbdp: Properly handle TYPEC_STATE_SAFE and TYPEC_STATE_USB Handle TYPEC_STATE_SAFE and TYPEC_STATE_USB Type-C state events, so that the muxing is properly updated when exiting DP AltMode. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Closes: https://sashiko.dev/#/message/20260619155020.CC7361F000E9%40smtp.kernel.org Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 7866ecf6876950..fffbde84637938 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1292,17 +1292,26 @@ static const struct phy_ops rk_udphy_usb3_phy_ops = { .owner = THIS_MODULE, }; +static bool rk_udphy_is_supported_mode(struct typec_mux_state *state) +{ + /* Handle Safe State and USB State */ + if (state->mode < TYPEC_STATE_MODAL) + return true; + + /* Handle DP AltMode */ + if (state->alt && state->alt->svid == USB_TYPEC_DP_SID) + return true; + + return false; +} + 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); - /* - * 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) + /* Ignore mux events not involving USB or DP */ + if (!rk_udphy_is_supported_mode(state)) return 0; mutex_lock(&udphy->mutex); From 446a893f86921376e5a53cdfe98242a008485e1e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 13:43:09 +0200 Subject: [PATCH 054/202] 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 fffbde84637938..d1716245d011a2 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 @@ -649,14 +650,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) @@ -666,8 +668,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; } @@ -1019,26 +1019,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; } @@ -1046,10 +1045,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; } @@ -1254,35 +1253,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; } @@ -1314,12 +1308,10 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, if (!rk_udphy_is_supported_mode(state)) 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 d20f7098fb255ebc0863afa95ddfc67fe679f24d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 18 Jun 2026 19:18:48 +0200 Subject: [PATCH 055/202] 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 Closes: https://lore.kernel.org/linux-phy/20260612164627.23D391F000E9@smtp.kernel.org/ 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 d1716245d011a2..b242b137cfe64d 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1153,6 +1153,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 8e6b6a26024d3ab483b5197e014f3492bd1e4425 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 11 Jun 2026 20:07:48 +0200 Subject: [PATCH 056/202] 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 | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index b242b137cfe64d..7d68eee7f9cf92 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -461,6 +462,8 @@ static int rk_udphy_reset_deassert(struct rk_udphy *udphy, char *name) return reset_control_deassert(list[idx].rstc); } + dev_err(udphy->dev, "failed to de-assert missing reset line: %s\n", name); + return -EINVAL; } @@ -487,6 +490,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); } @@ -661,8 +666,10 @@ static int rk_udphy_orien_sw_set(struct typec_switch_dev *sw, return 0; } - if (udphy->flip != flipped) + if (udphy->flip != flipped) { + dev_dbg(udphy->dev, "cable orientation changed, PHY re-init required.\n"); udphy->phy_needs_reinit = true; + } udphy->flip = flipped; rk_udphy_set_typec_default_mapping(udphy); @@ -780,6 +787,11 @@ static int rk_udphy_init(struct rk_udphy *udphy) const struct rk_udphy_cfg *cfg = udphy->cfgs; int ret; + dev_dbg(udphy->dev, "reinit PHY with USB3=%s and DP=%s (%u lanes) flipped=%s\n", + str_on_off(udphy->mode & UDPHY_MODE_USB), + str_on_off(udphy->mode & UDPHY_MODE_DP), + udphy->dp_lanes, str_yes_no(udphy->flip)); + rk_udphy_reset_assert_all(udphy); usleep_range(10000, 11000); @@ -850,6 +862,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"); @@ -868,6 +882,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); } @@ -1307,8 +1322,12 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, struct rk_udphy *udphy = typec_mux_get_drvdata(mux); /* Ignore mux events not involving USB or DP */ - if (!rk_udphy_is_supported_mode(state)) + if (!rk_udphy_is_supported_mode(state)) { + dev_dbg(udphy->dev, "ignore mux event with mode=%lu\n", state->mode); return 0; + } + + dev_dbg(udphy->dev, "new mode: %lu\n", state->mode); guard(mutex)(&udphy->mutex); From 604b5e7c514936ad427d147b3f145dc0ac22375b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 22 Jun 2026 18:36:38 +0200 Subject: [PATCH 057/202] phy: rockchip: usbdp: Avoid xHCI SErrors The USBDP PHY provides the PIPE clock to the USB3 controller, which means the PHY must be fully running when anything tries to access the xHCI registers. When switching between USB3-only, USB3 + DP and DP-only mode, the PHY must be re-initialized resulting in a short period of the PHY being disabled. If the DWC3 driver decides to access the xHCI at this point the system will fail with an SError. This patch avoids the problems by disabling the USB3 port before re-initializing it. This does a couple of things: - forces phystatus to 0 from GRF (not from PHY) - switches PIPE clock source from PHY to UTMI (safe fallback clock) - num_u3_port=0 The last part will be ignored, as DWC3 already probed, but the clock re-routing will avoid the SError. There is a small delay afterwards to make sure the mux happened. The datasheet gives no hints how long it takes, so delay time is a guess. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 7d68eee7f9cf92..31e526a5bffe0e 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -999,12 +999,15 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; } else if (udphy->phy_needs_reinit) { - if (udphy->mode == UDPHY_MODE_DP) - rk_udphy_u3_port_disable(udphy, true); + rk_udphy_u3_port_disable(udphy, true); + udelay(10); ret = rk_udphy_init(udphy); if (ret) return ret; + + if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) + rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; } From 5e6a6da3588dfb0dad76ad68ff3490c8e8ab1ea6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 30 Jun 2026 16:27:26 +0200 Subject: [PATCH 058/202] phy: rockchip: usbdp: Handle rk_udphy_reset_deassert errors Handle rk_udphy_reset_deassert returning errors to avoid theoretical (Rockchip reset controller driver does not return errors) SError. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Closes: https://sashiko.dev/#/message/20260626211151.2332F1F000E9%40smtp.kernel.org Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 31e526a5bffe0e..d223b844c87eba 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -802,8 +802,12 @@ static int rk_udphy_init(struct rk_udphy *udphy) /* Step 1: power on pma and deassert apb rstn */ rk_udphy_grfreg_write(udphy->udphygrf, &cfg->grfcfg.low_pwrn, true); - rk_udphy_reset_deassert(udphy, "pma_apb"); - rk_udphy_reset_deassert(udphy, "pcs_apb"); + ret = rk_udphy_reset_deassert(udphy, "pma_apb"); + if (ret) + goto assert_resets; + ret = rk_udphy_reset_deassert(udphy, "pcs_apb"); + if (ret) + goto assert_resets; /* Step 2: set init sequence and phy refclk */ ret = regmap_multi_reg_write(udphy->pma_regmap, rk_udphy_init_sequence, @@ -829,8 +833,11 @@ static int rk_udphy_init(struct rk_udphy *udphy) FIELD_PREP(CMN_DP_LANE_EN_ALL, 0)); /* Step 4: deassert init rstn and wait for 200ns from datasheet */ - if (udphy->mode & UDPHY_MODE_USB) - rk_udphy_reset_deassert(udphy, "init"); + if (udphy->mode & UDPHY_MODE_USB) { + ret = rk_udphy_reset_deassert(udphy, "init"); + if (ret) + goto assert_resets; + } if (udphy->mode & UDPHY_MODE_DP) { regmap_update_bits(udphy->pma_regmap, CMN_DP_RSTN_OFFSET, @@ -842,8 +849,14 @@ static int rk_udphy_init(struct rk_udphy *udphy) /* Step 5: deassert cmn/lane rstn */ if (udphy->mode & UDPHY_MODE_USB) { - rk_udphy_reset_deassert(udphy, "cmn"); - rk_udphy_reset_deassert(udphy, "lane"); + ret = rk_udphy_reset_deassert(udphy, "cmn"); + if (ret) + goto assert_resets; + + ret = rk_udphy_reset_deassert(udphy, "lane"); + if (ret) + goto assert_resets; + } /* Step 6: wait for lock done of pll */ From 2c4ab38998ead71de536b122aae219b9810fd4c9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 30 Jun 2026 17:40:58 +0200 Subject: [PATCH 059/202] phy: rockchip: usbdp: Only enable USB3 when not in high-speed mode Ensure that USB3 mode is not accidently enabled during PHY re-init for systems that are configured as high-speed only via DT. Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Reported-by: Sashiko Closes: https://sashiko.dev/#/message/20260626212424.C215E1F000E9%40smtp.kernel.org Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index d223b844c87eba..e494bb31dd4d87 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1008,7 +1008,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) if (ret) return ret; - if (udphy->mode & UDPHY_MODE_USB) + if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; } else if (udphy->phy_needs_reinit) { From 731d398dd891504b4a2b31d2215bff34f4e237e7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 29 Jun 2026 21:03:52 +0200 Subject: [PATCH 060/202] phy: core: add notifier infrastructure Some PHY devices with multiple ports (e.g. USB3 and DP) require a reset if the configuration changes or cable orientation changes. This is a problem, as the consumer device will run into undefined behavior. With the new PHY notifier API introduced in this patch, the consumer driver can hook into reset events coming from a PHY device to handle the PHY going down gracefully. Note that this uses -ENOSYS instead of the more sensible -ENOTSUP for the stub functions when GENERIC_PHY is disabled to stay consistent with the existing ones. Signed-off-by: Sebastian Reichel --- drivers/phy/phy-core.c | 65 +++++++++++++++++++++++++++++++++++++++++ include/linux/phy/phy.h | 40 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/drivers/phy/phy-core.c b/drivers/phy/phy-core.c index 21aaf2f76e53eb..51d261daae7a9b 100644 --- a/drivers/phy/phy-core.c +++ b/drivers/phy/phy-core.c @@ -542,6 +542,70 @@ int phy_notify_state(struct phy *phy, union phy_notify state) } EXPORT_SYMBOL_GPL(phy_notify_state); +/** + * phy_register_notifier() - register a notifier for PHY events + * @phy: the phy returned by phy_get() + * @nb: notifier block to register + * + * Allows PHY consumers to receive notifications about PHY reset events. + * PHY providers can signal these events using phy_notify_reset(). + * + * Returns: %0 if successful, a negative error code otherwise + */ +int phy_register_notifier(struct phy *phy, struct notifier_block *nb) +{ + if (!phy) + return 0; + + return blocking_notifier_chain_register(&phy->notifier, nb); +} +EXPORT_SYMBOL_GPL(phy_register_notifier); + +/** + * phy_unregister_notifier() - unregister a notifier for PHY events + * @phy: the phy returned by phy_get() + * @nb: notifier block to unregister + * + * Returns: %0 if successful, a negative error code otherwise + */ +int phy_unregister_notifier(struct phy *phy, struct notifier_block *nb) +{ + if (!phy) + return 0; + + return blocking_notifier_chain_unregister(&phy->notifier, nb); +} +EXPORT_SYMBOL_GPL(phy_unregister_notifier); + +/** + * phy_notify_reset() - notify consumers of a PHY reset event + * @phy: the phy that is being reset + * @event: the notification event (PRE_RESET or POST_RESET) + * + * Called by PHY providers to notify consumers that the PHY is about to + * be reset or has completed a reset. This allows consumers to quiesce + * hardware before the PHY becomes unavailable. + * + * This may be called from within PHY provider callbacks (e.g. set_mode, + * power_on) where phy->mutex is held. Consumer notification handlers must + * therefore NOT call back into the PHY framework (e.g. phy_power_off, + * phy_exit) on the same PHY, as this would result in a deadlock. + * + * Returns: %0 if successful or no notifiers registered, a negative error + * code if a notifier returns an error (for PRE_RESET only) + */ +int phy_notify_reset(struct phy *phy, enum phy_notification event) +{ + int ret; + + if (!phy) + return 0; + + ret = blocking_notifier_call_chain(&phy->notifier, event, phy); + return notifier_to_errno(ret); +} +EXPORT_SYMBOL_GPL(phy_notify_reset); + /** * phy_configure() - Changes the phy parameters * @phy: the phy returned by phy_get() @@ -1018,6 +1082,7 @@ struct phy *phy_create(struct device *dev, struct device_node *node, device_initialize(&phy->dev); lockdep_register_key(&phy->lockdep_key); mutex_init_with_key(&phy->mutex, &phy->lockdep_key); + BLOCKING_INIT_NOTIFIER_HEAD(&phy->notifier); phy->dev.class = &phy_class; phy->dev.parent = dev; diff --git a/include/linux/phy/phy.h b/include/linux/phy/phy.h index ea47975e288aea..3779a4d0a02c3b 100644 --- a/include/linux/phy/phy.h +++ b/include/linux/phy/phy.h @@ -11,6 +11,7 @@ #define __DRIVERS_PHY_H #include +#include #include #include #include @@ -53,6 +54,16 @@ enum phy_media { PHY_MEDIA_DAC, }; +/** + * enum phy_notification - PHY notification events + * @PHY_NOTIFY_PRE_RESET: PHY is about to be reset, consumers should quiesce + * @PHY_NOTIFY_POST_RESET: PHY reset is complete, consumers may resume + */ +enum phy_notification { + PHY_NOTIFY_PRE_RESET, + PHY_NOTIFY_POST_RESET, +}; + enum phy_ufs_state { PHY_UFS_HIBERN8_ENTER, PHY_UFS_HIBERN8_EXIT, @@ -170,6 +181,7 @@ struct phy_attrs { * @power_count: used to protect when the PHY is used by multiple consumers * @attrs: used to specify PHY specific attributes * @pwr: power regulator associated with the phy + * @notifier: notifier head for PHY reset events * @debugfs: debugfs directory */ struct phy { @@ -182,6 +194,7 @@ struct phy { int power_count; struct phy_attrs attrs; struct regulator *pwr; + struct blocking_notifier_head notifier; struct dentry *debugfs; }; @@ -267,6 +280,9 @@ int phy_calibrate(struct phy *phy); int phy_notify_connect(struct phy *phy, int port); int phy_notify_disconnect(struct phy *phy, int port); int phy_notify_state(struct phy *phy, union phy_notify state); +int phy_register_notifier(struct phy *phy, struct notifier_block *nb); +int phy_unregister_notifier(struct phy *phy, struct notifier_block *nb); +int phy_notify_reset(struct phy *phy, enum phy_notification event); static inline int phy_get_bus_width(struct phy *phy) { return phy->attrs.bus_width; @@ -428,6 +444,30 @@ static inline int phy_notify_state(struct phy *phy, union phy_notify state) return -ENOSYS; } +static inline int phy_register_notifier(struct phy *phy, + struct notifier_block *nb) +{ + if (!phy) + return 0; + return -ENOSYS; +} + +static inline int phy_unregister_notifier(struct phy *phy, + struct notifier_block *nb) +{ + if (!phy) + return 0; + return -ENOSYS; +} + +static inline int phy_notify_reset(struct phy *phy, + enum phy_notification event) +{ + if (!phy) + return 0; + return -ENOSYS; +} + static inline int phy_configure(struct phy *phy, union phy_configure_opts *opts) { From 9eb617abcae43ed7b19facf5e568d2f68803c3d6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 29 Jun 2026 21:08:13 +0200 Subject: [PATCH 061/202] usb: dwc3: core: support PHY reset notifications On recent Rockchip platforms (at least RK3588 & RK3576), DWC3 IP is used with a USBDP PHY providing USB3 and DP. This PHY needs to be reset when the mode changes, which may happen when plugging in different USB-C devices. If the USBDP PHY resets with the DWC3 IP running, its internal state corrupts resulting in the USBDP PHY not being able to lock some PLL clocks, which effectively renders USB3 unusable. To fix the issue this adds handling for the new PHY framework reset notifications, which will assert PHYSOFTRST before the actual PHY is disabled and will deassert it once the PHY returns. Signed-off-by: Sebastian Reichel --- drivers/usb/dwc3/core.c | 101 ++++++++++++++++++++++++++++++++++++++++ drivers/usb/dwc3/core.h | 18 +++++++ 2 files changed, 119 insertions(+) diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index ceb49f2f800418..a9f2008044eeac 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -880,6 +881,103 @@ static int dwc3_phy_init(struct dwc3 *dwc) return ret; } +static int dwc3_usb3_phy_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct dwc3_phy_nb *pnb = container_of(nb, struct dwc3_phy_nb, nb); + struct dwc3 *dwc = pnb->dwc; + int port = pnb->port_index; + unsigned long flags; + u32 reg; + int ret; + + switch (action) { + case PHY_NOTIFY_PRE_RESET: + /* + * If already suspended, the resume path will reinit GUSB3PIPECTL + * via dwc3_core_init(). A forced resume is not possible as that + * would call phy_init() resulting in a deadlock. Due to the + * phy_init() in the resume path there is also no need to block + * async RPM resume on our side, since the PHY synchronizes it + * for us. + * + * pm_runtime_get_if_active() returns 0 when suspended (skip), + * 1 when active (ref held), or -EINVAL when PM is disabled + * (device always active). In the -EINVAL case PM ref counting + * is a no-op, so the unconditional put in POST_RESET is safe. + */ + ret = pm_runtime_get_if_active(dwc->dev); + if (!ret) + return NOTIFY_OK; + + /* + * Assert USB3 PHY soft reset within DWC3 before the external + * PHY resets. This disconnects the PIPE interface, preventing + * the DWC3 from interfering with PHY reinitialization and + * avoiding LCPLL lock failures. + */ + spin_lock_irqsave(&dwc->lock, flags); + dwc->phy_reset_active |= BIT(port); + reg = dwc3_readl(dwc, DWC3_GUSB3PIPECTL(port)); + reg |= DWC3_GUSB3PIPECTL_PHYSOFTRST; + dwc3_writel(dwc, DWC3_GUSB3PIPECTL(port), reg); + spin_unlock_irqrestore(&dwc->lock, flags); + break; + + case PHY_NOTIFY_POST_RESET: + spin_lock_irqsave(&dwc->lock, flags); + if (!(dwc->phy_reset_active & BIT(port))) { + spin_unlock_irqrestore(&dwc->lock, flags); + return NOTIFY_OK; + } + + dwc->phy_reset_active &= ~BIT(port); + + /* + * Deassert PHY soft reset to reconnect the PIPE interface + * after PHY reinitialization. + */ + reg = dwc3_readl(dwc, DWC3_GUSB3PIPECTL(port)); + reg &= ~DWC3_GUSB3PIPECTL_PHYSOFTRST; + dwc3_writel(dwc, DWC3_GUSB3PIPECTL(port), reg); + spin_unlock_irqrestore(&dwc->lock, flags); + + pm_runtime_put_autosuspend(dwc->dev); + break; + } + + return NOTIFY_OK; +} + +static void dwc3_phy_register_notifiers(struct dwc3 *dwc) +{ + int i; + + for (i = 0; i < dwc->num_usb3_ports; i++) { + dwc->usb3_phy_nb[i].nb.notifier_call = dwc3_usb3_phy_notify; + dwc->usb3_phy_nb[i].dwc = dwc; + dwc->usb3_phy_nb[i].port_index = i; + phy_register_notifier(dwc->usb3_generic_phy[i], + &dwc->usb3_phy_nb[i].nb); + } +} + +static void dwc3_phy_unregister_notifiers(struct dwc3 *dwc) +{ + int i; + + for (i = 0; i < dwc->num_usb3_ports; i++) + phy_unregister_notifier(dwc->usb3_generic_phy[i], + &dwc->usb3_phy_nb[i].nb); + + /* Release any PM references from in-flight resets */ + for (i = 0; i < dwc->num_usb3_ports; i++) { + if (dwc->phy_reset_active & BIT(i)) + pm_runtime_put_autosuspend(dwc->dev); + } + dwc->phy_reset_active = 0; +} + static void dwc3_phy_exit(struct dwc3 *dwc) { int i; @@ -2341,6 +2439,7 @@ int dwc3_core_probe(const struct dwc3_probe_data *data) dwc3_check_params(dwc); dwc3_debugfs_init(dwc); + dwc3_phy_register_notifiers(dwc); if (!data->skip_core_init_mode) { ret = dwc3_core_init_mode(dwc); @@ -2355,6 +2454,7 @@ int dwc3_core_probe(const struct dwc3_probe_data *data) return 0; err_exit_debugfs: + dwc3_phy_unregister_notifiers(dwc); dwc3_debugfs_exit(dwc); dwc3_event_buffers_cleanup(dwc); dwc3_phy_power_off(dwc); @@ -2412,6 +2512,7 @@ void dwc3_core_remove(struct dwc3 *dwc) dwc3_core_exit_mode(dwc); dwc3_debugfs_exit(dwc); + dwc3_phy_unregister_notifiers(dwc); dwc3_core_exit(dwc); dwc3_ulpi_exit(dwc); diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h index e0dee9d2874010..4e1791ecb591ae 100644 --- a/drivers/usb/dwc3/core.h +++ b/drivers/usb/dwc3/core.h @@ -11,6 +11,7 @@ #ifndef __DRIVERS_USB_DWC3_CORE_H #define __DRIVERS_USB_DWC3_CORE_H +#include #include #include #include @@ -1002,6 +1003,20 @@ struct dwc3_glue_ops { void (*pre_run_stop)(struct dwc3 *dwc, bool is_on); }; +struct dwc3; + +/** + * struct dwc3_phy_nb - wrapper for PHY notifier block + * @nb: notifier block + * @dwc: back-pointer to the DWC3 controller + * @port_index: USB3 port index this notifier is registered for + */ +struct dwc3_phy_nb { + struct notifier_block nb; + struct dwc3 *dwc; + u8 port_index; +}; + /** * struct dwc3 - representation of our controller * @drd_work: workqueue used for role swapping @@ -1065,6 +1080,7 @@ struct dwc3_glue_ops { * @usb3_phy: pointer to USB3 PHY * @usb2_generic_phy: pointer to array of USB2 PHYs * @usb3_generic_phy: pointer to array of USB3 PHYs + * @usb3_phy_nb: notifier blocks for USB3 PHY reset events * @num_usb2_ports: number of USB2 ports * @num_usb3_ports: number of USB3 ports * @phys_ready: flag to indicate that PHYs are ready @@ -1229,6 +1245,8 @@ struct dwc3 { struct phy *usb2_generic_phy[DWC3_USB2_MAX_PORTS]; struct phy *usb3_generic_phy[DWC3_USB3_MAX_PORTS]; + struct dwc3_phy_nb usb3_phy_nb[DWC3_USB3_MAX_PORTS]; + u8 phy_reset_active; u8 num_usb2_ports; u8 num_usb3_ports; From cf4fcec37b4effd8a6b251c6a05de82edce8ef04 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 29 Jun 2026 21:15:13 +0200 Subject: [PATCH 062/202] phy: rockchip: usbdp: Add phy reset notification support To resolve issues with running into permanent "cmn ana lcpll lock timeout" errors after a few device replugs, add support for reset notifications, which will be handled by the DWC3 driver to gracefully handle the PHY being disabled. This avoids corrupting the controller's internal state and the PIPE interface between the USB3 controller and the PHY, thus fixing the issue. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index e494bb31dd4d87..42d4c80042c063 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1004,24 +1004,39 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) } if (udphy->status == UDPHY_MODE_NONE) { + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_PRE_RESET); + + rk_udphy_u3_port_disable(udphy, true); + udelay(10); + ret = rk_udphy_setup(udphy); - if (ret) + if (ret) { + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); return ret; + } if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; + + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); } else if (udphy->phy_needs_reinit) { + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_PRE_RESET); + rk_udphy_u3_port_disable(udphy, true); udelay(10); ret = rk_udphy_init(udphy); - if (ret) + if (ret) { + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); return ret; + } if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; + + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); } udphy->status |= mode; From 21ecde97d78300123343d7999d4d731b05b7b591 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 26 Jun 2026 20:38:19 +0200 Subject: [PATCH 063/202] phy: rockchip: usbdp: Drop -EPROBE_DEFER hack The hack to return -EPROBE_DEFER when the lcpll lock timeouts is no longer needed. The driver now does a reset during its PHY init, which avoids the problem. Since rk_udphy_status_check() is called after the probe, it should not return -EPROBE_DEFER. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 42d4c80042c063..f4518515a2eaba 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -749,17 +749,7 @@ static int rk_udphy_status_check(struct rk_udphy *udphy) (val & CMN_ANA_LCPLL_LOCK_DONE), 200, 100000); if (ret) { dev_err(udphy->dev, "cmn ana lcpll lock timeout\n"); - /* - * If earlier software (U-Boot) enabled USB once already - * the PLL may have problems locking on the first try. - * It will be successful on the second try, so for the - * time being a -EPROBE_DEFER will solve the issue. - * - * This requires further investigation to understand the - * root cause, especially considering that the driver is - * asserting all reset lines at probe time. - */ - return -EPROBE_DEFER; + return ret; } if (!udphy->flip) { From 4a05cc6ebcb5cbc8c4258b50475f2d5b90f35aa1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 22:48:25 +0200 Subject: [PATCH 064/202] phy: rockchip: usbdp: Rename mode to hw_mode Rename mode field to hw_mode to make clear that this is the modes currently supported by the hardware, but not necessarily requested by software. I.e. it is only set by either the USB-C state machine or device-tree if the PHY is used in a fixed routing setup. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 48 +++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index f4518515a2eaba..707957e5cd4aec 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -173,7 +173,7 @@ struct rk_udphy { /* PHY status management */ bool flip; bool phy_needs_reinit; - u8 mode; + u8 hw_mode; /* modes currently supported by hardware */ u8 status; /* utilized for USB */ @@ -578,18 +578,18 @@ 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_mode_set(struct rk_udphy *udphy, u8 mode) +static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 hw_mode) { - if (udphy->mode == mode) + if (udphy->hw_mode == hw_mode) return; udphy->phy_needs_reinit = true; - udphy->mode = mode; + udphy->hw_mode = hw_mode; } static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state) { - u8 mode; + u8 hw_mode; switch (state) { case TYPEC_DP_STATE_C: @@ -598,7 +598,7 @@ static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state 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; + hw_mode = UDPHY_MODE_DP; udphy->dp_lanes = 4; break; @@ -615,12 +615,12 @@ static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; } - mode = UDPHY_MODE_DP_USB; + hw_mode = UDPHY_MODE_DP_USB; udphy->dp_lanes = 2; break; } - rk_udphy_mode_set(udphy, mode); + rk_udphy_mode_set(udphy, hw_mode); } static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) @@ -743,7 +743,7 @@ static int rk_udphy_status_check(struct rk_udphy *udphy) int ret; /* LCPLL check */ - if (udphy->mode & UDPHY_MODE_USB) { + if (udphy->hw_mode & UDPHY_MODE_USB) { ret = regmap_read_poll_timeout(udphy->pma_regmap, CMN_ANA_LCPLL_DONE_OFFSET, val, (val & CMN_ANA_LCPLL_AFC_DONE) && (val & CMN_ANA_LCPLL_LOCK_DONE), 200, 100000); @@ -778,15 +778,15 @@ static int rk_udphy_init(struct rk_udphy *udphy) int ret; dev_dbg(udphy->dev, "reinit PHY with USB3=%s and DP=%s (%u lanes) flipped=%s\n", - str_on_off(udphy->mode & UDPHY_MODE_USB), - str_on_off(udphy->mode & UDPHY_MODE_DP), + str_on_off(udphy->hw_mode & UDPHY_MODE_USB), + str_on_off(udphy->hw_mode & UDPHY_MODE_DP), udphy->dp_lanes, str_yes_no(udphy->flip)); rk_udphy_reset_assert_all(udphy); usleep_range(10000, 11000); /* enable rx lfps for usb */ - if (udphy->mode & UDPHY_MODE_USB) + if (udphy->hw_mode & UDPHY_MODE_USB) rk_udphy_grfreg_write(udphy->udphygrf, &cfg->grfcfg.rx_lfps, true); /* Step 1: power on pma and deassert apb rstn */ @@ -823,13 +823,13 @@ static int rk_udphy_init(struct rk_udphy *udphy) FIELD_PREP(CMN_DP_LANE_EN_ALL, 0)); /* Step 4: deassert init rstn and wait for 200ns from datasheet */ - if (udphy->mode & UDPHY_MODE_USB) { + if (udphy->hw_mode & UDPHY_MODE_USB) { ret = rk_udphy_reset_deassert(udphy, "init"); if (ret) goto assert_resets; } - if (udphy->mode & UDPHY_MODE_DP) { + if (udphy->hw_mode & UDPHY_MODE_DP) { regmap_update_bits(udphy->pma_regmap, CMN_DP_RSTN_OFFSET, CMN_DP_INIT_RSTN, FIELD_PREP(CMN_DP_INIT_RSTN, 0x1)); @@ -838,7 +838,7 @@ static int rk_udphy_init(struct rk_udphy *udphy) udelay(1); /* Step 5: deassert cmn/lane rstn */ - if (udphy->mode & UDPHY_MODE_USB) { + if (udphy->hw_mode & UDPHY_MODE_USB) { ret = rk_udphy_reset_deassert(udphy, "cmn"); if (ret) goto assert_resets; @@ -897,7 +897,7 @@ static int rk_udphy_parse_lane_mux_data(struct rk_udphy *udphy) num_lanes = device_property_count_u32(udphy->dev, "rockchip,dp-lane-mux"); if (num_lanes < 0) { dev_dbg(udphy->dev, "no dp-lane-mux, following dp alt mode\n"); - udphy->mode = UDPHY_MODE_USB; + udphy->hw_mode = UDPHY_MODE_USB; return 0; } @@ -926,10 +926,10 @@ static int rk_udphy_parse_lane_mux_data(struct rk_udphy *udphy) } } - udphy->mode = UDPHY_MODE_DP; + udphy->hw_mode = UDPHY_MODE_DP; udphy->dp_lanes = num_lanes; if (num_lanes == 1 || num_lanes == 2) { - udphy->mode |= UDPHY_MODE_USB; + udphy->hw_mode |= UDPHY_MODE_USB; udphy->flip = (udphy->lane_mux_sel[0] == PHY_LANE_MUX_DP) || (udphy->lane_mux_sel[1] == PHY_LANE_MUX_DP); } @@ -988,7 +988,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) { int ret; - if (!(udphy->mode & mode)) { + if (!(udphy->hw_mode & mode)) { dev_info(udphy->dev, "mode 0x%02x is not support\n", mode); return 0; } @@ -1005,7 +1005,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) return ret; } - if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) + if (!udphy->hs && udphy->hw_mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; @@ -1022,7 +1022,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) return ret; } - if (!udphy->hs && udphy->mode & UDPHY_MODE_USB) + if (!udphy->hs && udphy->hw_mode & UDPHY_MODE_USB) rk_udphy_u3_port_disable(udphy, false); udphy->phy_needs_reinit = false; @@ -1036,7 +1036,7 @@ static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) static void rk_udphy_power_off(struct rk_udphy *udphy, u8 mode) { - if (!(udphy->mode & mode)) { + if (!(udphy->hw_mode & mode)) { dev_info(udphy->dev, "mode 0x%02x is not support\n", mode); return; } @@ -1295,7 +1295,7 @@ static int rk_udphy_usb3_phy_init(struct phy *phy) guard(mutex)(&udphy->mutex); /* DP only or high-speed, disable U3 port */ - if (!(udphy->mode & UDPHY_MODE_USB) || udphy->hs) { + if (!(udphy->hw_mode & UDPHY_MODE_USB) || udphy->hs) { rk_udphy_u3_port_disable(udphy, true); return 0; } @@ -1310,7 +1310,7 @@ static int rk_udphy_usb3_phy_exit(struct phy *phy) guard(mutex)(&udphy->mutex); /* DP only or high-speed */ - if (!(udphy->mode & UDPHY_MODE_USB) || udphy->hs) + if (!(udphy->hw_mode & UDPHY_MODE_USB) || udphy->hs) return 0; rk_udphy_power_off(udphy, UDPHY_MODE_USB); From 235a747ce66e6d8cca6ba6776cd5cfe953ad94a8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 23:15:23 +0200 Subject: [PATCH 065/202] phy: rockchip: usbdp: Fix power state handling Restructure power state handling by introducing sw_mode in addition to the hw_mode field, so that the PHY knows about the currently supported modes from the hardware perspective, the current modes requested by software and the actual hardware status. Now anything updating either the hardware or software state can simply update the status field and call rk_udphy_update_power_state(). This makes it a lot more obvious what is going on and also fixes a few potential resource leaks identified by Sashiko as a side-effect. For example if USB3 is requested by software while the USB-C is in DP-only mode, things are decently handled after this. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 143 ++++++++++++++-------- 1 file changed, 90 insertions(+), 53 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 707957e5cd4aec..024d914419574d 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -172,9 +172,10 @@ struct rk_udphy { /* PHY status management */ bool flip; - bool phy_needs_reinit; + bool phy_needs_reinit; /* lane mux changed */ u8 hw_mode; /* modes currently supported by hardware */ - u8 status; + u8 sw_mode; /* modes currently requested */ + u8 status; /* current PHY power state */ /* utilized for USB */ bool hs; /* flag for high-speed */ @@ -984,70 +985,95 @@ static int rk_udphy_parse_dt(struct rk_udphy *udphy) return rk_udphy_reset_init(udphy, dev); } -static int rk_udphy_power_on(struct rk_udphy *udphy, u8 mode) +static int rk_udphy_update_power_state(struct rk_udphy *udphy) { + bool usb3_port_enable; + u8 target_mode; int ret; - if (!(udphy->hw_mode & mode)) { - dev_info(udphy->dev, "mode 0x%02x is not support\n", mode); + /* + * Initialize PHY mode according to the hardware setup (either described + * in DT or negotiated via the Type-C controller) instead of requesting + * only the needed PHY side, because that would break the USB/DP data + * streams when the other PHY is being requested. This is not an issue + * during the Type-C negotiation as that happens during the hotplug phase + * and not during normal operation. Also disable everything if the + * software has not requested anything, as there shouldn't be any active + * data streams in that case. + */ + target_mode = udphy->hw_mode; + if (udphy->sw_mode == UDPHY_MODE_NONE) + target_mode = UDPHY_MODE_NONE; + + usb3_port_enable = !udphy->hs && (target_mode & UDPHY_MODE_USB); + + if (!udphy->phy_needs_reinit && udphy->status == target_mode) { + if (udphy->sw_mode & UDPHY_MODE_USB) + rk_udphy_u3_port_disable(udphy, !usb3_port_enable); return 0; } - if (udphy->status == UDPHY_MODE_NONE) { - phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_PRE_RESET); + /* Avoid to re-init disabled PHY */ + if (udphy->status == target_mode && target_mode == UDPHY_MODE_NONE) + return 0; + /* + * Inform DWC3 driver, that we are about to reset the PHY, so that it can + * assert its PIPE reset lines and avoid DWC3 getting into a buggy state. + * This is intentionally done for a PHY disable, since that also changes + * the clocks routed to the PHY. + */ + ret = phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_PRE_RESET); + if (ret) + return ret; + + /* + * Disable USB3 port, which among other things re-routes a DWC3 clock to + * avoid SErrors when the DWC3 registers are accessed while the PHY is + * disabled. This is only done, when the DWC3 is running as the accessed + * GRF registers and in PD_USB. + */ + if (udphy->sw_mode & UDPHY_MODE_USB) { rk_udphy_u3_port_disable(udphy, true); udelay(10); + } + if (udphy->status == UDPHY_MODE_NONE) { + /* Power up (incl. clocks) */ ret = rk_udphy_setup(udphy); if (ret) { phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); return ret; } - - if (!udphy->hs && udphy->hw_mode & UDPHY_MODE_USB) - rk_udphy_u3_port_disable(udphy, false); - udphy->phy_needs_reinit = false; - - phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); - } else if (udphy->phy_needs_reinit) { - phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_PRE_RESET); - - rk_udphy_u3_port_disable(udphy, true); - udelay(10); - + } else if (target_mode == UDPHY_MODE_NONE) { + /* Power down (incl. clocks) */ + rk_udphy_disable(udphy); + } else { + /* Mode change => re-init */ ret = rk_udphy_init(udphy); if (ret) { phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); return ret; } - - if (!udphy->hs && udphy->hw_mode & UDPHY_MODE_USB) - rk_udphy_u3_port_disable(udphy, false); - udphy->phy_needs_reinit = false; - - phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); } - udphy->status |= mode; - - return 0; -} + /* Ensure USB3 support is enabled when supported */ + if (udphy->sw_mode & UDPHY_MODE_USB) + rk_udphy_u3_port_disable(udphy, !usb3_port_enable); -static void rk_udphy_power_off(struct rk_udphy *udphy, u8 mode) -{ - if (!(udphy->hw_mode & mode)) { - dev_info(udphy->dev, "mode 0x%02x is not support\n", mode); - return; - } - - if (!udphy->status) - return; + /* + * Inform DWC3, that we are done with the reset, so that it can deassert + * its PIPE reset line. This is sent in pair with a PRE_RESET allowing + * consumer driver to do paired resource requests (e.g. clocks) in their + * notification handlers. As we reroute the clocks, its also fine to + * send this after completely disabling the PHY. + */ + phy_notify_reset(udphy->phy_u3, PHY_NOTIFY_POST_RESET); - udphy->status &= ~mode; + udphy->status = target_mode; + udphy->phy_needs_reinit = false; - if (udphy->status == UDPHY_MODE_NONE) - rk_udphy_disable(udphy); + return 0; } static int rk_udphy_dp_phy_power_on(struct phy *phy) @@ -1056,11 +1082,15 @@ static int rk_udphy_dp_phy_power_on(struct phy *phy) int ret; scoped_guard(mutex, &udphy->mutex) { + udphy->sw_mode |= UDPHY_MODE_DP; + phy_set_bus_width(phy, udphy->dp_lanes); - ret = rk_udphy_power_on(udphy, UDPHY_MODE_DP); - if (ret) + ret = rk_udphy_update_power_state(udphy); + if (ret) { + udphy->sw_mode &= ~UDPHY_MODE_DP; return ret; + } rk_udphy_dp_lane_enable(udphy, udphy->dp_lanes); @@ -1083,10 +1113,10 @@ static int rk_udphy_dp_phy_power_off(struct phy *phy) guard(mutex)(&udphy->mutex); - rk_udphy_dp_lane_enable(udphy, 0); - rk_udphy_power_off(udphy, UDPHY_MODE_DP); + udphy->sw_mode &= ~UDPHY_MODE_DP; - return 0; + rk_udphy_dp_lane_enable(udphy, 0); + return rk_udphy_update_power_state(udphy); } /* @@ -1291,16 +1321,24 @@ 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); - /* DP only or high-speed, disable U3 port */ - if (!(udphy->hw_mode & UDPHY_MODE_USB) || udphy->hs) { + if (udphy->hs) { rk_udphy_u3_port_disable(udphy, true); return 0; } - return rk_udphy_power_on(udphy, UDPHY_MODE_USB); + udphy->sw_mode |= UDPHY_MODE_USB; + + ret = rk_udphy_update_power_state(udphy); + if (ret) { + udphy->sw_mode &= ~UDPHY_MODE_USB; + return ret; + } + + return 0; } static int rk_udphy_usb3_phy_exit(struct phy *phy) @@ -1309,13 +1347,12 @@ static int rk_udphy_usb3_phy_exit(struct phy *phy) guard(mutex)(&udphy->mutex); - /* DP only or high-speed */ - if (!(udphy->hw_mode & UDPHY_MODE_USB) || udphy->hs) + if (udphy->hs) return 0; - rk_udphy_power_off(udphy, UDPHY_MODE_USB); + udphy->sw_mode &= ~UDPHY_MODE_USB; - return 0; + return rk_udphy_update_power_state(udphy); } static const struct phy_ops rk_udphy_usb3_phy_ops = { From 872e39121575e362492e5584a219d7cd90dbb8a8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 23:37:32 +0200 Subject: [PATCH 066/202] phy: rockchip: usbdp: Re-init PHY on mux change Ensure that the right part of the PHY are powered up when the mode changes. This ensures the PHY is re-initialized in the following two scenarios, which are currently broken: - cable orientation changes without DP being involved - switching from DP-only into a mode with USB support Fixes: 2f70bbddeb45 ("phy: rockchip: add usbdp combo phy driver") Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 024d914419574d..1bf210cf9195dc 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -1391,7 +1391,7 @@ static int rk_udphy_typec_mux_set(struct typec_mux_dev *mux, rk_udphy_set_typec_state(udphy, state->mode); - return 0; + return rk_udphy_update_power_state(udphy); } static void rk_udphy_typec_mux_unregister(void *data) From 90aa0772656a4ca94e145f414ea8da4f4397fea4 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 2 Jul 2026 20:38:36 +0200 Subject: [PATCH 067/202] phy: rockchip: usbdp: Add USB-C state without DP enabled The driver currently only differs between 4 lanes DP mode or combined DP + USB3 mode. This makes sense from a lane routing point of view, as the hardware only has 2 lanes of USB3. But adding a separate state for USB-only helps with power management, since we always power up all PHY parts according to the current hardware setup to avoid data stream interruptions. Even if some lanes are muxed to the DP controller there is no need to keep the DP side enabled if something without DP AltMode is plugged into USB-C. This potentially triggers some more USB reconnections during the PD AltMode negotiation when switching from USB-only to combined USB+DP mode. This should be fine, as the cable is freshly plugged at this point. Signed-off-by: Sebastian Reichel --- drivers/phy/rockchip/phy-rockchip-usbdp.c | 57 +++++++++++++---------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-usbdp.c b/drivers/phy/rockchip/phy-rockchip-usbdp.c index 1bf210cf9195dc..998e8daf0b4de8 100644 --- a/drivers/phy/rockchip/phy-rockchip-usbdp.c +++ b/drivers/phy/rockchip/phy-rockchip-usbdp.c @@ -579,32 +579,14 @@ 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_mode_set(struct rk_udphy *udphy, u8 hw_mode) +static void rk_udphy_set_lane_mux(struct rk_udphy *udphy) { - if (udphy->hw_mode == hw_mode) - return; - - udphy->phy_needs_reinit = true; - udphy->hw_mode = hw_mode; -} - -static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state) -{ - u8 hw_mode; - - switch (state) { - case TYPEC_DP_STATE_C: - case TYPEC_DP_STATE_E: + if (udphy->dp_lanes == 4) { 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; - hw_mode = UDPHY_MODE_DP; - udphy->dp_lanes = 4; - break; - - case TYPEC_DP_STATE_D: - default: + } else { if (udphy->flip) { udphy->lane_mux_sel[0] = PHY_LANE_MUX_DP; udphy->lane_mux_sel[1] = PHY_LANE_MUX_DP; @@ -616,12 +598,39 @@ static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state udphy->lane_mux_sel[2] = PHY_LANE_MUX_DP; udphy->lane_mux_sel[3] = PHY_LANE_MUX_DP; } - hw_mode = UDPHY_MODE_DP_USB; - udphy->dp_lanes = 2; + } +} + +static void rk_udphy_mode_set(struct rk_udphy *udphy, u8 hw_mode, u8 dp_lanes) +{ + if (udphy->hw_mode == hw_mode && udphy->dp_lanes == dp_lanes) + return; + + udphy->phy_needs_reinit = true; + udphy->hw_mode = hw_mode; + udphy->dp_lanes = dp_lanes; +} + +static void rk_udphy_set_typec_state(struct rk_udphy *udphy, unsigned long state) +{ + switch (state) { + case TYPEC_DP_STATE_C: + case TYPEC_DP_STATE_E: + rk_udphy_mode_set(udphy, UDPHY_MODE_DP, 4); + break; + + case TYPEC_DP_STATE_D: + rk_udphy_mode_set(udphy, UDPHY_MODE_DP_USB, 2); + break; + + case TYPEC_STATE_SAFE: + case TYPEC_STATE_USB: + default: + rk_udphy_mode_set(udphy, UDPHY_MODE_USB, 0); break; } - rk_udphy_mode_set(udphy, hw_mode); + rk_udphy_set_lane_mux(udphy); } static void rk_udphy_set_typec_default_mapping(struct rk_udphy *udphy) From 55a4012635bb231a2d24a84b48c36a767afee93b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 28 Jul 2025 16:58:21 +0200 Subject: [PATCH 068/202] 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 680481f94fc80b..7db3dcffb3f8c7 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"; @@ -362,21 +378,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>; }; }; }; @@ -1023,18 +1039,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>; + }; }; }; }; @@ -1055,9 +1094,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>; + }; }; }; }; @@ -1096,3 +1150,10 @@ remote-endpoint = <&hdmi1_in_vp1>; }; }; + +&vp2 { + vp2_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp2>; + }; +}; From 1f358283fd1f7099b004d607a4dfa7e982287ec2 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 8 Oct 2025 18:15:24 +0200 Subject: [PATCH 069/202] 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 b338c63dbcbb37..2a69bd3106d91f 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-rock-4d.dts @@ -930,6 +930,9 @@ }; &ufshc { + vcc-supply = <&vcc_3v3_s0>; + vccq-supply = <&vcc_1v2_ufs_vccq_s0>; + vccq2-supply = <&vcc_1v8_ufs_vccq2_s0>; status = "okay"; }; From 6328548b124ee784e2c1023ddbc6b860c0e4a377 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 6 Nov 2025 16:21:05 +0100 Subject: [PATCH 070/202] 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 d56ca494b0638c..02c5b5e02f7b82 100644 --- a/drivers/net/phy/realtek/realtek_main.c +++ b/drivers/net/phy/realtek/realtek_main.c @@ -968,11 +968,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); @@ -981,6 +983,9 @@ static int rtl821x_resume(struct phy_device *phydev) msleep(20); + if (reinit) + phy_init_hw(phydev); + return 0; } From 1b156b80db6e01bf602bab95606c7b90d1c5d156 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 5 Nov 2025 18:44:09 +0100 Subject: [PATCH 071/202] 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 4c82980a9f63ae..f3ede9465d14a5 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-evb1-v10.dts @@ -813,6 +813,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>; @@ -828,6 +829,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 ec6240a8ac171f130954a71e5eb16fac44e6c918 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 10 Jan 2026 04:29:23 +0100 Subject: [PATCH 072/202] 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 654a102cb5bc55..c84df0a9cf6e74 100644 --- a/arch/arm64/configs/defconfig +++ b/arch/arm64/configs/defconfig @@ -939,6 +939,7 @@ CONFIG_VIDEO_RENESAS_VSP1=m CONFIG_VIDEO_RCAR_DRIF=m CONFIG_VIDEO_ROCKCHIP_RGA=m CONFIG_VIDEO_ROCKCHIP_CIF=m +CONFIG_VIDEO_ROCKCHIP_VDEC=m CONFIG_VIDEO_SAMSUNG_EXYNOS_GSC=m CONFIG_VIDEO_SAMSUNG_S5P_JPEG=m CONFIG_VIDEO_SAMSUNG_S5P_MFC=m From 32211de5735ad59765540566b790ed4d0b2f46d1 Mon Sep 17 00:00:00 2001 From: Benjamin Gaignard Date: Wed, 15 Apr 2026 09:23:41 +0200 Subject: [PATCH 073/202] 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 c84df0a9cf6e74..c7d9dfd120e9a5 100644 --- a/arch/arm64/configs/defconfig +++ b/arch/arm64/configs/defconfig @@ -1612,6 +1612,7 @@ CONFIG_ROCKCHIP_IOMMU=y CONFIG_TEGRA_IOMMU_SMMU=y CONFIG_APPLE_DART=m CONFIG_MTK_IOMMU=y +CONFIG_VSI_IOMMU=m CONFIG_REMOTEPROC=y CONFIG_IMX_REMOTEPROC=y CONFIG_MTK_SCP=m From 6dc307018daa115a1d87000254e6bb9f13c98197 Mon Sep 17 00:00:00 2001 From: Andy Yan Date: Fri, 9 Jan 2026 18:01:19 +0800 Subject: [PATCH 074/202] [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 4ac4465e39a512..0b224a885f69f7 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"; @@ -722,20 +738,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>; }; }; }; @@ -998,14 +1016,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>; }; }; }; @@ -1025,9 +1043,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>; + }; }; }; }; @@ -1046,3 +1096,10 @@ remote-endpoint = <&hdmi_in_vp0>; }; }; + +&vp1 { + vp1_out_dp0: endpoint@a { + reg = ; + remote-endpoint = <&dp0_in_vp1>; + }; +}; From 3d8f3b678f9c04d62765292f1f6b61545cec574f Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:06 +0800 Subject: [PATCH 075/202] 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 f3ba684d7e7c1e698bbd9c23e112842bb4521b1c Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:07 +0800 Subject: [PATCH 076/202] 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 913432a405d8dfa09cdc80fc0f2e06d301b69479 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:08 +0800 Subject: [PATCH 077/202] 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 c3aa6f1dd6ccf7f59364e25df592fb5d02ee959f Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:09 +0800 Subject: [PATCH 078/202] 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 c4be49bb478ed9c31118d48e119ce9b2a77cc99e Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:10 +0800 Subject: [PATCH 079/202] 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 47193f09fc3043abc3e6c9d93a732181a54cc079 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 19 Jan 2026 13:11:40 +0100 Subject: [PATCH 080/202] 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 c688f3eaf4686f..b4fb1c1e6d997a 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 11ef386ce256711cc4bd1e34590d8125baa179a0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 22 Jan 2026 14:57:54 +0100 Subject: [PATCH 081/202] 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 d77a2250f0bd32eb6a4c9083ee580ef5f6b4c9e7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 23:57:28 +0100 Subject: [PATCH 082/202] 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 df9eaeea1e41fc..5a0479ebc4cdff 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 17eda592b1833a..3ee18f58c52be8 100644 --- a/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c +++ b/drivers/gpu/drm/rockchip/rockchip_vop2_reg.c @@ -558,18 +558,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, }, @@ -774,6 +777,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, @@ -784,6 +788,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, @@ -792,6 +797,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, @@ -1060,6 +1066,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, @@ -1069,6 +1076,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, }, { @@ -1077,12 +1085,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 ee7adef02f255a7dd9546ac4f2e8941c0b52873d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:44:57 +0100 Subject: [PATCH 083/202] [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 0b224a885f69f7..73ec60dc092eec 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -940,6 +940,7 @@ no-sdio; no-sd; non-removable; + /delete-property/ supports-cqe; status = "okay"; }; From 717b42a1778b537c49f9e8d14da2aef6bb63e0e1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 2 Mar 2026 22:37:40 +0100 Subject: [PATCH 084/202] 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 70298f766cdb03..3d80aac21e865a 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -5629,20 +5629,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, @@ -5664,10 +5669,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 06bfd95e12eaf5b269e9f9be000e3913495d1b56 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Mar 2026 18:16:31 +0100 Subject: [PATCH 085/202] 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 3d80aac21e865a..c172e817a8d7ec 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; @@ -3372,11 +3381,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: @@ -3573,11 +3579,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: @@ -3895,11 +3898,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 ba0f70e6ce0ff8ea4d2f474809775a5a6d9e062e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:45 +0200 Subject: [PATCH 086/202] 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 e4e360025662874ee8ad2227aa2b7ab922aab925 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:46 +0200 Subject: [PATCH 087/202] 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 b032e9bc91a9d9bfcabefd4f6114a04ac24d0f8e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:47 +0200 Subject: [PATCH 088/202] 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 4816a918d1230d29665aa1f0c61c5c46e2ed40eb Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:48 +0200 Subject: [PATCH 089/202] 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 7eae149db1bcc26e64a8b2f0de61ea8697ee4b3b Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:49 +0200 Subject: [PATCH 090/202] 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 64ea0c1b94e66707e366ed9964a8ee7db5b20da6 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:50 +0200 Subject: [PATCH 091/202] 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 b5332f33e92e3de514537bd36ab1709b82773ada Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Feb 2026 20:36:26 +0200 Subject: [PATCH 092/202] [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 3cae8057c8c7484f575ebb3a3e383e288c9e22d1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:43:25 +0100 Subject: [PATCH 093/202] [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 5f7ca5e0fe85afda18882ed8170c71dd6fbebced Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 16 Jul 2026 23:43:55 +0200 Subject: [PATCH 094/202] drm/bridge: synopsys: dw-dp: Fix incorrect resource lifetimes in bind callback Currently the Synopsys DesignWare DP controller driver's bind function requests lots of resources using device managed functions. These are free'd on driver removal instead of at unbind time. Fix this discrepancy by introducing a new probe helper function and moving over most of the early resource acquisitions. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 128 +++++++++++----------- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 53 +++++---- include/drm/bridge/dw_dp.h | 5 +- 3 files changed, 99 insertions(+), 87 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 36ee6e027af528..13181bc0a6fbc8 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1954,20 +1954,72 @@ static const struct regmap_config dw_dp_regmap_config = { .rd_table = &dw_dp_readable_table, }; -static void dw_dp_phy_exit(void *data) +int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) { - struct dw_dp *dp = data; + struct device *dev = dp->dev; + struct drm_bridge *bridge; + int ret; + + bridge = &dp->bridge; + bridge->of_node = dev->of_node; + bridge->ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID | DRM_BRIDGE_OP_HPD; + bridge->type = DRM_MODE_CONNECTOR_DisplayPort; + bridge->ycbcr_420_allowed = true; + + drm_bridge_add(bridge); + + dp->aux.dev = dev; + dp->aux.drm_dev = encoder->dev; + dp->aux.name = dev_name(dev); + dp->aux.transfer = dw_dp_aux_transfer; + ret = drm_dp_aux_register(&dp->aux); + if (ret) { + dev_err_probe(dev, ret, "Aux register failed\n"); + goto unregister_bridge; + } + + ret = drm_bridge_attach(encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR); + if (ret) { + dev_err_probe(dev, ret, "Failed to attach bridge\n"); + goto unregister_aux; + } + + dw_dp_init_hw(dp); + ret = phy_init(dp->phy); + if (ret) { + dev_err_probe(dev, ret, "phy init failed\n"); + goto unregister_aux; + } + + enable_irq(dp->irq); + + return 0; + +unregister_aux: + drm_dp_aux_unregister(&dp->aux); + +unregister_bridge: + drm_bridge_remove(bridge); + + return ret; +} +EXPORT_SYMBOL_GPL(dw_dp_bind); + +void dw_dp_unbind(struct dw_dp *dp) +{ + disable_irq(dp->irq); phy_exit(dp->phy); + drm_dp_aux_unregister(&dp->aux); + drm_bridge_remove(&dp->bridge); } +EXPORT_SYMBOL_GPL(dw_dp_unbind); -struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, - const struct dw_dp_plat_data *plat_data) +struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data) { - struct platform_device *pdev = to_platform_device(dev); - struct dw_dp *dp; - struct drm_bridge *bridge; + struct device *dev = &pdev->dev; void __iomem *res; + struct dw_dp *dp; int ret; dp = devm_drm_bridge_alloc(dev, struct dw_dp, bridge, &dw_dp_bridge_funcs); @@ -1976,9 +2028,8 @@ 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.max_link_rate = plat_data->max_link_rate; - bridge = &dp->bridge; + mutex_init(&dp->irq_lock); INIT_WORK(&dp->hpd_work, dw_dp_hpd_work); init_completion(&dp->complete); @@ -2035,69 +2086,20 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, return ERR_CAST(dp->rstc); } - bridge->of_node = dev->of_node; - bridge->ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID | DRM_BRIDGE_OP_HPD; - bridge->type = DRM_MODE_CONNECTOR_DisplayPort; - bridge->ycbcr_420_allowed = true; - - ret = devm_drm_bridge_add(dev, bridge); - if (ret) - return ERR_PTR(ret); - - dp->aux.dev = dev; - dp->aux.drm_dev = encoder->dev; - dp->aux.name = dev_name(dev); - dp->aux.transfer = dw_dp_aux_transfer; - ret = drm_dp_aux_register(&dp->aux); - if (ret) { - dev_err_probe(dev, ret, "Aux register failed\n"); - return ERR_PTR(ret); - } - - ret = drm_bridge_attach(encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR); - if (ret) { - dev_err_probe(dev, ret, "Failed to attach bridge\n"); - goto unregister_aux; - } - - dw_dp_init_hw(dp); - - ret = phy_init(dp->phy); - if (ret) { - dev_err_probe(dev, ret, "phy init failed\n"); - goto unregister_aux; - } - - ret = devm_add_action_or_reset(dev, dw_dp_phy_exit, dp); - if (ret) - goto unregister_aux; - dp->irq = platform_get_irq(pdev, 0); - if (dp->irq < 0) { - ret = dp->irq; - goto unregister_aux; - } + if (dp->irq < 0) + return ERR_PTR(dp->irq); ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, - IRQF_ONESHOT, dev_name(dev), dp); + IRQF_ONESHOT | IRQF_NO_AUTOEN, dev_name(dev), dp); if (ret) { dev_err_probe(dev, ret, "failed to request irq\n"); - goto unregister_aux; + return ERR_PTR(ret); } return dp; - -unregister_aux: - drm_dp_aux_unregister(&dp->aux); - return ERR_PTR(ret); } -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); +EXPORT_SYMBOL_GPL(dw_dp_probe); MODULE_AUTHOR("Andy Yan "); MODULE_DESCRIPTION("DW DP Core Library"); diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 32bc73a1d5e456..cb51661818530e 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -27,7 +27,7 @@ struct rockchip_dw_dp { struct dw_dp *base; struct device *dev; - struct rockchip_encoder encoder; + struct rockchip_encoder *encoder; }; static int dw_dp_encoder_atomic_check(struct drm_encoder *encoder, @@ -74,37 +74,28 @@ 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 rockchip_dw_dp *dp = dev_get_drvdata(dev); struct drm_device *drm_dev = data; - struct rockchip_dw_dp *dp; struct drm_encoder *encoder; struct drm_connector *connector; int ret; - dp = drmm_kzalloc(drm_dev, sizeof(*dp), GFP_KERNEL); - if (!dp) + dp->encoder = drmm_kzalloc(drm_dev, sizeof(*dp->encoder), GFP_KERNEL); + if (!dp->encoder) return -ENOMEM; - dp->dev = dev; - platform_set_drvdata(pdev, dp); - - plat_data = of_device_get_match_data(dev); - if (!plat_data) - return -ENODEV; - - encoder = &dp->encoder.encoder; + 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); + rockchip_drm_encoder_set_crtc_endpoint_id(dp->encoder, dev->of_node, 0, 0); ret = drmm_encoder_init(drm_dev, encoder, NULL, DRM_MODE_ENCODER_TMDS, NULL); if (ret) return ret; drm_encoder_helper_add(encoder, &dw_dp_encoder_helper_funcs); - dp->base = dw_dp_bind(dev, encoder, plat_data); - if (IS_ERR(dp->base)) - return PTR_ERR(dp->base); + ret = dw_dp_bind(dp->base, encoder); + if (ret) + return ret; connector = drm_bridge_connector_init(drm_dev, encoder); if (IS_ERR(connector)) @@ -118,12 +109,30 @@ static const struct component_ops dw_dp_rockchip_component_ops = { .bind = dw_dp_rockchip_bind, }; -static int dw_dp_probe(struct platform_device *pdev) +static int dw_dp_rockchip_probe(struct platform_device *pdev) { + const struct dw_dp_plat_data *plat_data; + struct device *dev = &pdev->dev; + struct rockchip_dw_dp *dp; + + plat_data = of_device_get_match_data(dev); + if (!plat_data) + return -ENODEV; + + dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL); + if (!dp) + return -ENOMEM; + platform_set_drvdata(pdev, dp); + dp->dev = dev; + + dp->base = dw_dp_probe(pdev, plat_data); + if (IS_ERR(dp->base)) + return PTR_ERR(dp->base); + return component_add(&pdev->dev, &dw_dp_rockchip_component_ops); } -static void dw_dp_remove(struct platform_device *pdev) +static void dw_dp_rockchip_remove(struct platform_device *pdev) { component_del(&pdev->dev, &dw_dp_rockchip_component_ops); } @@ -151,8 +160,8 @@ static const struct of_device_id dw_dp_of_match[] = { MODULE_DEVICE_TABLE(of, dw_dp_of_match); struct platform_driver dw_dp_driver = { - .probe = dw_dp_probe, - .remove = dw_dp_remove, + .probe = dw_dp_rockchip_probe, + .remove = dw_dp_rockchip_remove, .driver = { .name = "dw-dp", .of_match_table = dw_dp_of_match, diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index 22105c3e8e4d66..a82412a9e76956 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -22,7 +22,8 @@ struct dw_dp_plat_data { u8 pixel_mode; }; -struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, - const struct dw_dp_plat_data *plat_data); +int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder); void dw_dp_unbind(struct dw_dp *dp); + +struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data); #endif /* __DW_DP__ */ From 4a6374c5672a64fb505c65ad7ac952374242c546 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 23 Jul 2026 17:31:47 +0200 Subject: [PATCH 095/202] drm/bridge: synopsys: dw-dp: Cancel pending HPD work on unbind There is a race condition during device unbind: If the HPD interrupt started an HPD worker exactly when the unbind happens, the worker thread might access invalid resources. Avoid this by stopping any pending work immediately after disabling the interrupt. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 13181bc0a6fbc8..75eebe541dc19b 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2009,6 +2009,7 @@ EXPORT_SYMBOL_GPL(dw_dp_bind); void dw_dp_unbind(struct dw_dp *dp) { disable_irq(dp->irq); + cancel_work_sync(&dp->hpd_work); phy_exit(dp->phy); drm_dp_aux_unregister(&dp->aux); drm_bridge_remove(&dp->bridge); From a33005ed03d696f7dfd3ec0690436dc3cad0bd39 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 22 Jul 2026 20:03:14 +0200 Subject: [PATCH 096/202] drm/bridge: synopsys: dw-dp: Add missing mutex cleanups on module removal The driver is currently missing to fully clean up after itself. Ensure that the mutex is cleaned up. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 75eebe541dc19b..55e6544a9e7c6a 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2031,10 +2031,13 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ dp->pixel_mode = plat_data->pixel_mode; dp->plat_data.max_link_rate = plat_data->max_link_rate; - mutex_init(&dp->irq_lock); INIT_WORK(&dp->hpd_work, dw_dp_hpd_work); init_completion(&dp->complete); + ret = devm_mutex_init(dev, &dp->irq_lock); + if (ret) + return ERR_PTR(ret); + res = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(res)) return ERR_CAST(res); From 5dc823ef900bd267838aea81b67bcc1f81a0cb1c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Jul 2026 15:04:19 +0200 Subject: [PATCH 097/202] drm/bridge: synopsys: dw-dp: Add missing reinit_completion The DP AUX transfer method uses a completion triggered by an interrupt, which can timeout. If the function runs into the timeout and the interrupt fires afterwards, the following DP aux transfer completion would trigger immediately without waiting for the interrupt. This in turn means the next one would also be broken and so on. Fix this potential issue by re-initializing the completion before sending the AUX command. Reported-by: Sashiko Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 55e6544a9e7c6a..06e85195761252 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; + reinit_completion(&dp->complete); + switch (msg->request & ~DP_AUX_I2C_MOT) { case DP_AUX_NATIVE_WRITE: case DP_AUX_I2C_WRITE: From 11283df0235145e30e295a7b95d88aaf2b33997b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Jul 2026 15:28:55 +0200 Subject: [PATCH 098/202] drm/bridge: synopsys: dw-dp: Free output_fmts when none are valid If dw_dp_bandwidth_ok() returns false for all formats, *num_output_fmts might end up becoming 0. In this case functions calling it assume that nothing needs to be free'd, so free output_fmts within the function to avoid leaking memory. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 06e85195761252..44c52e48f54b87 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1797,6 +1797,11 @@ static u32 *dw_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge, output_fmts[j++] = fmt->bus_format; } + if (j == 0) { + kfree(output_fmts); + output_fmts = NULL; + } + *num_output_fmts = j; return output_fmts; From 4a258768af8180ed0515433f68279f82f594d264 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 6 Mar 2026 14:26:19 +0100 Subject: [PATCH 099/202] 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. For decent support of this, two areas are changed: 1. In atomic_check, resolving MEDIA_BUS_FMT_FIXED output format by using the negotiated input format. 2. Implementing a custom .atomic_get_input_bus_fmts hook that, on MEDIA_BUS_FMT_FIXED, advertises all bandwidth-validated formats from dw_dp_bridge_atomic_get_output_bus_fmts(). This lets the upstream encoder negotiate the best mutually supported format. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 34 +++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 44c52e48f54b87..f37565bc082b6c 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1530,6 +1530,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; @@ -1540,7 +1541,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 = bridge_state->input_bus_cfg.format; + + fmt = dw_dp_get_output_format(out_bus_format); if (!fmt) return -EINVAL; @@ -1807,6 +1811,32 @@ static u32 *dw_dp_bridge_atomic_get_output_bus_fmts(struct drm_bridge *bridge, return output_fmts; } +static u32 * +dw_dp_bridge_atomic_get_input_bus_fmts(struct drm_bridge *bridge, + struct drm_bridge_state *bridge_state, + struct drm_crtc_state *crtc_state, + struct drm_connector_state *conn_state, + u32 output_fmt, + unsigned int *num_input_fmts) +{ + /* + * MEDIA_BUS_FMT_FIXED means the downstream bridge does not constrain + * the bus format. In that case, advertise all formats supported by the + * DP link so the upstream encoder can negotiate the best match. + */ + if (output_fmt == MEDIA_BUS_FMT_FIXED) + return dw_dp_bridge_atomic_get_output_bus_fmts(bridge, + bridge_state, + crtc_state, + conn_state, + num_input_fmts); + + return drm_atomic_helper_bridge_propagate_bus_fmt(bridge, bridge_state, + crtc_state, conn_state, + output_fmt, + num_input_fmts); +} + static struct drm_bridge_state *dw_dp_bridge_atomic_duplicate_state(struct drm_bridge *bridge) { struct dw_dp_bridge_state *state; @@ -1824,7 +1854,7 @@ 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, .atomic_reset = drm_atomic_helper_bridge_reset, - .atomic_get_input_bus_fmts = drm_atomic_helper_bridge_propagate_bus_fmt, + .atomic_get_input_bus_fmts = dw_dp_bridge_atomic_get_input_bus_fmts, .atomic_get_output_bus_fmts = dw_dp_bridge_atomic_get_output_bus_fmts, .atomic_check = dw_dp_bridge_atomic_check, .mode_valid = dw_dp_bridge_mode_valid, From 71d272e361af76a7ad0de0f46ab41cd2b00a04f2 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:12:53 +0100 Subject: [PATCH 100/202] 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 It's fine to fatally error out when there is no follow-up bridge as the Rockchip Designware Displayport controller is the only user of the bridge helper and has the port marked as required in its binding. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index f37565bc082b6c..b46aef82789190 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -329,6 +329,8 @@ struct dw_dp { struct dw_dp_plat_data plat_data; u8 pixel_mode; + struct drm_bridge *next_bridge; + DECLARE_BITMAP(sdp_reg_bank, SDP_REG_BANK_SIZE); }; @@ -2021,18 +2023,35 @@ int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) goto unregister_aux; } + dp->next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 1, 0); + if (IS_ERR(dp->next_bridge)) { + ret = PTR_ERR(dp->next_bridge); + dev_err_probe(dev, ret, "failed to get follow-up bridge.\n"); + goto unregister_aux; + } + + ret = drm_bridge_attach(encoder, dp->next_bridge, bridge, + DRM_BRIDGE_ATTACH_NO_CONNECTOR); + if (ret) { + dev_err_probe(dev, ret, "Failed to attach next bridge\n"); + goto put_next_bridge; + } + dw_dp_init_hw(dp); ret = phy_init(dp->phy); if (ret) { dev_err_probe(dev, ret, "phy init failed\n"); - goto unregister_aux; + goto put_next_bridge; } enable_irq(dp->irq); return 0; +put_next_bridge: + drm_bridge_put(dp->next_bridge); + unregister_aux: drm_dp_aux_unregister(&dp->aux); @@ -2048,6 +2067,7 @@ void dw_dp_unbind(struct dw_dp *dp) disable_irq(dp->irq); cancel_work_sync(&dp->hpd_work); phy_exit(dp->phy); + drm_bridge_put(dp->next_bridge); drm_dp_aux_unregister(&dp->aux); drm_bridge_remove(&dp->bridge); } From 8126cada840b5dbb64dec4b1ebced85ca812f61d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 26 Feb 2026 16:42:44 +0100 Subject: [PATCH 101/202] 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 649969fca1413b..f711d1dbe2e6d9 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 4ba3a5deef9a60..94e79481372210 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 fd2920706dff93de430c44701417f6060ff79a22 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:29:45 +0100 Subject: [PATCH 102/202] 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, 41 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index b46aef82789190..97a3834ffebf18 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1852,6 +1852,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, @@ -1864,6 +1877,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) @@ -1993,6 +2007,19 @@ static const struct regmap_config dw_dp_regmap_config = { .rd_table = &dw_dp_readable_table, }; +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"); +} + int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) { struct device *dev = dp->dev; @@ -2037,6 +2064,13 @@ int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) goto put_next_bridge; } + 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); @@ -2086,6 +2120,10 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ 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; INIT_WORK(&dp->hpd_work, dw_dp_hpd_work); diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index a82412a9e76956..79b2cdf0df996b 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); }; int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder); From ee2f75e1c9f1632e332ba332887e8d2efa1be884 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:34:43 +0100 Subject: [PATCH 103/202] 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. Thus this patch prevents massive SError problems once runtime PM is enabled. Apart from that it avoids 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 | 124 +++++++++++++++++++++- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index cb51661818530e..53b7fa9dd97949 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -7,9 +7,12 @@ */ #include +#include #include +#include #include #include +#include #include #include @@ -24,12 +27,54 @@ #include "rockchip_drm_drv.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) @@ -72,6 +117,35 @@ static const struct drm_encoder_helper_funcs dw_dp_encoder_helper_funcs = { .atomic_check = dw_dp_encoder_atomic_check, }; +static struct regmap *dw_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, potential future platforms + * reusing the driver should just add a VO GRF phandle in the DisplayPort + * controller 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) { struct rockchip_dw_dp *dp = dev_get_drvdata(dev); @@ -111,19 +185,53 @@ static const struct component_ops dw_dp_rockchip_component_ops = { static int dw_dp_rockchip_probe(struct platform_device *pdev) { - const struct dw_dp_plat_data *plat_data; + const struct rockchip_dw_dp_plat_data *plat_data_const; + struct dw_dp_plat_data *plat_data; struct device *dev = &pdev->dev; struct rockchip_dw_dp *dp; + struct resource *res; + int id; - 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; + plat_data = devm_kzalloc(dev, sizeof(*plat_data), GFP_KERNEL); + if (!plat_data) + return -ENOMEM; + dp = devm_kzalloc(dev, sizeof(*dp), GFP_KERNEL); if (!dp) return -ENOMEM; platform_set_drvdata(pdev, dp); dp->dev = dev; + dp->pdata = plat_data_const; + + res = platform_get_mem_or_io(pdev, 0); + if (!res) + return -ENODEV; + + /* 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 = dw_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; dp->base = dw_dp_probe(pdev, plat_data); if (IS_ERR(dp->base)) @@ -137,14 +245,20 @@ static void dw_dp_rockchip_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 7372662b7d3bb83f62a3337f1a3e7ce97a17bbeb Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:40:50 +0100 Subject: [PATCH 104/202] 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 | 107 ++++++++++++++++++++++-- include/drm/bridge/dw_dp.h | 3 + 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 97a3834ffebf18..5c87dcd13a55b5 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -328,6 +328,7 @@ struct dw_dp { struct dw_dp_link link; struct dw_dp_plat_data plat_data; u8 pixel_mode; + bool usbc_mode; struct drm_bridge *next_bridge; @@ -1467,6 +1468,11 @@ static ssize_t dw_dp_aux_transfer(struct drm_dp_aux *aux, if (WARN_ON(msg->size > 16)) return -E2BIG; + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + reinit_completion(&dp->complete); switch (msg->request & ~DP_AUX_I2C_MOT) { @@ -1659,6 +1665,12 @@ static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_connector_state *conn_state; int ret; + ret = pm_runtime_get_active(dp->dev, RPM_TRANSPARENT); + if (ret) { + dev_err(dp->dev, "runtime PM failure\n"); + return; + } + connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); if (!connector) { dev_err(dp->dev, "failed to get connector\n"); @@ -1713,6 +1725,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) @@ -1733,6 +1746,10 @@ static enum drm_connector_status dw_dp_bridge_detect(struct drm_bridge *bridge, { struct dw_dp *dp = bridge_to_dp(bridge); + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + if (PM_RUNTIME_ACQUIRE_ERR(&pm)) + return connector_status_disconnected; + if (!dw_dp_hpd_detect(dp)) return connector_status_disconnected; @@ -1915,6 +1932,11 @@ static void dw_dp_hpd_work(struct work_struct *work) bool long_hpd; int ret; + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return; + mutex_lock(&dp->irq_lock); long_hpd = dp->hotplug.long_hpd; mutex_unlock(&dp->irq_lock); @@ -1966,6 +1988,9 @@ static irqreturn_t dw_dp_irq(int irq, void *data) struct dw_dp *dp = data; u32 value; + /* interrupt can only trigger for running device */ + guard(pm_runtime_noresume)(dp->dev); + regmap_read(dp->regmap, DW_DP_GENERAL_INTERRUPT, &value); if (!value) return IRQ_NONE; @@ -2064,25 +2089,51 @@ int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) goto put_next_bridge; } - if (dw_dp_is_routed_to_usb_c(encoder)) { - dev_dbg(dev, "USB-C mode\n"); + dp->usbc_mode = dw_dp_is_routed_to_usb_c(encoder); - if (dp->plat_data.hpd_sw_sel) - dp->plat_data.hpd_sw_sel(dp->plat_data.data, 1); + ret = pm_runtime_get_active(dev, RPM_TRANSPARENT); + if (ret) { + dev_err_probe(dev, ret, "Failed to get runtime PM\n"); + goto put_next_bridge; } - dw_dp_init_hw(dp); + /* resume once if runtime PM is disabled */ + if (!pm_runtime_enabled(dev)) { + ret = dw_dp_runtime_resume(dp); + if (ret) + goto put_runtime_pm; + } + + if (dp->plat_data.hpd_sw_sel) + dp->plat_data.hpd_sw_sel(dp->plat_data.data, dp->usbc_mode); ret = phy_init(dp->phy); if (ret) { dev_err_probe(dev, ret, "phy init failed\n"); - goto put_next_bridge; + goto put_manual_pm; } enable_irq(dp->irq); + /* + * USB-C has out-of-band hotplug detection, so device may enter + * runtime suspend. Native mode needs to be resumed for working + * hotplug detection. + */ + if (dp->usbc_mode) { + dev_dbg(dev, "USB-C mode\n"); + pm_runtime_put_autosuspend(dp->dev); + } + return 0; +put_manual_pm: + if (!pm_runtime_enabled(dev)) + dw_dp_runtime_suspend(dp); + +put_runtime_pm: + pm_runtime_put_sync(dp->dev); + put_next_bridge: drm_bridge_put(dp->next_bridge); @@ -2101,6 +2152,11 @@ void dw_dp_unbind(struct dw_dp *dp) disable_irq(dp->irq); cancel_work_sync(&dp->hpd_work); phy_exit(dp->phy); + if (!dp->usbc_mode) + pm_runtime_put_sync(dp->dev); + /* suspend once if runtime PM is disabled */ + if (!pm_runtime_enabled(dp->dev)) + dw_dp_runtime_suspend(dp); drm_bridge_put(dp->next_bridge); drm_dp_aux_unregister(&dp->aux); drm_bridge_remove(&dp->bridge); @@ -2149,13 +2205,13 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ return ERR_CAST(dp->phy); } - dp->apb_clk = devm_clk_get_enabled(dev, "apb"); + dp->apb_clk = devm_clk_get(dev, "apb"); if (IS_ERR(dp->apb_clk)) { dev_err_probe(dev, PTR_ERR(dp->apb_clk), "failed to get apb clock\n"); return ERR_CAST(dp->apb_clk); } - dp->aux_clk = devm_clk_get_enabled(dev, "aux"); + dp->aux_clk = devm_clk_get(dev, "aux"); if (IS_ERR(dp->aux_clk)) { dev_err_probe(dev, PTR_ERR(dp->aux_clk), "failed to get aux clock\n"); return ERR_CAST(dp->aux_clk); @@ -2200,6 +2256,41 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ } EXPORT_SYMBOL_GPL(dw_dp_probe); +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) +{ + int ret; + + ret = clk_prepare_enable(dp->apb_clk); + if (ret) + return ret; + + ret = clk_prepare_enable(dp->aux_clk); + if (ret) { + clk_disable_unprepare(dp->apb_clk); + return ret; + } + + dw_dp_init_hw(dp); + + /* + * HPD_HOT_PLUG bit is asserted only after the sink holds HPD + * high for at least 100ms. + */ + msleep(110); + + 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 79b2cdf0df996b..f20d2c2c40ca2c 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -29,4 +29,7 @@ int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder); void dw_dp_unbind(struct dw_dp *dp); struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data); + +int dw_dp_runtime_suspend(struct dw_dp *dp); +int dw_dp_runtime_resume(struct dw_dp *dp); #endif /* __DW_DP__ */ From 61e35cacc9abdbac36da303c5ac180cb9c953c6d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:44:22 +0100 Subject: [PATCH 105/202] 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 | 50 ++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 53b7fa9dd97949..169e63cbab7093 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -55,9 +56,15 @@ 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]; + int ret; dp->hpd_sel = force_hpd_from_sw; + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return; + regmap_write(dp->vo_grf, hpd_reg, FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL, dp->hpd_sel)); } @@ -66,11 +73,17 @@ 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]; + int ret; dev_dbg(dp->dev, "Force HPD connected=%s\n", str_yes_no(hpd)); dp->hpd_cfg = hpd; + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return; + regmap_write(dp->vo_grf, hpd_reg, FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG, dp->hpd_cfg)); } @@ -190,7 +203,7 @@ static int dw_dp_rockchip_probe(struct platform_device *pdev) struct device *dev = &pdev->dev; struct rockchip_dw_dp *dp; struct resource *res; - int id; + int id, ret; plat_data_const = device_get_match_data(dev); if (!plat_data_const) @@ -237,6 +250,12 @@ static int dw_dp_rockchip_probe(struct platform_device *pdev) 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 ret; + return component_add(&pdev->dev, &dw_dp_rockchip_component_ops); } @@ -245,6 +264,34 @@ static void dw_dp_rockchip_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}, @@ -279,5 +326,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 460bea093cbc06bffb78b7b07f3cabdf7729f41b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 23 Jul 2026 22:59:37 +0200 Subject: [PATCH 106/202] drm/bridge: synopsys: dw-dp: Protect sdp_reg_bank from concurrent access Right now sdp_reg_bank is only used during atomic enable/disable and thus there is no risk of two threads accidently claiming the same bit. This changes once more SDP users (like audio support) are added, so introduce a mutex to protect concurrent access to the bitmap. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 5c87dcd13a55b5..8d1ff6039c4113 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -322,6 +322,8 @@ struct dw_dp { struct dw_dp_hotplug hotplug; /* Serialize hpd status access */ struct mutex irq_lock; + /* Serialize sdp_reg_bank access */ + struct mutex sdp_lock; struct drm_dp_aux aux; @@ -1045,11 +1047,13 @@ static int dw_dp_send_sdp(struct dw_dp *dp, struct dw_dp_sdp *sdp) u32 reg; int i, nr; - nr = find_first_zero_bit(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); - if (nr < SDP_REG_BANK_SIZE) - set_bit(nr, dp->sdp_reg_bank); - else - return -EBUSY; + scoped_guard(mutex, &dp->sdp_lock) { + nr = find_first_zero_bit(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); + if (nr < SDP_REG_BANK_SIZE) + set_bit(nr, dp->sdp_reg_bank); + else + return -EBUSY; + } reg = DW_DP_SDP_REGISTER_BANK + nr * 9 * 4; @@ -1683,7 +1687,8 @@ static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, return; } - set_bit(0, dp->sdp_reg_bank); + scoped_guard(mutex, &dp->sdp_lock) + set_bit(0, dp->sdp_reg_bank); ret = dw_dp_link_enable(dp); if (ret < 0) { @@ -1723,7 +1728,8 @@ static void dw_dp_bridge_atomic_disable(struct drm_bridge *bridge, dw_dp_video_disable(dp); dw_dp_link_disable(dp); - bitmap_zero(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); + scoped_guard(mutex, &dp->sdp_lock) + bitmap_zero(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); dw_dp_reset(dp); pm_runtime_put_autosuspend(dp->dev); } @@ -2189,6 +2195,10 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ if (ret) return ERR_PTR(ret); + ret = devm_mutex_init(dev, &dp->sdp_lock); + if (ret) + return ERR_PTR(ret); + res = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(res)) return ERR_CAST(res); From e474e7c4d43f30bc9e549a7e505690c879a873a4 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 23 Jul 2026 23:03:29 +0200 Subject: [PATCH 107/202] drm/bridge: synopsys: dw-dp: Use regmap_set_bits in dw_dp_send_sdp Simplify dw_dp_send_sdp() a little bit by making use of regmap_set_bits. No functional change intended. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 8d1ff6039c4113..d4a9e52ced5d25 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1066,14 +1066,12 @@ static int dw_dp_send_sdp(struct dw_dp *dp, struct dw_dp_sdp *sdp) FIELD_PREP(SDP_REGS, get_unaligned_le32(payload))); if (sdp->flags & DW_DP_SDP_VERTICAL_INTERVAL) - regmap_update_bits(dp->regmap, DW_DP_SDP_VERTICAL_CTRL, - EN_VERTICAL_SDP << nr, - EN_VERTICAL_SDP << nr); + regmap_set_bits(dp->regmap, DW_DP_SDP_VERTICAL_CTRL, + EN_VERTICAL_SDP << nr); if (sdp->flags & DW_DP_SDP_HORIZONTAL_INTERVAL) - regmap_update_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, - EN_HORIZONTAL_SDP << nr, - EN_HORIZONTAL_SDP << nr); + regmap_set_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, + EN_HORIZONTAL_SDP << nr); return 0; } From 194d863832d313a376428e93e4cc0d1abc9a997d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 23 Jan 2026 23:10:22 +0100 Subject: [PATCH 108/202] 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 an argument is needed to to select the right interface. In addition to that the RK3576 DisplayPort controller is 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 from this patch has only been tested with the simple single stream transport (SST) setup as the Linux driver does not yet support MST. Once MST support is added, the plan is to simply add more numbers to the argument, so that it looks like this for RK3576: 0 = I2S on stream 0, 1 = S/PDIF on stream 0 2 = I2S on stream 1, 3 = S/PDIF on stream 1 4 = I2S on stream 2, 5 = S/PDIF on stream 2 As the arguments are not part of the binding itself the audio side is also ready for MST after this change. Switching '#sound-dai-cells' from 0 to 1 without keeping compatibility is an ABI break. The rationale for going that way is, that there is not a single known driver implementation for the current binding. It's also unclear how the current binding would be used (only support I2S or S/PDIF for stream 0?). The mainline rk3588 DTS include sets it to 0, but does not have any soundcard using the DAI. This will be fixed up separately. The RK3576 does not set it at all. Reviewed-by: Krzysztof Kozlowski Signed-off-by: Sebastian Reichel --- .../bindings/display/rockchip/rockchip,dw-dp.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 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..c4f8959dd65da9 100644 --- a/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml +++ b/Documentation/devicetree/bindings/display/rockchip/rockchip,dw-dp.yaml @@ -25,7 +25,7 @@ description: | * Supports up to 8/10 bits per color component * Supports RBG, YCbCr4:4:4, YCbCr4:2:2, YCbCr4:2:0 * Pixel clock up to 594MHz - * I2S, SPDIF audio interface + * I2S, S/PDIF audio interface properties: compatible: @@ -46,7 +46,7 @@ properties: - description: DisplayPort AUX clock - description: HDCP clock - description: I2S interface clock - - description: SPDIF interfce clock + - description: S/PDIF interfce clock clock-names: minItems: 3 @@ -83,7 +83,8 @@ properties: maxItems: 1 "#sound-dai-cells": - const: 0 + const: 1 + description: 0 for I2S, 1 for S/PDIF 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 5c90c3b5204b737b02ba2484e04ff845f2a078fa Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 05:22:05 +0100 Subject: [PATCH 109/202] 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 | 263 +++++++++++++++++++++++- 1 file changed, 261 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index d4a9e52ced5d25..50f7035027d8b3 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,9 @@ 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; + int audio_sdp_nr; /* Serialize hpd status access */ struct mutex irq_lock; /* Serialize sdp_reg_bank access */ @@ -1073,7 +1099,19 @@ static int dw_dp_send_sdp(struct dw_dp *dp, struct dw_dp_sdp *sdp) regmap_set_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, EN_HORIZONTAL_SDP << nr); - return 0; + return nr; +} + +static void dw_dp_clear_sdp(struct dw_dp *dp, int nr) +{ + regmap_clear_bits(dp->regmap, DW_DP_SDP_VERTICAL_CTRL, + EN_VERTICAL_SDP << nr); + + regmap_clear_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, + EN_HORIZONTAL_SDP << nr); + + scoped_guard(mutex, &dp->sdp_lock) + clear_bit(nr, dp->sdp_reg_bank); } static int dw_dp_send_vsc_sdp(struct dw_dp *dp) @@ -1886,6 +1924,212 @@ 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"); + + return pm_runtime_resume_and_get(dp->dev); +} + +static void dw_dp_audio_unprepare(struct drm_bridge *bridge, + struct drm_connector *connector) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + if (dp->audio_sdp_nr >= 0) { + dw_dp_clear_sdp(dp, dp->audio_sdp_nr); + dp->audio_sdp_nr = -1; + } + + regmap_clear_bits(dp->regmap, DW_DP_SDP_VERTICAL_CTRL, + EN_AUDIO_STREAM_SDP | EN_AUDIO_TIMESTAMP_SDP); + regmap_clear_bits(dp->regmap, DW_DP_SDP_HORIZONTAL_CTRL, + EN_AUDIO_STREAM_SDP); + + regmap_clear_bits(dp->regmap, DW_DP_AUD_CONFIG1, AUDIO_DATA_IN_EN); + + 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; + enum dw_dp_audio_interface audio_interface; + 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); + + /* The hardware is limited to 1,2 or 8 channels */ + 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; + } + + if (params->sample_width < 16 || params->sample_width > 24) { + dev_err(dp->dev, "invalid data sample width %d\n", params->sample_width); + return -EINVAL; + } + + switch (daifmt->fmt) { + case HDMI_SPDIF: + 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. + */ + 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 != audio_interface) { + dev_err(dp->dev, "unsupported DAI %d\n", daifmt->fmt); + return -EINVAL; + } + + ret = clk_prepare_enable(dp->spdif_clk); + if (ret) + return ret; + + ret = clk_prepare_enable(dp->i2s_clk); + if (ret) { + clk_disable_unprepare(dp->spdif_clk); + return ret; + } + + 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, audio_interface) | + FIELD_PREP(HBR_MODE_ENABLE, 0)); + + /* Wait for inf switch */ + usleep_range(20, 40); + + if (audio_interface == DW_DP_AUDIO_I2S) + clk_disable_unprepare(dp->spdif_clk); + else if (audio_interface == DW_DP_AUDIO_SPDIF) + clk_disable_unprepare(dp->i2s_clk); + + dp->audio_interface = audio_interface; + + /* + * 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)); + + dp->audio_sdp_nr = dw_dp_audio_infoframe_send(dp); + if (dp->audio_sdp_nr < 0) { + dev_err(dp->dev, "failed to send audio infoframe\n"); + dw_dp_audio_unprepare(bridge, connector); + return dp->audio_sdp_nr; + } + + 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, @@ -1899,6 +2143,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) @@ -2057,10 +2306,20 @@ int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) bridge = &dp->bridge; 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; + dp->audio_sdp_nr = -1; + 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; + drm_bridge_add(bridge); dp->aux.dev = dev; From 29960f5cb2fb1bed591f853abfba640fff3875d8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:01:21 +0100 Subject: [PATCH 110/202] 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 e12a2a0cfb8916..49e48bd050313c 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 69692d1b72e8abc8ee474968f3b96a04fad37e20 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:02:48 +0100 Subject: [PATCH 111/202] 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 73ec60dc092eec..9bcddcee70186a 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"; @@ -975,6 +979,10 @@ status = "okay"; }; +&spdif_tx3 { + status = "okay"; +}; + &u2phy0 { status = "okay"; }; From 7721ea83f173285361605508ff785bbccd256f6b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 22:22:54 +0100 Subject: [PATCH 112/202] 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 07d43083f39cde..73cf05de707a7a 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3588-base.dtsi @@ -1860,7 +1860,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 52e21020cd5e04f21b96b8083d1a3fae20c16856 Mon Sep 17 00:00:00 2001 From: Chaoyi Chen Date: Fri, 17 Jul 2026 15:23:19 +0800 Subject: [PATCH 113/202] drm/bridge: Implement generic USB Type-C DP HPD bridge The HPD function of Type-C DP is implemented through drm_connector_oob_hotplug_event(). For embedded DP, it is required that the DRM connector fwnode corresponds to the Type-C port fwnode. To describe the relationship between the DP controller and the Type-C port device, we usually using drm_bridge to build a bridge chain. Now several USB-C controller drivers have already implemented the DP HPD bridge function provided by aux-hpd-bridge.c, it will build a DP HPD bridge on USB-C connector port device. But this requires the USB-C controller driver to manually register the HPD bridge. If the driver does not implement this feature, the bridge will not be create. So this patch implements a generic DP HPD bridge based on aux-hpd-bridge.c. It will monitor Type-C bus events, and when a Type-C port device containing the DP svid is registered, it will create an HPD bridge for it without the need for the USB-C controller driver to implement it. Signed-off-by: Chaoyi Chen Reviewed-by: Heikki Krogerus Reviewed-by: Nicolas Frattaroli Link: https://lore.kernel.org/r/20260717072323.96-2-kernel@airkyi.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/Kconfig | 10 ++++ drivers/gpu/drm/bridge/Makefile | 1 + .../gpu/drm/bridge/aux-hpd-typec-dp-bridge.c | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c diff --git a/drivers/gpu/drm/bridge/Kconfig b/drivers/gpu/drm/bridge/Kconfig index 4a57d49b4c6d3a..9739b2a1975865 100644 --- a/drivers/gpu/drm/bridge/Kconfig +++ b/drivers/gpu/drm/bridge/Kconfig @@ -30,6 +30,16 @@ config DRM_AUX_HPD_BRIDGE Simple bridge that terminates the bridge chain and provides HPD support. +if DRM_AUX_HPD_BRIDGE +config DRM_AUX_HPD_TYPEC_BRIDGE + tristate + depends on TYPEC || !TYPEC + default TYPEC + help + Simple bridge that terminates the bridge chain and provides HPD + support. It build bridge on each USB-C connector device node. +endif + menu "Display Interface Bridges" depends on DRM && DRM_BRIDGE diff --git a/drivers/gpu/drm/bridge/Makefile b/drivers/gpu/drm/bridge/Makefile index 15cc821d85b7ea..d88a9e1ccc9af4 100644 --- a/drivers/gpu/drm/bridge/Makefile +++ b/drivers/gpu/drm/bridge/Makefile @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 obj-$(CONFIG_DRM_AUX_BRIDGE) += aux-bridge.o obj-$(CONFIG_DRM_AUX_HPD_BRIDGE) += aux-hpd-bridge.o +obj-$(CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE) += aux-hpd-typec-dp-bridge.o obj-$(CONFIG_DRM_CHIPONE_ICN6211) += chipone-icn6211.o obj-$(CONFIG_DRM_CHRONTEL_CH7033) += chrontel-ch7033.o obj-$(CONFIG_DRM_CROS_EC_ANX7688) += cros-ec-anx7688.o diff --git a/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c new file mode 100644 index 00000000000000..c09579ff7ab917 --- /dev/null +++ b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (C) 2026 Rockchip Electronics Co., Ltd. + * + * Author: Chaoyi Chen + */ +#include +#include +#include + +#include + +static int drm_typec_bus_event(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct device *dev = (struct device *)data; + struct typec_altmode *alt = to_typec_altmode(dev); + + if (action != BUS_NOTIFY_ADD_DEVICE) + return NOTIFY_OK; + + /* + * alt->dev.parent->parent : USB-C controller device + * alt->dev.parent : USB-C connector device + */ + if (is_typec_port_altmode(&alt->dev) && alt->svid == USB_TYPEC_DP_SID) + drm_dp_hpd_bridge_register(alt->dev.parent->parent, + to_of_node(alt->dev.parent->fwnode)); + + return NOTIFY_OK; +} + +static struct notifier_block drm_typec_event_nb = { + .notifier_call = drm_typec_bus_event, +}; + +static void drm_aux_hpd_typec_dp_bridge_module_exit(void) +{ + bus_unregister_notifier(&typec_bus, &drm_typec_event_nb); +} + +static int __init drm_aux_hpd_typec_dp_bridge_module_init(void) +{ + bus_register_notifier(&typec_bus, &drm_typec_event_nb); + + return 0; +} + +module_init(drm_aux_hpd_typec_dp_bridge_module_init); +module_exit(drm_aux_hpd_typec_dp_bridge_module_exit); + +MODULE_AUTHOR("Chaoyi Chen "); +MODULE_DESCRIPTION("DRM TYPEC DP HPD BRIDGE"); +MODULE_LICENSE("GPL"); From 73d8ab302780e9ef3cb1d29cbed4186915bc5a56 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 16:23:15 +0200 Subject: [PATCH 114/202] 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 c172e817a8d7ec..58a98a25e9c145 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -8664,6 +8664,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 ff34b8b1ed05374a0fd38019b6c3f8a45e25d072 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 17:41:17 +0200 Subject: [PATCH 115/202] 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 2c58deca65ef5a..7148f0f3c1f6a2 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 7696295718bca42f13992b7acf4d9b42bc58915a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:01:37 +0200 Subject: [PATCH 116/202] 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 7148f0f3c1f6a2..a28b90b98e4306 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 8a6639780058167ee4e6a46ca6b1299da8cf5c8a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:03:15 +0200 Subject: [PATCH 117/202] 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 a28b90b98e4306..622c1257fdaa53 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 050f638958859332ced9b51bbbb279496f500a7b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:10:26 +0200 Subject: [PATCH 118/202] 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 622c1257fdaa53..4d8bae2a522e56 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 126928e7fefd5c99dfe9300afd2805dd5b4c195e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:17:02 +0200 Subject: [PATCH 119/202] 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 4d8bae2a522e56..f5c0cc453a8924 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 2d00a845361dfa8b5283e2a2a27d95a6d76ceb1b Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:17:57 +0200 Subject: [PATCH 120/202] 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 879b93b0462046b154bdc0021e30674e8a8dbc8a Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:43:24 +0200 Subject: [PATCH 121/202] 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 589a4bbd606676093a97e121c3e1bcf414566c39 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Apr 2026 04:19:28 +0300 Subject: [PATCH 122/202] [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 b85c7d28edf8c0d27136323803de33abe8b63445 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sun, 2 Mar 2025 19:47:18 +0200 Subject: [PATCH 123/202] [DEBUG] drm/rockchip: vop2: Log DCLK setup Signed-off-by: Cristian Ciocaltea --- drivers/gpu/drm/rockchip/rockchip_drm_vop2.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop2.c index 5a0479ebc4cdff..36982e99ccc356 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,7 +1825,9 @@ static void vop2_crtc_atomic_enable(struct drm_crtc *crtc, } } + dev_info(vop2->dev, "current dclk rate=%lu\n", clk_get_rate(vp->dclk)); clk_set_rate(vp->dclk, clock); + dev_info(vop2->dev, "setting dclk to rate=%lu: result=%lu\n", clock, clk_get_rate(vp->dclk)); vop2_post_config(crtc); From 2bc622189548f9d2366aa9332c82835dc2d4aac0 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 23:51:17 +0300 Subject: [PATCH 124/202] 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 0c94d258817644c3ab47f307fffa9c83262b1da9 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 22:48:01 +0200 Subject: [PATCH 125/202] 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 687b36eea0c703..224217d0d8152a 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -1361,9 +1361,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 @@ -1372,12 +1372,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 94e79481372210..e77ccba4886641 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), /** @@ -1586,6 +1614,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 0c74cc8cf6b0dfdee90f0dd984061c0557397b03 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 22 Apr 2026 01:26:32 +0300 Subject: [PATCH 126/202] 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 f711d1dbe2e6d9..29b6d16faccaca 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 2c6537b8a8e130462de10cdf491f21590eeb8252 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 23:04:23 +0200 Subject: [PATCH 127/202] 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 29b6d16faccaca..20f6cf4ebd4ce0 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 11553546e4bba1b40c70c637674637b5e0fff017 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 13 Sep 2024 17:30:35 +0300 Subject: [PATCH 128/202] 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 | 187 +++++++++++++++++-- 1 file changed, 171 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 1c214a8e6dc2db..d45c6d4b643e27 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 @@ -38,6 +41,7 @@ #define DDC_CI_ADDR 0x37 #define DDC_SEGMENT_ADDR 0x30 +#define SCDC_MAX_SOURCE_VERSION 0x1 #define SCRAMB_POLL_DELAY_MS 3000 /* @@ -162,6 +166,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; @@ -747,28 +756,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_commit *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 > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) + dw_hdmi_qp_enable_scramb(hdmi); } else { dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__); op_mode = OPMODE_DVI; @@ -779,7 +884,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, @@ -789,14 +894,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); @@ -806,7 +946,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 * @@ -830,12 +983,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 - * HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ. - */ - if (rate > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) { + if (hdmi->no_hpd && rate > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) { + dev_dbg(hdmi->dev, "Unsupported TMDS char rate in no_hpd mode: %lld\n", rate); + return MODE_CLOCK_HIGH; + } + + if (rate > HDMI_2_0_TMDS_CHAR_RATE_MAX_HZ) { dev_dbg(hdmi->dev, "Unsupported TMDS char rate: %lld\n", rate); return MODE_CLOCK_HIGH; } @@ -1195,7 +1348,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, @@ -1285,6 +1438,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 6bc4bfcff24e117a79863339c70a4c0c1902a94e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:08:12 +0300 Subject: [PATCH 129/202] 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 d45c6d4b643e27..92b900d4806d19 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -570,7 +570,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; } @@ -581,7 +581,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 5b8190ab70ff4569db0ee461336c720a14fbddda Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:21:36 +0300 Subject: [PATCH 130/202] 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 f35484715c2d1d..296f9a3ba66ae8 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 756d4c674189795362f5224ef86fea511cd9bba5 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:38:26 +0300 Subject: [PATCH 131/202] 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 296f9a3ba66ae8..46df5abe31a463 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 0; From e979f960130442e7edb58de9a71c54eb5bff1ac8 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 23:55:39 +0300 Subject: [PATCH 132/202] 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 | 14 +++++--------- 1 file changed, 5 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 46df5abe31a463..0ea38f23cfeb05 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,11 @@ 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 0; + 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 44cd2e78b23a92b8b364dabc3fc6befb23f02fce Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 22:42:12 +0300 Subject: [PATCH 133/202] 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 | 19 ++++++++++--------- 1 file changed, 10 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 0ea38f23cfeb05..d9309bebe99e74 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,9 +588,9 @@ 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"); return devm_request_threaded_irq(dev, irq, @@ -608,6 +607,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 = { @@ -642,8 +643,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 c7990eb175d987aa5f546c39683d2b3e30c1e430 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 18:16:54 +0200 Subject: [PATCH 134/202] 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 fb21cc744fbcf4ade1c00d24ac48e02f85139c3b Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 17:55:32 +0200 Subject: [PATCH 135/202] 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 c93ae5bcf6859cc030a422126dc092ee68c75e8d Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sat, 28 Mar 2026 22:08:28 +0200 Subject: [PATCH 136/202] 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 ea3c87c4f7b31beccec9c5d56c1d7883e3025370 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:34:26 +0200 Subject: [PATCH 137/202] 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 4867edbf2622bf..63b2f5b2961b08 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 5f5ca023cd654d..a01cc847920d17 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 09a782a510d94241223815d5e59c28c5940d334b Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:27:05 +0200 Subject: [PATCH 138/202] 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 20f6cf4ebd4ce0..0a02977a5ee324 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 e77ccba4886641..e36b9b94285442 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 db7f2abd51d3143f9415c6263be52498f123ef0a Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 8 Apr 2026 00:23:35 +0300 Subject: [PATCH 139/202] 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 36982e99ccc356..34a147aac2619c 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 667ef6ee26f36c3249cdd20a826deeaeeb55dd7e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 31 Mar 2026 17:52:43 +0300 Subject: [PATCH 140/202] 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 d9309bebe99e74..7252ce84c91e26 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 819371aed9db31e5a56e5c01217240df7eb41c0c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 4 Feb 2026 01:11:56 +0200 Subject: [PATCH 141/202] 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 92b900d4806d19..3e082aa42298ac 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -854,6 +854,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, @@ -866,9 +868,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 bebce384b05330194c7508bf2883046193d0c0ac Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Mar 2026 22:42:11 +0200 Subject: [PATCH 142/202] 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 3e082aa42298ac..166b1ff8c5bbd3 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -100,9 +100,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, }, }; /* @@ -121,9 +118,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 { @@ -229,7 +223,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; @@ -320,7 +314,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 de3f00897b73e753fce277d3438b026a7ccefb8f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:39:51 +0300 Subject: [PATCH 143/202] 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 166b1ff8c5bbd3..d80c92d122eaae 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -44,6 +44,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 @@ -165,10 +181,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; }; @@ -216,6 +235,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) @@ -297,10 +359,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); @@ -750,6 +820,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) @@ -847,7 +1362,8 @@ static void dw_hdmi_qp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_atomic_commit *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; @@ -861,6 +1377,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; @@ -872,7 +1390,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 > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) + if (!link_cfg->frl_enabled && + conn_state->hdmi.tmds_char_rate > HDMI_1_3_TMDS_CHAR_RATE_MAX_HZ) dw_hdmi_qp_enable_scramb(hdmi); } else { dev_dbg(hdmi->dev, "%s mode=DVI\n", __func__); @@ -884,7 +1403,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, @@ -894,7 +1427,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); @@ -907,14 +1444,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); @@ -934,8 +1482,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) { @@ -948,10 +1498,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; @@ -996,12 +1550,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; } @@ -1082,8 +1671,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; } @@ -1351,6 +1940,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, @@ -1428,7 +2018,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); } @@ -1439,6 +2030,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 419887c65b63e0e04bde1ddea8275c71eebf6126 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 13:28:59 +0300 Subject: [PATCH 144/202] 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 d80c92d122eaae..55cffde2686aab 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-hdmi-qp.c @@ -896,12 +896,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 @@ -927,7 +928,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 */ @@ -1000,8 +1001,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; @@ -1033,7 +1048,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); @@ -1074,8 +1089,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 7e6e724329b1926f76f0418fbf28838a55d00f8d Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:31:24 +0300 Subject: [PATCH 145/202] 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 7252ce84c91e26..4c1d18f203f420 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 8db1e3af8849f104434db6500ca4ad5b9f607f62 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:33:45 +0300 Subject: [PATCH 146/202] 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 4c1d18f203f420..cd98a0f7020991 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 6398cd74c75ea359f3413d1578faec0289091b0b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:42:59 +0200 Subject: [PATCH 147/202] 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 e7f49fe845eaec..ce63ff134267b1 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 79e7ebeb3eea6e491dad67ed14ec9e809033c660 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:57:35 +0200 Subject: [PATCH 148/202] 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 ce63ff134267b1..12188ad1fb4aa0 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 4a1e027ed5321ecaf360b2cc0c90459762a3c82b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 2 Jul 2026 03:54:15 +0200 Subject: [PATCH 149/202] arm64: dts: rockchip: Support power sink on USB-C for ArmSom Sige5 Receiving 5V on the USB-C port is something, which can always happen when plugging in a USB-A to USB-C cable. As far as I can see from the Sige5 schematics, the hardware should be fine receiving some voltage, but won't use it for anything. Also the voltage shouldn't go higher than 5V to ensure staying within the components voltage ratings. This is effectively required to use the USB-C port in gadget mode with Linux, as the TCPM code does not trigger the PHY's Vbus detection without it. Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index 9bcddcee70186a..7a1cab2bb4a778 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -723,11 +723,14 @@ compatible = "usb-c-connector"; label = "USB-C"; data-role = "dual"; + op-sink-microwatt = <10>; /* fusb302 supports PD Rev 2.0 Ver 1.2 */ pd-revision = /bits/ 8 <0x2 0x0 0x1 0x2>; - power-role = "source"; + power-role = "dual"; + sink-pdos = ; source-pdos = ; + try-power-role = "source"; altmodes { displayport { From 21cc1035a76583a574092e3bd55a5dbbd9172ede Mon Sep 17 00:00:00 2001 From: RD Babiera Date: Fri, 10 Jul 2026 20:03:08 +0000 Subject: [PATCH 150/202] usb: typec: tcpm: implement retry mechanism for Discover Identity VDMs The current mechanism for sending Discover Identity in the ready state presents a flaw where tcpm_queue_vdm can collide with non interruptible AMSes such as GET_SINK_CAP or VCONN_SWAP. vdm_run_state_machine will hit the VDM_STATE_BUSY state, and Discover SVIDs or Discover Modes will not retry. This patch introduces a state machine under the enum vdm_discovery_states. The tcpm_port field vdm_discovery_state tracks which step of the Discover Identity process has been completed. The current TCPM implementation utilizes the send_discover and send_discover_prime booleans to queue Discover Identity in the aforementioned collision case. These booleans are removed in place of vdm_discovery_state and send_discover_work is replaced by vdm_discovery_work, which runs unconditionally in the ready state. When the Discovery process is complete, the port will move to the VDM_DISCOVERY_COMPLETE state and vdm_discovery_work becomes a no-op. When there are still Discovery VDMs to be sent, vdm_discovery_work will continue based on the last received response from the port partner or cable. Signed-off-by: RD Babiera Link: https://patch.msgid.link/20260710200308.1401321-2-rdbabiera@google.com Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 403 ++++++++++++++++++++++------------ 1 file changed, 261 insertions(+), 142 deletions(-) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index 58a98a25e9c145..9b92c749b1138a 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -212,6 +212,24 @@ static const char * const tcpm_ams_str[] = { FOREACH_AMS(GENERATE_STRING) }; +#define FOREACH_VDM_DISCOVERY(S) \ + S(VDM_DISCOVERY_UNKNOWN), \ + S(VDM_DISCOVERY_PARTNER_IDENT), \ + S(VDM_DISCOVERY_CABLE_IDENT), \ + S(VDM_DISCOVERY_PARTNER_SVIDS), \ + S(VDM_DISCOVERY_PARTNER_MODES), \ + S(VDM_DISCOVERY_CABLE_SVIDS), \ + S(VDM_DISCOVERY_CABLE_MODES), \ + S(VDM_DISCOVERY_COMPLETE) + +enum vdm_discovery_states { + FOREACH_VDM_DISCOVERY(GENERATE_ENUM) +}; + +static const char * const vdm_discovery_state_strings[] = { + FOREACH_VDM_DISCOVERY(GENERATE_STRING) +}; + enum vdm_states { VDM_STATE_ERR_BUSY = -3, VDM_STATE_ERR_SEND = -2, @@ -274,7 +292,7 @@ enum frs_typec_current { #define ALTMODE_DISCOVERY_MAX (SVID_DISCOVERY_MAX * MODE_DISCOVERY_MAX) #define GET_SINK_CAP_RETRY_MS 100 -#define SEND_DISCOVER_RETRY_MS 100 +#define SEND_DISCOVERY_VDM_RETRY_MS 100 struct pd_mode_data { int svid_index; /* current SVID index */ @@ -483,8 +501,6 @@ struct tcpm_port { bool vbus_source; bool vbus_charge; - /* Set to true when Discover_Identity Command is expected to be sent in Ready states. */ - bool send_discover; bool op_vsafe5v; int try_role; @@ -510,8 +526,8 @@ struct tcpm_port { struct kthread_work vdm_state_machine; struct hrtimer enable_frs_timer; struct kthread_work enable_frs; - struct hrtimer send_discover_timer; - struct kthread_work send_discover_work; + struct hrtimer vdm_discovery_timer; + struct kthread_work vdm_discovery_work; bool state_machine_running; /* Set to true when VDM State Machine has following actions. */ bool vdm_sm_running; @@ -578,6 +594,9 @@ struct tcpm_port { u32 bist_request; + /* VDM Discovery State to determine message sent */ + enum vdm_discovery_states vdm_discovery_state; + /* PD state for Vendor Defined Messages */ enum vdm_states vdm_state; u32 vdm_retries; @@ -641,12 +660,6 @@ struct tcpm_port { bool potential_contaminant; /* SOP* Related Fields */ - /* - * Flag to determine if SOP' Discover Identity is available. The flag - * is set if Discover Identity on SOP' does not immediately follow - * Discover Identity on SOP. - */ - bool send_discover_prime; /* * tx_sop_type determines which SOP* a message is being sent on. * For messages that are queued and not sent immediately such as in @@ -771,6 +784,9 @@ static const char * const pd_rev[] = { #define tcpm_wait_for_discharge(port) \ (((port)->auto_vbus_discharge_enabled && !(port)->vbus_vsafe0v) ? PD_T_SAFE_0V : 0) +#define tcpm_can_send_vdm(state) \ + ((state == SRC_READY || state == SNK_READY || state == SRC_VDM_IDENTITY_REQUEST)) + static enum tcpm_state tcpm_default_state(struct tcpm_port *port) { if (port->port_type == TYPEC_PORT_DRP) { @@ -1561,13 +1577,19 @@ static void mod_enable_frs_delayed_work(struct tcpm_port *port, unsigned int del } } -static void mod_send_discover_delayed_work(struct tcpm_port *port, unsigned int delay_ms) +static void mod_vdm_discovery_cancel_delayed_work(struct tcpm_port *port) +{ + hrtimer_cancel(&port->vdm_discovery_timer); + kthread_cancel_work_sync(&port->vdm_discovery_work); +} + +static void mod_vdm_discovery_delayed_work(struct tcpm_port *port, unsigned int delay_ms) { if (delay_ms) { - hrtimer_start(&port->send_discover_timer, ms_to_ktime(delay_ms), HRTIMER_MODE_REL); + hrtimer_start(&port->vdm_discovery_timer, ms_to_ktime(delay_ms), HRTIMER_MODE_REL); } else { - hrtimer_cancel(&port->send_discover_timer); - kthread_queue_work(port->wq, &port->send_discover_work); + hrtimer_cancel(&port->vdm_discovery_timer); + kthread_queue_work(port->wq, &port->vdm_discovery_work); } } @@ -1784,16 +1806,11 @@ static void tcpm_queue_vdm(struct tcpm_port *port, const u32 header, WARN_ON(!mutex_is_locked(&port->lock)); /* If is sending discover_identity, handle received message first */ - if (PD_VDO_SVDM(vdo_hdr) && PD_VDO_CMD(vdo_hdr) == CMD_DISCOVER_IDENT) { - if (tx_sop_type == TCPC_TX_SOP_PRIME) - port->send_discover_prime = true; - else - port->send_discover = true; - mod_send_discover_delayed_work(port, SEND_DISCOVER_RETRY_MS); - } else { + if (PD_VDO_SVDM(vdo_hdr) && PD_VDO_CMD(vdo_hdr) == CMD_DISCOVER_IDENT) + mod_vdm_discovery_delayed_work(port, SEND_DISCOVERY_VDM_RETRY_MS); + else /* Make sure we are not still processing a previous VDM packet */ WARN_ON(port->vdm_state > VDM_STATE_DONE); - } port->vdo_count = cnt + 1; port->vdo_data[0] = header; @@ -1816,8 +1833,7 @@ static void tcpm_queue_vdm_work(struct kthread_work *work) struct tcpm_port *port = event->port; mutex_lock(&port->lock); - if (port->state != SRC_READY && port->state != SNK_READY && - port->state != SRC_VDM_IDENTITY_REQUEST) { + if (!tcpm_can_send_vdm(port->state)) { tcpm_log_force(port, "dropping altmode_vdm_event"); goto port_unlock; } @@ -2165,6 +2181,19 @@ static bool tcpm_cable_vdm_supported(struct tcpm_port *port) tcpm_can_communicate_sop_prime(port); } +static void tcpm_update_vdm_discovery_state(struct tcpm_port *port, + enum vdm_discovery_states new_state) +{ + enum vdm_discovery_states old_state = port->vdm_discovery_state; + + if (old_state != new_state) + tcpm_log_force(port, "vdm discovery state changed: %s -> %s", + vdm_discovery_state_strings[old_state], + vdm_discovery_state_strings[new_state]); + + port->vdm_discovery_state = new_state; +} + static int tcpm_handle_discover_mode(struct tcpm_port *port, u32 *response, enum tcpm_transmit_type rx_sop_type, enum tcpm_transmit_type *response_tx_sop_type) @@ -2182,6 +2211,7 @@ static int tcpm_handle_discover_mode(struct tcpm_port *port, u32 *response, response[0] = VDO(svid, 1, typec_get_negotiated_svdm_version(typec), CMD_DISCOVER_MODES); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_PARTNER_MODES); return 1; } @@ -2190,10 +2220,12 @@ static int tcpm_handle_discover_mode(struct tcpm_port *port, u32 *response, response[0] = VDO(USB_SID_PD, 1, typec_get_cable_svdm_version(typec), CMD_DISCOVER_SVID); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_PARTNER_MODES); return 1; } tcpm_register_partner_altmodes(port); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); } else if (rx_sop_type == TCPC_TX_SOP_PRIME) { modep = &port->mode_data_prime; modep->svid_index++; @@ -2204,11 +2236,13 @@ static int tcpm_handle_discover_mode(struct tcpm_port *port, u32 *response, response[0] = VDO(svid, 1, typec_get_cable_svdm_version(typec), CMD_DISCOVER_MODES); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_CABLE_MODES); return 1; } tcpm_register_plug_altmodes(port); tcpm_register_partner_altmodes(port); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); } return 0; @@ -2387,18 +2421,18 @@ static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, typec_cable_set_svdm_version(port->cable, svdm_version); } + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_PARTNER_IDENT); + /* 6.4.4.3.1 */ svdm_consume_identity(port, p, cnt); /* Attempt Vconn swap, delay SOP' discovery if necessary */ if (tcpm_attempt_vconn_swap_discovery(port)) { - port->send_discover_prime = true; port->upcoming_state = VCONN_SWAP_SEND; ret = tcpm_ams_start(port, VCONN_SWAP); if (!ret) return 0; /* Cannot perform Vconn swap */ port->upcoming_state = INVALID_STATE; - port->send_discover_prime = false; } /* @@ -2409,7 +2443,6 @@ static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, if (IS_ERR_OR_NULL(port->cable) && tcpm_can_communicate_sop_prime(port)) { *response_tx_sop_type = TCPC_TX_SOP_PRIME; - port->send_discover_prime = true; response[0] = VDO(USB_SID_PD, 1, typec_get_negotiated_svdm_version(typec), CMD_DISCOVER_IDENT); @@ -2437,6 +2470,7 @@ static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, tcpm_set_state(port, SRC_SEND_CAPABILITIES, 0); return 0; } + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_CABLE_IDENT); *response_tx_sop_type = TCPC_TX_SOP; response[0] = VDO(USB_SID_PD, 1, @@ -2456,16 +2490,27 @@ static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, rlen = 1; } else { if (rx_sop_type == TCPC_TX_SOP) { + tcpm_update_vdm_discovery_state(port, + VDM_DISCOVERY_PARTNER_SVIDS); if (modep->nsvids && supports_modal(port)) { response[0] = VDO(modep->svids[0], 1, svdm_version, CMD_DISCOVER_MODES); rlen = 1; + } else { + tcpm_update_vdm_discovery_state(port, + VDM_DISCOVERY_COMPLETE); } } else if (rx_sop_type == TCPC_TX_SOP_PRIME) { + tcpm_update_vdm_discovery_state(port, + VDM_DISCOVERY_CABLE_SVIDS); if (modep_prime->nsvids) { response[0] = VDO(modep_prime->svids[0], 1, svdm_version, CMD_DISCOVER_MODES); rlen = 1; + } else { + tcpm_register_partner_altmodes(port); + tcpm_update_vdm_discovery_state(port, + VDM_DISCOVERY_COMPLETE); } } } @@ -2516,8 +2561,13 @@ static int tcpm_pd_svdm(struct tcpm_port *port, struct typec_altmode *adev, case CMDT_RSP_NAK: tcpm_ams_finish(port); switch (cmd) { + /* + * The cable is not allowed to respond with NAK so this must've happened over SOP + */ case CMD_DISCOVER_IDENT: case CMD_DISCOVER_SVID: + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); + break; case VDO_CMD_VENDOR(0) ... VDO_CMD_VENDOR(15): break; case CMD_DISCOVER_MODES: @@ -2697,44 +2747,6 @@ static void tcpm_handle_vdm_request(struct tcpm_port *port, port->vdm_sm_running = false; } -static void tcpm_send_vdm(struct tcpm_port *port, u32 vid, int cmd, - const u32 *data, int count, enum tcpm_transmit_type tx_sop_type) -{ - int svdm_version; - u32 header; - - switch (tx_sop_type) { - case TCPC_TX_SOP_PRIME: - /* - * If the port partner is discovered, then the port partner's - * SVDM Version will be returned - */ - svdm_version = typec_get_cable_svdm_version(port->typec_port); - if (svdm_version < 0) - svdm_version = SVDM_VER_MAX; - break; - case TCPC_TX_SOP: - svdm_version = typec_get_negotiated_svdm_version(port->typec_port); - if (svdm_version < 0) - return; - break; - default: - svdm_version = typec_get_negotiated_svdm_version(port->typec_port); - if (svdm_version < 0) - return; - break; - } - - if (WARN_ON(count > VDO_MAX_SIZE - 1)) - count = VDO_MAX_SIZE - 1; - - /* set VDM header with VID & CMD */ - header = VDO(vid, ((vid & USB_SID_PD) == USB_SID_PD) ? - 1 : (PD_VDO_CMD(cmd) <= CMD_ATTENTION), - svdm_version, cmd); - tcpm_queue_vdm(port, header, data, count, tx_sop_type); -} - static unsigned int vdm_ready_timeout(u32 vdm_hdr) { unsigned int timeout; @@ -2780,8 +2792,7 @@ static void vdm_run_state_machine(struct tcpm_port *port) * if there's traffic or we're not in PDO ready state don't send * a VDM. */ - if (port->state != SRC_READY && port->state != SNK_READY && - port->state != SRC_VDM_IDENTITY_REQUEST) { + if (!tcpm_can_send_vdm(port->state)) { port->vdm_sm_running = false; break; } @@ -2791,22 +2802,10 @@ static void vdm_run_state_machine(struct tcpm_port *port) switch (PD_VDO_CMD(vdo_hdr)) { case CMD_DISCOVER_IDENT: res = tcpm_ams_start(port, DISCOVER_IDENTITY); - if (res == 0) { - switch (port->tx_sop_type) { - case TCPC_TX_SOP_PRIME: - port->send_discover_prime = false; - break; - case TCPC_TX_SOP: - port->send_discover = false; - break; - default: - port->send_discover = false; - break; - } - } else if (res == -EAGAIN) { + if (res == -EAGAIN) { port->vdo_data[0] = 0; - mod_send_discover_delayed_work(port, - SEND_DISCOVER_RETRY_MS); + mod_vdm_discovery_delayed_work(port, + SEND_DISCOVERY_VDM_RETRY_MS); } break; case CMD_DISCOVER_SVID: @@ -2864,6 +2863,7 @@ static void vdm_run_state_machine(struct tcpm_port *port) */ if (port->state == SRC_VDM_IDENTITY_REQUEST) { tcpm_ams_finish(port); + port->vdo_data[0] = 0; port->vdm_state = VDM_STATE_DONE; tcpm_set_state(port, SRC_SEND_CAPABILITIES, 0); /* @@ -2880,6 +2880,7 @@ static void vdm_run_state_machine(struct tcpm_port *port) tcpm_ams_finish(port); } else { tcpm_ams_finish(port); + port->vdo_data[0] = 0; if (port->tx_sop_type == TCPC_TX_SOP) break; /* Handle SOP' Transmission Errors */ @@ -2889,11 +2890,11 @@ static void vdm_run_state_machine(struct tcpm_port *port) * discovery process on SOP only. */ case CMD_DISCOVER_IDENT: - port->vdo_data[0] = 0; response[0] = VDO(USB_SID_PD, 1, typec_get_negotiated_svdm_version( port->typec_port), CMD_DISCOVER_SVID); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_CABLE_IDENT); tcpm_queue_vdm(port, response[0], &response[1], 0, TCPC_TX_SOP); break; @@ -2903,9 +2904,11 @@ static void vdm_run_state_machine(struct tcpm_port *port) */ case CMD_DISCOVER_SVID: tcpm_register_partner_altmodes(port); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); break; case CMD_DISCOVER_MODES: tcpm_register_partner_altmodes(port); + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); break; default: break; @@ -3812,7 +3815,8 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); } else { - if (port->send_discover && port->negotiated_rev < PD_REV30) { + if (port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN && + port->negotiated_rev < PD_REV30) { tcpm_queue_message(port, PD_MSG_CTRL_WAIT); break; } @@ -3828,7 +3832,8 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port, PD_MSG_CTRL_NOT_SUPP, NONE_AMS); } else { - if (port->send_discover && port->negotiated_rev < PD_REV30) { + if (port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN && + port->negotiated_rev < PD_REV30) { tcpm_queue_message(port, PD_MSG_CTRL_WAIT); break; } @@ -3837,7 +3842,8 @@ static void tcpm_pd_ctrl_request(struct tcpm_port *port, } break; case PD_CTRL_VCONN_SWAP: - if (port->send_discover && port->negotiated_rev < PD_REV30) { + if (port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN && + port->negotiated_rev < PD_REV30) { tcpm_queue_message(port, PD_MSG_CTRL_WAIT); break; } @@ -4855,8 +4861,7 @@ static int tcpm_src_attach(struct tcpm_port *port) port->partner = NULL; port->attached = true; - port->send_discover = true; - port->send_discover_prime = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_UNKNOWN); return 0; @@ -4933,6 +4938,8 @@ static void tcpm_reset_port(struct tcpm_port *port) port->in_ams = false; port->ams = NONE_AMS; port->vdm_sm_running = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_UNKNOWN); + mod_vdm_discovery_cancel_delayed_work(port); tcpm_unregister_altmodes(port); tcpm_typec_disconnect(port); port->attached = false; @@ -5016,8 +5023,7 @@ static int tcpm_snk_attach(struct tcpm_port *port) port->partner = NULL; port->attached = true; - port->send_discover = true; - port->send_discover_prime = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_UNKNOWN); return 0; } @@ -5424,16 +5430,11 @@ static void run_state_machine(struct tcpm_port *port) * as well. */ if (port->explicit_contract) { - if (port->send_discover_prime) { - port->tx_sop_type = TCPC_TX_SOP_PRIME; - } else { - port->tx_sop_type = TCPC_TX_SOP; + if (port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN) tcpm_set_initial_svdm_version(port); - } - mod_send_discover_delayed_work(port, 0); + mod_vdm_discovery_delayed_work(port, 0); } else { - port->send_discover = false; - port->send_discover_prime = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); } /* @@ -5828,16 +5829,11 @@ static void run_state_machine(struct tcpm_port *port) * as well. */ if (port->explicit_contract) { - if (port->send_discover_prime) { - port->tx_sop_type = TCPC_TX_SOP_PRIME; - } else { - port->tx_sop_type = TCPC_TX_SOP; + if (port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN) tcpm_set_initial_svdm_version(port); - } - mod_send_discover_delayed_work(port, 0); + mod_vdm_discovery_delayed_work(port, 0); } else { - port->send_discover = false; - port->send_discover_prime = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); } power_supply_changed(port->psy); @@ -5883,8 +5879,8 @@ static void run_state_machine(struct tcpm_port *port) port->tcpc->set_pd_rx(port->tcpc, false); tcpm_unregister_altmodes(port); port->nr_sink_caps = 0; - port->send_discover = true; - port->send_discover_prime = false; + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_UNKNOWN); + mod_vdm_discovery_cancel_delayed_work(port); if (port->pwr_role == TYPEC_SOURCE) tcpm_set_state(port, SRC_HARD_RESET_VBUS_OFF, PD_T_PS_HARD_RESET); @@ -6031,25 +6027,15 @@ static void run_state_machine(struct tcpm_port *port) /* DR_Swap states */ case DR_SWAP_SEND: tcpm_pd_send_control(port, PD_CTRL_DR_SWAP, TCPC_TX_SOP); - if (port->data_role == TYPEC_DEVICE || port->negotiated_rev > PD_REV20) { - port->send_discover = true; - port->send_discover_prime = false; - } tcpm_set_state_cond(port, DR_SWAP_SEND_TIMEOUT, PD_T_SENDER_RESPONSE); break; case DR_SWAP_ACCEPT: tcpm_pd_send_control(port, PD_CTRL_ACCEPT, TCPC_TX_SOP); - if (port->data_role == TYPEC_DEVICE || port->negotiated_rev > PD_REV20) { - port->send_discover = true; - port->send_discover_prime = false; - } tcpm_set_state_cond(port, DR_SWAP_CHANGE_DR, 0); break; case DR_SWAP_SEND_TIMEOUT: tcpm_swap_complete(port, -ETIMEDOUT); - port->send_discover = false; - port->send_discover_prime = false; tcpm_ams_finish(port); tcpm_set_state(port, ready_state(port), 0); break; @@ -6061,6 +6047,8 @@ static void run_state_machine(struct tcpm_port *port) else tcpm_set_roles(port, true, TYPEC_STATE_USB, port->pwr_role, TYPEC_HOST); + if (port->data_role == TYPEC_HOST || port->negotiated_rev > PD_REV20) + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_UNKNOWN); tcpm_ams_finish(port); tcpm_set_state(port, ready_state(port), 0); break; @@ -6344,9 +6332,7 @@ static void run_state_machine(struct tcpm_port *port) /* Cable states */ case SRC_VDM_IDENTITY_REQUEST: - port->send_discover_prime = true; - port->tx_sop_type = TCPC_TX_SOP_PRIME; - mod_send_discover_delayed_work(port, 0); + mod_vdm_discovery_delayed_work(port, 0); port->upcoming_state = SRC_SEND_CAPABILITIES; break; @@ -7050,8 +7036,8 @@ static void tcpm_enable_frs_work(struct kthread_work *work) goto unlock; /* Send when the state machine is idle */ - if (port->state != SNK_READY || port->vdm_sm_running || port->send_discover || - port->send_discover_prime) + if (port->state != SNK_READY || port->vdm_sm_running || + port->vdm_discovery_state == VDM_DISCOVERY_UNKNOWN) goto resched; port->upcoming_state = GET_SINK_CAP; @@ -7068,30 +7054,163 @@ static void tcpm_enable_frs_work(struct kthread_work *work) mutex_unlock(&port->lock); } -static void tcpm_send_discover_work(struct kthread_work *work) +static void tcpm_vdm_discovery_work(struct kthread_work *work) { - struct tcpm_port *port = container_of(work, struct tcpm_port, send_discover_work); + struct tcpm_port *port = container_of(work, struct tcpm_port, vdm_discovery_work); + enum tcpm_transmit_type tx_sop_type = TCPC_TX_SOP; + struct typec_port *typec = port->typec_port; + struct pd_mode_data *modep, *modep_prime; + u32 msg[2] = { }; + int svdm_version; mutex_lock(&port->lock); - /* No need to send DISCOVER_IDENTITY anymore */ - if (!port->send_discover && !port->send_discover_prime) + + tcpm_log_force(port, "%s state [%s]", __func__, + vdm_discovery_state_strings[port->vdm_discovery_state]); + + /* No need to perform work if Discovery process is complete */ + if (port->vdm_discovery_state == VDM_DISCOVERY_COMPLETE) goto unlock; - if (port->data_role == TYPEC_DEVICE && port->negotiated_rev < PD_REV30) { - port->send_discover = false; - port->send_discover_prime = false; + /* Retry if the port is not idle */ + if (!tcpm_can_send_vdm(port->state) || port->vdm_sm_running) { + mod_vdm_discovery_delayed_work(port, SEND_DISCOVERY_VDM_RETRY_MS); goto unlock; } - /* Retry if the port is not idle */ - if ((port->state != SRC_READY && port->state != SNK_READY && - port->state != SRC_VDM_IDENTITY_REQUEST) || port->vdm_sm_running) { - mod_send_discover_delayed_work(port, SEND_DISCOVER_RETRY_MS); + modep = &port->mode_data; + modep_prime = &port->mode_data_prime; + + svdm_version = typec_get_negotiated_svdm_version(typec); + + switch (port->vdm_discovery_state) { + /* + * The port has not received a Discover Identity response from the port partner. + * + * 1. The port will send Discover Identity to the partner over SOP in the SRC_READY and + * SNK_READY states if there is an explicit contract + * 2. The port will send Discover Identity to the cable over SOP' in the + * SRC_VDM_IDENTITY_REQUEST state if capable of doing so. + */ + case VDM_DISCOVERY_UNKNOWN: + /* Can't send Discover Identity, VDM discovery is complete */ + if (port->data_role == TYPEC_DEVICE && port->negotiated_rev < PD_REV30) { + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); + goto unlock; + } + + if (port->state == SRC_VDM_IDENTITY_REQUEST) { + tx_sop_type = TCPC_TX_SOP_PRIME; + svdm_version = SVDM_VER_MAX; + } + + msg[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_IDENT); + break; + /* + * The port has received a Discover Identity ACK from the port partner. + * + * 1. The port will send Discover Identity to the cable over SOP' in the SRC_READY and + * SNK_READY states if it did not previously discover the cable but is capable of doing + * so. + * 2. The port will send Discover SVIDs to the partner over SOP in the SRC_READY and + * SNK_READY states otherwise. + */ + case VDM_DISCOVERY_PARTNER_IDENT: + if (tcpm_can_communicate_sop_prime(port) && !port->cable) { + tx_sop_type = TCPC_TX_SOP_PRIME; + msg[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_IDENT); + } else { + if (tcpm_can_communicate_sop_prime(port)) + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_CABLE_IDENT); + msg[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_SVID); + } + break; + /* + * The port has received a Discover Identity ACK from the cable. + * + * 1. The port will send Discover SVIDs to the partner over SOP. + */ + case VDM_DISCOVERY_CABLE_IDENT: + msg[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_SVID); + break; + /* + * The port has received a Discover SVIDs ACK from the partner or the last SVIDs supported + * by the partner. + * + * 1. The port will send Discover Modes for the first SVID over SOP if the partner supports + * modal operation and valid SVIDs were registered. + * 2. The vdm_discovery_state will move to VDM_DISCOVERY_COMPLETE otherwise. + */ + case VDM_DISCOVERY_PARTNER_SVIDS: + if (modep->nsvids && supports_modal(port)) { + msg[0] = VDO(modep->svids[0], 1, svdm_version, CMD_DISCOVER_MODES); + } else { + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); + goto unlock; + } + break; + /* + * The port has received a Discover Modes ACK from the partner for any mode. + * + * 1. The port will send Discover Modes for the next SVID that has not been discovered to + * the port partner over SOP. + * 2. The port will send Discover SVIDs over SOP' if the port can communicate over SOP' + * and the cable supports VDMs. + * 3. The vdm_discovery_state will move to VDM_DISCOVERY_COMPLETE otherwise. + */ + case VDM_DISCOVERY_PARTNER_MODES: + /* Not all modes have been discovered yet */ + if (modep->svid_index < modep->nsvids) { + msg[0] = VDO(modep_prime->svids[modep->svid_index], 1, svdm_version, + CMD_DISCOVER_MODES); + } else if (tcpm_can_communicate_sop_prime(port) && tcpm_cable_vdm_supported(port)) { + tx_sop_type = TCPC_TX_SOP_PRIME; + svdm_version = typec_get_cable_svdm_version(typec); + msg[0] = VDO(USB_SID_PD, 1, svdm_version, CMD_DISCOVER_SVID); + } else { + tcpm_update_vdm_discovery_state(port, VDM_DISCOVERY_COMPLETE); + goto unlock; + } + break; + /* + * The port has received a Discover SVIDs ACK from the cable over SOP'. + * + * 1. The port will send Discover Modes for the first SVID over SOP'. + */ + case VDM_DISCOVERY_CABLE_SVIDS: + if (modep_prime->nsvids) { + tx_sop_type = TCPC_TX_SOP_PRIME; + svdm_version = typec_get_cable_svdm_version(typec); + msg[0] = VDO(modep_prime->svids[0], 1, svdm_version, CMD_DISCOVER_MODES); + } else { + goto unlock; + } + break; + /* + * The port has received a Discover Modes ACK from the cable for any mode. + * + * 1. The port will send Discover Modes for the next SVID that has not been discovered to + * the cable over SOP'. + */ + case VDM_DISCOVERY_CABLE_MODES: + if (modep_prime->svid_index < modep_prime->nsvids) { + tx_sop_type = TCPC_TX_SOP_PRIME; + svdm_version = typec_get_cable_svdm_version(typec); + msg[0] = VDO(modep_prime->svids[modep->svid_index], 1, svdm_version, + CMD_DISCOVER_MODES); + } else { + goto unlock; + } + break; + default: goto unlock; } - tcpm_send_vdm(port, USB_SID_PD, CMD_DISCOVER_IDENT, NULL, 0, port->tx_sop_type); + /* No port partner exists, and Discover Identity */ + if (svdm_version < 0) + goto unlock; + tcpm_queue_vdm(port, msg[0], &msg[1], 0, tx_sop_type); unlock: mutex_unlock(&port->lock); } @@ -8506,12 +8625,12 @@ static enum hrtimer_restart enable_frs_timer_handler(struct hrtimer *timer) return HRTIMER_NORESTART; } -static enum hrtimer_restart send_discover_timer_handler(struct hrtimer *timer) +static enum hrtimer_restart vdm_discovery_timer_handler(struct hrtimer *timer) { - struct tcpm_port *port = container_of(timer, struct tcpm_port, send_discover_timer); + struct tcpm_port *port = container_of(timer, struct tcpm_port, vdm_discovery_timer); if (port->registered) - kthread_queue_work(port->wq, &port->send_discover_work); + kthread_queue_work(port->wq, &port->vdm_discovery_work); return HRTIMER_NORESTART; } @@ -8545,14 +8664,14 @@ struct tcpm_port *tcpm_register_port(struct device *dev, struct tcpc_dev *tcpc) kthread_init_work(&port->vdm_state_machine, vdm_state_machine_work); kthread_init_work(&port->event_work, tcpm_pd_event_handler); kthread_init_work(&port->enable_frs, tcpm_enable_frs_work); - kthread_init_work(&port->send_discover_work, tcpm_send_discover_work); + kthread_init_work(&port->vdm_discovery_work, tcpm_vdm_discovery_work); hrtimer_setup(&port->state_machine_timer, state_machine_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL); hrtimer_setup(&port->vdm_state_machine_timer, vdm_state_machine_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL); hrtimer_setup(&port->enable_frs_timer, enable_frs_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL); - hrtimer_setup(&port->send_discover_timer, send_discover_timer_handler, CLOCK_MONOTONIC, + hrtimer_setup(&port->vdm_discovery_timer, vdm_discovery_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL); spin_lock_init(&port->pd_event_lock); @@ -8647,7 +8766,7 @@ void tcpm_unregister_port(struct tcpm_port *port) port->registered = false; kthread_destroy_worker(port->wq); - hrtimer_cancel(&port->send_discover_timer); + hrtimer_cancel(&port->vdm_discovery_timer); hrtimer_cancel(&port->enable_frs_timer); hrtimer_cancel(&port->vdm_state_machine_timer); hrtimer_cancel(&port->state_machine_timer); From a234ec44b84908e36aeda12ed0b8acc39f7ba4d6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 27 Jul 2026 19:30:07 +0200 Subject: [PATCH 151/202] arm64: dts: rockchip: Fix USB-C DP AltMode vdo Set sensible VDO for DP AltMode. The new value is commonly used by other boards and dissects into the following: 0x0002 DP_CAP_DFP_D + 0x0004 DP_CAP_SIGNALLING_HBR3 + 0x0040 DP_CAP_RECEPTACLE + 0x1c00 DP_CAP_PIN_ASSIGN_DFP_D(DP_PIN_ASSIGN_C | DP_PIN_ASSIGN_D | DP_PIN_ASSIGN_E) ------ 0x1c46 DP_CAP_DFP_D: the RK3588 and RK3576 only have a DisplayPort transmitter, but no receiver DP_CAP_SIGNALLING_HBR3: HBR3 is the limit of the DisplayPort controller in RK3588 and RK3576 DP_CAP_RECEPTACLE: The DisplayPort interface is presented on a receptacle (disabled bit means plug) DP_CAP_USB: This is not set, which means USB2 "may be required". My understanding is, that unsetting this means USB2 is now available at all. DP_CAP_PIN_ASSIGN_DFP_D(DP_PIN_ASSIGN_C | DP_PIN_ASSIGN_D | DP_PIN_ASSIGN_E): The USBDP PHY can handle muxing any of these Signed-off-by: Sebastian Reichel --- arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts | 2 +- arch/arm64/boot/dts/rockchip/rk3588-rock-5b-5bp-5t.dtsi | 2 +- arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts index 7a1cab2bb4a778..658154c2c7109f 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -735,7 +735,7 @@ altmodes { displayport { svid = /bits/ 16 <0xff01>; - vdo = <0xffffffff>; + vdo = <0x00001c46>; }; }; 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 7db3dcffb3f8c7..8635b454887d75 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 @@ -367,7 +367,7 @@ altmodes { displayport { svid = /bits/ 16 <0xff01>; - vdo = <0xffffffff>; + vdo = <0x00001c46>; }; }; diff --git a/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts b/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts index ed36c27c2320d2..300a5ea745adde 100644 --- a/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts +++ b/arch/arm64/boot/dts/rockchip/rk3588s-indiedroid-nova.dts @@ -393,7 +393,7 @@ altmodes { displayport { svid = /bits/ 16 <0xff01>; - vdo = <0xffffffff>; + vdo = <0x00001c46>; }; }; From ef077d72a31e1509c670314addc62aac4ea638d9 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:38 +0200 Subject: [PATCH 152/202] 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 2f9472c1a09072..92877f95d5de65 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23427,6 +23427,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 7ecd615be72d30fd7c912ba55e53b4c2d439e06f Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:39 +0200 Subject: [PATCH 153/202] 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 92877f95d5de65..bb8937f2680ea6 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23433,6 +23433,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 763ce6a34782bd..1cebad21a26ecf 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -1371,6 +1371,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 dd4bb7e77c336c..4a6a2e24a829c8 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -230,6 +230,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 dc0b3c57ee8cba36c81c780513a7b5fa82c83695 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:40 +0200 Subject: [PATCH 154/202] 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 bb8937f2680ea6..a83d930bed86cf 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23434,6 +23434,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 e8886a9b64d968..854f9383e901ef 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -637,6 +637,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 5630a521a7cffe..5a9b4af59895be 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -57,6 +57,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..bece37c9a3690e --- /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_set_of_node_reused(dev); + 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 486945ca5ca7e3d84c2e6a0e5b7d55e6a867f6e1 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:41 +0200 Subject: [PATCH 155/202] 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 a83d930bed86cf..a9fa7e65bc132e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23433,6 +23433,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 66d849f922880df8eefd1669b468185875d57298 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:42 +0200 Subject: [PATCH 156/202] 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 49e48bd050313c..8b4206bd234102 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 c50ebcb87a7b591146701af77636d212e9539aed Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:43 +0200 Subject: [PATCH 157/202] 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 2a69bd3106d91f..9dce138c0050d3 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"; @@ -792,6 +800,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>; @@ -801,6 +839,13 @@ }; &pinctrl { + fan { + fan_pwm: fan-pwm { + rockchip,pins = + <4 RK_PC5 14 &pcfg_pull_down_drv_level_5>; + }; + }; + hdmi { hdmi_tx_on_h: hdmi-tx-on-h { rockchip,pins = <2 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; @@ -857,6 +902,11 @@ }; }; +&pwm2_8ch_5 { + pinctrl-0 = <&fan_pwm>; + status = "okay"; +}; + &sai1 { pinctrl-names = "default"; pinctrl-0 = <&sai1m0_lrck From b13fb37cbe8885e2659a85d7194a3a89a21d8997 Mon Sep 17 00:00:00 2001 From: Anton Burticica Date: Tue, 23 Dec 2025 11:34:32 +0400 Subject: [PATCH 158/202] 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 89f61710a21045..43b19267be762c 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 0a2b74dbbd3d5ec4260ff5584f7f51ddb8b348dc Mon Sep 17 00:00:00 2001 From: Pavel Zhovner Date: Mon, 29 Dec 2025 17:11:42 +0000 Subject: [PATCH 159/202] 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 cde8f6b5519af526a8eb32e6b188a45653b52944 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 19:03:10 +0400 Subject: [PATCH 160/202] 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 658154c2c7109f..40ed2e16e5b357 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-armsom-sige5.dts @@ -888,10 +888,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 f8fc78afe541171bcfe6fca8e039d53fd5097fd2 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 12:30:40 +0400 Subject: [PATCH 161/202] 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 6f5d628cc36ebe41c6a4e29870a91c389e680843 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 26 Jan 2026 18:40:51 +0400 Subject: [PATCH 162/202] 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 54a5f54e945795f68c4b9b4888245adbf49b99c6 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 13:36:56 +0400 Subject: [PATCH 163/202] 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 fb86c1530ab82c26e4eebe1cfa23edfcc4503532 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 15:42:13 +0400 Subject: [PATCH 164/202] 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 1e7926fc029e3ae4420526a9eb6637c6d6304873 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:31:15 +0400 Subject: [PATCH 165/202] 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 396044f368e7cf..3768c0cbbfb6e6 100644 --- a/Documentation/devicetree/bindings/vendor-prefixes.yaml +++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml @@ -602,6 +602,8 @@ patternProperties: description: Fitipower Integrated Technology Inc. "^flipkart,.*": description: Flipkart Inc. + "^flipper,.*": + description: Flipper FZCO "^focaltech,.*": description: FocalTech Systems Co.,Ltd "^forlinx,.*": From 4d97665508b6e9412c85b98376e63f206408d547 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:38:30 +0400 Subject: [PATCH 166/202] 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 47041182fc1c9fd42953ba5a475fd7efaeabe419 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 12:30:56 +0400 Subject: [PATCH 167/202] 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 --- drivers/gpu/drm/tiny/Kconfig | 14 + drivers/gpu/drm/tiny/Makefile | 31 +- drivers/gpu/drm/tiny/flipper-one-display.c | 366 +++++++++++++++++++++ 3 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 drivers/gpu/drm/tiny/flipper-one-display.c 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..b53a5bb1a95e65 --- /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_commit *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_commit *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_commit *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_commit *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_commit *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[] = { + {"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 e33fd2808d5e37d8f05e6f92913bbcc80550cfdc Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:40:17 +0400 Subject: [PATCH 168/202] 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 af3cfee636bb3007017b2d7c45eb6731bc62f0e5 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:41:58 +0400 Subject: [PATCH 169/202] 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 + .../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 +++++++++++++++++ 6 files changed, 1999 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-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..b47b4b682594dd --- /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 = <0x00001c46>; + }; + }; + + 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 882b9293f6ba4bd14b9ea7203e7698552cc94ee0 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 17:50:37 +0400 Subject: [PATCH 170/202] 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 577b277efd7fc3..b8d6d578285c8a 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; }; @@ -144,6 +143,7 @@ static int bq257xx_regulator_probe(struct platform_device *pdev) struct device *dev = &pdev->dev; struct bq257xx_reg_data *pdata; struct regulator_config cfg = {}; + struct regulator_dev *rdev; device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent); @@ -162,9 +162,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 e35ea23bb72120b22c47831f819e42d2601bb7c6 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:15:16 +0400 Subject: [PATCH 171/202] 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 b8d6d578285c8a..c450e23ee7a64a 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 regulator_config cfg = {}; struct regulator_dev *rdev; @@ -151,7 +237,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 203c283cdc2145149a2bd6000b606ec141071757 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:46:02 +0400 Subject: [PATCH 172/202] 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 | 14 + 2 files changed, 541 insertions(+), 1 deletion(-) 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 4ec72eb920f2c1..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) From f4a3a1158efe6bc9fb8f0c9ea364898b2fd60527 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 19 Feb 2026 17:29:09 +0400 Subject: [PATCH 173/202] 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 a9fa7e65bc132e..45bf5dfffc19bd 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12696,6 +12696,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 9918047eb4223483cab6de8bb4fadf4302e48e2b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 20 Feb 2026 16:48:29 +0400 Subject: [PATCH 174/202] 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 45bf5dfffc19bd..e29c02f3f91343 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12701,6 +12701,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 2bfbcc033d599c..d22d7b71c5cfad 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -2355,6 +2355,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 63effc0ab8d113..4693d3fc6dbd61 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -106,6 +106,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 9c6e6c5df1b5c9b7983572bf6615ffb87e816017 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 13 Mar 2026 13:02:31 +0400 Subject: [PATCH 175/202] 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 7ac7000b2d1392..ab9d48bfbb7198 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 @@ -834,6 +835,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; @@ -886,12 +888,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 05273ac9c99147083c3e433210e6b8ffc0090c3a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:49:48 +0400 Subject: [PATCH 176/202] WIP: mfd: Add support for Flipper One's MCU Signed-off-by: Alexey Charkov --- .../ABI/testing/sysfs-driver-flipper-one-mcu | 33 ++ MAINTAINERS | 12 + drivers/mfd/Kconfig | 15 + drivers/mfd/Makefile | 1 + drivers/mfd/flipper-one-mcu.c | 333 ++++++++++++++++++ include/linux/mfd/flipper-one-mcu.h | 101 ++++++ 6 files changed, 495 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-driver-flipper-one-mcu create mode 100644 drivers/mfd/flipper-one-mcu.c create mode 100644 include/linux/mfd/flipper-one-mcu.h diff --git a/Documentation/ABI/testing/sysfs-driver-flipper-one-mcu b/Documentation/ABI/testing/sysfs-driver-flipper-one-mcu new file mode 100644 index 00000000000000..3614b308a79b9c --- /dev/null +++ b/Documentation/ABI/testing/sysfs-driver-flipper-one-mcu @@ -0,0 +1,33 @@ +What: /sys/bus/i2c/devices//cpustate +Date: July 2026 +KernelVersion: 7.1 +Contact: Alexey Charkov +Description: (RW) Reports the CPU/system power state to the Flipper One MCU + so it can present meaningful on-device feedback to the user. + + Reading returns the last logical state name known to the driver. + + Writing lets userspace announce transitions that the kernel + cannot observe on its own. Only the following values are + accepted; any other value returns -EINVAL: + + - ``online``: boot is complete and userspace is up. This is a + sticky state and is restored automatically after resuming + from suspend. + - ``suspend-request``: userspace is preparing to suspend (for + example, running its pre-suspend hooks). Announced before the + kernel actually enters suspend. + - ``reboot-request``: userspace has begun a reboot and is + stopping services. Announced before the kernel starts its + own shutdown sequence. + - ``poweroff-request``: userspace has begun a power-off and is + stopping services. Announced before the kernel starts its + own shutdown sequence. + + The ``*-request`` states are transient announcements and are not + restored after resume. + + Other states (bootloader, kernel-init, suspend, shutting-down, + powered-off) are managed by the kernel and cannot be written. + + Format: %s. diff --git a/MAINTAINERS b/MAINTAINERS index e29c02f3f91343..1446207bda0cf0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -10141,6 +10141,18 @@ S: Maintained F: Documentation/devicetree/bindings/counter/fsl,ftm-quaddec.yaml F: drivers/counter/ftm-quaddec.c +FLIPPER ONE SUPPORT +M: Alexey Charkov +S: Maintained +F: Documentation/ABI/testing/sysfs-driver-flipper-one-mcu +F: drivers/gpu/drm/tiny/flipper-one-display.c +F: drivers/input/misc/flipper-one-haptic.c +F: drivers/input/misc/flipper-one-input.c +F: drivers/leds/rgb/leds-flipper-one.c +F: drivers/mfd/flipper-one-mcu.c +F: drivers/usb/typec/ucsi/ucsi_flipper_one.c +F: include/linux/mfd/flipper-one-mcu.h + FLOPPY DRIVER M: Denis Efremov L: linux-block@vger.kernel.org diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 1cebad21a26ecf..bb3bec9a58fb33 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 4a6a2e24a829c8..e3cf1bc7e26016 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..38111d89159631 --- /dev/null +++ b/drivers/mfd/flipper-one-mcu.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Flipper One MCU interconnect driver + * Copyright (C) 2026 Flipper FZCO + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const struct regmap_range fomcu_writeable_reg_ranges[] = { + regmap_reg_range(FOMCU_REG_CPUSTATE, FOMCU_REG_CPUSTATE), + 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"), + DEFINE_RES_IRQ_NAMED(FOMCU_INT_INPUT_SWBTN, "flipper-one-input-swbtn"), +}; + +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_set_cpustate(struct fomcu_device *ddata, + enum fomcu_cpu_states state) +{ + int ret; + + ret = regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, state); + if (ret) + return ret; + + ddata->cpustate = state; + return 0; +} + +static const char * const fomcu_state_names[FOMCU_CPUSTATE_NUM_STATES] = { + [FOMCU_CPUSTATE_UNKNOWN] = "unknown", + [FOMCU_CPUSTATE_BOOTLOADER] = "bootloader", + [FOMCU_CPUSTATE_KERNEL_INIT] = "kernel-init", + [FOMCU_CPUSTATE_ONLINE] = "online", + [FOMCU_CPUSTATE_SUSPEND_REQ] = "suspend-request", + [FOMCU_CPUSTATE_SUSPEND] = "suspend", + [FOMCU_CPUSTATE_REBOOT_REQ] = "reboot-request", + [FOMCU_CPUSTATE_POWEROFF_REQ] = "poweroff-request", + [FOMCU_CPUSTATE_SHUTTING_DOWN] = "shutting-down", + [FOMCU_CPUSTATE_POWERED_OFF] = "powered-off", +}; + +/* States userspace is permitted to report through the cpustate sysfs file. */ +static const enum fomcu_cpu_states fomcu_user_writable_states[] = { + FOMCU_CPUSTATE_ONLINE, + FOMCU_CPUSTATE_SUSPEND_REQ, + FOMCU_CPUSTATE_REBOOT_REQ, + FOMCU_CPUSTATE_POWEROFF_REQ, +}; + +static ssize_t cpustate_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct fomcu_device *ddata = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%s\n", fomcu_state_names[ddata->cpustate]); +} + +static ssize_t cpustate_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fomcu_device *ddata = dev_get_drvdata(dev); + enum fomcu_cpu_states state; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(fomcu_user_writable_states); i++) { + state = fomcu_user_writable_states[i]; + if (sysfs_streq(buf, fomcu_state_names[state])) + break; + } + + if (i == ARRAY_SIZE(fomcu_user_writable_states)) + return -EINVAL; + + /* + * Only ONLINE is a sticky state that resume should restore. The + * *-request states are transient announcements of an imminent + * transition, so they must not become the resume restore-point + * (otherwise a suspend-request/suspend/resume cycle would come back + * as "suspend-request" instead of "online"). + */ + if (state == FOMCU_CPUSTATE_ONLINE) + ret = fomcu_set_cpustate(ddata, state); + else + ret = regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, state); + + return ret ? : count; +} +static DEVICE_ATTR_RW(cpustate); + +static struct attribute *fomcu_attrs[] = { + &dev_attr_cpustate.attr, + NULL, +}; +ATTRIBUTE_GROUPS(fomcu); + +static int fomcu_reboot_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct fomcu_device *ddata = + container_of(nb, struct fomcu_device, reboot_nb); + + /* Runs before device_shutdown() for both reboot and power-off */ + regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, + FOMCU_CPUSTATE_SHUTTING_DOWN); + + return NOTIFY_DONE; +} + +static int fomcu_power_off(struct sys_off_data *data) +{ + struct fomcu_device *ddata = data->cb_data; + + /* + * Runs after device_shutdown(), just before the machine is actually + * powered off. Only reached on power-off, not on reboot, so this is + * where we tell the MCU it may cut power to the PMIC. + */ + regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, + FOMCU_CPUSTATE_POWERED_OFF); + + return NOTIFY_DONE; +} + +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"); + + ddata->reboot_nb.notifier_call = fomcu_reboot_notify; + ret = devm_register_reboot_notifier(&client->dev, &ddata->reboot_nb); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to register reboot notifier\n"); + + ret = devm_register_sys_off_handler(&client->dev, + SYS_OFF_MODE_POWER_OFF_PREPARE, + SYS_OFF_PRIO_DEFAULT, + fomcu_power_off, ddata); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to register power-off handler\n"); + + /* + * Signal that early kernel init has reached this driver. Userspace is + * expected to move the MCU to FOMCU_CPUSTATE_ONLINE once boot completes. + */ + return fomcu_set_cpustate(ddata, FOMCU_CPUSTATE_KERNEL_INIT); +} + +static int fomcu_suspend(struct device *dev) +{ + struct fomcu_device *ddata = dev_get_drvdata(dev); + + /* + * Leave ddata->cpustate untouched so that resume can restore whatever + * state we were in before suspending (KERNEL_INIT if userspace had not + * come up yet, ONLINE otherwise). + */ + return regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, FOMCU_CPUSTATE_SUSPEND); +} + +static int fomcu_resume(struct device *dev) +{ + struct fomcu_device *ddata = dev_get_drvdata(dev); + + return regmap_write(ddata->regmap, FOMCU_REG_CPUSTATE, ddata->cpustate); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(fomcu_pm_ops, fomcu_suspend, fomcu_resume); + +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, + .pm = pm_sleep_ptr(&fomcu_pm_ops), + .dev_groups = fomcu_groups, + }, + .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..a44cbb55adeee8 --- /dev/null +++ b/include/linux/mfd/flipper-one-mcu.h @@ -0,0 +1,101 @@ +/* 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 +#include + +enum fomcu_interrupts { + FOMCU_INT_INPUT_BTN, + FOMCU_INT_INPUT_TOUCH, + FOMCU_INT_INPUT_HEADSET, + FOMCU_INT_INPUT_SWBTN, + FOMCU_INT_UCSI_EVENT, +}; + +#define FOMCU_REG_INTSTS 0x0000 + +#define FOMCU_REG_CPUSTATE 0x0040 + +/* + * These values are part of the wire protocol shared with the MCU firmware; + * keep them stable and in sync with the MCU side when adding new states. + */ +enum fomcu_cpu_states { + FOMCU_CPUSTATE_UNKNOWN = 0, + FOMCU_CPUSTATE_BOOTLOADER, /* set by the MCU/bootloader itself */ + FOMCU_CPUSTATE_KERNEL_INIT, /* this driver has probed */ + FOMCU_CPUSTATE_ONLINE, /* userspace up / resumed from suspend */ + FOMCU_CPUSTATE_SUSPEND_REQ, /* userspace preparing to suspend */ + FOMCU_CPUSTATE_SUSPEND, /* kernel about to suspend */ + FOMCU_CPUSTATE_REBOOT_REQ, /* userspace preparing to reboot */ + FOMCU_CPUSTATE_POWEROFF_REQ, /* userspace preparing to power off */ + FOMCU_CPUSTATE_SHUTTING_DOWN, /* kernel reboot/power-off in progress */ + FOMCU_CPUSTATE_POWERED_OFF, /* safe to cut power to the PMIC */ + FOMCU_CPUSTATE_NUM_STATES +}; + +#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_INTSTS_INPUT_SWBTN BIT(3) + +#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_INPUT_SWBTNS 0x020a + +#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; + struct notifier_block reboot_nb; + /* Last non-transient CPU state, restored on resume. */ + enum fomcu_cpu_states cpustate; +}; + +#endif /* __LINUX_MFD_FLIPPER_ONE_MCU_H */ From 8bed9129490190c6855f24526ba3da6d8ed0d7da Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:50:54 +0400 Subject: [PATCH 177/202] 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 1f6c57dba03081..56ae17c69c4690 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 2281d6803fce92..cecf72e54a8c0e 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 451421262da0edfd196ff8a5f8f350d06e5ffbe2 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Apr 2026 14:15:26 +0400 Subject: [PATCH 178/202] 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 6e9ab5f60714b2..e895adc7e7dde3 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 cc0f2df6628692..6639695a6a0092 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_LP5860_CORE) += leds-lp5860-core.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 7b8fafc4ce1224a9e19507f25c845f1a971f3f22 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 15:02:56 +0400 Subject: [PATCH 179/202] 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 56ae17c69c4690..bb03fcaa4b4368 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 cecf72e54a8c0e..8c7eab80ea222b 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 973163c5943c0da870e24f8338a491125d1614c7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 18:29:58 +0400 Subject: [PATCH 180/202] 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 401ab11c015ec9c739b0148a26e3ea254ff02f0c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:50:45 +0400 Subject: [PATCH 181/202] 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 8b4206bd234102..e5c6aa37dd4ce8 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 03310dfe91120592e867aecf257d305ebbe8233e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Jul 2026 13:24:14 +0400 Subject: [PATCH 182/202] clk: rockchip: pll: Fix the fractional part denominator on RK3588/RK3576 According to the TRM, the fractional PLL coefficient should be divided by 65536 rather than 65535 to obtain the output rate. Fix the denominator and add a comment with the TRM provided clock formulae for future reference. See RK3576 TRM Part 1 V1.2 section 2.13.1.4 Setting Guide on P, M, S and K or equivalently RK3588 TRM part 1 V1.0 section 2.17.1.4 Setting Guide on P, M, S and K. Fractional PLL rates don't seem to be used by any current mainline consumers, so this is purely a correctness fix. It will also be important to properly support DisplayPort output going forward, as the video output controller derives its pixel clock from system PLLs with no dedicated PHY PLL option for DP unlike HDMI, and some display modes are only achievable with fractional PLL rates. Fixes: 8f6594494b1c ("clk: rockchip: add pll type for RK3588") Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk-pll.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/clk/rockchip/clk-pll.c b/drivers/clk/rockchip/clk-pll.c index 6b853800cb6bc6..bf8acf7cee0d97 100644 --- a/drivers/clk/rockchip/clk-pll.c +++ b/drivers/clk/rockchip/clk-pll.c @@ -900,6 +900,13 @@ static void rockchip_rk3588_pll_get_params(struct rockchip_clk_pll *pll, rate->k = ((pllcon >> RK3588_PLLCON2_K_SHIFT) & RK3588_PLLCON2_K_MASK); } +/* + * 2250 MHz <= Fvco <= 4500 MHz + * For Fvco > 3 GHz: period jitter +-1% frac PLL, +-0.75% int PLL + * For Fvco < 3 GHz: period jitter +-2% frac PLL, +-1.50% int PLL + * Fvco = ((m + k / 65536) * Fin) / p + * Fout = ((m + k / 65536) * Fin) / (p * 2^s) + */ static unsigned long rockchip_rk3588_pll_recalc_rate(struct clk_hw *hw, unsigned long prate) { struct rockchip_clk_pll *pll = to_rockchip_clk_pll(hw); @@ -915,7 +922,7 @@ static unsigned long rockchip_rk3588_pll_recalc_rate(struct clk_hw *hw, unsigned /* fractional mode */ u64 frac_rate64 = prate * cur.k; - postdiv = cur.p * 65535; + postdiv = cur.p * 65536; do_div(frac_rate64, postdiv); rate64 += frac_rate64; } From ff231099ba6638693f0a8ddf39053b4525322530 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Jul 2026 22:23:07 +0400 Subject: [PATCH 183/202] clk: rockchip: Fractional PLL coefficient on RK3588/RK3576 is two's complement When the PLL rates table was first committed for RK3588 (and later reused for RK3576), the fractional PLL coefficient was defined as an unsigned value, while the TRM clearly states that it is a two's complement 16-bit value. Treating the fractional PLL coefficient as unsigned in rate recalculation results in a kernel-visible rate which deviates from what the hardware actually generates by Fin / (p * 2^s), or 2 MHz for the two affected table entries. Rockchip's downstream kernel later revised the fractional PLL code [1] to account for the two's complement nature of the coefficient, but that change wasn't upstreamed. Change the PLL table definition to use two's complement for the fractional coefficient and update its users accordingly. Note that a negative fractional coefficient is meant to be subtracted from the next larger integer multiplier, so the m values in the table are also adjusted accordingly for the two negative-k entries. Rockchip's downstream commit introducing the two's complement logic for k also does unrelated tweaks to the PLL parameters which are not explained by the switch to the two's complement, so they are not replicated here. If any of the parameters prove to need further tweaks (e.g. for precision or jitter) that would better be done in targeted follow-up commits. Fractional PLL rates don't seem to be used by any current mainline consumers, so this is purely a correctness fix. It will also be important to properly support DisplayPort output going forward, as the video output controller derives its pixel clock from system PLLs with no dedicated PHY PLL option for DP unlike HDMI, and some display modes are only achievable using fractional PLL rates. Link: https://github.com/flipperdevices/rockchip-linux/commit/7a72bc05dcc3a51e85ae531749e6270bf9b9212d [1] Fixes: f1c506d152ff ("clk: rockchip: add clock controller for the RK3588") Fixes: cc40f5baa91b ("clk: rockchip: Add clock controller for the RK3576") Signed-off-by: Alexey Charkov --- drivers/clk/rockchip/clk-pll.c | 7 ++++--- drivers/clk/rockchip/clk-rk3576.c | 4 ++-- drivers/clk/rockchip/clk-rk3588.c | 4 ++-- drivers/clk/rockchip/clk.h | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/drivers/clk/rockchip/clk-pll.c b/drivers/clk/rockchip/clk-pll.c index bf8acf7cee0d97..706ca4b344d399 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" @@ -906,6 +907,7 @@ static void rockchip_rk3588_pll_get_params(struct rockchip_clk_pll *pll, * For Fvco < 3 GHz: period jitter +-2% frac PLL, +-1.50% int PLL * Fvco = ((m + k / 65536) * Fin) / p * Fout = ((m + k / 65536) * Fin) / (p * 2^s) + * -32768 <= k <= 32767 (only available in frac PLLs, not int PLLs) */ static unsigned long rockchip_rk3588_pll_recalc_rate(struct clk_hw *hw, unsigned long prate) { @@ -920,11 +922,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 = (s64)prate * cur.k; postdiv = cur.p * 65536; - do_div(frac_rate64, postdiv); - rate64 += frac_rate64; + rate64 += div_s64(frac_rate64, postdiv); } rate64 = rate64 >> cur.s; diff --git a/drivers/clk/rockchip/clk-rk3576.c b/drivers/clk/rockchip/clk-rk3576.c index 2557358e0b9d87..63f229e73a4540 100644 --- a/drivers/clk/rockchip/clk-rk3576.c +++ b/drivers/clk/rockchip/clk-rk3576.c @@ -79,13 +79,13 @@ static struct rockchip_pll_rate_table rk3576_pll_rates[] = { RK3588_PLL_RATE(1008000000, 2, 336, 2, 0), RK3588_PLL_RATE(1000000000, 3, 500, 2, 0), RK3588_PLL_RATE(983040000, 4, 655, 2, 23592), - RK3588_PLL_RATE(955520000, 3, 477, 2, 49806), + RK3588_PLL_RATE(955520000, 3, 478, 2, -15730), RK3588_PLL_RATE(903168000, 6, 903, 2, 11009), RK3588_PLL_RATE(900000000, 2, 300, 2, 0), RK3588_PLL_RATE(816000000, 2, 272, 2, 0), RK3588_PLL_RATE(786432000, 2, 262, 2, 9437), RK3588_PLL_RATE(786000000, 1, 131, 2, 0), - RK3588_PLL_RATE(785560000, 3, 392, 2, 51117), + RK3588_PLL_RATE(785560000, 3, 393, 2, -14419), RK3588_PLL_RATE(722534400, 8, 963, 2, 24850), RK3588_PLL_RATE(600000000, 2, 200, 2, 0), RK3588_PLL_RATE(594000000, 2, 198, 2, 0), diff --git a/drivers/clk/rockchip/clk-rk3588.c b/drivers/clk/rockchip/clk-rk3588.c index 2ba9976654cf15..1de6d04e6ef4bc 100644 --- a/drivers/clk/rockchip/clk-rk3588.c +++ b/drivers/clk/rockchip/clk-rk3588.c @@ -79,14 +79,14 @@ static struct rockchip_pll_rate_table rk3588_pll_rates[] = { RK3588_PLL_RATE(1008000000, 2, 336, 2, 0), RK3588_PLL_RATE(1000000000, 3, 500, 2, 0), RK3588_PLL_RATE(983040000, 4, 655, 2, 23592), - RK3588_PLL_RATE(955520000, 3, 477, 2, 49806), + RK3588_PLL_RATE(955520000, 3, 478, 2, -15730), RK3588_PLL_RATE(903168000, 6, 903, 2, 11009), RK3588_PLL_RATE(900000000, 2, 300, 2, 0), RK3588_PLL_RATE(850000000, 3, 425, 2, 0), RK3588_PLL_RATE(816000000, 2, 272, 2, 0), RK3588_PLL_RATE(786432000, 2, 262, 2, 9437), RK3588_PLL_RATE(786000000, 1, 131, 2, 0), - RK3588_PLL_RATE(785560000, 3, 392, 2, 51117), + RK3588_PLL_RATE(785560000, 3, 393, 2, -14419), RK3588_PLL_RATE(722534400, 8, 963, 2, 24850), RK3588_PLL_RATE(600000000, 2, 200, 2, 0), RK3588_PLL_RATE(594000000, 2, 198, 2, 0), diff --git a/drivers/clk/rockchip/clk.h b/drivers/clk/rockchip/clk.h index 9e3503e2ffc23b..72b36bba315238 100644 --- a/drivers/clk/rockchip/clk.h +++ b/drivers/clk/rockchip/clk.h @@ -635,10 +635,10 @@ struct rockchip_pll_rate_table { }; struct { /* for RK3588 */ - unsigned int m; - unsigned int p; - unsigned int s; - unsigned int k; + unsigned int m; /* main divider, 10 bit unsigned */ + unsigned int p; /* pre-divider, 6 bit unsigned */ + unsigned int s; /* scaler, 3 bit unsigned */ + s16 k; /* fractional part, 16 bit two's complement */ }; }; }; From d61a5a99d3e242b2455c8e70223ac4ee2223072a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Apr 2026 13:51:32 +0400 Subject: [PATCH 184/202] 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 63f229e73a4540..19b16c4779fdaf 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, 393, 2, -14419), 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 dee15b45335d93ba9a927b4be8a33c4dbb274df8 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:46:19 +0400 Subject: [PATCH 185/202] 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 19b16c4779fdaf..5d5fcc1da3da62 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 b6c75335a8aa58de13b86522cff0362ea541185a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 20 Apr 2026 21:08:31 +0400 Subject: [PATCH 186/202] 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 ef52ad8d13b3f40183ebc4c1f7400a2b747d5c3d Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 19 May 2026 17:29:49 +0400 Subject: [PATCH 187/202] 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 8f7ed469cac682..2fbe0ec04337a1 100644 --- a/drivers/bluetooth/btusb.c +++ b/drivers/bluetooth/btusb.c @@ -4124,6 +4124,49 @@ static int btusb_probe(struct usb_interface *intf, if (err) goto err_free_data; + /* + * 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 e2d9dd39ae57b845961e21900c6721a266ec227e Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 1 Jun 2026 15:22:06 +0400 Subject: [PATCH 188/202] 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 34a147aac2619c..cb6a8cb54d1276 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 36af36c3b290f545925b4e42c73b68625221addd Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 2 Jun 2026 16:58:06 +0400 Subject: [PATCH 189/202] 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 --- .../gpu/drm/rockchip/dw_hdmi_qp-rockchip.c | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c b/drivers/gpu/drm/rockchip/dw_hdmi_qp-rockchip.c index cd98a0f7020991..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,18 @@ 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"); + return devm_request_threaded_irq(dev, irq, cfg->ctrl_ops->hardirq_callback, cfg->ctrl_ops->irq_callback, From a5a1385973f40e00acc01e086f2c1c6074fe9ac8 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 10 Apr 2026 00:53:11 +0400 Subject: [PATCH 190/202] 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 830164e991a79e..ba5b5ba38cfe63 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 3e058a7d9825c73b89dd1fdc4ea7f42b9770cbdc Mon Sep 17 00:00:00 2001 From: Simon Wright Date: Wed, 10 Jun 2026 23:54:26 +1200 Subject: [PATCH 191/202] 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 9173af3969f9121d5683db069913a457e6464f94 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 7 Jul 2026 12:05:55 +0400 Subject: [PATCH 192/202] arm64: dts: rockchip: add overlay for UART2 on the GPIO header on Flipper One Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/Makefile | 5 +++++ .../boot/dts/rockchip/rk3576-flipper-one-uart2.dtso | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart2.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index ff84b2fe5100d4..6f693aea54d4cc 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -175,6 +175,7 @@ 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-flipper-one-uart2.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 @@ -324,6 +325,10 @@ 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) += rk3576-flipper-one-uart2.dtb +rk3576-flipper-one-uart2-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-uart2.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-flipper-one-uart2.dtso b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart2.dtso new file mode 100644 index 00000000000000..a9b167e7b6ddf7 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart2.dtso @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay to enable UART2 on the GPIO header on Flipper One + */ + +/dts-v1/; +/plugin/; + +&uart2 { + pinctrl-names = "default"; + pinctrl-0 = <&uart2m1_xfer>; + status = "okay"; +}; From 2085e015df2496dc6187e48aaed83b09da94915a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 22 Jun 2026 18:37:11 +0400 Subject: [PATCH 193/202] Input: flipper-one-input: add support for SW buttons Extend the flipper-one-input driver to support the MCU software driven buttons. While at that, refactor the code to reduce duplication. Signed-off-by: Alexey Charkov --- drivers/input/misc/flipper-one-input.c | 262 ++++++++++++++----------- 1 file changed, 144 insertions(+), 118 deletions(-) diff --git a/drivers/input/misc/flipper-one-input.c b/drivers/input/misc/flipper-one-input.c index eb0cbeb9c529f0..15c60af33ba7e8 100644 --- a/drivers/input/misc/flipper-one-input.c +++ b/drivers/input/misc/flipper-one-input.c @@ -4,14 +4,20 @@ * Copyright (C) 2026 Flipper FZCO */ -#include +#include +#include +#include +#include +#include #include +#include #include -#include -#include +#include +#include #include +#include #include -#include +#include #include @@ -36,23 +42,12 @@ #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 *); -}; +#define FO_SWBTN_POWER BIT(0) 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 input_dev *idev = data; + struct regmap *regmap = input_get_drvdata(idev); struct device *parent = idev->dev.parent; unsigned int reg; int err; @@ -81,13 +76,29 @@ static irqreturn_t fo_input_btn_handler(int irq, void *data) return IRQ_HANDLED; } +static void fo_input_setup_btn(struct input_dev *idev) +{ + input_set_capability(idev, EV_KEY, KEY_ENTER); /* D-pad center */ + input_set_capability(idev, EV_KEY, KEY_UP); /* D-pad up */ + input_set_capability(idev, EV_KEY, KEY_DOWN); /* D-pad down */ + input_set_capability(idev, EV_KEY, KEY_LEFT); /* D-pad left */ + input_set_capability(idev, EV_KEY, KEY_RIGHT); /* D-pad right */ + input_set_capability(idev, EV_KEY, KEY_TAB); /* App switcher */ + input_set_capability(idev, EV_KEY, KEY_BACKSPACE); /* Back */ + input_set_capability(idev, EV_KEY, KEY_A); /* PTT */ + input_set_capability(idev, EV_KEY, KEY_Z); /* Escape */ + input_set_capability(idev, EV_KEY, KEY_X); /* View */ + input_set_capability(idev, EV_KEY, KEY_C); /* Power */ + input_set_capability(idev, EV_KEY, KEY_V); /* Edit */ + input_set_capability(idev, EV_KEY, KEY_B); /* Run */ +} + 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 input_dev *idev = data; + struct regmap *regmap = input_get_drvdata(idev); struct device *parent = idev->dev.parent; - uint16_t buf[3]; + u16 buf[3]; int err; err = regmap_bulk_read(regmap, FOMCU_REG_INPUT_TOUCH_X, &buf, ARRAY_SIZE(buf)); @@ -106,11 +117,20 @@ static irqreturn_t fo_input_touch_handler(int irq, void *data) return IRQ_HANDLED; } +static void fo_input_setup_touch(struct input_dev *idev) +{ + input_set_capability(idev, EV_KEY, BTN_TOUCH); + input_set_capability(idev, EV_KEY, BTN_TOOL_FINGER); + input_set_abs_params(idev, ABS_X, 0, 1024, 0, 0); + input_set_abs_params(idev, ABS_Y, 0, 800, 0, 0); + input_set_abs_params(idev, ABS_PRESSURE, 0, 12288, 0, 0); + __set_bit(INPUT_PROP_POINTER, idev->propbit); +} + 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 input_dev *idev = data; + struct regmap *regmap = input_get_drvdata(idev); struct device *parent = idev->dev.parent; unsigned int reg; int err; @@ -132,118 +152,124 @@ static irqreturn_t fo_input_headset_handler(int irq, void *data) 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 void fo_input_setup_headset(struct input_dev *idev) +{ + input_set_capability(idev, EV_SW, SW_HEADPHONE_INSERT); + input_set_capability(idev, EV_SW, SW_MICROPHONE_INSERT); + input_set_capability(idev, EV_KEY, KEY_PLAYPAUSE); + input_set_capability(idev, EV_KEY, KEY_VOLUMEUP); + input_set_capability(idev, EV_KEY, KEY_VOLUMEDOWN); + input_set_capability(idev, EV_KEY, KEY_VOICECOMMAND); +} + +static irqreturn_t fo_input_swbtn_handler(int irq, void *data) +{ + struct input_dev *idev = data; + struct regmap *regmap = input_get_drvdata(idev); + struct device *parent = idev->dev.parent; + unsigned int reg; + int err; + + err = regmap_read(regmap, FOMCU_REG_INPUT_SWBTNS, ®); + if (err) { + dev_err(parent, "Failed to read SW button states: %d\n", err); + return IRQ_NONE; + } + + input_report_key(idev, KEY_POWER, FO_SWBTN_POWER & reg); + input_sync(idev); + + return IRQ_HANDLED; +} + +static void fo_input_setup_swbtn(struct input_dev *idev) +{ + input_set_capability(idev, EV_KEY, KEY_POWER); +} + +struct fo_input_config { + const char *name; + const char *phys; + const char *irq_name; + irqreturn_t (*handler)(int irq, void *data); + void (*setup)(struct input_dev *idev); +}; + +static const struct fo_input_config fo_input_configs[] = { + { + .name = "Flipper One Buttons", + .phys = "flipper-one-input/input0", + .irq_name = "flipper-one-input-btn", + .handler = fo_input_btn_handler, + .setup = fo_input_setup_btn, + }, + { + .name = "Flipper One Touchpad", + .phys = "flipper-one-input/input1", + .irq_name = "flipper-one-input-touch", + .handler = fo_input_touch_handler, + .setup = fo_input_setup_touch, + }, + { + .name = "Flipper One Headset", + .phys = "flipper-one-input/input2", + .irq_name = "flipper-one-input-headset", + .handler = fo_input_headset_handler, + .setup = fo_input_setup_headset, + }, + { + .name = "Flipper One Software Buttons", + .phys = "flipper-one-input/input3", + .irq_name = "flipper-one-input-swbtn", + .handler = fo_input_swbtn_handler, + .setup = fo_input_setup_swbtn, + }, }; 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; + err = devm_device_init_wakeup(dev); + if (err) + return dev_err_probe(dev, err, "Failed to init wakeup\n"); - idev_btn->name = "Flipper One Buttons"; - idev_btn->phys = "flipper-one-input/input0"; - idev_btn->id.bustype = BUS_I2C; + for (i = 0; i < ARRAY_SIZE(fo_input_configs); i++) { + const struct fo_input_config *cfg = &fo_input_configs[i]; + struct input_dev *idev; - 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 = devm_input_allocate_device(dev); + if (!idev) + return dev_err_probe(dev, -ENOMEM, + "Failed to allocate %s input device\n", + cfg->name); - idev_touch->name = "Flipper One Touchpad"; - idev_touch->phys = "flipper-one-input/input1"; - idev_touch->id.bustype = BUS_I2C; + idev->name = cfg->name; + idev->phys = cfg->phys; + idev->id.bustype = BUS_I2C; + input_set_drvdata(idev, fomcu->regmap); + cfg->setup(idev); - 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); + irq = platform_get_irq_byname(pdev, cfg->irq_name); if (irq < 0) return dev_err_probe(dev, irq, "Failed to get IRQ %s\n", - fo_irqs[i].name); + cfg->irq_name); - err = devm_request_threaded_irq(dev, irq, NULL, fo_irqs[i].handler, + err = devm_request_threaded_irq(dev, irq, NULL, cfg->handler, IRQF_ONESHOT | IRQF_NO_SUSPEND, - fo_irqs[i].name, input); + cfg->irq_name, idev); if (err) return dev_err_probe(dev, err, "Failed to request IRQ %s\n", - fo_irqs[i].name); - } + cfg->irq_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"); + err = input_register_device(idev); + if (err) + return dev_err_probe(dev, err, + "Failed to register %s input device\n", + cfg->name); + } return 0; } From b9934316a32db1671d167a799d263eb3cf59eeea Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 14 Jul 2026 17:45:02 +0400 Subject: [PATCH 194/202] phy: rockchip: naneng-combphy: Always configure SSC spread direction Commit 0b31f297557f ("phy: rockchip: naneng-combphy: Consolidate SSC configuration") moved the SSC spread spectrum direction setup into the new rk_combphy_common_cfg_ssc() helper. That helper returns early when the 'rockchip,enable-ssc' property is absent, whereas the equivalent RK3568_PHYREG32 direction writes previously ran unconditionally in the per-type switch statements, independent of whether SSC modulation was actually enabled. As no in-tree board sets 'rockchip,enable-ssc', this changed the behavior at least for USB3 on RK3576, which now fails to bring up the link. USB 2.0 still enumerates, but USB 3.0 does not, and the SuperSpeed root port floods the log every second with: usb usb2-port1: Cannot enable. Maybe the USB cable is bad? This was observed on two different RK3576 devices with a CoreChips SL6341 USB 2.0/3.0 hub connected to the USB DRD controller running in host mode. Perform the SSC direction writes for PCIe/USB3 (and SATA) before the enable_ssc check so that they always run, as they did before the consolidation. Cc: stable@vger.kernel.org Fixes: 0b31f297557f ("phy: rockchip: naneng-combphy: Consolidate SSC configuration") Signed-off-by: Alexey Charkov --- drivers/phy/rockchip/phy-rockchip-naneng-combphy.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c b/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c index 2b0f152f547091..7843356a4dd472 100644 --- a/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c +++ b/drivers/phy/rockchip/phy-rockchip-naneng-combphy.c @@ -452,9 +452,6 @@ static void rk_combphy_common_cfg_ssc(struct rockchip_combphy_priv *priv, unsign struct device_node *np = priv->dev->of_node; u32 val; - if (!priv->enable_ssc) - return; - /* Set SSC downward spread spectrum for PCIe and USB3 */ if (priv->type == PHY_TYPE_PCIE || priv->type == PHY_TYPE_USB3) { val = FIELD_PREP(RK3568_PHYREG32_SSC_MASK, RK3568_PHYREG32_SSC_DOWNWARD); @@ -471,6 +468,9 @@ static void rk_combphy_common_cfg_ssc(struct rockchip_combphy_priv *priv, unsign RK3568_PHYREG32); } + if (!priv->enable_ssc) + return; + /* Enable SSC */ val = readl(priv->mmio + RK3568_PHYREG8); val |= RK3568_PHYREG8_SSC_EN; From 878847a077079ed654009fb4fe5144ae8561213c Mon Sep 17 00:00:00 2001 From: Yury Smirnov Date: Fri, 10 Jul 2026 14:31:36 +0200 Subject: [PATCH 195/202] arm64: dts: rockchip: add "No Graphics" overlay for RK3576 boards Add a single DT overlay that disables the display pipeline so it can be powered down when booting to a headless "No Graphics" target: the VOP display controller and its IOMMU, the HDMI and DP interfaces and their PHYs, the HDMI/DP audio cards, and the rockchip-drm display-subsystem aggregator. The Mali GPU is deliberately left enabled: a headless system may still drive a directly-attached panel (e.g. the Flipper One SPI panel, its own drm/tiny device) whose userspace renders via GLES/EGL, which needs the GPU. The GPU idles into its own power domain via runtime PM when unused, so leaving it enabled costs practically nothing. All disabled nodes are SoC nodes defined in rk3576.dtsi, so one overlay applies to any RK3576 board; nodes a given board does not enable simply stay disabled. Overlay-application tests are provided for the EVB1, ArmSoM Sige5, NanoPi M5, Rock 4D and Flipper One boards. With these consumers disabled, genpd powers off the PD_VO0 and PD_VO1 domains. PD_VOP is the genpd parent of PD_USB/PD_VO0/PD_VO1, so it stays powered while USB/UFS are in use - a hardware power-tree constraint; the VOP controller itself is left unbound and idle. Panels driven directly over a peripheral bus rather than through the VOP (e.g. the Flipper One SPI panel) are unaffected and keep working. Signed-off-by: Yury Smirnov --- arch/arm64/boot/dts/rockchip/Makefile | 21 +++++++ .../boot/dts/rockchip/rk3576-no-graphics.dtso | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-no-graphics.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index 6f693aea54d4cc..270005f924996a 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -180,6 +180,7 @@ 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 dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-nanopi-r76s.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-no-graphics.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-roc-pc.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-rock-4d.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3582-radxa-e52c.dtb @@ -301,6 +302,10 @@ 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-no-graphics.dtb +rk3576-armsom-sige5-no-graphics-dtbs := rk3576-armsom-sige5.dtb \ + rk3576-no-graphics.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 @@ -309,6 +314,10 @@ 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-no-graphics.dtb +rk3576-evb1-v10-no-graphics-dtbs := rk3576-evb1-v10.dtb \ + rk3576-no-graphics.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtb rk3576-evb1-v10-pcie1-dtbs := rk3576-evb1-v10.dtb \ rk3576-evb1-v10-pcie1.dtbo @@ -321,6 +330,10 @@ 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-no-graphics.dtb +rk3576-flipper-one-no-graphics-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-no-graphics.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 @@ -329,6 +342,14 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-uart2.dtb rk3576-flipper-one-uart2-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ rk3576-flipper-one-uart2.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-nanopi-m5-no-graphics.dtb +rk3576-nanopi-m5-no-graphics-dtbs := rk3576-nanopi-m5.dtb \ + rk3576-no-graphics.dtbo + +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-rock-4d-no-graphics.dtb +rk3576-rock-4d-no-graphics-dtbs := rk3576-rock-4d.dtb \ + rk3576-no-graphics.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-no-graphics.dtso b/arch/arm64/boot/dts/rockchip/rk3576-no-graphics.dtso new file mode 100644 index 00000000000000..47fa0fe8785987 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-no-graphics.dtso @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay for a "No Graphics" (headless) target on RK3576 boards: disables + * the display pipeline so it can be powered down - the VOP display controller + * and its IOMMU, the HDMI and DP interfaces and their PHYs, the HDMI/DP audio + * cards, and the rockchip-drm display-subsystem aggregator. + * + * The Mali GPU is deliberately left enabled: a headless system may still drive + * a directly-attached panel (e.g. the Flipper One SPI panel, its own drm/tiny + * device) whose userspace renders via GLES/EGL, which needs the GPU. The GPU + * also idles into its own power domain via runtime PM when unused, so leaving + * it enabled costs practically nothing. + * + * All disabled nodes are SoC nodes defined in rk3576.dtsi, so this single + * overlay applies to any RK3576 board. Nodes a given board does not enable stay + * disabled - a harmless no-op that also keeps the overlay robust against + * stacked overlays. + * + * With these consumers disabled, genpd powers off the PD_VO0 and PD_VO1 power + * domains. Note that PD_VOP is the genpd parent of PD_USB/PD_VO0/PD_VO1, so it + * stays powered as long as USB/UFS (children of PD_USB) are in use - a hardware + * power-tree constraint. The VOP controller itself is left unbound and idle. + * + * Panels driven directly over a peripheral bus rather than through the VOP + * (e.g. the Flipper One SPI panel) are unaffected and keep working. + */ + +/dts-v1/; +/plugin/; + +&display_subsystem { + status = "disabled"; +}; + +&vop { + status = "disabled"; +}; + +&vop_mmu { + status = "disabled"; +}; + +&hdmi { + status = "disabled"; +}; + +&hdptxphy { + status = "disabled"; +}; + +&hdmi_sound { + status = "disabled"; +}; + +&dp { + status = "disabled"; +}; + +&dp0_sound { + status = "disabled"; +}; From 907d363ba0c9eede8b8e877f66ae6e95a60f0c24 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 16 Jul 2026 17:10:14 +0400 Subject: [PATCH 196/202] kbuild: deb-pkg: install kernel image and friends under /lib/modules The deb-pkg target installs the kernel image, System.map and config only under /boot, unlike rpm-pkg which also places them under /lib/modules/${KERNELRELEASE} (i.e. /usr/lib/modules on a usr-merged system). The latter layout keeps these files available to kernel-install even when the /boot filesystem is not distributed together with /usr. Additionally install the image (as vmlinuz), System.map and config under /lib/modules/${KERNELRELEASE} for non-UML architectures, mirroring the layout produced by scripts/package/kernel.spec. Signed-off-by: Alexey Charkov --- scripts/package/builddeb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/package/builddeb b/scripts/package/builddeb index ba1defc616524f..7609bc0e10824e 100755 --- a/scripts/package/builddeb +++ b/scripts/package/builddeb @@ -65,7 +65,17 @@ install_linux_image () { esac cp "$(${MAKE} -s -f ${srctree}/Makefile image_name)" "${pdir}/${installed_image_path}" + # Additionally install the image, System.map and config under + # /lib/modules/${KERNELRELEASE} (i.e. /usr/lib/modules on a usr-merged + # system) so that they remain available to kernel-install even when the + # /boot filesystem is not shipped alongside /usr. This mirrors the + # layout produced by the rpm-pkg target (scripts/package/kernel.spec). if [ "${ARCH}" != um ]; then + mkdir -p "${pdir}/lib/modules/${KERNELRELEASE}" + cp "$(${MAKE} -s -f ${srctree}/Makefile image_name)" "${pdir}/lib/modules/${KERNELRELEASE}/vmlinuz" + cp System.map "${pdir}/lib/modules/${KERNELRELEASE}/System.map" + cp ${KCONFIG_CONFIG} "${pdir}/lib/modules/${KERNELRELEASE}/config" + install_maint_scripts "${pdir}" fi } From 0fa34d165330b14b30a82770a57f9761fb01f6ec Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 20 Jul 2026 16:33:33 +0400 Subject: [PATCH 197/202] arm64: dts: rockchip: Increase audio regulator ramp delay for Flipper One Some of our boards seem to take more than 500us to ramp up the audio regulator, which causes the audio codec to fail to initialize. Increase the enable ramp delay to 1000us to avoid this issue. This shouldn't be noticeable to the user anyway. Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi index b47b4b682594dd..73dba78035c9b2 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi @@ -793,7 +793,7 @@ }; vcca_3v3_s0: pldo-reg4 { - regulator-enable-ramp-delay = <500>; + regulator-enable-ramp-delay = <1000>; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-ramp-delay = <12500>; From e846cb3c3fb8d6e75ce1fc46753a0393ea01714d Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Mon, 27 Jul 2026 20:26:27 +0000 Subject: [PATCH 198/202] mfd: flipper-one-mcu: register the SW button interrupt Signed-off-by: Cole Munz --- drivers/mfd/flipper-one-mcu.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/mfd/flipper-one-mcu.c b/drivers/mfd/flipper-one-mcu.c index 38111d89159631..2567e5de094176 100644 --- a/drivers/mfd/flipper-one-mcu.c +++ b/drivers/mfd/flipper-one-mcu.c @@ -74,6 +74,7 @@ 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(INPUT, SWBTN), FOMCU_IRQ_REG(UCSI, EVENT), }; From 2e8b959108bd1aaeb8afa860ccf8a5e80ad8cb22 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 28 Jul 2026 18:02:26 +0400 Subject: [PATCH 199/202] arm64: dts: rockchip: rk3576-flipper-one: add UART6 overlay Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/Makefile | 5 +++++ .../boot/dts/rockchip/rk3576-flipper-one-uart6.dtso | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart6.dtso diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index 270005f924996a..f334196fe00356 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -176,6 +176,7 @@ 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-flipper-one-uart2.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-uart6.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 @@ -342,6 +343,10 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-uart2.dtb rk3576-flipper-one-uart2-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ rk3576-flipper-one-uart2.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-uart6.dtb +rk3576-flipper-one-uart6-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-uart6.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-nanopi-m5-no-graphics.dtb rk3576-nanopi-m5-no-graphics-dtbs := rk3576-nanopi-m5.dtb \ rk3576-no-graphics.dtbo diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart6.dtso b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart6.dtso new file mode 100644 index 00000000000000..0103b81fd3ca62 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-uart6.dtso @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: (GPL-2.0+ OR MIT) +/* + * DT-overlay to enable UART6 on the GPIO header on Flipper One + */ + +/dts-v1/; +/plugin/; + +&uart6 { + pinctrl-names = "default"; + pinctrl-0 = <&uart6m0_xfer>; + status = "okay"; +}; From ffbee6b34b54d66b274684bf6992cb7141450e83 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Tue, 28 Jul 2026 11:12:52 -0500 Subject: [PATCH 200/202] arm64: dts: rockchip: rk3576: add cache hierarchy information to CPU nodes The RK3576 CPU nodes carry no cache properties, so cache_setup_of_node() in the generic cacheinfo core fails with -ENOENT on the first CPU. That error propagates out of cache_shared_cpu_map_setup(), which discards the topology arm64 had already derived from CLIDR and prints "cacheinfo: Unable to detect cache hierarchy for CPU 0" on every boot. Add L1 i/d cache size, line-size and sets to all eight CPU nodes, plus per-cluster unified L2 nodes wired up through next-level-cache. Sizes come from the RK3576 datasheet (A72 cluster: 48KB/32KB L1 I/D, 1MB L2; A53 cluster: 32KB/32KB L1 I/D, 512KB L2). Line size and associativity are architecturally fixed per the Cortex-A53 and Cortex-A72 TRMs, and the *-sets values follow as size / (line-size * ways). Same shape as the rk3399 fix that landed upstream in rk3399-base.dtsi. Mainline rk3576.dtsi has the identical gap. Signed-off-by: Cole Munz --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index e5c6aa37dd4ce8..0490a879cce471 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -117,6 +117,13 @@ dynamic-power-coefficient = <120>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0x8000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <128>; + next-level-cache = <&l2_cache_l>; }; cpu_l1: cpu@1 { @@ -129,6 +136,13 @@ operating-points-v2 = <&cluster0_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0x8000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <128>; + next-level-cache = <&l2_cache_l>; }; cpu_l2: cpu@2 { @@ -141,6 +155,13 @@ operating-points-v2 = <&cluster0_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0x8000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <128>; + next-level-cache = <&l2_cache_l>; }; cpu_l3: cpu@3 { @@ -153,6 +174,13 @@ operating-points-v2 = <&cluster0_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0x8000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <128>; + next-level-cache = <&l2_cache_l>; }; cpu_b0: cpu@100 { @@ -166,6 +194,13 @@ dynamic-power-coefficient = <320>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0xc000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <256>; + next-level-cache = <&l2_cache_b>; }; cpu_b1: cpu@101 { @@ -178,6 +213,13 @@ operating-points-v2 = <&cluster1_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0xc000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <256>; + next-level-cache = <&l2_cache_b>; }; cpu_b2: cpu@102 { @@ -190,6 +232,13 @@ operating-points-v2 = <&cluster1_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0xc000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <256>; + next-level-cache = <&l2_cache_b>; }; cpu_b3: cpu@103 { @@ -202,6 +251,31 @@ operating-points-v2 = <&cluster1_opp_table>; cpu-idle-states = <&CPU_SLEEP>; #cooling-cells = <2>; + i-cache-size = <0xc000>; + i-cache-line-size = <64>; + i-cache-sets = <256>; + d-cache-size = <0x8000>; + d-cache-line-size = <64>; + d-cache-sets = <256>; + next-level-cache = <&l2_cache_b>; + }; + + l2_cache_l: l2-cache-cluster0 { + compatible = "cache"; + cache-level = <2>; + cache-unified; + cache-size = <0x80000>; + cache-line-size = <64>; + cache-sets = <512>; + }; + + l2_cache_b: l2-cache-cluster1 { + compatible = "cache"; + cache-level = <2>; + cache-unified; + cache-size = <0x100000>; + cache-line-size = <64>; + cache-sets = <1024>; }; idle-states { From 22ef53f702faff6e428f959a4d85405ee86fc925 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 30 Jul 2026 11:36:02 +0400 Subject: [PATCH 201/202] fixup! WIP: mfd: Add support for Flipper One's MCU --- include/linux/mfd/flipper-one-mcu.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/linux/mfd/flipper-one-mcu.h b/include/linux/mfd/flipper-one-mcu.h index a44cbb55adeee8..d50defed48a775 100644 --- a/include/linux/mfd/flipper-one-mcu.h +++ b/include/linux/mfd/flipper-one-mcu.h @@ -83,8 +83,8 @@ enum fomcu_cpu_states { #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_REG_UCSI_MESSAGE_IN 0x0510 +#define FOMCU_REG_UCSI_MESSAGE_OUT 0x0610 #define FOMCU_UCSI_MESSAGE_LEN 256 #define FOMCU_REG_MAX (FOMCU_REG_UCSI_MESSAGE_OUT + \ From cf3748b323c3899cf19ba541c4469984d0fbf1a2 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Sat, 1 Aug 2026 01:38:16 -0500 Subject: [PATCH 202/202] usb: dwc3: gadget: don't error on dequeue of a completed request Dequeuing a request that has already been given back logs an error and returns -EINVAL: dwc3 23000000.usb: request 00000000ad92f1c4 was not queued to ep0out f_fs hits this on every teardown. functionfs_unbind() dequeues ep0req unconditionally before freeing it, which commit ce405d561b02 ("usb: gadget: f_fs: Ensure ep0req is dequeued before free_request") made deliberate to close a use-after-free. By then the control transfer has long completed, so dwc3_gadget_ep_dequeue() finds the request on none of cancelled_list, pending_list or started_list and falls through to the error path. Nothing is actually wrong. The request is not queued, which is what the caller asked for, and both callers ignore the return value and free the request straight after. The only effect is an error line in every gadget teardown, which buries real USB errors. dwc3 already tracks enough to tell the two cases apart. dwc3_gadget_ep_alloc_request() sets DWC3_REQUEST_STATUS_UNKNOWN, both __dwc3_gadget_ep_queue() and __dwc3_gadget_ep0_queue() set DWC3_REQUEST_STATUS_QUEUED, and dwc3_gadget_giveback() sets DWC3_REQUEST_STATUS_COMPLETED. A request that reaches the end of dequeue with status COMPLETED was queued to this endpoint and has finished. Anything else was never queued here, or the driver lost track of it. Keep the error for those, and return success for a completed request. This is narrower than the cdnsp fix for the same caller, commit 34f08eb0ba6e ("usb: cdnsp: Fixes issue with dequeuing not queued requests"), which returns 0 whenever usb_request::status is not -EINPROGRESS. That also swallows a request that was never queued, since status is zero out of allocation. Going by dwc3's own request status keeps that case an error, which is what was asked for when a separate ep0 dequeue was proposed in 2022. Link: https://lore.kernel.org/linux-usb/20221117054917.30104-1-quic_ugoswami@quicinc.com/ Signed-off-by: Cole Munz --- drivers/usb/dwc3/gadget.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/drivers/usb/dwc3/gadget.c b/drivers/usb/dwc3/gadget.c index 1082e9c9afaa89..10861269a26e77 100644 --- a/drivers/usb/dwc3/gadget.c +++ b/drivers/usb/dwc3/gadget.c @@ -2181,9 +2181,20 @@ static int dwc3_gadget_ep_dequeue(struct usb_ep *ep, } } - dev_err(dwc->dev, "request %p was not queued to %s\n", - request, ep->name); - ret = -EINVAL; + /* + * The request is on none of this endpoint's lists. That is the + * expected state once it has been given back: a function may dequeue + * a request before freeing it, and f_fs does so unconditionally for + * ep0req in functionfs_unbind(). Nothing is queued, which is what the + * caller asked for, so report success. Any other status means the + * request was never queued here or the driver lost track of it, and + * stays an error. + */ + if (req->status != DWC3_REQUEST_STATUS_COMPLETED) { + dev_err(dwc->dev, "request %p was not queued to %s\n", + request, ep->name); + ret = -EINVAL; + } out: spin_unlock_irqrestore(&dwc->lock, flags);