From cf0eaa6bac9e78b8236e19fa428e2705939fdbce Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Mar 2026 02:55:07 +0200 Subject: [PATCH 001/258] [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 892cfe1f4a90583fcb78c2628ecea00ee50366cc Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 1 Jun 2026 19:13:45 +0300 Subject: [PATCH 002/258] [MERGED] drm/rockchip: dw_dp: Add missing newline in dev_err_probe() message Add the missing trailing newline to dev_err_probe() call in dw_dp_rockchip_bind(). Fixes: d68ba7bac955 ("drm/rockchip: Add RK3588 DPTX output support") Fixes: 26cb3e26efa7 ("drm/rockchip: dw_dp: Simplify error handling") Signed-off-by: Cristian Ciocaltea Signed-off-by: Heiko Stuebner Link: https://patch.msgid.link/20260601-drm-rk-fixes-v4-2-c3f3f123e1da@collabora.com --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 32bc73a1d5e456..f137f699737cd2 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -109,7 +109,7 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * connector = drm_bridge_connector_init(drm_dev, encoder); if (IS_ERR(connector)) return dev_err_probe(dev, PTR_ERR(connector), - "Failed to init bridge connector"); + "Failed to init bridge connector\n"); return 0; } From 4f6decc5b294a7a0eb421cf13ab9f948403885f4 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 1 Jun 2026 19:13:46 +0300 Subject: [PATCH 003/258] [MERGED] drm/rockchip: dw_dp: Release core resources Core resources such as the DisplayPort AUX channel get initialized and registered during dw_dp_bind(), but are never unregistered, which may lead to memory leaks and/or use-after-free: [ 224.661371] BUG: KASAN: slab-use-after-free in device_is_dependent+0xe0/0x2b0 [ 224.662015] Read of size 8 at addr ffff00011aee8550 by task modprobe/658 [ 224.662612] [ 224.662752] CPU: 7 UID: 0 PID: 658 Comm: modprobe Not tainted 7.0.0-rc2-next-20260305 #14 PREEMPT [ 224.662759] Hardware name: Radxa ROCK 5B (DT) [ 224.662762] Call trace: [ 224.662764] show_stack+0x20/0x38 (C) [ 224.662772] dump_stack_lvl+0x6c/0x98 [ 224.662777] print_report+0x160/0x4b8 [ 224.662783] kasan_report+0xb4/0xe0 [ 224.662790] __asan_report_load8_noabort+0x20/0x30 [ 224.662796] device_is_dependent+0xe0/0x2b0 [ 224.662802] device_is_dependent+0x108/0x2b0 [ 224.662808] device_link_add+0x1f8/0x10b0 [ 224.662813] devm_of_phy_get_by_index+0x120/0x200 [ 224.662819] dw_dp_bind+0x34c/0xb10 [dw_dp] [ 224.662830] dw_dp_rockchip_bind+0x194/0x250 [rockchipdrm] [ 224.662864] component_bind_all+0x3a8/0x720 [ 224.662869] rockchip_drm_bind+0x120/0x390 [rockchipdrm] [ 224.662899] try_to_bring_up_aggregate_device+0x76c/0x838 [ 224.662904] component_master_add_with_match+0x1f4/0x230 [ 224.662909] rockchip_drm_platform_probe+0x420/0x538 [rockchipdrm] [ 224.662939] platform_probe+0xe8/0x168 [ 224.662945] really_probe+0x340/0x828 [ 224.662950] __driver_probe_device+0x2e0/0x350 [ 224.662954] driver_probe_device+0x80/0x140 [ 224.662959] __driver_attach+0x398/0x460 [ 224.662964] bus_for_each_dev+0xe0/0x198 [ 224.662968] driver_attach+0x50/0x68 [ 224.662972] bus_add_driver+0x2a0/0x4c0 [ 224.662977] driver_register+0x294/0x360 [ 224.662982] __platform_driver_register+0x7c/0x98 [ 224.662987] rockchip_drm_init+0xc4/0xff8 [rockchipdrm] Since a previous commit exported dw_dp_unbind() function in DW DP core library to take care of the necessary cleanup, use this in the component's unbind() callback, as well as in its bind() error path. Fixes: d68ba7bac955 ("drm/rockchip: Add RK3588 DPTX output support") Signed-off-by: Cristian Ciocaltea Signed-off-by: Heiko Stuebner Link: https://patch.msgid.link/20260601-drm-rk-fixes-v4-3-c3f3f123e1da@collabora.com --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index f137f699737cd2..0de822360c8db9 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -107,15 +107,26 @@ static int dw_dp_rockchip_bind(struct device *dev, struct device *master, void * return PTR_ERR(dp->base); connector = drm_bridge_connector_init(drm_dev, encoder); - if (IS_ERR(connector)) + if (IS_ERR(connector)) { + dw_dp_unbind(dp->base); return dev_err_probe(dev, PTR_ERR(connector), "Failed to init bridge connector\n"); + } return 0; } +static void dw_dp_rockchip_unbind(struct device *dev, struct device *master, + void *data) +{ + struct rockchip_dw_dp *dp = dev_get_drvdata(dev); + + dw_dp_unbind(dp->base); +} + static const struct component_ops dw_dp_rockchip_component_ops = { .bind = dw_dp_rockchip_bind, + .unbind = dw_dp_rockchip_unbind, }; static int dw_dp_probe(struct platform_device *pdev) From 621d59ae4ab97ee18d45ed1154e692ec9300ffc8 Mon Sep 17 00:00:00 2001 From: Diogo Silva Date: Sat, 4 Jul 2026 11:12:02 +0200 Subject: [PATCH 004/258] [MERGED] drm/rockchip: Remove dependency on DRM simple helpers Simple KMS helper are deprecated since they only add an intermediate layer between drivers and the atomic modesetting. This patch removes the drm_simple_encoder_init() helper usage in the rockchip drivers by open coding it and using the encoder atomic helpers directly. This is a step to eventually get rid of this simple KMS helper, once all drivers that use it have been converted. Reviewed-by: Javier Martinez Canillas Signed-off-by: Diogo Silva Signed-off-by: Heiko Stuebner Link: https://patch.msgid.link/20260704-rockchip-drm-simple-v5-1-a333f527a4f9@gmail.com --- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 0de822360c8db9..b23efb153c9e66 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -20,7 +20,6 @@ #include #include #include -#include #include "rockchip_drm_drv.h" From 2bd2730d15f8064fc8ce00ae2f5a67fcabf86a93 Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:08 +0200 Subject: [PATCH 005/258] [MERGED] drm/atomic-state-helper: Rename __drm_atomic_helper_bridge_reset() __drm_atomic_helper_bridge_reset() is used to initialize a newly allocated drm_bridge_state, and is being typically called by the drm_bridge_funcs.atomic_reset implementation. Since we want to consolidate DRM objects state allocation around the atomic_create_state callback that will only allocate and initialize a new drm_bridge_state instance, we will need to call __drm_atomic_helper_bridge_reset() from both the atomic_reset and atomic_create_state hooks. To avoid any confusion, we can thus rename __drm_atomic_helper_bridge_reset() to __drm_atomic_helper_bridge_state_init(). Reviewed-by: Thomas Zimmermann Reviewed-by: Laurent Pinchart Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-3-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c | 2 +- drivers/gpu/drm/drm_atomic_state_helper.c | 8 ++++---- include/drm/drm_atomic_state_helper.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c index 36c07b71fe04bf..4e3015d10a977d 100644 --- a/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c +++ b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c @@ -1929,7 +1929,7 @@ cdns_mhdp_bridge_atomic_reset(struct drm_bridge *bridge) if (!cdns_mhdp_state) return NULL; - __drm_atomic_helper_bridge_reset(bridge, &cdns_mhdp_state->base); + __drm_atomic_helper_bridge_state_init(bridge, &cdns_mhdp_state->base); return &cdns_mhdp_state->base; } diff --git a/drivers/gpu/drm/drm_atomic_state_helper.c b/drivers/gpu/drm/drm_atomic_state_helper.c index cc70508d4fdba1..f79d259fe5506c 100644 --- a/drivers/gpu/drm/drm_atomic_state_helper.c +++ b/drivers/gpu/drm/drm_atomic_state_helper.c @@ -812,7 +812,7 @@ void drm_atomic_helper_bridge_destroy_state(struct drm_bridge *bridge, EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); /** - * __drm_atomic_helper_bridge_reset() - Initialize a bridge state to its + * __drm_atomic_helper_bridge_state_init() - Initialize a bridge state to its * default * @bridge: the bridge this state refers to * @state: bridge state to initialize @@ -821,14 +821,14 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); * by the bridge &drm_bridge_funcs.atomic_reset hook for bridges that subclass * the bridge state. */ -void __drm_atomic_helper_bridge_reset(struct drm_bridge *bridge, +void __drm_atomic_helper_bridge_state_init(struct drm_bridge *bridge, struct drm_bridge_state *state) { memset(state, 0, sizeof(*state)); __drm_atomic_helper_private_obj_create_state(&bridge->base, &state->base); state->bridge = bridge; } -EXPORT_SYMBOL(__drm_atomic_helper_bridge_reset); +EXPORT_SYMBOL(__drm_atomic_helper_bridge_state_init); /** * drm_atomic_helper_bridge_reset() - Allocate and initialize a bridge state @@ -848,7 +848,7 @@ drm_atomic_helper_bridge_reset(struct drm_bridge *bridge) if (!bridge_state) return ERR_PTR(-ENOMEM); - __drm_atomic_helper_bridge_reset(bridge, bridge_state); + __drm_atomic_helper_bridge_state_init(bridge, bridge_state); return bridge_state; } EXPORT_SYMBOL(drm_atomic_helper_bridge_reset); diff --git a/include/drm/drm_atomic_state_helper.h b/include/drm/drm_atomic_state_helper.h index 61a3b38ad49fd1..1e8f43814b6cef 100644 --- a/include/drm/drm_atomic_state_helper.h +++ b/include/drm/drm_atomic_state_helper.h @@ -96,7 +96,7 @@ struct drm_bridge_state * drm_atomic_helper_bridge_duplicate_state(struct drm_bridge *bridge); void drm_atomic_helper_bridge_destroy_state(struct drm_bridge *bridge, struct drm_bridge_state *state); -void __drm_atomic_helper_bridge_reset(struct drm_bridge *bridge, +void __drm_atomic_helper_bridge_state_init(struct drm_bridge *bridge, struct drm_bridge_state *state); struct drm_bridge_state * drm_atomic_helper_bridge_reset(struct drm_bridge *bridge); From e62c87ee5081e5489e035ff408a5aa7f623d664f Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:09 +0200 Subject: [PATCH 006/258] [MERGED] drm/atomic-state-helper: Reorder __drm_atomic_helper_bridge_state_init() arguments The convention for state init helpers is to pass the state pointer as the first argument and the object pointer second. __drm_atomic_helper_bridge_state_init() has them in the opposite order. Swap the arguments to follow the convention, and update the cdns-mhdp8546 caller. Reviewed-by: Thomas Zimmermann Reviewed-by: Laurent Pinchart Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-4-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c | 2 +- drivers/gpu/drm/drm_atomic_state_helper.c | 8 ++++---- include/drm/drm_atomic_state_helper.h | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c index 4e3015d10a977d..063f073034c150 100644 --- a/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c +++ b/drivers/gpu/drm/bridge/cadence/cdns-mhdp8546-core.c @@ -1929,7 +1929,7 @@ cdns_mhdp_bridge_atomic_reset(struct drm_bridge *bridge) if (!cdns_mhdp_state) return NULL; - __drm_atomic_helper_bridge_state_init(bridge, &cdns_mhdp_state->base); + __drm_atomic_helper_bridge_state_init(&cdns_mhdp_state->base, bridge); return &cdns_mhdp_state->base; } diff --git a/drivers/gpu/drm/drm_atomic_state_helper.c b/drivers/gpu/drm/drm_atomic_state_helper.c index f79d259fe5506c..17fe7b50916102 100644 --- a/drivers/gpu/drm/drm_atomic_state_helper.c +++ b/drivers/gpu/drm/drm_atomic_state_helper.c @@ -814,15 +814,15 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); /** * __drm_atomic_helper_bridge_state_init() - Initialize a bridge state to its * default - * @bridge: the bridge this state refers to * @state: bridge state to initialize + * @bridge: the bridge this state refers to * * Initializes the bridge state to default values. This is meant to be called * by the bridge &drm_bridge_funcs.atomic_reset hook for bridges that subclass * the bridge state. */ -void __drm_atomic_helper_bridge_state_init(struct drm_bridge *bridge, - struct drm_bridge_state *state) +void __drm_atomic_helper_bridge_state_init(struct drm_bridge_state *state, + struct drm_bridge *bridge) { memset(state, 0, sizeof(*state)); __drm_atomic_helper_private_obj_create_state(&bridge->base, &state->base); @@ -848,7 +848,7 @@ drm_atomic_helper_bridge_reset(struct drm_bridge *bridge) if (!bridge_state) return ERR_PTR(-ENOMEM); - __drm_atomic_helper_bridge_state_init(bridge, bridge_state); + __drm_atomic_helper_bridge_state_init(bridge_state, bridge); return bridge_state; } EXPORT_SYMBOL(drm_atomic_helper_bridge_reset); diff --git a/include/drm/drm_atomic_state_helper.h b/include/drm/drm_atomic_state_helper.h index 1e8f43814b6cef..d30bc18ebbee49 100644 --- a/include/drm/drm_atomic_state_helper.h +++ b/include/drm/drm_atomic_state_helper.h @@ -96,7 +96,7 @@ struct drm_bridge_state * drm_atomic_helper_bridge_duplicate_state(struct drm_bridge *bridge); void drm_atomic_helper_bridge_destroy_state(struct drm_bridge *bridge, struct drm_bridge_state *state); -void __drm_atomic_helper_bridge_state_init(struct drm_bridge *bridge, - struct drm_bridge_state *state); +void __drm_atomic_helper_bridge_state_init(struct drm_bridge_state *state, + struct drm_bridge *bridge); struct drm_bridge_state * drm_atomic_helper_bridge_reset(struct drm_bridge *bridge); From 60aef9490df07253d243bb56c151b233de531b80 Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:10 +0200 Subject: [PATCH 007/258] [MERGED] drm/atomic-state-helper: Drop memset from __drm_atomic_helper_bridge_state_init() __drm_atomic_helper_bridge_state_init() is always called on a freshly kzalloc-ed state, so the memset is redundant. Drop it and document the expectation that the state is already zeroed. Reviewed-by: Thomas Zimmermann Reviewed-by: Luca Ceresoli Reviewed-by: Laurent Pinchart Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-5-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/drm_atomic_state_helper.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/drm_atomic_state_helper.c b/drivers/gpu/drm/drm_atomic_state_helper.c index 17fe7b50916102..2a694698aca150 100644 --- a/drivers/gpu/drm/drm_atomic_state_helper.c +++ b/drivers/gpu/drm/drm_atomic_state_helper.c @@ -817,6 +817,8 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); * @state: bridge state to initialize * @bridge: the bridge this state refers to * + * @state is assumed to be zeroed. + * * Initializes the bridge state to default values. This is meant to be called * by the bridge &drm_bridge_funcs.atomic_reset hook for bridges that subclass * the bridge state. @@ -824,7 +826,6 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); void __drm_atomic_helper_bridge_state_init(struct drm_bridge_state *state, struct drm_bridge *bridge) { - memset(state, 0, sizeof(*state)); __drm_atomic_helper_private_obj_create_state(&bridge->base, &state->base); state->bridge = bridge; } From d68df7549394456f1aa11912f871e75e39bfb539 Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:11 +0200 Subject: [PATCH 008/258] [MERGED] drm/bridge: Add new atomic_create_state callback Commit 47b5ac7daa46 ("drm/atomic: Add new atomic_create_state callback to drm_private_obj") introduced a new pattern for allocating drm object states: atomic_create_state, a dedicated hook that allocates and initializes a pristine state without any side effect. The bridge atomic_reset callback is already fallible and in practice only allocates and initializes state without touching hardware. However, the reset name does not make this contract clear: callers and implementers cannot tell from the name alone whether the hardware will be affected or when the hook is safe to call. Add an atomic_create_state callback to drm_bridge_funcs to make the contract explicit: allocate a pristine state, initialize it, no side effects. The core calls it when available, falling back to atomic_reset otherwise. Reviewed-by: Thomas Zimmermann Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-6-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/drm_atomic_state_helper.c | 5 +++-- drivers/gpu/drm/drm_bridge.c | 8 ++++++-- include/drm/drm_bridge.h | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/drm_atomic_state_helper.c b/drivers/gpu/drm/drm_atomic_state_helper.c index 2a694698aca150..414acb6b4d2474 100644 --- a/drivers/gpu/drm/drm_atomic_state_helper.c +++ b/drivers/gpu/drm/drm_atomic_state_helper.c @@ -820,8 +820,9 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_destroy_state); * @state is assumed to be zeroed. * * Initializes the bridge state to default values. This is meant to be called - * by the bridge &drm_bridge_funcs.atomic_reset hook for bridges that subclass - * the bridge state. + * by the bridge &drm_bridge_funcs.atomic_create_state or + * &drm_bridge_funcs.atomic_reset hook for bridges that subclass the bridge + * state. */ void __drm_atomic_helper_bridge_state_init(struct drm_bridge_state *state, struct drm_bridge *bridge) diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index 687b36eea0c703..ef06c1aa509adf 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -500,7 +500,10 @@ drm_bridge_atomic_create_priv_state(struct drm_private_obj *obj) struct drm_bridge *bridge = drm_priv_to_bridge(obj); struct drm_bridge_state *state; - state = bridge->funcs->atomic_reset(bridge); + if (bridge->funcs->atomic_create_state) + state = bridge->funcs->atomic_create_state(bridge); + else + state = bridge->funcs->atomic_reset(bridge); if (IS_ERR(state)) return ERR_CAST(state); @@ -515,7 +518,8 @@ static const struct drm_private_state_funcs drm_bridge_priv_state_funcs = { static bool drm_bridge_is_atomic(struct drm_bridge *bridge) { - return bridge->funcs->atomic_reset != NULL; + return (bridge->funcs->atomic_create_state || + bridge->funcs->atomic_reset); } /** diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index 4ba3a5deef9a60..c0703af611bae5 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -530,6 +530,22 @@ struct drm_bridge_funcs { */ struct drm_bridge_state *(*atomic_reset)(struct drm_bridge *bridge); + /** + * @atomic_create_state: + * + * Allocate a pristine, initialized, state for the bridge + * object and return it. This callback must have no side + * effects: in particular, the returned state must not be + * assigned to the object's state pointer and it must not affect + * the hardware state. + * + * RETURNS: + * + * A new, pristine, bridge state instance or an error pointer + * on failure. + */ + struct drm_bridge_state *(*atomic_create_state)(struct drm_bridge *bridge); + /** * @detect: * @@ -1371,7 +1387,8 @@ drm_bridge_get_current_state(struct drm_bridge *bridge) * drm_atomic_private_obj_init(), so we need to make sure we're * working with one before we try to use the lock. */ - if (!bridge->funcs || !bridge->funcs->atomic_reset) + if (!bridge->funcs || + !(bridge->funcs->atomic_reset || bridge->funcs->atomic_create_state)) return NULL; drm_modeset_lock_assert_held(&bridge->base.lock); From 96068f5f75317761c77b1e8d285b9610a821fe54 Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:12 +0200 Subject: [PATCH 009/258] [MERGED] drm/atomic-state-helper: Add drm_atomic_helper_bridge_create_state() The drm_atomic_helper_bridge_reset() helper is deprecated in favour of the new atomic_create_state callback. Add drm_atomic_helper_bridge_create_state() as the counterpart helper for this new callback, and make drm_atomic_helper_bridge_reset() call this new helper. Reviewed-by: Thomas Zimmermann Reviewed-by: Laurent Pinchart Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-7-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/drm_atomic_state_helper.c | 22 +++++++++++++++++++++- include/drm/drm_atomic_state_helper.h | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/drm_atomic_state_helper.c b/drivers/gpu/drm/drm_atomic_state_helper.c index 414acb6b4d2474..ee24d5dbec4e22 100644 --- a/drivers/gpu/drm/drm_atomic_state_helper.c +++ b/drivers/gpu/drm/drm_atomic_state_helper.c @@ -799,6 +799,7 @@ EXPORT_SYMBOL(drm_atomic_helper_bridge_duplicate_state); * @state: bridge state to destroy * * Destroys a bridge state previously created by + * &drm_atomic_helper_bridge_create_state(), * &drm_atomic_helper_bridge_reset() or * &drm_atomic_helper_bridge_duplicate_state(). This helper is meant to be * used as a bridge &drm_bridge_funcs.atomic_destroy_state hook for bridges @@ -843,6 +844,25 @@ EXPORT_SYMBOL(__drm_atomic_helper_bridge_state_init); */ struct drm_bridge_state * drm_atomic_helper_bridge_reset(struct drm_bridge *bridge) +{ + return drm_atomic_helper_bridge_create_state(bridge); +} +EXPORT_SYMBOL(drm_atomic_helper_bridge_reset); + +/** + * drm_atomic_helper_bridge_create_state - default + * &drm_bridge_funcs.atomic_create_state hook for bridges + * @bridge: bridge object + * + * Allocates and initializes pristine @drm_bridge_state. + * + * This is useful for drivers that don't subclass @drm_bridge_state. + * + * RETURNS: + * Pointer to new bridge state, or ERR_PTR on failure. + */ +struct drm_bridge_state * +drm_atomic_helper_bridge_create_state(struct drm_bridge *bridge) { struct drm_bridge_state *bridge_state; @@ -853,4 +873,4 @@ drm_atomic_helper_bridge_reset(struct drm_bridge *bridge) __drm_atomic_helper_bridge_state_init(bridge_state, bridge); return bridge_state; } -EXPORT_SYMBOL(drm_atomic_helper_bridge_reset); +EXPORT_SYMBOL(drm_atomic_helper_bridge_create_state); diff --git a/include/drm/drm_atomic_state_helper.h b/include/drm/drm_atomic_state_helper.h index d30bc18ebbee49..5fd744182b1486 100644 --- a/include/drm/drm_atomic_state_helper.h +++ b/include/drm/drm_atomic_state_helper.h @@ -99,4 +99,6 @@ void drm_atomic_helper_bridge_destroy_state(struct drm_bridge *bridge, void __drm_atomic_helper_bridge_state_init(struct drm_bridge_state *state, struct drm_bridge *bridge); struct drm_bridge_state * +drm_atomic_helper_bridge_create_state(struct drm_bridge *bridge); +struct drm_bridge_state * drm_atomic_helper_bridge_reset(struct drm_bridge *bridge); From 4749d7bca64ddb22bf7071a6738dcf0579e17065 Mon Sep 17 00:00:00 2001 From: Luca Ceresoli Date: Tue, 30 Jun 2026 17:34:04 +0200 Subject: [PATCH 010/258] [MERGED] drm/bridge: rename drm_for_each_bridge_in_chain_scoped() to drm_for_each_bridge_in_chain() drm_for_each_bridge_in_chain_scoped() was added in commit e46efc6a7d28 ("drm/bridge: add drm_for_each_bridge_in_chain_scoped()") to provide a safer alternative to drm_for_each_bridge_in_chain(). Following commits converted all users to the _scoped variant. Finally commit 2f08387a444c ("drm/bridge: remove drm_for_each_bridge_in_chain()") removed the old drm_for_each_bridge_in_chain() macro. It's time to rename drm_for_each_bridge_in_chain_scoped() back to the original name. Reviewed-by: Louis Chauvet Link: https://patch.msgid.link/20260630-drm-bridge-alloc-getput-for_each_bridge-2-v2-1-e0a1094cd1eb@bootlin.com Signed-off-by: Luca Ceresoli [Adapted for v7.2-rc5] Signed-off-by: Sebastian Reichel --- .clang-format | 2 +- drivers/gpu/drm/display/drm_bridge_connector.c | 4 ++-- drivers/gpu/drm/drm_atomic.c | 2 +- drivers/gpu/drm/drm_bridge.c | 2 +- include/drm/drm_bridge.h | 11 +++++------ 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.clang-format b/.clang-format index 6a3de86ab27a4f..5ef5743b77c95f 100644 --- a/.clang-format +++ b/.clang-format @@ -167,7 +167,7 @@ ForEachMacros: - 'drm_connector_for_each_possible_encoder' - 'drm_exec_for_each_locked_object' - 'drm_exec_for_each_locked_object_reverse' - - 'drm_for_each_bridge_in_chain_scoped' + - 'drm_for_each_bridge_in_chain' - 'drm_for_each_connector_iter' - 'drm_for_each_crtc' - 'drm_for_each_crtc_reverse' diff --git a/drivers/gpu/drm/display/drm_bridge_connector.c b/drivers/gpu/drm/display/drm_bridge_connector.c index 649969fca1413b..521b3958b48f25 100644 --- a/drivers/gpu/drm/display/drm_bridge_connector.c +++ b/drivers/gpu/drm/display/drm_bridge_connector.c @@ -147,7 +147,7 @@ static void drm_bridge_connector_hpd_notify(struct drm_connector *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) { + drm_for_each_bridge_in_chain(bridge_connector->encoder, bridge) { if (bridge->funcs->hpd_notify) bridge->funcs->hpd_notify(bridge, connector, status); } @@ -823,7 +823,7 @@ struct drm_connector *drm_bridge_connector_init(struct drm_device *drm, * detection are available, we don't support hotplug detection at all. */ connector_type = DRM_MODE_CONNECTOR_Unknown; - drm_for_each_bridge_in_chain_scoped(encoder, bridge) { + drm_for_each_bridge_in_chain(encoder, bridge) { if (!bridge->interlace_allowed) connector->interlace_allowed = false; if (!bridge->ycbcr_420_allowed) diff --git a/drivers/gpu/drm/drm_atomic.c b/drivers/gpu/drm/drm_atomic.c index 080aec5a977467..7e90580549a021 100644 --- a/drivers/gpu/drm/drm_atomic.c +++ b/drivers/gpu/drm/drm_atomic.c @@ -1474,7 +1474,7 @@ drm_atomic_add_encoder_bridges(struct drm_atomic_commit *state, "Adding all bridges for [encoder:%d:%s] to %p\n", encoder->base.id, encoder->name, state); - drm_for_each_bridge_in_chain_scoped(encoder, bridge) { + drm_for_each_bridge_in_chain(encoder, bridge) { /* Skip bridges that don't implement the atomic state hooks. */ if (!bridge->funcs->atomic_duplicate_state) continue; diff --git a/drivers/gpu/drm/drm_bridge.c b/drivers/gpu/drm/drm_bridge.c index ef06c1aa509adf..4a8128bbe5f247 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -1709,7 +1709,7 @@ static int encoder_bridges_show(struct seq_file *m, void *data) struct drm_printer p = drm_seq_file_printer(m); unsigned int idx = 0; - drm_for_each_bridge_in_chain_scoped(encoder, bridge) + drm_for_each_bridge_in_chain(encoder, bridge) drm_bridge_debugfs_show_bridge(&p, bridge, idx++, false, true); return 0; diff --git a/include/drm/drm_bridge.h b/include/drm/drm_bridge.h index c0703af611bae5..0a3acf10a022fa 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -1498,9 +1498,9 @@ static inline struct drm_bridge *__drm_for_each_bridge_in_chain_next(struct drm_ DEFINE_FREE(__drm_for_each_bridge_in_chain_cleanup, struct drm_bridge *, if (_T) { mutex_unlock(&_T->encoder->bridge_chain_mutex); drm_bridge_put(_T); }) -/* Internal to drm_for_each_bridge_in_chain_scoped() */ +/* Internal to drm_for_each_bridge_in_chain() */ static inline struct drm_bridge * -__drm_for_each_bridge_in_chain_scoped_start(struct drm_encoder *encoder) +__drm_for_each_bridge_in_chain_start(struct drm_encoder *encoder) { mutex_lock(&encoder->bridge_chain_mutex); @@ -1513,8 +1513,7 @@ __drm_for_each_bridge_in_chain_scoped_start(struct drm_encoder *encoder) } /** - * drm_for_each_bridge_in_chain_scoped - iterate over all bridges attached - * to an encoder + * drm_for_each_bridge_in_chain - iterate over all bridges attached to an encoder * @encoder: the encoder to iterate bridges on * @bridge: a bridge pointer updated to point to the current bridge at each * iteration @@ -1524,9 +1523,9 @@ __drm_for_each_bridge_in_chain_scoped_start(struct drm_encoder *encoder) * Automatically gets/puts the bridge reference while iterating and locks * the encoder chain mutex to prevent chain modifications while iterating. */ -#define drm_for_each_bridge_in_chain_scoped(encoder, bridge) \ +#define drm_for_each_bridge_in_chain(encoder, bridge) \ for (struct drm_bridge *bridge __free(__drm_for_each_bridge_in_chain_cleanup) = \ - __drm_for_each_bridge_in_chain_scoped_start((encoder)); \ + __drm_for_each_bridge_in_chain_start((encoder)); \ bridge; \ bridge = __drm_for_each_bridge_in_chain_next(bridge)) \ From c4217c2905aefd4736c43d531d0525fb83154f66 Mon Sep 17 00:00:00 2001 From: Maxime Ripard Date: Fri, 19 Jun 2026 14:24:38 +0200 Subject: [PATCH 011/258] [MERGED] drm/bridge: dw-dp: Switch to atomic_create_state The drm_bridge_funcs.atomic_reset callback and its drm_atomic_helper_bridge_reset() helper are deprecated. Switch to the atomic_create_state callback and its drm_atomic_helper_bridge_create_state() counterpart. Reviewed-by: Thomas Zimmermann Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli # imx8mp + sn65dsi84 + bridge hotplug Link: https://patch.msgid.link/20260619-drm-no-more-bridge-reset-v3-33-ff399263111b@kernel.org Signed-off-by: Maxime Ripard --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 36ee6e027af528..3445c82e6f50e8 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1816,7 +1816,7 @@ static struct drm_bridge_state *dw_dp_bridge_atomic_duplicate_state(struct drm_b 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_create_state = drm_atomic_helper_bridge_create_state, .atomic_get_input_bus_fmts = drm_atomic_helper_bridge_propagate_bus_fmt, .atomic_get_output_bus_fmts = dw_dp_bridge_atomic_get_output_bus_fmts, .atomic_check = dw_dp_bridge_atomic_check, From 88c62e3e8f2edb16678f8d879167c01bd1db90b9 Mon Sep 17 00:00:00 2001 From: Pan Chuang Date: Thu, 23 Jul 2026 21:16:37 +0800 Subject: [PATCH 012/258] [MERGED] drm/bridge: synopsys: dw-dp: Remove redundant dev_err_probe() Since commit 55b48e23f5c4 ("genirq/devres: Add error handling in devm_request_*_irq()"), devm_request_threaded_irq() automatically logs detailed error messages on failure. Remove the now-redundant driver-specific dev_err_probe() call. Signed-off-by: Pan Chuang Reviewed-by: Luca Ceresoli Tested-by: Luca Ceresoli Link: https://patch.msgid.link/20260723131649.134127-6-panchuang@vivo.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 3445c82e6f50e8..aea8973fb8259a 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2080,10 +2080,8 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, IRQF_ONESHOT, dev_name(dev), dp); - if (ret) { - dev_err_probe(dev, ret, "failed to request irq\n"); + if (ret) goto unregister_aux; - } return dp; From d395a34f19b63424d6b60f38016b836d47d9a7f2 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 10 Jan 2026 04:29:23 +0100 Subject: [PATCH 013/258] arm64: defconfig: enable Rockchip Multimedia drivers Enable multimedia related drivers used by the Radxa ROCK 5B (used as an example, the config options are relevant for most Rockchip RK3588 and RK3576 boards), so that all hardware is supported by the default config. * Synopsys MIPI CSI2RX - RK3588 CSI2 Controller * RKVDEC - RK3588/RK3576 video decoder for H.264 and H.265 * Sony IMX415 - Sensor used by the Radxa Cam 4K module * Rocket - RK3588 NPU driver * Verisilicon IOMMU - IOMMU used by RK3588 AV1 video decoder * Innosilicon CSI D-PHY - RK3588 CSI PHY (one of them) Signed-off-by: Sebastian Reichel --- arch/arm64/configs/defconfig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/arch/arm64/configs/defconfig b/arch/arm64/configs/defconfig index 654a102cb5bc55..49f836ba67a5da 100644 --- a/arch/arm64/configs/defconfig +++ b/arch/arm64/configs/defconfig @@ -915,6 +915,7 @@ CONFIG_SDR_PLATFORM_DRIVERS=y CONFIG_V4L_MEM2MEM_DRIVERS=y CONFIG_VIDEO_AMPHION_VPU=m CONFIG_VIDEO_CADENCE_CSI2RX=m +CONFIG_VIDEO_DW_MIPI_CSI2RX=m CONFIG_VIDEO_WAVE_VPU=m CONFIG_VIDEO_E5010_JPEG_ENC=m CONFIG_VIDEO_MEDIATEK_JPEG=m @@ -939,6 +940,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 @@ -949,6 +951,7 @@ CONFIG_VIDEO_TI_J721E_CSI2RX=m CONFIG_VIDEO_HANTRO=m CONFIG_VIDEO_IMX219=m CONFIG_VIDEO_IMX412=m +CONFIG_VIDEO_IMX415=m CONFIG_VIDEO_OV5640=m CONFIG_VIDEO_OV5645=m CONFIG_VIDEO_S5KJN1=m @@ -1063,6 +1066,8 @@ CONFIG_BACKLIGHT_GPIO=m CONFIG_LOGO=y # CONFIG_LOGO_LINUX_MONO is not set # CONFIG_LOGO_LINUX_VGA16 is not set +CONFIG_DRM_ACCEL=y +CONFIG_DRM_ACCEL_ROCKET=m CONFIG_SOUND=m CONFIG_SND=m CONFIG_SND_ALOOP=m @@ -1611,6 +1616,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 @@ -1785,6 +1791,7 @@ CONFIG_PHY_RZ_G3E_USB3=m CONFIG_PHY_ROCKCHIP_EMMC=y CONFIG_PHY_ROCKCHIP_INNO_HDMI=m CONFIG_PHY_ROCKCHIP_INNO_USB2=y +CONFIG_PHY_ROCKCHIP_INNO_CSIDPHY=m CONFIG_PHY_ROCKCHIP_INNO_DSIDPHY=m CONFIG_PHY_ROCKCHIP_NANENG_COMBO_PHY=m CONFIG_PHY_ROCKCHIP_PCIE=m From b583241921adbd826b08529600d5ccf220c4e212 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 4 Apr 2025 20:04:49 +0200 Subject: [PATCH 014/258] [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 ad43c416b919363d55332349837a2e09b28dc16e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 1 Jul 2025 22:58:37 +0200 Subject: [PATCH 015/258] [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 d9693f0e1198728c11249cb27974ef69b6b1c773 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:09:35 +0200 Subject: [PATCH 016/258] 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 437b8017f91d532f99a218924caedc3d4b091367 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 16:13:50 +0200 Subject: [PATCH 017/258] 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 de1688701466d4423c7dd7d373871d0d5104c615 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Oct 2023 18:09:57 +0200 Subject: [PATCH 018/258] 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 6527b096a24866f057426e040590140785162b0a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:35:43 +0100 Subject: [PATCH 019/258] 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 f47061828fe8db0b29e79116ed9e32fa612f0ff1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 2 Jan 2024 09:39:11 +0100 Subject: [PATCH 020/258] 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 11bb1f39379d275ee238aed08010427e3733563b Mon Sep 17 00:00:00 2001 From: "Carsten Haitzler (Rasterman)" Date: Tue, 6 Feb 2024 10:12:54 +0000 Subject: [PATCH 021/258] 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 46b29d6b3ce0211fd508fed365618d93a04c0128 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 25 Jul 2024 18:12:24 +0200 Subject: [PATCH 022/258] 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 15765590a28811a4c1956077487051d38285c6e8 Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:40 -0500 Subject: [PATCH 023/258] 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 78997ec42791a1079758c3a967f67d477f497b8a Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:41 -0500 Subject: [PATCH 024/258] 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 ccb710a522708b415a547fd121db607fd2ec5fe8 Mon Sep 17 00:00:00 2001 From: Detlev Casanova Date: Fri, 15 Nov 2024 11:20:42 -0500 Subject: [PATCH 025/258] 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 e1f0f4c6cbc67efc82b27560de45fbcf0078b288 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 8 Nov 2024 17:35:44 +0100 Subject: [PATCH 026/258] 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 d4ff3321021629d4a624f28bd167abf02d34999a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 14 Nov 2024 17:36:18 +0100 Subject: [PATCH 027/258] 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 b3aaa84b9dd1f98bb45c669c6b5a24aff0059a66 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 29 Jul 2025 18:21:03 +0200 Subject: [PATCH 028/258] 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 2708e5bfd825bf5eb03636e729d342aef9df44a0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Sat, 15 Mar 2025 09:34:02 +0100 Subject: [PATCH 029/258] 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 b719a945659028b25da0e1fff73ff6247e80ddad Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Tue, 10 Jun 2025 16:07:09 +0200 Subject: [PATCH 030/258] 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 323aa3956c48da2179f88e661660f96dab44da13 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:24 +0200 Subject: [PATCH 031/258] 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 a7486223e5cfce2d5b4f5aeb33a9f3f72a695f05 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:25 +0200 Subject: [PATCH 032/258] 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 c9cf15d9af0b5aa0e3c91bdd76b8f948b25f0e85 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 30 Jun 2025 12:19:26 +0200 Subject: [PATCH 033/258] 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 b185b9d83bf83078d71b8030ca36ab3099ceeb58 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 3 Jul 2025 01:21:14 +0200 Subject: [PATCH 034/258] 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 0d2321bd18c840..85960af66eae95 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 b2ba7f890e35b171a3b1184df00e366501ccf2a5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 8 Jul 2025 19:36:31 +0200 Subject: [PATCH 035/258] 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 671dc95a3c04f4200b5b66919c6f098f2b318aa8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 21 Nov 2025 19:27:00 +0100 Subject: [PATCH 036/258] 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 fd54132012ec6ad7973fffe8fffa9877c0099989 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 22 Dec 2025 20:25:56 +0100 Subject: [PATCH 037/258] 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 1ffb61383f7adf2e169d684ab157987f5ee8545a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:06:36 +0200 Subject: [PATCH 038/258] 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 9d308a1fbb24fb8a623d32d973c90aba89b54efa Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:21:02 +0200 Subject: [PATCH 039/258] 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 bd135d3feca385e50c13fded3c9a17060056c937 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:27:04 +0200 Subject: [PATCH 040/258] 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 c31b080d0a959c0cb50ece38fc88cd2f8eb3b544 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:30:11 +0200 Subject: [PATCH 041/258] 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 ea9427ededcd05f985ad08fe82f47575409c5e59 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:38:54 +0200 Subject: [PATCH 042/258] 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 bcad1e46941cdbb1b26372f305410ba4729df8b8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 20 Oct 2025 20:49:34 +0200 Subject: [PATCH 043/258] 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 2fd2d92b0573f2cd24382f139a0dcc94f61fdf5a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 24 Nov 2025 20:06:39 +0100 Subject: [PATCH 044/258] [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 64d27701a5e6eaa6c6f416d74ad19d44d0cb1616 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 22 Aug 2025 18:49:35 +0200 Subject: [PATCH 045/258] 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 86f601941c37bb7249cd012b7fc92d8c16b38a56 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 16:28:38 +0200 Subject: [PATCH 046/258] 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/ Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 3b20bbd748c0d9d8c5c871bad81241095b1dc013 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 01:22:02 +0100 Subject: [PATCH 047/258] 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") Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 41d366347e7672617a39d2bd4169c3b6da4667c3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 13:45:06 +0200 Subject: [PATCH 048/258] 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/ Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 e930c9759a0282339e35591e7d7e7e0551f6c356 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 19 Jun 2026 21:42:03 +0200 Subject: [PATCH 049/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 813195b162c92d118f090568fe1b44d835a1f53d Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 17 Jun 2026 15:51:07 +0200 Subject: [PATCH 050/258] 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/ Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 d60a6faa16c8f08c33ce3906e2f6555eda9a4df4 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Feb 2026 19:26:12 +0100 Subject: [PATCH 051/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 edd8c8cc3d09fdce3b157a87bfb525110812f35e Mon Sep 17 00:00:00 2001 From: Frank Wang Date: Fri, 16 Jan 2026 20:28:08 +0100 Subject: [PATCH 052/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 1fc7299c925646e6b765d9a000634e09ae47b18b Mon Sep 17 00:00:00 2001 From: William Wu Date: Fri, 16 Jan 2026 20:31:20 +0100 Subject: [PATCH 053/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 7cd5b35479222d29051714ac4fe0dcfd1d826e93 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 21:32:59 +0100 Subject: [PATCH 054/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 7296e9d65976e30b079fd91d441360ff1714b441 Mon Sep 17 00:00:00 2001 From: Zhang Yubing Date: Thu, 29 Jan 2026 13:04:47 +0100 Subject: [PATCH 055/258] phy: rockchip: usbdp: Support single-lane DP Implement support for using just a single DisplayPort line. Signed-off-by: Zhang Yubing Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 17b847ba3b84fccbbe6ed4cb7744c1bba2758a68 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 16 Jun 2026 18:05:39 +0200 Subject: [PATCH 056/258] 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/ Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 f6a6380b6e8e0cefe325bc5e552475b099cca64e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 22:45:18 +0100 Subject: [PATCH 057/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 a497a20f5f605ddaf90274122c3aa70b844c5e29 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:15:23 +0100 Subject: [PATCH 058/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 8342cc03a289d8319ecff2c2d5da7577368e4470 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 15:24:20 +0100 Subject: [PATCH 059/258] phy: rockchip: usbdp: Cleanup DP lane selection function Use FIELD_PREP_WM16() helpers to simplify the DP lane selection logic. Reviewed-by: Neil Armstrong Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 9bb3deb71df52b537f8a9f1787ea23f3747495ad Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:06:48 +0100 Subject: [PATCH 060/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 14a78c059c5fd8b48a214c66be340596244a48aa Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:53:21 +0100 Subject: [PATCH 061/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 195e9d32949c06fde4e7f4fd0df30d4efa5b2aab Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 11:25:21 +0200 Subject: [PATCH 062/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 2ac0e6fe86f9f9381e746b762664a06755e6d356 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:25:32 +0200 Subject: [PATCH 063/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 0c091ca89128e9fafbe4ab4aa458b43a9b74ff3e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 12:45:50 +0200 Subject: [PATCH 064/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 4b9f42306a4396627e1fba64e9e261677418a6a7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 19 Jun 2026 23:02:03 +0200 Subject: [PATCH 065/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 b5f5ab0a8625af0bc98b0da23ecbc9c3e5aacbc8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Apr 2026 13:43:09 +0200 Subject: [PATCH 066/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 da5d3ab7f3a009298d6f3ef64901b2b58294da75 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 18 Jun 2026 19:18:48 +0200 Subject: [PATCH 067/258] 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/ Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 92a2e8fa8cf564990c5a0d5cd2b2821d24ac317c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 11 Jun 2026 20:07:48 +0200 Subject: [PATCH 068/258] phy: rockchip: usbdp: Add some extra debug messages It's useful to log PHY reinit to ease debugging issues around USB-C hotplugging. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 8c7c0fd103d404fa6d2d87b019da257bdc4a7937 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 22 Jun 2026 18:36:38 +0200 Subject: [PATCH 069/258] 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") Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 a247772b14c04730f9137f21550a91c205dd1c8c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 30 Jun 2026 16:27:26 +0200 Subject: [PATCH 070/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 8f326dc867cc46cf3771671d645c55faf960bbee Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 30 Jun 2026 17:40:58 +0200 Subject: [PATCH 071/258] 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 Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 62ef81f722bc2722337e18e6d2ba2aaf5143d80e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 29 Jun 2026 21:03:52 +0200 Subject: [PATCH 072/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 26329d1b2d03e1b0cc5d1904ef4db01c5b9ce296 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 10 Aug 2026 17:56:27 +0200 Subject: [PATCH 073/258] usb: dwc3: rockchip: introduce glue driver Introduce Rockchip specific glue code for the Synopsys DWC3 USB driver. For now this handles things identical to the default glue. Tested-by: Igor Paunovic # Orange Pi 5 Plus Signed-off-by: Sebastian Reichel --- drivers/usb/dwc3/Kconfig | 11 +++ drivers/usb/dwc3/Makefile | 1 + drivers/usb/dwc3/core.c | 15 ++++ drivers/usb/dwc3/dwc3-rockchip.c | 115 +++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 drivers/usb/dwc3/dwc3-rockchip.c diff --git a/drivers/usb/dwc3/Kconfig b/drivers/usb/dwc3/Kconfig index 18169727a413ee..3c120ea9746d97 100644 --- a/drivers/usb/dwc3/Kconfig +++ b/drivers/usb/dwc3/Kconfig @@ -190,6 +190,17 @@ config USB_DWC3_OCTEON Only the host mode is currently supported. Say 'Y' or 'M' here if you have one such device. +config USB_DWC3_ROCKCHIP + tristate "Rockchip DWC3 Platform Driver" + depends on ARCH_ROCKCHIP || COMPILE_TEST + depends on OF + default USB_DWC3 + help + Rockchip SoCs with DesignWare Core USB3 IP inside, + and IP Core configured for USB 2.0 and USB 3.0 in host + or dual-role mode. + Say 'Y' or 'M' if you have such device. + config USB_DWC3_RTK tristate "Realtek DWC3 Platform Driver" depends on OF && ARCH_REALTEK diff --git a/drivers/usb/dwc3/Makefile b/drivers/usb/dwc3/Makefile index f37971197203e1..444e7e7f34b23b 100644 --- a/drivers/usb/dwc3/Makefile +++ b/drivers/usb/dwc3/Makefile @@ -58,6 +58,7 @@ obj-$(CONFIG_USB_DWC3_IMX8MP) += dwc3-imx8mp.o obj-$(CONFIG_USB_DWC3_IMX) += dwc3-imx.o obj-$(CONFIG_USB_DWC3_XILINX) += dwc3-xilinx.o obj-$(CONFIG_USB_DWC3_OCTEON) += dwc3-octeon.o +obj-$(CONFIG_USB_DWC3_ROCKCHIP) += dwc3-rockchip.o obj-$(CONFIG_USB_DWC3_RTK) += dwc3-rtk.o obj-$(CONFIG_USB_DWC3_GENERIC_PLAT) += dwc3-generic-plat.o obj-$(CONFIG_USB_DWC3_GOOGLE) += dwc3-google.o diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index ceb49f2f800418..5b66257118accc 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -2380,11 +2380,26 @@ int dwc3_core_probe(const struct dwc3_probe_data *data) } EXPORT_SYMBOL_GPL(dwc3_core_probe); +/* + * List of compatibles, which have "synopsys,dwc3" as a fallback + * compatible, but have a vendor specific glue driver that should + * be used instead of this one. + */ +static const char *const dwc3_compatible_blocklist[] = { + "rockchip,rk3588-dwc3", + "rockchip,rk3576-dwc3", +}; + static int dwc3_probe(struct platform_device *pdev) { struct dwc3_probe_data probe_data = {}; struct resource *res; struct dwc3 *dwc; + int i; + + for (i = 0; i < ARRAY_SIZE(dwc3_compatible_blocklist); i++) + if (device_is_compatible(&pdev->dev, dwc3_compatible_blocklist[i])) + return -ENODEV; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { diff --git a/drivers/usb/dwc3/dwc3-rockchip.c b/drivers/usb/dwc3/dwc3-rockchip.c new file mode 100644 index 00000000000000..1df33625b69f80 --- /dev/null +++ b/drivers/usb/dwc3/dwc3-rockchip.c @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026, Collabora Ltd. */ +#include +#include +#include +#include "glue.h" + +struct dwc3_rockchip { + struct dwc3 dwc; +}; + +static int dwc3_rockchip_probe(struct platform_device *pdev) +{ + struct dwc3_probe_data probe_data = {}; + struct resource *res; + struct dwc3_rockchip *dwc_rk; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(&pdev->dev, "missing memory resource\n"); + return -ENODEV; + } + + dwc_rk = devm_kzalloc(&pdev->dev, sizeof(*dwc_rk), GFP_KERNEL); + if (!dwc_rk) + return -ENOMEM; + + dwc_rk->dwc.dev = &pdev->dev; + dwc_rk->dwc.glue_ops = NULL; + + probe_data.dwc = &dwc_rk->dwc; + probe_data.res = res; + probe_data.properties = DWC3_DEFAULT_PROPERTIES; + + return dwc3_core_probe(&probe_data); +} + +static void dwc3_rockchip_remove(struct platform_device *pdev) +{ + dwc3_core_remove(platform_get_drvdata(pdev)); +} + +#ifdef CONFIG_PM +static int dwc3_rockchip_runtime_suspend(struct device *dev) +{ + return dwc3_runtime_suspend(dev_get_drvdata(dev)); +} + +static int dwc3_rockchip_runtime_resume(struct device *dev) +{ + return dwc3_runtime_resume(dev_get_drvdata(dev)); +} + +static int dwc3_rockchip_runtime_idle(struct device *dev) +{ + return dwc3_runtime_idle(dev_get_drvdata(dev)); +} +#endif + +#ifdef CONFIG_PM_SLEEP +static int dwc3_rockchip_suspend(struct device *dev) +{ + return dwc3_pm_suspend(dev_get_drvdata(dev)); +} + +static int dwc3_rockchip_resume(struct device *dev) +{ + return dwc3_pm_resume(dev_get_drvdata(dev)); +} + +static void dwc3_rockchip_complete(struct device *dev) +{ + dwc3_pm_complete(dev_get_drvdata(dev)); +} + +static int dwc3_rockchip_prepare(struct device *dev) +{ + return dwc3_pm_prepare(dev_get_drvdata(dev)); +} +#endif + +static const struct dev_pm_ops dwc3_rockchip_dev_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(dwc3_rockchip_suspend, dwc3_rockchip_resume) + .complete = dwc3_rockchip_complete, + .prepare = dwc3_rockchip_prepare, + /* + * Runtime suspend halts the controller on disconnection. It relies on + * platforms with custom connection notification to start the controller + * again. + */ + SET_RUNTIME_PM_OPS(dwc3_rockchip_runtime_suspend, dwc3_rockchip_runtime_resume, + dwc3_rockchip_runtime_idle) +}; + +static const struct of_device_id dwc3_rockchip_of_match[] = { + { .compatible = "rockchip,rk3588-dwc3" }, + { .compatible = "rockchip,rk3576-dwc3" }, + { } +}; +MODULE_DEVICE_TABLE(of, dwc3_rockchip_of_match); + +static struct platform_driver dwc3_rockchip_driver = { + .probe = dwc3_rockchip_probe, + .remove = dwc3_rockchip_remove, + .driver = { + .name = "dwc3-rockchip", + .pm = pm_ptr(&dwc3_rockchip_dev_pm_ops), + .of_match_table = dwc3_rockchip_of_match, + }, +}; + +module_platform_driver(dwc3_rockchip_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("DesignWare DWC3 Rockchip Glue Driver"); From 3e63f15b01a18abf05d342670fee3659afebf2d0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 11 Aug 2026 18:41:31 +0200 Subject: [PATCH 074/258] usb: dwc3: core: add post PHY registration hook for platform glue Add support for custom PHY handling steps in platform glue code by adding a post registration hook. This will be used for handling PHY reset notifications on the Rockchip platform. Tested-by: Igor Paunovic # Orange Pi 5 Plus Signed-off-by: Sebastian Reichel --- drivers/usb/dwc3/core.c | 2 +- drivers/usb/dwc3/core.h | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index 5b66257118accc..62d9f96d0ec1e0 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -1613,7 +1613,7 @@ static int dwc3_core_get_phy(struct dwc3 *dwc) } } - return 0; + return dwc3_post_phy_registration(dwc); } static int dwc3_core_init_mode(struct dwc3 *dwc) diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h index e0dee9d2874010..8572d23a80a959 100644 --- a/drivers/usb/dwc3/core.h +++ b/drivers/usb/dwc3/core.h @@ -996,10 +996,12 @@ struct dwc3_scratchpad_array { * need to be passed on to glue layer * @pre_set_role: Notify glue of role switch notifications * @pre_run_stop: Notify run stop enable/disable information to glue + * @post_phy_registration: Called directly after PHY got registered */ struct dwc3_glue_ops { void (*pre_set_role)(struct dwc3 *dwc, enum usb_role role); void (*pre_run_stop)(struct dwc3 *dwc, bool is_on); + int (*post_phy_registration)(struct dwc3 *dwc); }; /** @@ -1650,6 +1652,13 @@ static inline void dwc3_pre_run_stop(struct dwc3 *dwc, bool is_on) dwc->glue_ops->pre_run_stop(dwc, is_on); } +static inline int dwc3_post_phy_registration(struct dwc3 *dwc) +{ + if (dwc->glue_ops && dwc->glue_ops->post_phy_registration) + return dwc->glue_ops->post_phy_registration(dwc); + return 0; +} + #if IS_ENABLED(CONFIG_USB_DWC3_HOST) || IS_ENABLED(CONFIG_USB_DWC3_DUAL_ROLE) int dwc3_host_init(struct dwc3 *dwc); void dwc3_host_exit(struct dwc3 *dwc); From 58f7aab4b47bfd788b86152fb607d01b3a957208 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 11 Aug 2026 18:57:28 +0200 Subject: [PATCH 075/258] usb: dwc3: rockchip: 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus Signed-off-by: Sebastian Reichel --- drivers/usb/dwc3/dwc3-rockchip.c | 127 ++++++++++++++++++++++++++++++- drivers/usb/dwc3/trace.c | 3 + 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/drivers/usb/dwc3/dwc3-rockchip.c b/drivers/usb/dwc3/dwc3-rockchip.c index 1df33625b69f80..9e91e5f7e53d60 100644 --- a/drivers/usb/dwc3/dwc3-rockchip.c +++ b/drivers/usb/dwc3/dwc3-rockchip.c @@ -2,11 +2,136 @@ /* Copyright (c) 2026, Collabora Ltd. */ #include #include +#include #include #include "glue.h" +#include "io.h" + +struct dwc3_rockchip; + +/** + * struct dwc3_rk_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_rk_phy_nb { + struct notifier_block nb; + struct dwc3_rockchip *dwc_rk; + u8 port_index; +}; struct dwc3_rockchip { struct dwc3 dwc; + struct dwc3_rk_phy_nb usb3_phy_nb[DWC3_USB3_MAX_PORTS]; + u8 phy_reset_active; +}; + +static int dwc3_usb3_phy_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct dwc3_rk_phy_nb *pnb = container_of(nb, struct dwc3_rk_phy_nb, nb); + struct dwc3_rockchip *dwc_rk = pnb->dwc_rk; + struct dwc3 *dwc = &dwc_rk->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_rk->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_rk->phy_reset_active & BIT(port))) { + spin_unlock_irqrestore(&dwc->lock, flags); + return NOTIFY_OK; + } + + dwc_rk->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_rk_phy_unregister_notifiers(void *data) +{ + struct dwc3_rockchip *dwc_rk = data; + struct dwc3 *dwc = &dwc_rk->dwc; + int i; + + for (i = 0; i < dwc->num_usb3_ports; i++) + phy_unregister_notifier(dwc->usb3_generic_phy[i], + &dwc_rk->usb3_phy_nb[i].nb); + + /* Release any PM references from in-flight resets */ + for (i = 0; i < dwc->num_usb3_ports; i++) { + if (dwc_rk->phy_reset_active & BIT(i)) + pm_runtime_put_autosuspend(dwc->dev); + } + dwc_rk->phy_reset_active = 0; +} + +static int dwc3_rk_phy_register_notifiers(struct dwc3 *dwc) +{ + struct dwc3_rockchip *dwc_rk = container_of(dwc, struct dwc3_rockchip, dwc); + int i; + + for (i = 0; i < dwc->num_usb3_ports; i++) { + dwc_rk->usb3_phy_nb[i].nb.notifier_call = dwc3_usb3_phy_notify; + dwc_rk->usb3_phy_nb[i].dwc_rk = dwc_rk; + dwc_rk->usb3_phy_nb[i].port_index = i; + phy_register_notifier(dwc->usb3_generic_phy[i], + &dwc_rk->usb3_phy_nb[i].nb); + } + + return devm_add_action_or_reset(dwc->dev, dwc3_rk_phy_unregister_notifiers, dwc_rk); +} + +static struct dwc3_glue_ops dwc3_rockchip_glue_ops = { + .post_phy_registration = dwc3_rk_phy_register_notifiers, }; static int dwc3_rockchip_probe(struct platform_device *pdev) @@ -26,7 +151,7 @@ static int dwc3_rockchip_probe(struct platform_device *pdev) return -ENOMEM; dwc_rk->dwc.dev = &pdev->dev; - dwc_rk->dwc.glue_ops = NULL; + dwc_rk->dwc.glue_ops = &dwc3_rockchip_glue_ops; probe_data.dwc = &dwc_rk->dwc; probe_data.res = res; diff --git a/drivers/usb/dwc3/trace.c b/drivers/usb/dwc3/trace.c index 088995885678b5..8c4e2a7b142e0d 100644 --- a/drivers/usb/dwc3/trace.c +++ b/drivers/usb/dwc3/trace.c @@ -9,3 +9,6 @@ #define CREATE_TRACE_POINTS #include "trace.h" + +EXPORT_TRACEPOINT_SYMBOL_GPL(dwc3_readl); +EXPORT_TRACEPOINT_SYMBOL_GPL(dwc3_writel); From ae5b481877b05b818c36dd660786c6fb372424c9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 13 Aug 2026 16:48:53 +0200 Subject: [PATCH 076/258] usb: dwc3: rockchip: fix USB-C reconnect in gadget mode When USB-C is configured in gadget mode and the cable is unplugged the USB controller is suspended. After plugging in the cable again, the USB controller stays suspended and thus the port status remains not-attached. Fix this by triggering a runtime PM resume when the role is changed. The Runtime PM reference counter is immediately decreased again - the auto-suspend time is big enough to detect the connection status, which will then keep its own reference. Signed-off-by: Sebastian Reichel --- drivers/usb/dwc3/dwc3-rockchip.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/drivers/usb/dwc3/dwc3-rockchip.c b/drivers/usb/dwc3/dwc3-rockchip.c index 9e91e5f7e53d60..246d7dcafc68f1 100644 --- a/drivers/usb/dwc3/dwc3-rockchip.c +++ b/drivers/usb/dwc3/dwc3-rockchip.c @@ -25,8 +25,17 @@ struct dwc3_rockchip { struct dwc3 dwc; struct dwc3_rk_phy_nb usb3_phy_nb[DWC3_USB3_MAX_PORTS]; u8 phy_reset_active; + enum usb_role role; }; +static void dwc3_rockchip_vbus_handler(struct dwc3 *dwc, bool present) +{ + if (!dwc->gadget || !dwc->gadget_driver) + return; + + usb_udc_vbus_handler(dwc->gadget, present); +} + static int dwc3_usb3_phy_notify(struct notifier_block *nb, unsigned long action, void *data) { @@ -57,6 +66,8 @@ static int dwc3_usb3_phy_notify(struct notifier_block *nb, if (!ret) return NOTIFY_OK; + dwc3_rockchip_vbus_handler(dwc, false); + /* * Assert USB3 PHY soft reset within DWC3 before the external * PHY resets. This disconnects the PIPE interface, preventing @@ -69,6 +80,7 @@ static int dwc3_usb3_phy_notify(struct notifier_block *nb, reg |= DWC3_GUSB3PIPECTL_PHYSOFTRST; dwc3_writel(dwc, DWC3_GUSB3PIPECTL(port), reg); spin_unlock_irqrestore(&dwc->lock, flags); + break; case PHY_NOTIFY_POST_RESET: @@ -89,6 +101,8 @@ static int dwc3_usb3_phy_notify(struct notifier_block *nb, dwc3_writel(dwc, DWC3_GUSB3PIPECTL(port), reg); spin_unlock_irqrestore(&dwc->lock, flags); + dwc3_rockchip_vbus_handler(dwc, dwc_rk->role == USB_ROLE_DEVICE); + pm_runtime_put_autosuspend(dwc->dev); break; } @@ -130,7 +144,16 @@ static int dwc3_rk_phy_register_notifiers(struct dwc3 *dwc) return devm_add_action_or_reset(dwc->dev, dwc3_rk_phy_unregister_notifiers, dwc_rk); } +static void dwc3_rockchip_set_role(struct dwc3 *dwc, enum usb_role role) +{ + struct dwc3_rockchip *dwc_rk = container_of(dwc, struct dwc3_rockchip, dwc); + + dwc_rk->role = role; + dwc3_rockchip_vbus_handler(dwc, role == USB_ROLE_DEVICE); +} + static struct dwc3_glue_ops dwc3_rockchip_glue_ops = { + .pre_set_role = dwc3_rockchip_set_role, .post_phy_registration = dwc3_rk_phy_register_notifiers, }; From 5f289577fb6561e90b5644b0fc78c29f350146a1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 29 Jun 2026 21:15:13 +0200 Subject: [PATCH 077/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 1f309e8bfce621a675891fef9c08510b8426ca5c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 26 Jun 2026 20:38:19 +0200 Subject: [PATCH 078/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 847741ad5511d16e37023de2053efdff90beb626 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 22:48:25 +0200 Subject: [PATCH 079/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 3c88ccb137a643993c64ac03727eb4fa087c635c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 23:15:23 +0200 Subject: [PATCH 080/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 3583e85f315859d66c00ef7f074420ae0f1e03b4 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 1 Jul 2026 23:37:32 +0200 Subject: [PATCH 081/258] 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") Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 a22921b629ed783bb7467c421c4c13ffda3d3bf1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 2 Jul 2026 20:38:36 +0200 Subject: [PATCH 082/258] 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. Tested-by: Igor Paunovic # Orange Pi 5 Plus 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 6cdaf5730e787c6b3eef76589b7306c3f3da0bb2 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 28 Jul 2025 16:58:21 +0200 Subject: [PATCH 083/258] 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 0877a0bc2ec1b6f691bd63ac876784bb25175dcc Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 8 Oct 2025 18:15:24 +0200 Subject: [PATCH 084/258] 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 de78f4e5b98c771a9f5b1a788479e35667e5a06b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 6 Nov 2025 16:21:05 +0100 Subject: [PATCH 085/258] 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 85960af66eae95..1ef84ca47fa577 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 b2c21afac24858e7d2f1c404247f28a44c0730c0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 5 Nov 2025 18:44:09 +0100 Subject: [PATCH 086/258] 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 bb1e88e5715eeb003919c86c682b81410b1e1b31 Mon Sep 17 00:00:00 2001 From: Andy Yan Date: Fri, 9 Jan 2026 18:01:19 +0800 Subject: [PATCH 087/258] [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 fcb4c3b8c1a4069af8cf6f8d9eb948671aa4a743 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:06 +0800 Subject: [PATCH 088/258] 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 7af592c9532a866224dfba1f4a7fe16c43da288d Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:07 +0800 Subject: [PATCH 089/258] 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 3309818f6c3cac8214968fbcb0cb3fcbaefed6ea Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:08 +0800 Subject: [PATCH 090/258] 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 8f436c5df3a3903bd88b281b8f8abc338017e18b Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:09 +0800 Subject: [PATCH 091/258] 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 e1c57a21b26ffe2b7ae785b268b9641aa25b97a5 Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Wed, 24 Dec 2025 15:10:10 +0800 Subject: [PATCH 092/258] 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 2b22abd1d6fe0b021d1ae43f09ee7960b14e69f1 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 19 Jan 2026 13:11:40 +0100 Subject: [PATCH 093/258] 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 360bec5785e25f5f37958050c571292da6f1a4b5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 22 Jan 2026 14:57:54 +0100 Subject: [PATCH 094/258] 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 b6bf7c97ea479fd8fa3ab8a22a51e5a5b6d67253 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 29 Jan 2026 23:57:28 +0100 Subject: [PATCH 095/258] 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 c9d1599cdbaae66aab4b1f686e6a7973413682df Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:44:57 +0100 Subject: [PATCH 096/258] [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 92fab0f89415a78365c424b7cc102d722eec8834 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 2 Mar 2026 22:37:40 +0100 Subject: [PATCH 097/258] 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 7b1e42ce7007fc6eb7b0305d351cf6c8c1cb81cd Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 24 Mar 2026 18:16:31 +0100 Subject: [PATCH 098/258] 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 31dca5ca8f01a11d6b76d5aae9662ef60ec8561c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:45 +0200 Subject: [PATCH 099/258] 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 5bdea7a0f6b642344f0898f147c5bd1781b97a1f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:46 +0200 Subject: [PATCH 100/258] 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 012ebed022ce7a30a2cf923dacbc5ab5feecf2f3 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:47 +0200 Subject: [PATCH 101/258] 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 663af9e8636cb78ba9916ef0db6c4ba9a5286d0e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:48 +0200 Subject: [PATCH 102/258] 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 e8d1a2904101fcdf3a6cd7e5c40d7606e39f99cb Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:49 +0200 Subject: [PATCH 103/258] 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 44fe45a832a6f7fe7e9523f3bcc2f961bdc4079f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 27 Feb 2026 22:48:50 +0200 Subject: [PATCH 104/258] 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 4e36b87bd4b583e18fbefc3a10fd36da7d01e5d2 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Feb 2026 20:36:26 +0200 Subject: [PATCH 105/258] [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 309b974bbc58169536f6f81b3ee04c64d4b7bc78 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 17 Mar 2026 15:43:25 +0100 Subject: [PATCH 106/258] [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 2cd10da8ebd5568cb408b39703b3fec0e808840c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 29 Jul 2026 18:55:32 +0200 Subject: [PATCH 107/258] drm/bridge: synopsys: dw-dp: Register DP AUX on bridge attach Unregister the DP AUX device at the right spot as documented in the drm_dp_aux_register() function description. This helps that it is only accessed when the DRM device is ready and the bridge is powered and initialized (further fixes are required for that). 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 | 55 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index aea8973fb8259a..dc513ecfd71d22 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1813,7 +1813,36 @@ static struct drm_bridge_state *dw_dp_bridge_atomic_duplicate_state(struct drm_b return &state->base; } +static int dw_dp_bridge_attach(struct drm_bridge *bridge, + struct drm_encoder *encoder, + enum drm_bridge_attach_flags flags) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + struct device *dev = dp->dev; + int 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(dev, "Aux register failed: %d\n", ret); + + return ret; +} + +static void dw_dp_bridge_detach(struct drm_bridge *bridge) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + drm_dp_aux_unregister(&dp->aux); +} + static const struct drm_bridge_funcs dw_dp_bridge_funcs = { + .attach = dw_dp_bridge_attach, + .detach = dw_dp_bridge_detach, .atomic_duplicate_state = dw_dp_bridge_atomic_duplicate_state, .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, .atomic_create_state = drm_atomic_helper_bridge_create_state, @@ -2044,20 +2073,10 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, 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; + return ERR_PTR(ret); } dw_dp_init_hw(dp); @@ -2065,35 +2084,31 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, ret = phy_init(dp->phy); if (ret) { dev_err_probe(dev, ret, "phy init failed\n"); - goto unregister_aux; + return ERR_PTR(ret); } ret = devm_add_action_or_reset(dev, dw_dp_phy_exit, dp); if (ret) - goto unregister_aux; + return ERR_PTR(ret); dp->irq = platform_get_irq(pdev, 0); if (dp->irq < 0) { ret = dp->irq; - goto unregister_aux; + return ERR_PTR(ret); } ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, IRQF_ONESHOT, dev_name(dev), dp); if (ret) - 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); + /* nothing to do */ } EXPORT_SYMBOL_GPL(dw_dp_unbind); From 0a5fde4e933f7a7dfd53be4c6845efbca09bf004 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 16 Jul 2026 23:43:55 +0200 Subject: [PATCH 108/258] 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 the whole bind function. This results in a fully functional DRM bridge once probe succeeded. The only thing still happening when the component is bound is the bridge attachment, which requires the encoder. The interrupt is kept disabled while the bridge is detached to ensure no spurious interrupts can arrive as the interrupt handler triggers a worker, which accesses the DRM device. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Acked-by: Andy Yan Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 69 ++++++++++++----------- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 53 +++++++++-------- include/drm/bridge/dw_dp.h | 5 +- 3 files changed, 70 insertions(+), 57 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index dc513ecfd71d22..1f1bfb9e8b8d83 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1827,16 +1827,22 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, dp->aux.transfer = dw_dp_aux_transfer; ret = drm_dp_aux_register(&dp->aux); - if (ret) + if (ret) { dev_err(dev, "Aux register failed: %d\n", ret); + return ret; + } - return ret; + enable_irq(dp->irq); + + return 0; } static void dw_dp_bridge_detach(struct drm_bridge *bridge) { struct dw_dp *dp = bridge_to_dp(bridge); + disable_irq(dp->irq); + cancel_work_sync(&dp->hpd_work); drm_dp_aux_unregister(&dp->aux); } @@ -1983,6 +1989,18 @@ static const struct regmap_config dw_dp_regmap_config = { .rd_table = &dw_dp_readable_table, }; +int dw_dp_bind(struct dw_dp *dp, struct drm_encoder *encoder) +{ + return drm_bridge_attach(encoder, &dp->bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR); +} +EXPORT_SYMBOL_GPL(dw_dp_bind); + +void dw_dp_unbind(struct dw_dp *dp) +{ + /* nothing to do as bridge is detached automatically */ +} +EXPORT_SYMBOL_GPL(dw_dp_unbind); + static void dw_dp_phy_exit(void *data) { struct dw_dp *dp = data; @@ -1990,13 +2008,12 @@ static void dw_dp_phy_exit(void *data) phy_exit(dp->phy); } -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 device *dev = &pdev->dev; struct drm_bridge *bridge; void __iomem *res; + struct dw_dp *dp; int ret; dp = devm_drm_bridge_alloc(dev, struct dw_dp, bridge, &dw_dp_bridge_funcs); @@ -2005,9 +2022,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); @@ -2064,21 +2080,15 @@ 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; + dp->irq = platform_get_irq(pdev, 0); + if (dp->irq < 0) + return ERR_PTR(dp->irq); - ret = devm_drm_bridge_add(dev, bridge); + ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, + IRQF_ONESHOT | IRQF_NO_AUTOEN, dev_name(dev), dp); if (ret) 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"); - return ERR_PTR(ret); - } - dw_dp_init_hw(dp); ret = phy_init(dp->phy); @@ -2091,26 +2101,19 @@ struct dw_dp *dw_dp_bind(struct device *dev, struct drm_encoder *encoder, if (ret) return ERR_PTR(ret); - dp->irq = platform_get_irq(pdev, 0); - if (dp->irq < 0) { - ret = dp->irq; - return ERR_PTR(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; - ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, - IRQF_ONESHOT, dev_name(dev), dp); + ret = devm_drm_bridge_add(dev, bridge); if (ret) return ERR_PTR(ret); return dp; } -EXPORT_SYMBOL_GPL(dw_dp_bind); - -void dw_dp_unbind(struct dw_dp *dp) -{ - /* nothing to do */ -} -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 b23efb153c9e66..38e8fe75718e45 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -26,7 +26,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, @@ -73,37 +73,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 dev_err_probe(dev, ret, "failed to bind DW-DP bridge\n"); connector = drm_bridge_connector_init(drm_dev, encoder); if (IS_ERR(connector)) { @@ -128,12 +119,30 @@ static const struct component_ops dw_dp_rockchip_component_ops = { .unbind = dw_dp_rockchip_unbind, }; -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); } @@ -161,8 +170,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 ee58d72df6c39a9a5855fc1e4659207d8ee777dd Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 3 Aug 2026 19:37:14 +0200 Subject: [PATCH 109/258] drm/bridge: synopsys: dw-dp: Fix error handling for DP link enablement dw_dp_link_disable() may be called in atomic mode disable even when dw_dp_link_enable() (or an earlier step) failed during atomic mode enable as there is no error tracking. This would result in broken PHY power state. This is fixed by introducing a new enabled state in the link structure to ensure the link disabling only happens if it has been properly enabled in the first place. The patch also adds missing error handling in dw_dp_link_enable() itself to ensure the link enablement becomes an atomic operation. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Acked-by: Andy Yan Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 1f1bfb9e8b8d83..44bff389613b36 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -280,6 +280,7 @@ struct dw_dp_link { unsigned char revision; unsigned int rate; unsigned int lanes; + bool enabled; u8 sink_count; u8 vsc_sdp_supported; struct dw_dp_link_caps caps; @@ -1615,6 +1616,9 @@ static void dw_dp_link_disable(struct dw_dp *dp) { struct dw_dp_link *link = &dp->link; + if (!link->enabled) + return; + if (dw_dp_hpd_detect(dp)) drm_dp_link_power_down(&dp->aux, dp->link.revision); @@ -1624,6 +1628,7 @@ static void dw_dp_link_disable(struct dw_dp *dp) link->train.clock_recovered = false; link->train.channel_equalized = false; + link->enabled = false; } static int dw_dp_link_enable(struct dw_dp *dp) @@ -1636,10 +1641,22 @@ static int dw_dp_link_enable(struct dw_dp *dp) ret = drm_dp_link_power_up(&dp->aux, dp->link.revision); if (ret < 0) - return ret; + goto err_phy_power_off; ret = dw_dp_link_train(dp); + if (ret < 0) + goto err_link_power_down; + + dp->link.enabled = true; + return 0; + +err_link_power_down: + drm_dp_link_power_down(&dp->aux, dp->link.revision); + dw_dp_phy_xmit_enable(dp, 0); + +err_phy_power_off: + phy_power_off(dp->phy); return ret; } From 3c301e60599f91bedc55e5451f1d1825d77c8c5e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 29 Jul 2026 20:31:06 +0200 Subject: [PATCH 110/258] drm/bridge: synopsys: dw-dp: Document missing reset line deassert If the driver uses devm_reset_control_get_exclusive_deasserted() instead of devm_reset_control_get() and thus automatically deasserts during probe, the SoC will hang when the device is unbound. This does not happen, when runtime PM is being used (not yet supported in mainline), which suggests the power-domain involved requires this reset line to be deasserted. Even with runtime PM there is no gurantee that the power-domain is disabled as it is shared. Considering the power-domain does not have the reset dependency described in DT, document the problem but leave things in the current state until a better solution is found as the reset line is deasserted by default on all supported platforms. Reported-by: Sashiko Reviewed-by: Andy Yan Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 44bff389613b36..e3e8dd3301ae12 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2091,6 +2091,10 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ return ERR_CAST(dp->hdcp_clk); } + /* + * This reset line is deasserted by default; asserting it hangs the SoC if the + * related power-domain is still active. + */ dp->rstc = devm_reset_control_get(dev, NULL); if (IS_ERR(dp->rstc)) { dev_err_probe(dev, PTR_ERR(dp->rstc), "failed to get reset control\n"); From 52aa4492e021a4602e3edaa727ca976e324d7c48 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 22 Jul 2026 20:03:14 +0200 Subject: [PATCH 111/258] 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 Reviewed-by: Andy Yan 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 e3e8dd3301ae12..6a3762cef42b2b 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -2041,10 +2041,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 a072d555e291f7bfc1291a1c9438018c831651ae Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Jul 2026 15:04:19 +0200 Subject: [PATCH 112/258] drm/bridge: synopsys: dw-dp: Fix AUX transfer timeout race condition 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 directly before sending the AUX command. As this is racy (the interrupt might arrive between the completion re-init and the new command being programmed), also reset the AUX controller on timeouts and synchronize pending interrupts to guarantee that there are no pending AUX transfers when the dw_dp_aux_transfer() returns. Due to lack of a sink, which generates AUX timeouts, this change is effectively untested. 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 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 6a3762cef42b2b..2bed440fdd4344 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1466,6 +1466,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: @@ -1492,6 +1494,12 @@ static ssize_t dw_dp_aux_transfer(struct drm_dp_aux *aux, status = wait_for_completion_timeout(&dp->complete, timeout); if (!status) { dev_err(dp->dev, "timeout waiting for AUX reply\n"); + regmap_update_bits(dp->regmap, DW_DP_SOFT_RESET_CTRL, + AUX_RESET, FIELD_PREP(AUX_RESET, 1)); + usleep_range(10, 20); + regmap_update_bits(dp->regmap, DW_DP_SOFT_RESET_CTRL, + AUX_RESET, FIELD_PREP(AUX_RESET, 0)); + synchronize_irq(dp->irq); return -ETIMEDOUT; } From 8c35ebaba7d1d011dac4cf21e776909698ea795e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 29 Jul 2026 21:58:06 +0200 Subject: [PATCH 113/258] drm/bridge: synopsys: dw-dp: Fix support for short I2C reads The transfer functions returns the amount of bytes read for DP_AUX_I2C_READ. By returning -EBUSY for short reads, the caller has less information available what is going wrong and possibly simply resends the read request. On sinks not supporting long reads, this will simply run into the same issue again. Instead it makes more sense to return the data from the short read with the length information, which allows drm_dp_i2c_do_msg() to read data in smaller chunks and succeed in the end. Due to lack of a sink, which only supports short reads, this change is effectively untested. Fixes: 86eecc3a9c2e ("drm/bridge: synopsys: Add DW DPTX Controller support library") Reported-by: Sashiko Reviewed-by: Andy Yan Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 2bed440fdd4344..7d3e56097057b4 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1513,7 +1513,7 @@ static ssize_t dw_dp_aux_transfer(struct drm_dp_aux *aux, if (msg->request & DP_AUX_I2C_READ) { size_t count = FIELD_GET(AUX_BYTES_READ, value) - 1; - if (count != msg->size) + if (!count || count > msg->size) return -EBUSY; ret = dw_dp_aux_read_data(dp, msg->buffer, count); From 4ebb19a0b7993e0dd92a2c4993043668316a8587 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 24 Jul 2026 15:28:55 +0200 Subject: [PATCH 114/258] 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 Reviewed-by: Andy Yan 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 7d3e56097057b4..9e220a5b0a4563 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1820,6 +1820,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 8944da6638a950855a2f0daa199246eb3a2d838a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 6 Mar 2026 14:26:19 +0100 Subject: [PATCH 115/258] 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 9e220a5b0a4563..4052656da47c62 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1537,6 +1537,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; @@ -1547,7 +1548,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; @@ -1830,6 +1834,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; @@ -1882,7 +1912,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_create_state = drm_atomic_helper_bridge_create_state, - .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 cf41523bf4ee2f8ce9bd6dde0851177b5655a7b5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 5 Mar 2026 18:12:53 +0100 Subject: [PATCH 116/258] 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. Reviewed-by: Chaoyi Chen Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 4052656da47c62..30ca1800ea8c2a 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; + struct drm_bridge *next_bridge; u8 pixel_mode; DECLARE_BITMAP(sdp_reg_bank, SDP_REG_BANK_SIZE); @@ -1894,7 +1895,22 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, enable_irq(dp->irq); + ret = drm_bridge_attach(encoder, dp->next_bridge, bridge, + DRM_BRIDGE_ATTACH_NO_CONNECTOR); + if (ret) { + dev_err(dev, "Failed to attach next bridge: %d\n", ret); + goto err_disable_irq; + } + return 0; + +err_disable_irq: + disable_irq(dp->irq); + cancel_work_sync(&dp->hpd_work); + + drm_dp_aux_unregister(&dp->aux); + + return ret; } static void dw_dp_bridge_detach(struct drm_bridge *bridge) @@ -2061,6 +2077,13 @@ void dw_dp_unbind(struct dw_dp *dp) } EXPORT_SYMBOL_GPL(dw_dp_unbind); +static void dw_dp_put_next_bridge(void *data) +{ + struct dw_dp *dp = data; + + drm_bridge_put(dp->next_bridge); +} + static void dw_dp_phy_exit(void *data) { struct dw_dp *dp = data; @@ -2156,6 +2179,16 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ if (ret) return ERR_PTR(ret); + dp->next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 1, 0); + if (IS_ERR(dp->next_bridge)) { + dev_err_probe(dev, PTR_ERR(dp->next_bridge), "failed to get follow-up bridge\n"); + return ERR_CAST(dp->next_bridge); + } + + ret = devm_add_action_or_reset(dev, dw_dp_put_next_bridge, dp); + if (ret) + return ERR_PTR(ret); + dw_dp_init_hw(dp); ret = phy_init(dp->phy); From 2dad229df63c68f86080a04091d2b3768e698e1b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 26 Feb 2026 16:42:44 +0100 Subject: [PATCH 117/258] 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 521b3958b48f25..82880b56782dbc 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(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 0a3acf10a022fa..c3bd6b95645033 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -662,6 +662,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 7ce6c24a3d67153b8af8bc7b8525d3867647d0f5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:29:45 +0100 Subject: [PATCH 118/258] 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 30ca1800ea8c2a..c925397b1a1ca9 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1874,6 +1874,19 @@ static struct drm_bridge_state *dw_dp_bridge_atomic_duplicate_state(struct drm_b return &state->base; } +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"); +} + static int dw_dp_bridge_attach(struct drm_bridge *bridge, struct drm_encoder *encoder, enum drm_bridge_attach_flags flags) @@ -1902,6 +1915,13 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, goto err_disable_irq; } + 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); + } + return 0; err_disable_irq: @@ -1922,6 +1942,19 @@ static void dw_dp_bridge_detach(struct drm_bridge *bridge) drm_dp_aux_unregister(&dp->aux); } +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 = { .attach = dw_dp_bridge_attach, .detach = dw_dp_bridge_detach, @@ -1936,6 +1969,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) @@ -2105,6 +2139,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 9e6c644e5a2e20437283d89dc82920c3026fe329 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 4 Mar 2026 15:34:43 +0100 Subject: [PATCH 119/258] 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 implemented (and enabled) in the DP driver. 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 | 118 +++++++++++++++++++++- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index 38e8fe75718e45..9e49e7dbf420f3 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 @@ -23,12 +26,48 @@ #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; + u64 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; }; +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]; + + regmap_write(dp->vo_grf, hpd_reg, + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_SEL, force_hpd_from_sw)); +} + +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)); + + regmap_write(dp->vo_grf, hpd_reg, + FIELD_PREP_WM16(ROCKCHIP_VO_GRF_DP_SINK_HPD_CFG, hpd)); +} + static int dw_dp_encoder_atomic_check(struct drm_encoder *encoder, struct drm_crtc_state *crtc_state, struct drm_connector_state *conn_state) @@ -71,6 +110,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); @@ -121,19 +189,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)) @@ -147,14 +249,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 745c6054b35093cd69df0589785cd4ce4978d9e7 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:40:50 +0100 Subject: [PATCH 120/258] 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 platform specific glue code. The early bits of the dw_dp_probe function are split into a new function called dw_dp_alloc, so that the platform driver can assign it before running dw_dp_probe. This is necessary because the runtime PM resume/suspend events land at the platform driver and must be forwarded to the helper once runtime PM is enabled in the middle of the probe function. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 205 ++++++++++++++++++---- drivers/gpu/drm/rockchip/dw_dp-rockchip.c | 8 +- include/drm/bridge/dw_dp.h | 7 +- 3 files changed, 185 insertions(+), 35 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index c925397b1a1ca9..4d6524cf4e9afd 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -330,6 +330,9 @@ struct dw_dp { struct dw_dp_plat_data plat_data; struct drm_bridge *next_bridge; u8 pixel_mode; + bool usbc_mode; + bool usbc_hpd; + bool pm_active; DECLARE_BITMAP(sdp_reg_bank, SDP_REG_BANK_SIZE); }; @@ -1467,6 +1470,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) { @@ -1681,6 +1689,13 @@ 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; + } + dp->pm_active = true; + connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder); if (!connector) { dev_err(dp->dev, "failed to get connector\n"); @@ -1731,10 +1746,15 @@ static void dw_dp_bridge_atomic_disable(struct drm_bridge *bridge, { struct dw_dp *dp = bridge_to_dp(bridge); + if (!dp->pm_active) + return; + dp->pm_active = false; + dw_dp_video_disable(dp); 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) @@ -1755,6 +1775,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; @@ -1895,6 +1919,10 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, struct device *dev = dp->dev; int ret; + ret = pm_runtime_get_active(dp->dev, RPM_TRANSPARENT); + if (ret) + return ret; + dp->aux.dev = dev; dp->aux.drm_dev = encoder->dev; dp->aux.name = dev_name(dev); @@ -1903,7 +1931,7 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, ret = drm_dp_aux_register(&dp->aux); if (ret) { dev_err(dev, "Aux register failed: %d\n", ret); - return ret; + goto err_runtime_pm_put; } enable_irq(dp->irq); @@ -1915,11 +1943,15 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, goto err_disable_irq; } - 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, dp->usbc_mode); - if (dp->plat_data.hpd_sw_sel) - dp->plat_data.hpd_sw_sel(dp->plat_data.data, 1); + /* USB-C has out-of-band hotplug detection, so device may runtime suspend */ + if (dp->usbc_mode) { + dev_dbg(dev, "USB-C mode\n"); + pm_runtime_put_autosuspend(dp->dev); } return 0; @@ -1930,6 +1962,9 @@ static int dw_dp_bridge_attach(struct drm_bridge *bridge, drm_dp_aux_unregister(&dp->aux); +err_runtime_pm_put: + pm_runtime_put_autosuspend(dp->dev); + return ret; } @@ -1940,6 +1975,9 @@ static void dw_dp_bridge_detach(struct drm_bridge *bridge) disable_irq(dp->irq); cancel_work_sync(&dp->hpd_work); drm_dp_aux_unregister(&dp->aux); + + if (!dp->usbc_mode) + pm_runtime_put_autosuspend(dp->dev); } static void dw_dp_bridge_oob_notify(struct drm_bridge *bridge, @@ -1948,6 +1986,14 @@ static void dw_dp_bridge_oob_notify(struct drm_bridge *bridge, { bool hpd_high = status != connector_status_disconnected; struct dw_dp *dp = bridge_to_dp(bridge); + int ret; + + dp->usbc_hpd = hpd_high; + + PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dp->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return; if (dp->plat_data.hpd_sw_cfg) dp->plat_data.hpd_sw_cfg(dp->plat_data.data, hpd_high); @@ -2007,6 +2053,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); @@ -2125,13 +2176,24 @@ static void dw_dp_phy_exit(void *data) phy_exit(dp->phy); } -struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data) +static void dw_dp_manual_suspend(void *data) +{ + struct dw_dp *dp = data; + + dw_dp_runtime_suspend(dp); +} + +static void dw_dp_enable_irq(void *data) +{ + struct dw_dp *dp = data; + + enable_irq(dp->irq); +} + +struct dw_dp *dw_dp_alloc(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data) { struct device *dev = &pdev->dev; - struct drm_bridge *bridge; - void __iomem *res; struct dw_dp *dp; - int ret; dp = devm_drm_bridge_alloc(dev, struct dw_dp, bridge, &dw_dp_bridge_funcs); if (IS_ERR(dp)) @@ -2144,58 +2206,71 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ 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; + dp->plat_data.autosuspend_delay = plat_data->autosuspend_delay; INIT_WORK(&dp->hpd_work, dw_dp_hpd_work); init_completion(&dp->complete); + return dp; +} +EXPORT_SYMBOL_GPL(dw_dp_alloc); + +int dw_dp_probe(struct dw_dp *dp) +{ + struct device *dev = dp->dev; + struct platform_device *pdev = to_platform_device(dev); + struct drm_bridge *bridge; + void __iomem *res; + int ret; + ret = devm_mutex_init(dev, &dp->irq_lock); if (ret) - return ERR_PTR(ret); + return ret; res = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(res)) - return ERR_CAST(res); + return PTR_ERR(res); dp->regmap = devm_regmap_init_mmio(dev, res, &dw_dp_regmap_config); if (IS_ERR(dp->regmap)) { dev_err_probe(dev, PTR_ERR(dp->regmap), "failed to create regmap\n"); - return ERR_CAST(dp->regmap); + return PTR_ERR(dp->regmap); } dp->phy = devm_of_phy_get(dev, dev->of_node, NULL); if (IS_ERR(dp->phy)) { dev_err_probe(dev, PTR_ERR(dp->phy), "failed to get phy\n"); - return ERR_CAST(dp->phy); + return PTR_ERR(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); + return PTR_ERR(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); + return PTR_ERR(dp->aux_clk); } dp->i2s_clk = devm_clk_get_optional(dev, "i2s"); if (IS_ERR(dp->i2s_clk)) { dev_err_probe(dev, PTR_ERR(dp->i2s_clk), "failed to get i2s clock\n"); - return ERR_CAST(dp->i2s_clk); + return PTR_ERR(dp->i2s_clk); } dp->spdif_clk = devm_clk_get_optional(dev, "spdif"); if (IS_ERR(dp->spdif_clk)) { dev_err_probe(dev, PTR_ERR(dp->spdif_clk), "failed to get spdif clock\n"); - return ERR_CAST(dp->spdif_clk); + return PTR_ERR(dp->spdif_clk); } dp->hdcp_clk = devm_clk_get(dev, "hdcp"); if (IS_ERR(dp->hdcp_clk)) { dev_err_probe(dev, PTR_ERR(dp->hdcp_clk), "failed to get hdcp clock\n"); - return ERR_CAST(dp->hdcp_clk); + return PTR_ERR(dp->hdcp_clk); } /* @@ -2205,39 +2280,65 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ dp->rstc = devm_reset_control_get(dev, NULL); if (IS_ERR(dp->rstc)) { dev_err_probe(dev, PTR_ERR(dp->rstc), "failed to get reset control\n"); - return ERR_CAST(dp->rstc); + return PTR_ERR(dp->rstc); } dp->irq = platform_get_irq(pdev, 0); if (dp->irq < 0) - return ERR_PTR(dp->irq); + return dp->irq; ret = devm_request_threaded_irq(dev, dp->irq, NULL, dw_dp_irq, IRQF_ONESHOT | IRQF_NO_AUTOEN, dev_name(dev), dp); if (ret) - return ERR_PTR(ret); + return ret; + + /* + * Disable IRQ a second time; this ensures the interrupt is only + * enabled when the bridge is attached AND runtime PM is enabled. + * Also register a devm action to restore the correct balance during + * device removal. + */ + disable_irq(dp->irq); + + ret = devm_add_action_or_reset(dev, dw_dp_enable_irq, dp); + if (ret) + return ret; dp->next_bridge = of_drm_get_bridge_by_endpoint(dev->of_node, 1, 0); if (IS_ERR(dp->next_bridge)) { dev_err_probe(dev, PTR_ERR(dp->next_bridge), "failed to get follow-up bridge\n"); - return ERR_CAST(dp->next_bridge); + return PTR_ERR(dp->next_bridge); } ret = devm_add_action_or_reset(dev, dw_dp_put_next_bridge, dp); if (ret) - return ERR_PTR(ret); + return ret; - dw_dp_init_hw(dp); + if (dp->plat_data.autosuspend_delay > 0) { + pm_runtime_use_autosuspend(dev); + pm_runtime_set_autosuspend_delay(dev, dp->plat_data.autosuspend_delay); + ret = devm_pm_runtime_enable(dev); + if (ret) + return ret; + } + + if (!pm_runtime_enabled(dev)) { + dw_dp_runtime_resume(dp); + + ret = devm_add_action_or_reset(dev, dw_dp_manual_suspend, dp); + if (ret) + return ret; + } ret = phy_init(dp->phy); if (ret) { dev_err_probe(dev, ret, "phy init failed\n"); - return ERR_PTR(ret); + return ret; } ret = devm_add_action_or_reset(dev, dw_dp_phy_exit, dp); if (ret) - return ERR_PTR(ret); + return ret; bridge = &dp->bridge; bridge->of_node = dev->of_node; @@ -2245,13 +2346,53 @@ struct dw_dp *dw_dp_probe(struct platform_device *pdev, const struct dw_dp_plat_ bridge->type = DRM_MODE_CONNECTOR_DisplayPort; bridge->ycbcr_420_allowed = true; - ret = devm_drm_bridge_add(dev, bridge); + return devm_drm_bridge_add(dev, bridge); +} +EXPORT_SYMBOL_GPL(dw_dp_probe); + +int dw_dp_runtime_suspend(struct dw_dp *dp) +{ + disable_irq(dp->irq); + + 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 ERR_PTR(ret); + return ret; - return dp; + ret = clk_prepare_enable(dp->aux_clk); + if (ret) { + clk_disable_unprepare(dp->apb_clk); + return ret; + } + + if (dp->plat_data.hpd_sw_sel) + dp->plat_data.hpd_sw_sel(dp->plat_data.data, dp->usbc_mode); + if (dp->plat_data.hpd_sw_cfg) + dp->plat_data.hpd_sw_cfg(dp->plat_data.data, dp->usbc_hpd); + + dw_dp_init_hw(dp); + + enable_irq(dp->irq); + + /* + * 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_probe); +EXPORT_SYMBOL_GPL(dw_dp_runtime_resume); 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 9e49e7dbf420f3..ffcfb887d0d2a3 100644 --- a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c +++ b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c @@ -194,7 +194,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,10 +237,14 @@ static int dw_dp_rockchip_probe(struct platform_device *pdev) plat_data->hpd_sw_cfg = dw_dp_rockchip_hpd_sw_cfg; plat_data->data = dp; - dp->base = dw_dp_probe(pdev, plat_data); + dp->base = dw_dp_alloc(pdev, plat_data); if (IS_ERR(dp->base)) return PTR_ERR(dp->base); + ret = dw_dp_probe(dp->base); + if (ret) + return ret; + return component_add(&pdev->dev, &dw_dp_rockchip_component_ops); } diff --git a/include/drm/bridge/dw_dp.h b/include/drm/bridge/dw_dp.h index 79b2cdf0df996b..1e23180b565e45 100644 --- a/include/drm/bridge/dw_dp.h +++ b/include/drm/bridge/dw_dp.h @@ -18,6 +18,7 @@ enum { }; struct dw_dp_plat_data { + int autosuspend_delay; u32 max_link_rate; u8 pixel_mode; void *data; @@ -28,5 +29,9 @@ struct dw_dp_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); +struct dw_dp *dw_dp_alloc(struct platform_device *pdev, const struct dw_dp_plat_data *plat_data); +int dw_dp_probe(struct dw_dp *dp); + +int dw_dp_runtime_suspend(struct dw_dp *dp); +int dw_dp_runtime_resume(struct dw_dp *dp); #endif /* __DW_DP__ */ From 7eef280361dfc0915717671156de1408e809849b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 19 Mar 2026 16:44:22 +0100 Subject: [PATCH 121/258] 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 | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/drivers/gpu/drm/rockchip/dw_dp-rockchip.c b/drivers/gpu/drm/rockchip/dw_dp-rockchip.c index ffcfb887d0d2a3..770ab042a1879f 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 @@ -231,6 +232,7 @@ static int dw_dp_rockchip_probe(struct platform_device *pdev) if (IS_ERR(dp->vo_grf)) return PTR_ERR(dp->vo_grf); + plat_data->autosuspend_delay = 500; 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; @@ -253,6 +255,24 @@ 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); + + return dw_dp_runtime_resume(dp->base); +} + +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}, @@ -287,5 +307,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 f66f783b65a82cc19df2520d858bbfe9cd2736bf Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 23 Jul 2026 22:59:37 +0200 Subject: [PATCH 122/258] 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 4d6524cf4e9afd..61f918feaeec26 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -323,6 +323,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; @@ -1047,11 +1049,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; @@ -1708,7 +1712,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) { @@ -1752,7 +1757,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); } @@ -2227,6 +2233,10 @@ int dw_dp_probe(struct dw_dp *dp) if (ret) return ret; + ret = devm_mutex_init(dev, &dp->sdp_lock); + if (ret) + return ret; + res = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(res)) return PTR_ERR(res); From 399acff1e289ce87964f77a5aec1d05ae0ee79da Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 29 Jul 2026 20:52:19 +0200 Subject: [PATCH 123/258] drm/bridge: synopsys: dw-dp: Drop useless reservation of first slot The origin of this reservation is unclear, but it is a problem in the atomic_enable code since it potentially races with the audio SDP reservation once that feature is added. I suppose it was either meant to be bitmap_zero(), but that is obviously not needed (and would also be a problem for audio support) or some left-over development code before the VSC SDP slot was allocated automatically. From my tests SDP slot 0 works fine and can be used. If SDP slot 0 really needs to be reserved for some reason, the bit should be set in the probe function to ensure there are no race conditions. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 61f918feaeec26..125ac0c9b6329b 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1712,9 +1712,6 @@ static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, return; } - scoped_guard(mutex, &dp->sdp_lock) - set_bit(0, dp->sdp_reg_bank); - ret = dw_dp_link_enable(dp); if (ret < 0) { dev_err(dp->dev, "failed to enable link: %d\n", ret); From 31286c048a4f1ffc6b821c9fa2bbd04a6271af3c Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 28 Jul 2026 02:46:18 +0200 Subject: [PATCH 124/258] drm/bridge: synopsys: dw-dp: Clear only enabled SDPs on atomic disable dw_dp_bridge_atomic_disable() bulk-cleared the whole SDP register bank allocation bitmap via bitmap_zero() resulting in the loss of all tracking information. This results in a slot potentially being handed out again by dw_dp_send_sdp(), which is still considered to be held by the previous owner. Then the previous owner might free up the wrong SDP later on. Instead of bulk clearing the tracking information, the new implementation only clears the SDPs actually configured during dw_dp_bridge_atomic_enable() instead of the entire bank. The introduced functionality for that will also be used by the to-be-added audio infrastructure. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/synopsys/dw-dp.c | 27 +++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index 125ac0c9b6329b..fa4e9956bc9e6b 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -336,6 +336,7 @@ struct dw_dp { bool usbc_hpd; bool pm_active; + int vsc_sdp_nr; DECLARE_BITMAP(sdp_reg_bank, SDP_REG_BANK_SIZE); }; @@ -1077,7 +1078,19 @@ static int dw_dp_send_sdp(struct dw_dp *dp, struct dw_dp_sdp *sdp) EN_HORIZONTAL_SDP << nr, 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) @@ -1395,7 +1408,7 @@ static int dw_dp_video_enable(struct dw_dp *dp) FIELD_PREP(VIDEO_STREAM_ENABLE, 1)); if (dw_dp_video_need_vsc_sdp(dp)) - dw_dp_send_vsc_sdp(dp); + dp->vsc_sdp_nr = dw_dp_send_vsc_sdp(dp); return 0; } @@ -1754,8 +1767,12 @@ static void dw_dp_bridge_atomic_disable(struct drm_bridge *bridge, dw_dp_video_disable(dp); dw_dp_link_disable(dp); - scoped_guard(mutex, &dp->sdp_lock) - bitmap_zero(dp->sdp_reg_bank, SDP_REG_BANK_SIZE); + + if (dp->vsc_sdp_nr >= 0) { + dw_dp_clear_sdp(dp, dp->vsc_sdp_nr); + dp->vsc_sdp_nr = -1; + } + dw_dp_reset(dp); pm_runtime_put_autosuspend(dp->dev); } @@ -2347,6 +2364,8 @@ int dw_dp_probe(struct dw_dp *dp) if (ret) return ret; + dp->vsc_sdp_nr = -1; + bridge = &dp->bridge; bridge->of_node = dev->of_node; bridge->ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID | DRM_BRIDGE_OP_HPD; From 0e2f9646b21e9f0f691fd82caa573ff31da2b7da Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 23 Jul 2026 23:03:29 +0200 Subject: [PATCH 125/258] 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 fa4e9956bc9e6b..a6d8638bc105de 100644 --- a/drivers/gpu/drm/bridge/synopsys/dw-dp.c +++ b/drivers/gpu/drm/bridge/synopsys/dw-dp.c @@ -1069,14 +1069,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 nr; } From e22188c4fee2ccb57d8adc5d6c2b8aca1c420d08 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Fri, 23 Jan 2026 23:10:22 +0100 Subject: [PATCH 126/258] 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 9a739240b33874d5b003b4c9299ed162ff6f0d1e Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 05:22:05 +0100 Subject: [PATCH 127/258] 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 | 314 +++++++++++++++++++++++- 1 file changed, 313 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/synopsys/dw-dp.c b/drivers/gpu/drm/bridge/synopsys/dw-dp.c index a6d8638bc105de..d5a210979a8905 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; @@ -306,6 +316,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; @@ -321,10 +344,18 @@ 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_channel_allocation; + int audio_sample_width; + bool audio_muted; + int audio_sdp_nr; /* Serialize hpd status access */ struct mutex irq_lock; /* Serialize sdp_reg_bank access */ struct mutex sdp_lock; + /* Serialize audio state */ + struct mutex audio_lock; struct drm_dp_aux aux; @@ -1696,6 +1727,261 @@ static int dw_dp_link_enable(struct dw_dp *dp) return ret; } +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; + frame.channel_allocation = dp->audio_channel_allocation; + + 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 void dw_dp_audio_infoframe_clear(struct dw_dp *dp) +{ + 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); +} + +static void __dw_dp_audio_disable(struct dw_dp *dp) +{ + dw_dp_audio_infoframe_clear(dp); + + 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_enable(struct dw_dp *dp) +{ + u8 audio_data_in_en; + + switch (dp->audio_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: + return -EINVAL; + } + + regmap_update_bits(dp->regmap, DW_DP_AUD_CONFIG1, + AUDIO_DATA_IN_EN | NUM_CHANNELS | AUDIO_DATA_WIDTH | + AUDIO_INF_SELECT | HBR_MODE_ENABLE | AUDIO_MUTE, + FIELD_PREP(AUDIO_DATA_IN_EN, audio_data_in_en) | + FIELD_PREP(NUM_CHANNELS, dp->audio_channels - 1) | + FIELD_PREP(AUDIO_DATA_WIDTH, dp->audio_sample_width) | + FIELD_PREP(AUDIO_INF_SELECT, dp->audio_interface) | + FIELD_PREP(HBR_MODE_ENABLE, 0) | + FIELD_PREP(AUDIO_MUTE, dp->audio_muted)); + + /* Wait for inf switch */ + usleep_range(20, 40); + + /* + * 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)); + + if (dp->audio_sdp_nr >= 0) { + dw_dp_clear_sdp(dp, dp->audio_sdp_nr); + dp->audio_sdp_nr = -1; + } + + dp->audio_sdp_nr = dw_dp_audio_infoframe_send(dp); + if (dp->audio_sdp_nr < 0) { + dw_dp_audio_infoframe_clear(dp); + return dp->audio_sdp_nr; + } + + return 0; +} + +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_get_active(dp->dev, RPM_TRANSPARENT); +} + +static void dw_dp_audio_unprepare(struct drm_bridge *bridge, + struct drm_connector *connector) +{ + struct dw_dp *dp = bridge_to_dp(bridge); + + guard(mutex)(&dp->audio_lock); + + __dw_dp_audio_disable(dp); +} + +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 supported_audio_interfaces; + enum dw_dp_audio_interface audio_interface; + u32 cfg1; + int ret; + + guard(mutex)(&dp->audio_lock); + + /* + * 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_disable(dp); + + /* The hardware is limited to 1,2 or 8 channels */ + switch (params->cea.channels) { + case 1: + case 2: + case 8: + break; + default: + dev_err(dp->dev, "invalid audio channels %d\n", params->cea.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; + } + + 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_channels = params->cea.channels; + dp->audio_channel_allocation = params->cea.channel_allocation; + dp->audio_sample_width = params->sample_width; + dp->audio_interface = audio_interface; + + ret = __dw_dp_audio_enable(dp); + if (ret < 0) { + dev_err(dp->dev, "failed to enable audio\n"); + __dw_dp_audio_disable(dp); + return ret; + } + + 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"); + + guard(mutex)(&dp->audio_lock); + + dp->audio_muted = enable; + + regmap_update_bits(dp->regmap, DW_DP_AUD_CONFIG1, AUDIO_MUTE, + FIELD_PREP(AUDIO_MUTE, enable)); + + return 0; +} + static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_atomic_commit *state) { @@ -1734,6 +2020,14 @@ static void dw_dp_bridge_atomic_enable(struct drm_bridge *bridge, dev_err(dp->dev, "failed to enable video: %d\n", ret); return; } + + scoped_guard(mutex, &dp->audio_lock) { + if (dp->audio_interface != DW_DP_AUDIO_UNUSED) { + ret = __dw_dp_audio_enable(dp); + if (ret < 0) + dev_err(dp->dev, "failed to restore audio: %d\n", ret); + } + } } static void dw_dp_reset(struct dw_dp *dp) @@ -2034,6 +2328,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) @@ -2249,6 +2548,10 @@ int dw_dp_probe(struct dw_dp *dp) if (ret) return ret; + ret = devm_mutex_init(dev, &dp->audio_lock); + if (ret) + return ret; + res = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(res)) return PTR_ERR(res); @@ -2363,12 +2666,21 @@ int dw_dp_probe(struct dw_dp *dp) return ret; dp->vsc_sdp_nr = -1; + dp->audio_interface = DW_DP_AUDIO_UNUSED; + dp->audio_sdp_nr = -1; 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; + 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; return devm_drm_bridge_add(dev, bridge); } From 1801e812c125ab06c18517d6fe7ea4a1f6fc416b Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:01:21 +0100 Subject: [PATCH 128/258] 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 949a6ffc39c2576c2b2269fc95900eaba31deda2 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Wed, 21 Jan 2026 06:02:48 +0100 Subject: [PATCH 129/258] 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 bd966f4a7422a5b02edea5937445c2f825ede1e3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 12 Feb 2026 22:22:54 +0100 Subject: [PATCH 130/258] 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 ac0d2fc4477acd0523edcfa317ceb04d072461b8 Mon Sep 17 00:00:00 2001 From: Chaoyi Chen Date: Tue, 4 Aug 2026 15:07:24 +0800 Subject: [PATCH 131/258] drm/bridge: aux-hpd-bridge: Add drm_dev_has_dp_hpd_bridge() Add a new API to check whether a DisplayPort HPD bridge has already been registered. This helps avoid duplicate registration of the same HPD bridge, although the current framework allows doing so. Suggested-by: Sebastian Reichel Signed-off-by: Chaoyi Chen Link: https://patch.msgid.link/20260804070730.68-2-kernel@airkyi.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/bridge/aux-hpd-bridge.c | 42 ++++++++++++++++++++++++- include/drm/bridge/aux-bridge.h | 6 ++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/bridge/aux-hpd-bridge.c b/drivers/gpu/drm/bridge/aux-hpd-bridge.c index f02a38a2638add..a56c88eba00583 100644 --- a/drivers/gpu/drm/bridge/aux-hpd-bridge.c +++ b/drivers/gpu/drm/bridge/aux-hpd-bridge.c @@ -12,6 +12,8 @@ #include #include +#define DRM_AUX_HPD_BRIDGE_NAME "dp_hpd_bridge" + static DEFINE_IDA(drm_aux_hpd_bridge_ida); struct drm_aux_hpd_bridge_data { @@ -36,6 +38,44 @@ static void drm_aux_hpd_bridge_free_adev(void *_adev) auxiliary_device_uninit(_adev); } +static int hpd_bridge_match(struct device *dev, const void *data) +{ + const struct device_node *np = data; + struct auxiliary_device *adev; + + if (!dev_is_auxiliary(dev)) + return 0; + + adev = to_auxiliary_dev(dev); + if (strcmp(adev->name, DRM_AUX_HPD_BRIDGE_NAME)) + return 0; + + return adev->dev.platform_data == np; +} + +/** + * drm_dev_has_dp_hpd_bridge - check whether a HPD DisplayPort bridge is registered + * @parent: device instance providing this bridge + * @np: device node pointer corresponding to this bridge instance + * + * Walk the children of @parent and check whether a HPD DisplayPort bridge for + * the given @np has already been registered via devm_drm_dp_hpd_bridge_add(). + * + * Return: true if a HPD bridge for @parent / @np already exists, false otherwise + */ +bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np) +{ + struct device *child; + + child = device_find_child(parent, np, hpd_bridge_match); + if (child) { + put_device(child); + return true; + } + return false; +} +EXPORT_SYMBOL_GPL(drm_dev_has_dp_hpd_bridge); + /** * devm_drm_dp_hpd_bridge_alloc - allocate a HPD DisplayPort bridge * @parent: device instance providing this bridge @@ -63,7 +103,7 @@ struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent, str } adev->id = ret; - adev->name = "dp_hpd_bridge"; + adev->name = DRM_AUX_HPD_BRIDGE_NAME; adev->dev.parent = parent; adev->dev.release = drm_aux_hpd_bridge_release; adev->dev.platform_data = of_node_get(np); diff --git a/include/drm/bridge/aux-bridge.h b/include/drm/bridge/aux-bridge.h index c2f5a855512f36..cca07a8e2d454d 100644 --- a/include/drm/bridge/aux-bridge.h +++ b/include/drm/bridge/aux-bridge.h @@ -25,6 +25,7 @@ struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent, str int devm_drm_dp_hpd_bridge_add(struct device *dev, struct auxiliary_device *adev); struct device *drm_dp_hpd_bridge_register(struct device *parent, struct device_node *np); +bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np); void drm_aux_hpd_bridge_notify(struct device *dev, enum drm_connector_status status); #else static inline struct auxiliary_device *devm_drm_dp_hpd_bridge_alloc(struct device *parent, @@ -44,6 +45,11 @@ static inline struct device *drm_dp_hpd_bridge_register(struct device *parent, return NULL; } +static inline bool drm_dev_has_dp_hpd_bridge(struct device *parent, struct device_node *np) +{ + return false; +} + static inline void drm_aux_hpd_bridge_notify(struct device *dev, enum drm_connector_status status) { } From c8cfa4b8b6d51de62d2be74c7f52d9008b2e988b Mon Sep 17 00:00:00 2001 From: Chaoyi Chen Date: Tue, 4 Aug 2026 15:07:25 +0800 Subject: [PATCH 132/258] 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://patch.msgid.link/20260804070730.68-3-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 | 67 +++++++++++++++++++ 3 files changed, 78 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..682bc192e0934a --- /dev/null +++ b/drivers/gpu/drm/bridge/aux-hpd-typec-dp-bridge.c @@ -0,0 +1,67 @@ +// 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); + struct device_node *np; + + 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) { + np = to_of_node(alt->dev.parent->fwnode); + if (!drm_dev_has_dp_hpd_bridge(alt->dev.parent->parent, np)) + drm_dp_hpd_bridge_register(alt->dev.parent->parent, np); + } + + return NOTIFY_OK; +} + +static struct notifier_block drm_typec_event_nb = { + .notifier_call = drm_typec_bus_event, +}; + +static int check_device_already_added(struct device *dev, void *data) +{ + drm_typec_bus_event(NULL, BUS_NOTIFY_ADD_DEVICE, dev); + return 0; +} + +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); + /* + * Before module initialization, some devices may have already been added. + * Register the HPD bridge for these devices. + */ + bus_for_each_dev(&typec_bus, NULL, NULL, check_device_already_added); + 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 acad5b61e76fda4d22c2385defca3c5906e01c1e Mon Sep 17 00:00:00 2001 From: Chaoyi Chen Date: Tue, 4 Aug 2026 15:07:26 +0800 Subject: [PATCH 133/258] drm/display: Add soft depend for aux-hpd-typec-dp-bridge module The aux-hpd-typec-dp-bridge module serves as a generic TypeC DisplayPort HPD bridge and is not required by any other module. Therefore, it will not be auto-loaded. Given that the drm_display_helper module houses DisplayPort-related helper code, add a MODULE_SOFTDEP() within it to suggest loading the aux-hpd-typec-dp-bridge module beforehand. Suggested-by: Sebastian Reichel Signed-off-by: Chaoyi Chen Link: https://patch.msgid.link/20260804070730.68-4-kernel@airkyi.com Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/display/drm_display_helper_mod.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/gpu/drm/display/drm_display_helper_mod.c b/drivers/gpu/drm/display/drm_display_helper_mod.c index d8a6e62287736f..f0152d6b0b2d79 100644 --- a/drivers/gpu/drm/display/drm_display_helper_mod.c +++ b/drivers/gpu/drm/display/drm_display_helper_mod.c @@ -18,5 +18,6 @@ static void __exit drm_display_helper_module_exit(void) drm_dp_aux_dev_exit(); } +MODULE_SOFTDEP("pre: aux-hpd-typec-dp-bridge"); module_init(drm_display_helper_module_init); module_exit(drm_display_helper_module_exit); From 2b2cc4e11e2d8426970366ce0f8236710d953801 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 17 Aug 2026 17:40:34 +0200 Subject: [PATCH 134/258] usb: typec: tcpm: allocate pd_list entries non device managed The data is free'd manually, so there is no need for the device managed overhead. At the same time it conflicts when the TCPM itself is registered device managed (not yet supported). Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/tcpm.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c index c172e817a8d7ec..ff9e65d4d4b6e0 100644 --- a/drivers/usb/typec/tcpm/tcpm.c +++ b/drivers/usb/typec/tcpm/tcpm.c @@ -7745,7 +7745,7 @@ static void tcpm_port_unregister_pd(struct tcpm_port *port) for (i = 0; i < port->pd_count; i++) { usb_power_delivery_unregister_capabilities(port->pd_list[i]->sink_cap); usb_power_delivery_unregister_capabilities(port->pd_list[i]->source_cap); - devm_kfree(port->dev, port->pd_list[i]); + kfree(port->pd_list[i]); port->pd_list[i] = NULL; usb_power_delivery_unregister(port->pds[i]); port->pds[i] = NULL; @@ -8044,7 +8044,7 @@ static int tcpm_fw_get_caps(struct tcpm_port *port, struct fwnode_handle *fwnode } for (i = 0; i < port->pd_count; i++) { - port->pd_list[i] = devm_kzalloc(port->dev, sizeof(struct pd_data), GFP_KERNEL); + port->pd_list[i] = kzalloc_obj(*port->pd_list[i]); if (!port->pd_list[i]) { ret = -ENOMEM; goto put_capabilities; From 9bd3f4c676e11c8092d8d6a09b6864533c37f3d3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 16:23:15 +0200 Subject: [PATCH 135/258] 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 ff9e65d4d4b6e0..9e956f7d78ff34 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 8b0e284b6223516bad99a6799a38f2cd861958f0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 17:41:17 +0200 Subject: [PATCH 136/258] 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 4b662031ffd39decf0a2aab7961a37cc81f625e3 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:01:37 +0200 Subject: [PATCH 137/258] 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 eadbcde1f3ef1ca019537b41e0a97d056c06f9b5 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:03:15 +0200 Subject: [PATCH 138/258] 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 9dcb546d53ed8c44250c19b917fd75f7764e520a Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:10:26 +0200 Subject: [PATCH 139/258] 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 1a7243171e022ca986b2384361c96f24d4f888a8 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 21 Apr 2026 18:17:02 +0200 Subject: [PATCH 140/258] 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 1217e7afacd8a102aaaf20de243b4e3e47300ce0 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 6 Aug 2026 18:36:25 +0200 Subject: [PATCH 141/258] usb: typec: fusb302: drop explicit DRM DP HPD bridge support There is now generic USB Type-C DP HPD bridge support via CONFIG_DRM_AUX_HPD_TYPEC_BRIDGE, which is automatically enabled when CONFIG_DRM_AUX_HPD_BRIDGE and TYPEC are enabled. The manual explicit registration code has the same requirements and thus is pointless and can be dropped. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/tcpm/Kconfig | 2 -- drivers/usb/typec/tcpm/fusb302.c | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/drivers/usb/typec/tcpm/Kconfig b/drivers/usb/typec/tcpm/Kconfig index 00baa7503d4527..8cdd84ca5d6f76 100644 --- a/drivers/usb/typec/tcpm/Kconfig +++ b/drivers/usb/typec/tcpm/Kconfig @@ -58,8 +58,6 @@ config TYPEC_FUSB302 tristate "Fairchild FUSB302 Type-C chip driver" depends on I2C depends on EXTCON || !EXTCON - depends on DRM || DRM=n - select DRM_AUX_HPD_BRIDGE if DRM_BRIDGE && OF help The Fairchild FUSB302 Type-C chip driver that works with Type-C Port Controller Manager to provide USB PD and USB diff --git a/drivers/usb/typec/tcpm/fusb302.c b/drivers/usb/typec/tcpm/fusb302.c index f5c0cc453a8924..4566b7daa936e4 100644 --- a/drivers/usb/typec/tcpm/fusb302.c +++ b/drivers/usb/typec/tcpm/fusb302.c @@ -5,7 +5,6 @@ * Fairchild FUSB302 Type-C Chip Driver */ -#include #include #include #include @@ -1685,7 +1684,6 @@ static int fusb302_probe(struct i2c_client *client) { struct fusb302_chip *chip; struct i2c_adapter *adapter = client->adapter; - struct auxiliary_device *bridge_dev; struct device *dev = &client->dev; const char *name; int ret = 0; @@ -1749,11 +1747,6 @@ static int fusb302_probe(struct i2c_client *client) 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)) - return dev_err_probe(chip->dev, PTR_ERR(bridge_dev), - "failed to alloc bridge\n"); - 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), @@ -1777,10 +1770,6 @@ static int fusb302_probe(struct i2c_client *client) i2c_set_clientdata(client, chip); - ret = devm_drm_dp_hpd_bridge_add(chip->dev, bridge_dev); - if (ret) - return ret; - enable_irq_wake(chip->irq); return ret; From 573632f5ad33c0ff0d16afa2c5d4c64c310cdbad Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:17:57 +0200 Subject: [PATCH 142/258] 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 44fe9c1255e11e29f64bed2cbd60a559f558f300 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 18 Mar 2026 01:43:24 +0200 Subject: [PATCH 143/258] 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 f999190b3ca61e6a88efac0e16e2a791df815f7b Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Apr 2026 04:19:28 +0300 Subject: [PATCH 144/258] [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 ea6cfb0d0a6dcfb3ecdf03d708de84c3b551bafe Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sun, 2 Mar 2025 19:47:18 +0200 Subject: [PATCH 145/258] [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 0eebeb7c77bffe6f38fb2cf1651a18c798fc367c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 23:51:17 +0300 Subject: [PATCH 146/258] 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 d9966d02a8f110046642880b23571ca6eedd621e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 22:48:01 +0200 Subject: [PATCH 147/258] 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 4a8128bbe5f247..650402f8507785 100644 --- a/drivers/gpu/drm/drm_bridge.c +++ b/drivers/gpu/drm/drm_bridge.c @@ -1365,9 +1365,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 @@ -1376,12 +1376,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 c3bd6b95645033..a5a701664b2c6b 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -551,10 +551,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: * @@ -563,6 +565,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: * @@ -1032,8 +1060,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), /** @@ -1602,6 +1630,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 59a67173f6392dee3cd7ec7b312ac3ec35ea957e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 22 Apr 2026 01:26:32 +0300 Subject: [PATCH 148/258] 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 82880b56782dbc..65b2cecd328635 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 21a71f2b7bbea59bb44fec229292b72b65bf8313 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 10 Jan 2025 23:04:23 +0200 Subject: [PATCH 149/258] 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 65b2cecd328635..57f7d5a0c8a2ca 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 784188f88019e9c19ed54ec095058dfc08f7b105 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Fri, 13 Sep 2024 17:30:35 +0300 Subject: [PATCH 150/258] 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 ed8a37c19ab40d6ba5b9d339629dc22a587f94f3 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:08:12 +0300 Subject: [PATCH 151/258] 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 9bbed1b4e4436fc54e22251a8d11107b9dbdd47a Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:21:36 +0300 Subject: [PATCH 152/258] 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 630f5206f2b09ef8e3cf9003aca710e42643817c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 21 Apr 2026 14:38:26 +0300 Subject: [PATCH 153/258] 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 cca17c3adf74b7a463e6fc2bcbfe6146bfa56c32 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 23:55:39 +0300 Subject: [PATCH 154/258] 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 4dc655ab81f8de718e82eb1cecfbafe83e12a71d Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Mon, 20 Apr 2026 22:42:12 +0300 Subject: [PATCH 155/258] 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 42e21657460530e22a0eb461cfff7d90c9817c9c Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 18:16:54 +0200 Subject: [PATCH 156/258] 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 03b771595b1f4d670316efb848e67d063746fd2f Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Thu, 12 Mar 2026 17:55:32 +0200 Subject: [PATCH 157/258] 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 1516804570daf9cd0c9db2867760415b4413d2c1 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Sat, 28 Mar 2026 22:08:28 +0200 Subject: [PATCH 158/258] 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 6b2d0140f5b4c1e558d483073c5f8b1252bb211e Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:34:26 +0200 Subject: [PATCH 159/258] 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 cae0d85fb44079..1435a1f2e4e761 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 2871a97e34e978942b3fa151ed573a27477b60af Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 25 Mar 2026 01:27:05 +0200 Subject: [PATCH 160/258] 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 57f7d5a0c8a2ca..50f7dfbcd6b824 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 a5a701664b2c6b..4763b895fa99f1 100644 --- a/include/drm/drm_bridge.h +++ b/include/drm/drm_bridge.h @@ -725,6 +725,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 935279203733172ba6bb51c2d35b592c077f4273 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 8 Apr 2026 00:23:35 +0300 Subject: [PATCH 161/258] 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 e78750f76f61521c2345095c14e6c21ea39984f3 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 31 Mar 2026 17:52:43 +0300 Subject: [PATCH 162/258] 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 3d0add7a81f2045b2f04c793685756b3ab443e97 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Wed, 4 Feb 2026 01:11:56 +0200 Subject: [PATCH 163/258] 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 3eb914a24dd2019f14f72f828e10dc9a69a11980 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 17 Mar 2026 22:42:11 +0200 Subject: [PATCH 164/258] 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 09216decd9450549f14fe62d214ff023b4c6fcec Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:39:51 +0300 Subject: [PATCH 165/258] 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 6e1af7535010b9ef196fc98d0c817c9acb86100d Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 13:28:59 +0300 Subject: [PATCH 166/258] 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 c3f552ddb6675f3647cf6e73d25d0145cf831bc3 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:31:24 +0300 Subject: [PATCH 167/258] 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 9083ee53de092c06122195e784b53cbafbcd7c72 Mon Sep 17 00:00:00 2001 From: Cristian Ciocaltea Date: Tue, 7 Apr 2026 00:33:45 +0300 Subject: [PATCH 168/258] 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 eef8a229202f91856734eeb60216d7b232df8c59 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:42:59 +0200 Subject: [PATCH 169/258] 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 0cda36eda1800ec80853279561c23005c4d6b6f6 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 1 Jun 2026 15:57:35 +0200 Subject: [PATCH 170/258] 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 744a340bec078175ddbd43845101626d693840cf Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 2 Jul 2026 03:54:15 +0200 Subject: [PATCH 171/258] 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 4e7f0d636d91289055234902a3777aa600035e83 Mon Sep 17 00:00:00 2001 From: RD Babiera Date: Fri, 10 Jul 2026 20:03:08 +0000 Subject: [PATCH 172/258] 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 9e956f7d78ff34..c95635c536c159 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 995a7654e0def7519bea855a5314777e9e826260 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Mon, 27 Jul 2026 19:30:07 +0200 Subject: [PATCH 173/258] 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 b05bc43b06f1f6fe10a6cb23f56cdb6a4d0250be Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 13 Aug 2026 19:10:09 +0200 Subject: [PATCH 174/258] Reapply "usb: typec: mux: avoid duplicated mux switches" I've not yet investigated what's going on with the Qualcomm X1E devices, but we definetly want this on Rockchip to avoid all events being processed twice resulting in useless PHY resets. This reverts commit f576c75f95a52c71b30167d7efb6d47148f9c279. Signed-off-by: Sebastian Reichel --- drivers/usb/typec/mux.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index 9b908c46bd7df9..db5e4a4c0a9969 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -275,7 +275,9 @@ static int mux_fwnode_match(struct device *dev, const void *fwnode) static void *typec_mux_match(const struct fwnode_handle *fwnode, const char *id, void *data) { + struct typec_mux_dev **mux_devs = data; struct device *dev; + int i; /* * Device graph (OF graph) does not give any means to identify the @@ -291,6 +293,14 @@ static void *typec_mux_match(const struct fwnode_handle *fwnode, dev = class_find_device(&typec_mux_class, NULL, fwnode, mux_fwnode_match); + /* Skip duplicates */ + for (i = 0; i < TYPEC_MUX_MAX_DEVS; i++) + if (to_typec_mux_dev(dev) == mux_devs[i]) { + put_device(dev); + return NULL; + } + + return dev ? to_typec_mux_dev(dev) : ERR_PTR(-EPROBE_DEFER); } @@ -316,7 +326,8 @@ struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode) return ERR_PTR(-ENOMEM); count = fwnode_connection_find_matches(fwnode, "mode-switch", - NULL, typec_mux_match, + (void **)mux_devs, + typec_mux_match, (void **)mux_devs, ARRAY_SIZE(mux_devs)); if (count <= 0) { From ca1036c5226d054bd5b7d0d4790925438e6efafd Mon Sep 17 00:00:00 2001 From: Haobo Cheng Date: Tue, 4 Aug 2026 16:34:34 +0800 Subject: [PATCH 175/258] usb: typec: mux: initialize orientation switch array Commit a53b4f9c51a9 ("usb: typec: mux: avoid duplicated orientation switches") started using the orientation switch result array as state for duplicate detection, but left the array uninitialized. The first match therefore scans indeterminate stack contents before any result has been stored. A valid orientation switch may be incorrectly discarded as a duplicate, causing fwnode_typec_switch_get() to return no switch. Zero-initialize the array before collecting matches. Fixes: a53b4f9c51a9 ("usb: typec: mux: avoid duplicated orientation switches") Signed-off-by: Haobo Cheng Reviewed-by: Sebastian Reichel Link: https://patch.msgid.link/20260804083434.20885-1-i@4t.pw Signed-off-by: Sebastian Reichel --- drivers/usb/typec/mux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index db5e4a4c0a9969..36b45a194539c6 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -79,7 +79,7 @@ static void *typec_switch_match(const struct fwnode_handle *fwnode, */ struct typec_switch *fwnode_typec_switch_get(struct fwnode_handle *fwnode) { - struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS]; + struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS] = { }; struct typec_switch *sw; int count; int err; From e04cda785ef0ab9204fdf8077e3be6fefd49a4a9 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Thu, 13 Aug 2026 19:37:03 +0200 Subject: [PATCH 176/258] usb: typec: mux: initialize mux switch array Commit b145c3f29d62 ("usb: typec: mux: avoid duplicated mux switches") started using the orientation switch result array as state for duplicate detection, but left the array uninitialized. The first match therefore scans indeterminate stack contents before any result has been stored. A valid orientation switch may be incorrectly discarded as a duplicate, causing fwnode_typec_switch_get() to return no switch. Zero-initialize the array before collecting matches. Fixes: b145c3f29d62 ("usb: typec: mux: avoid duplicated mux switches") Signed-off-by: Sebastian Reichel --- drivers/usb/typec/mux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index 36b45a194539c6..f6266be26d95d7 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -315,7 +315,7 @@ static void *typec_mux_match(const struct fwnode_handle *fwnode, */ struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode) { - struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS]; + struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS] = { }; struct typec_mux *mux; int count; int err; From b4988462755a3d6ed607162ceb175c0f3c82a26f Mon Sep 17 00:00:00 2001 From: Marek Vasut Date: Mon, 17 Aug 2026 20:22:39 +0200 Subject: [PATCH 177/258] usb: typec: mux: Fix typec_switch_match() The fwnode_typec_switch_get() sporadically returns NULL instead of an -EPROBE_DEFER for orientation-switch described in DT. This makes it impossible to discern whether the DT does describe an orientation-switch which did not probe yet, or whether the DT does not describe the switch. This happens with gpio-sbu-mux connected to an I2C GPIO expander. The class_find_device() on typec_switch_match() may return NULL in case the mux did not probe just yet early on boot. The sw_devs[] array can be empty on boot as well. If these two conditions occur, then the conditional if (to_typec_switch_dev(dev) == sw_devs[i]) evaluates to true and the match function returns NULL, which propagates to fwnode_typec_switch_get() which makes it look as if the orientation-switch was not described in DT. This is incorrect, because the mux driver will probe a bit later on, but at that point, the caller of fwnode_typec_switch_get() already got the NULL return value. The NULL return value also does not trigger IS_ERR(), therefore the caller driver interprets this as if the orientation-switch is not described in DT, and does not return -EPROBE_DEFER to try again, even if it should. Fix this by checking the class_find_device() return value, and return -EPROBE_DEFER if it is NULL right away. If the return value is not NULL, perform the deduplication test, and if that test passes, consider the return value to be already non-NULL. Fixes: a53b4f9c51a9 ("usb: typec: mux: avoid duplicated orientation switches") Cc: stable@vger.kernel.org Signed-off-by: Marek Vasut Link: https://patch.msgid.link/20260817182302.146546-1-marex@nabladev.com Signed-off-by: Sebastian Reichel --- drivers/usb/typec/mux.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/usb/typec/mux.c b/drivers/usb/typec/mux.c index f6266be26d95d7..f953aca686e697 100644 --- a/drivers/usb/typec/mux.c +++ b/drivers/usb/typec/mux.c @@ -57,6 +57,8 @@ static void *typec_switch_match(const struct fwnode_handle *fwnode, */ dev = class_find_device(&typec_mux_class, NULL, fwnode, switch_fwnode_match); + if (!dev) + return ERR_PTR(-EPROBE_DEFER); /* Skip duplicates */ for (i = 0; i < TYPEC_MUX_MAX_DEVS; i++) @@ -65,7 +67,7 @@ static void *typec_switch_match(const struct fwnode_handle *fwnode, return NULL; } - return dev ? to_typec_switch_dev(dev) : ERR_PTR(-EPROBE_DEFER); + return to_typec_switch_dev(dev); } /** From 7d988b93f0192fb102eccf5dc71270376f7a3326 Mon Sep 17 00:00:00 2001 From: Sebastian Reichel Date: Tue, 18 Aug 2026 21:10:12 +0200 Subject: [PATCH 178/258] drm/connector: Cache out-of-band hotplug events When the USB-C state machine finished negotiating DP AltMode before the DRM device has been probed, the out-of-band hotplug events fired to early and are lost. Without replugging the display or reloading the USB-C driver, the DRM driver assumes nothing is plugged. Reproducing this race condition at boot time depends on kernel configuration and exact USB-C equipment due to timing, but it can easily be reproduced by reloading the DRM driver consuming the out-of-band hotplug events without reloading the USB-C driver. Signed-off-by: Sebastian Reichel --- drivers/gpu/drm/drm_connector.c | 106 ++++++++++++++++++++++++++++ drivers/gpu/drm/drm_crtc_internal.h | 1 + drivers/gpu/drm/drm_drv.c | 1 + 3 files changed, 108 insertions(+) diff --git a/drivers/gpu/drm/drm_connector.c b/drivers/gpu/drm/drm_connector.c index 11646453aaac9b..57e8ba813b2231 100644 --- a/drivers/gpu/drm/drm_connector.c +++ b/drivers/gpu/drm/drm_connector.c @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -81,6 +82,22 @@ static DEFINE_MUTEX(connector_list_lock); static LIST_HEAD(connector_list); +/* + * List of connector fwnodes with their last out-of-band hotplug status + * required to forward them to a connector on registration. This ensures + * the connector sees a HPD event, if the event arrived before the DRM + * driver was probed (either due to module reload, or because of bad + * timing during bootup). + */ +struct drm_oob_hotplug_state { + struct list_head head; + struct fwnode_handle *fwnode; + enum drm_connector_status status; +}; + +static DEFINE_MUTEX(oob_hotplug_list_lock); +static LIST_HEAD(oob_hotplug_list); + struct drm_conn_prop_enum_list { int type; const char *name; @@ -130,6 +147,19 @@ void drm_connector_ida_destroy(void) ida_destroy(&drm_connector_enum_list[i].ida); } +void drm_connector_oob_hotplug_cleanup(void) +{ + struct drm_oob_hotplug_state *e, *tmp; + + scoped_guard(mutex, &oob_hotplug_list_lock) { + list_for_each_entry_safe(e, tmp, &oob_hotplug_list, head) { + list_del(&e->head); + fwnode_handle_put(e->fwnode); + kfree(e); + } + } +} + /** * drm_get_connector_type_name - return a string for connector type * @type: The connector type (DRM_MODE_CONNECTOR_*) @@ -816,6 +846,36 @@ void drm_connector_cleanup(struct drm_connector *connector) } EXPORT_SYMBOL(drm_connector_cleanup); +/** + * drm_connector_replay_oob_hotplug_event - send cached OOB HPD event + * @connector: the connector that should receive the event + * + * Send the cached out-of-band hotplug as a new out-of-band hotplug event. + */ +static void drm_connector_replay_oob_hotplug_event(struct drm_connector *connector) +{ + struct fwnode_handle *fwnode = connector->fwnode; + enum drm_connector_status status; + struct drm_oob_hotplug_state *e; + bool found = false; + + if (!fwnode || !connector->funcs->oob_hotplug_event) + return; + + scoped_guard(mutex, &oob_hotplug_list_lock) { + list_for_each_entry(e, &oob_hotplug_list, head) { + if (e->fwnode == fwnode || fwnode->secondary == e->fwnode) { + status = e->status; + found = true; + break; + } + } + } + + if (found) + connector->funcs->oob_hotplug_event(connector, status); +} + /** * drm_connector_register - register a connector * @connector: the connector to register @@ -836,6 +896,7 @@ EXPORT_SYMBOL(drm_connector_cleanup); */ int drm_connector_register(struct drm_connector *connector) { + bool replay_oob_hotplug = false; int ret = 0; if (!connector->dev->registered) @@ -875,6 +936,7 @@ int drm_connector_register(struct drm_connector *connector) mutex_lock(&connector_list_lock); list_add_tail(&connector->global_connector_list_entry, &connector_list); mutex_unlock(&connector_list_lock); + replay_oob_hotplug = true; goto unlock; err_late_register: @@ -885,6 +947,10 @@ int drm_connector_register(struct drm_connector *connector) drm_sysfs_connector_remove(connector); unlock: mutex_unlock(&connector->mutex); + + if (replay_oob_hotplug) + drm_connector_replay_oob_hotplug_event(connector); + return ret; } EXPORT_SYMBOL(drm_connector_register); @@ -3501,6 +3567,41 @@ struct drm_connector *drm_connector_find_by_fwnode(struct fwnode_handle *fwnode) return found; } +/** + * drm_connector_record_oob_hotplug_status - Cache OOB hotplug status + * @fwnode - fwnode for the DRM connector + * @status - out-of-band status info + * + * Cache the latest out-of-band hotplug status for a fwnode so it can be + * (re)played from when the DRM device is (re)registered after this event + * arrived. + */ +static void drm_connector_record_oob_hotplug_status(struct fwnode_handle *fwnode, + enum drm_connector_status status) +{ + struct drm_oob_hotplug_state *e; + + if (!fwnode) + return; + + guard(mutex)(&oob_hotplug_list_lock); + + list_for_each_entry(e, &oob_hotplug_list, head) { + if (e->fwnode == fwnode) { + e->status = status; + return; + } + } + + e = kzalloc(sizeof(*e), GFP_KERNEL); + if (!e) + return; + + e->fwnode = fwnode_handle_get(fwnode); + e->status = status; + list_add_tail(&e->head, &oob_hotplug_list); +} + /** * drm_connector_oob_hotplug_event - Report out-of-band hotplug event to connector * @connector_fwnode: fwnode_handle to report the event on @@ -3513,12 +3614,17 @@ struct drm_connector *drm_connector_find_by_fwnode(struct fwnode_handle *fwnode) * * This function can be used to report these out-of-band events after obtaining * a drm_connector reference through calling drm_connector_find_by_fwnode(). + * + * The last status for each fwnode is cached and replayed when a matching DRM + * connector device is (re)registered. */ void drm_connector_oob_hotplug_event(struct fwnode_handle *connector_fwnode, enum drm_connector_status status) { struct drm_connector *connector; + drm_connector_record_oob_hotplug_status(connector_fwnode, status); + connector = drm_connector_find_by_fwnode(connector_fwnode); if (IS_ERR(connector)) return; diff --git a/drivers/gpu/drm/drm_crtc_internal.h b/drivers/gpu/drm/drm_crtc_internal.h index 83146ffef00cd2..c2714ea256a7fa 100644 --- a/drivers/gpu/drm/drm_crtc_internal.h +++ b/drivers/gpu/drm/drm_crtc_internal.h @@ -188,6 +188,7 @@ int drm_mode_getencoder(struct drm_device *dev, /* drm_connector.c */ void drm_connector_ida_init(void); void drm_connector_ida_destroy(void); +void drm_connector_oob_hotplug_cleanup(void); void drm_connector_unregister_all(struct drm_device *dev); int drm_connector_register_all(struct drm_device *dev); int drm_connector_set_obj_prop(struct drm_mode_object *obj, diff --git a/drivers/gpu/drm/drm_drv.c b/drivers/gpu/drm/drm_drv.c index 675675480da49e..f1952be857c7ce 100644 --- a/drivers/gpu/drm/drm_drv.c +++ b/drivers/gpu/drm/drm_drv.c @@ -1235,6 +1235,7 @@ static void drm_core_exit(void) drm_sysfs_destroy(); WARN_ON(!xa_empty(&drm_minors_xa)); drm_connector_ida_destroy(); + drm_connector_oob_hotplug_cleanup(); } static int __init drm_core_init(void) From f11636a37717cc3c5a5b5a18edb328119373c26c Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:38 +0200 Subject: [PATCH 179/258] 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 8014b9f8253edf..2fa66c0cb5c68f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23424,6 +23424,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 6ef8dc66edced626030ceedcf03a706133a1a8cc Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:39 +0200 Subject: [PATCH 180/258] 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 2fa66c0cb5c68f..1114e651f44724 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23430,6 +23430,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 5477d975f3ac781f65f5fed56036e78aab5360db Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:40 +0200 Subject: [PATCH 181/258] 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 1114e651f44724..d68781877f3be5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23431,6 +23431,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 49b8f9a48fa19a0fc54ee1c495eeededbef57912 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:41 +0200 Subject: [PATCH 182/258] 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 d68781877f3be5..f27ca1435db5fa 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23430,6 +23430,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 bda18810d7bb5758d4d138f367f414c6dc24aa95 Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:42 +0200 Subject: [PATCH 183/258] 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 e3bd4fca9b5659a9b453ae18c33b5c70a293181e Mon Sep 17 00:00:00 2001 From: Nicolas Frattaroli Date: Mon, 20 Apr 2026 15:52:43 +0200 Subject: [PATCH 184/258] 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 f5aa494684ac782d464b59a5bb52a0bf236166c2 Mon Sep 17 00:00:00 2001 From: Anton Burticica Date: Tue, 23 Dec 2025 11:34:32 +0400 Subject: [PATCH 185/258] 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 5f6a95cc6cc9b95eec5b72413846c0d12aa505b6 Mon Sep 17 00:00:00 2001 From: Pavel Zhovner Date: Mon, 29 Dec 2025 17:11:42 +0000 Subject: [PATCH 186/258] 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 195bfcc12c170b4e7f923e00425ddcf0edb88970 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 19:03:10 +0400 Subject: [PATCH 187/258] 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 ab2e08b2687f8307387a02373b2a3999ad912a7a Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 14 Jan 2026 12:30:40 +0400 Subject: [PATCH 188/258] 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 2434eca06ae942b5cd8db60ae9b427a92ee495bb Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 26 Jan 2026 18:40:51 +0400 Subject: [PATCH 189/258] 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 41aabbdde79ed2714857c300d875be9716274b15 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 13:36:56 +0400 Subject: [PATCH 190/258] 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 19ba72ab5e9a3ae76750c7b4abf7571d11003a51 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 2 Feb 2026 15:42:13 +0400 Subject: [PATCH 191/258] 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 88e9c5a2c5ea5bac1e0ad6a65d5e05bae66c9b43 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:31:15 +0400 Subject: [PATCH 192/258] 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 cdc81a51dd6c4bbba374c7ca92a9268e910dcd28 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:38:30 +0400 Subject: [PATCH 193/258] 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 d9cdd24a0e9ae2e421d623b9fee521eb53fdf204 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 12:30:56 +0400 Subject: [PATCH 194/258] 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 df3280c1c982bbed8d3c74a5653c7182e4938538 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:40:17 +0400 Subject: [PATCH 195/258] 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 ebb87935903426eec565f906b81296b532053b33 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 27 Jan 2026 11:41:58 +0400 Subject: [PATCH 196/258] 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 | 96 + .../dts/rockchip/rk3576-flipper-one-sata.dtso | 16 + .../boot/dts/rockchip/rk3576-flipper-one.dtsi | 1764 +++++++++++++++++ 6 files changed, 2000 insertions(+) create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-i2c2-free.dtso create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one-sata.dtso create mode 100644 arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index 761d82b4f4f2ac..83ac2a9b0e9726 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -170,6 +170,10 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-armsom-sige5-v1.2-wifibt.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb2-v10.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b0c1.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b1c2.dtb +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-i2c2-free.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-sata.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-khadas-edge-2l.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-luckfox-omni3576.dtb dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-nanopi-m5.dtb @@ -299,6 +303,14 @@ dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-evb1-v10-pcie1.dtb rk3576-evb1-v10-pcie1-dtbs := rk3576-evb1-v10.dtb \ rk3576-evb1-v10-pcie1.dtbo +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-i2c2-free.dtb +rk3576-flipper-one-i2c2-free-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-i2c2-free.dtbo + +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-sata.dtb +rk3576-flipper-one-sata-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-sata.dtbo + dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3588-edgeble-neu6a-wifi.dtb rk3588-edgeble-neu6a-wifi-dtbs := rk3588-edgeble-neu6a-io.dtb \ rk3588-edgeble-neu6a-wifi.dtbo diff --git a/arch/arm64/boot/dts/rockchip/rk3576-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..f7a0f8dc005696 --- /dev/null +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -0,0 +1,96 @@ +// 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>; + enable-active-high; + 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 0cfcd1cbafc998d922fb9b6f4e9919f2777d7f14 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 20 Jul 2026 16:33:33 +0400 Subject: [PATCH 197/258] 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 844dbdb2f8b33c2ca20f135b00ec606aa1d24543 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 11 Feb 2026 17:50:37 +0400 Subject: [PATCH 198/258] 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 cf2e402668d3ccd2eaf26a38b8bceaa06f5ad9c8 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:15:16 +0400 Subject: [PATCH 199/258] 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 f44dd4a6ce5c7940c4a7db49504c9486ab55e677 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 18 Feb 2026 15:46:02 +0400 Subject: [PATCH 200/258] 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 ff429bfb639f58c6c4e78376f494b7c16c04c5a7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 19 Feb 2026 17:29:09 +0400 Subject: [PATCH 201/258] 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 f27ca1435db5fa..4c7c6efc2b6f57 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12693,6 +12693,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 1105a189173e19157052860a0a9b9f91342806fb Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 20 Feb 2026 16:48:29 +0400 Subject: [PATCH 202/258] 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 4c7c6efc2b6f57..d824cea3e2695c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12698,6 +12698,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 d49a692dcaca6e841053a2fc7e9fd2ccd55e3b9b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 13 Mar 2026 13:02:31 +0400 Subject: [PATCH 203/258] 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 8cdd84ca5d6f76..7f95cb7fff83e7 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 2178291919292ed16dee8b448f3617036200afe0 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:49:48 +0400 Subject: [PATCH 204/258] 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 d824cea3e2695c..e732745f607b95 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -10137,6 +10137,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..d50defed48a775 --- /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 0x0510 +#define FOMCU_REG_UCSI_MESSAGE_OUT 0x0610 +#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 71810850f6dbcd526a0e0c0a560e5aa6fc296bad Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 25 Mar 2026 11:50:54 +0400 Subject: [PATCH 205/258] 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 ef601da1a5c742532831aab57f38cc57e97b2a2b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Apr 2026 14:15:26 +0400 Subject: [PATCH 206/258] 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 9c85a22cf38a3b7bad4636853bfbbf28d8755e73 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 15:02:56 +0400 Subject: [PATCH 207/258] 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 1641090fec263fa6aee840cc3aac0210bb1b4fe7 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 18 Jun 2026 18:29:58 +0400 Subject: [PATCH 208/258] 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 8ffed10ace0ffb945f3dac74289e9ad3b89cc831 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:50:45 +0400 Subject: [PATCH 209/258] 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 95f95bb3b6603e41aa3e408080d93e77c8fed6ea Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 23 Jul 2026 13:24:14 +0400 Subject: [PATCH 210/258] 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 5c321ffb1145ca34dfc2c7075e62693652f20642 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Jul 2026 22:23:07 +0400 Subject: [PATCH 211/258] 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 86953f9ffee388..be3658f8ac7438 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 587d9f0cb7d6ceceed89aa37dd53ee5904d37c83 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 21 Apr 2026 13:51:32 +0400 Subject: [PATCH 212/258] 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 864db163d88d047e05119f2e3b5f1475bae455d1 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 15 Apr 2026 18:46:19 +0400 Subject: [PATCH 213/258] 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 e33384871c001528b3dfb340ad73df0a967a40ed Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 20 Apr 2026 21:08:31 +0400 Subject: [PATCH 214/258] 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 5d1d5120a918e5f203ed6e31236cf7a33b53047c Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 19 May 2026 17:29:49 +0400 Subject: [PATCH 215/258] 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 184e95c1625e57..9dd96a410ad72d 100644 --- a/drivers/bluetooth/btusb.c +++ b/drivers/bluetooth/btusb.c @@ -4112,6 +4112,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 d4aabbe92724898d269f6b3f6336a86e9ea344c5 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 1 Jun 2026 15:22:06 +0400 Subject: [PATCH 216/258] 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 9a171f8f1d08ff6bf4fe9305d18057e8e12c6be4 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 2 Jun 2026 16:58:06 +0400 Subject: [PATCH 217/258] 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 8cfc9524bfe060f0484be4ea80d7c8effea0e083 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 10 Apr 2026 00:53:11 +0400 Subject: [PATCH 218/258] 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 d9c396a0a52dd225b551d60ba9f3e86cd8173e65 Mon Sep 17 00:00:00 2001 From: Simon Wright Date: Wed, 10 Jun 2026 23:54:26 +1200 Subject: [PATCH 219/258] 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 365c854b26ad08ba1eb83cd9416b4e836d7c29ef Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 7 Jul 2026 12:05:55 +0400 Subject: [PATCH 220/258] 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 74d763fc649487b1959799e894cf340e8a2d8d9f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 22 Jun 2026 18:37:11 +0400 Subject: [PATCH 221/258] 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 ba560e9289108ec92ef36d785ad591d108f08228 Mon Sep 17 00:00:00 2001 From: Yury Smirnov Date: Fri, 10 Jul 2026 14:31:36 +0200 Subject: [PATCH 222/258] 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 a5021f4ea7c027702bcb6e5cf9572892405dca92 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 16 Jul 2026 17:10:14 +0400 Subject: [PATCH 223/258] 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 50ffe38abb1cd037a515d79b6b74eedf3d2c5afc Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Mon, 27 Jul 2026 20:26:27 +0000 Subject: [PATCH 224/258] 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 b914a452fb2d9319acddda8a5e1b5c95cec1dc23 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Tue, 28 Jul 2026 18:02:26 +0400 Subject: [PATCH 225/258] 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 ea5ce68b74bd5f7dff59b66c3cdf6e2dbab36778 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Tue, 28 Jul 2026 11:12:52 -0500 Subject: [PATCH 226/258] 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 d659cd3bfc0fc2e14e5c731fd3bec2d6dfd96687 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 17 Aug 2026 16:07:41 +0400 Subject: [PATCH 227/258] power: supply: bq257xx: Don't ignore errors from bq257xx_get_state() The callback function bq257xx_get_state() can return an error code when its regmap access fails, but its caller bq257xx_external_power_changed() was ignoring those. Return early on errors and propagate the error code to the caller. Fixes: 1cc017b7f9c7 ("power: supply: bq257xx: Add support for BQ257XX charger") Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 7d02169248b17f..5b28330f792247 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -1053,7 +1053,9 @@ static void bq257xx_external_power_changed(struct power_supply *psy) int ret; int imax = pdata->iindpm_max; - pdata->chip->bq257xx_get_state(pdata); + ret = pdata->chip->bq257xx_get_state(pdata); + if (ret) + return; pdata->supplied = power_supply_am_i_supplied(pdata->charger); if (pdata->supplied < 0) From a02ed286aaa3d718fcda9be27f4c6e65c693b30f Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 17 Aug 2026 16:14:32 +0400 Subject: [PATCH 228/258] power: supply: bq257xx: Use psy directly instead of driver data Improve consistency in the bq257xx_external_power_changed() function by using the psy pointer directly instead of dereferencing the driver data. This makes all power_supply_*() calls follow the same pattern instead of power_supply_am_i_supplied() standing out for no reason. Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 5b28330f792247..02e66cbc4f2711 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -1057,7 +1057,7 @@ static void bq257xx_external_power_changed(struct power_supply *psy) if (ret) return; - pdata->supplied = power_supply_am_i_supplied(pdata->charger); + pdata->supplied = power_supply_am_i_supplied(psy); if (pdata->supplied < 0) return; From 820c4c37a0bc08e34503f038918574d09d0b0b46 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 17 Aug 2026 17:15:43 +0400 Subject: [PATCH 229/258] power: supply: core: prevent unregistering a power supply while a callback runs Once a power supply is registered, its callbacks can immediately start firing from other contexts, such as external_power_changed() triggered by the TCPM stack. If a power supply is unregistered while the callback is still running, the driver data can already be freed when the callback tries to access it, leading to a use-after-free. This happens e.g. when the hardware bus carrying the power supply device malfunctions (e.g. I2C is hogged down by another malfunctioning device) immediately after the power supply is registered, and thus the core is still processing the callbacks which were queued up when the driver starts the removal, leading in some cases to a kernel crash, e.g.: [ 11.645942] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000005 [ 11.646751] Mem abort info: [ 11.647006] ESR = 0x0000000096000004 [ 11.647338] EC = 0x25: DABT (current EL), IL = 32 bits [ 11.647806] SET = 0, FnV = 0 [ 11.648077] EA = 0, S1PTW = 0 [ 11.648356] FSC = 0x04: level 0 translation fault [ 11.648785] Data abort info: [ 11.649041] ISV = 0, ISS = 0x00000004, ISS2 = 0x00000000 [ 11.649524] CM = 0, WnR = 0, TnD = 0, TagAccess = 0 [ 11.649981] GCS = 0, Overlay = 0, DirtyBit = 0 [ 11.650390] [0000000000000005] user address but active_mm is swapper [ 11.650955] Internal error: Oops: 0000000096000004 [#1] SMP [ 11.651460] Modules linked in: [ 11.651742] CPU: 1 UID: 0 PID: 144 Comm: kworker/1:2 Not tainted 7.2.0-rc6-g62a9297af2cd #1 PREEMPT [ 11.652553] Hardware name: Flipper One rev. F0B1C2 (DT) [ 11.653024] Workqueue: events power_supply_changed_work [ 11.653511] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--) [ 11.654135] pc : __power_supply_is_supplied_by+0x18/0x100 [ 11.654624] lr : __power_supply_am_i_supplied+0x40/0xb8 [ 11.655098] sp : ffff80008192bb30 [ 11.655399] x29: ffff80008192bb30 x28: 0000000000000000 x27: 0000000000000000 [ 11.656049] x26: 0000000000000000 x25: 0000000000000000 x24: 0000000000000000 [ 11.656695] x23: ffff0000c19f4200 x22: ffffdb2232ba4ea8 x21: ffff80008192bc28 [ 11.657344] x20: ffff0000c1eef000 x19: ffff80008192bc18 x18: 00000000a0886e62 [ 11.657991] x17: 000000040044ffff x16: 04500072b5503510 x15: 0000000000000000 [ 11.658639] x14: 0000000000000000 x13: 0000000000000220 x12: 0000000000000000 [ 11.659286] x11: 0000000000000000 x10: ffff0000c1fdb2b0 x9 : ffffdb2232ba5590 [ 11.659934] x8 : 00000000e5b906e6 x7 : ffff0000c2502778 x6 : ffffdb22339793d0 [ 11.660581] x5 : ffff80008192bc18 x4 : ffff0000c19cbca0 x3 : 0000000000000000 [ 11.661228] x2 : ffff0000c1fdaf40 x1 : ffffffffffffffed x0 : ffff0000c1eef000 [ 11.661878] Call trace: [ 11.662103] __power_supply_is_supplied_by+0x18/0x100 (P) [ 11.662596] __power_supply_am_i_supplied+0x40/0xb8 [ 11.663040] psy_for_each_psy_cb+0x20/0x40 [ 11.663416] class_for_each_device+0x110/0x150 [ 11.663825] power_supply_am_i_supplied+0x68/0x100 [ 11.664262] bq257xx_external_power_changed+0x58/0x140 [ 11.664733] __power_supply_changed_work+0x60/0x80 [ 11.665170] psy_for_each_psy_cb+0x20/0x40 [ 11.665545] class_for_each_device+0x110/0x150 [ 11.665953] power_supply_changed_work+0x98/0x1b8 [ 11.666382] process_one_work+0x164/0x4c0 [ 11.666758] worker_thread+0x19c/0x320 [ 11.667104] kthread+0x138/0x150 [ 11.667408] ret_from_fork+0x10/0x20 [ 11.667744] Code: d503233f a9bd7bfd 910003fd a90153f3 (f9400c34) [ 11.668294] ---[ end trace 0000000000000000 ]--- Add a read-write semaphore between external_power_changed() and power_supply_unregister() to prevent the latter from returning (and thus the driver from freeing its data) while the callback is still running. Fixes: bc1540561c9e ("power_supply: Add API for safe access of power supply function attrs") Signed-off-by: Alexey Charkov --- drivers/power/supply/power_supply_core.c | 26 ++++++++++++++++++++++-- include/linux/power_supply.h | 9 ++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/drivers/power/supply/power_supply_core.c b/drivers/power/supply/power_supply_core.c index 2532e221b2e191..3a91a6686c4f68 100644 --- a/drivers/power/supply/power_supply_core.c +++ b/drivers/power/supply/power_supply_core.c @@ -1373,8 +1373,19 @@ int power_supply_property_is_writeable(struct power_supply *psy, void power_supply_external_power_changed(struct power_supply *psy) { - if (atomic_read(&psy->use_cnt) <= 0 || - !psy->desc->external_power_changed) + if (!psy->desc->external_power_changed) + return; + + /* + * Keep power_supply_unregister() from returning, and thus from letting + * the driver's data be freed, while the callback is running. The + * use_cnt check has to happen under the lock as well: on its own it + * only tells us the supply was registered when we looked, not that it + * still is by the time the callback dereferences its driver data. + */ + guard(rwsem_read)(&psy->epc_sem); + + if (atomic_read(&psy->use_cnt) <= 0) return; psy->desc->external_power_changed(psy); @@ -1621,6 +1632,7 @@ __power_supply_register(struct device *parent, } spin_lock_init(&psy->changed_lock); + init_rwsem(&psy->epc_sem); init_rwsem(&psy->extensions_sem); INIT_LIST_HEAD(&psy->extensions); @@ -1754,6 +1766,16 @@ void power_supply_unregister(struct power_supply *psy) { WARN_ON(atomic_dec_return(&psy->use_cnt)); psy->removing = true; + + /* + * use_cnt is now zero, so no new ->external_power_changed() call can + * start. Wait for one that is already running: it may be a *supplier's* + * changed_work, which cancel_work_sync() below does not cover, and it + * may still dereference driver data that the caller is about to free. + */ + down_write(&psy->epc_sem); + up_write(&psy->epc_sem); + cancel_work_sync(&psy->changed_work); cancel_delayed_work_sync(&psy->deferred_register_work); sysfs_remove_link(&psy->dev.kobj, "powers"); diff --git a/include/linux/power_supply.h b/include/linux/power_supply.h index 7a5e4c3242a01d..383943a236d47b 100644 --- a/include/linux/power_supply.h +++ b/include/linux/power_supply.h @@ -338,6 +338,15 @@ struct power_supply { bool removing; atomic_t use_cnt; struct power_supply_battery_info *battery_info; + /* + * Held for read while ->external_power_changed() runs, and for write by + * power_supply_unregister() so that it waits for an in-flight callback + * to finish. Without this a driver's data, typically devm-allocated on + * its own device, can be freed while the callback is still using it. + * Must not be shared with extensions_sem: callbacks may read their own + * properties, which takes that one for read. + */ + struct rw_semaphore epc_sem; struct rw_semaphore extensions_sem; /* protects "extensions" */ struct list_head extensions; #ifdef CONFIG_THERMAL From 4c172f3728c286a208d8bf1bdc65131c6fdb9962 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 17 Aug 2026 17:24:21 +0400 Subject: [PATCH 230/258] power: supply: core: Allow getting battery info before psy is registered Some power supplies, such as battery chargers, may need to program the device parameters based on what their connected battery allows. Current API requires registering the power supply to access battery information, which is problematic because a registered power supply is immediately available to the rest of the system, but the battery parameters may not be set yet in the charger. Given that the battery info helpers really only need a fwnode and a struct device to hang devres-allocated resourses on, add a pure dev-based get/put API alongside the existing psy-based one, which can be used to query the battery information before registering the power supply. Signed-off-by: Alexey Charkov --- drivers/power/supply/power_supply_core.c | 102 ++++++++++++++++------- include/linux/power_supply.h | 4 + 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/drivers/power/supply/power_supply_core.c b/drivers/power/supply/power_supply_core.c index 3a91a6686c4f68..9df55382991b87 100644 --- a/drivers/power/supply/power_supply_core.c +++ b/drivers/power/supply/power_supply_core.c @@ -572,21 +572,18 @@ struct power_supply *devm_power_supply_get_by_reference(struct device *dev, } EXPORT_SYMBOL_GPL(devm_power_supply_get_by_reference); -int power_supply_get_battery_info(struct power_supply *psy, - struct power_supply_battery_info **info_out) +static int __power_supply_get_battery_info(struct device *dev, + struct fwnode_handle *srcnode, + struct power_supply_battery_info **info_out) { struct power_supply_resistance_temp_table *resist_table; struct power_supply_battery_info *info; - struct fwnode_handle *srcnode, *fwnode; + struct fwnode_handle *fwnode; const char *value; int err, len, index, proplen; u32 *propdata __free(kfree) = NULL; u32 min_max[2]; - srcnode = dev_fwnode(&psy->dev); - if (!srcnode && psy->dev.parent) - srcnode = dev_fwnode(psy->dev.parent); - fwnode = fwnode_find_reference(srcnode, "monitored-battery", 0); if (IS_ERR(fwnode)) return PTR_ERR(fwnode); @@ -597,7 +594,7 @@ int power_supply_get_battery_info(struct power_supply *psy, /* Try static batteries first */ - err = samsung_sdi_battery_get_info(&psy->dev, value, &info); + err = samsung_sdi_battery_get_info(dev, value, &info); if (!err) goto out_ret_pointer; else if (err == -ENODEV) @@ -612,7 +609,7 @@ int power_supply_get_battery_info(struct power_supply *psy, goto out_put_node; } - info = devm_kzalloc(&psy->dev, sizeof(*info), GFP_KERNEL); + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); if (!info) { err = -ENOMEM; goto out_put_node; @@ -673,7 +670,7 @@ int power_supply_get_battery_info(struct power_supply *psy, else if (!strcmp("lithium-ion-manganese-oxide", value)) info->technology = POWER_SUPPLY_TECHNOLOGY_LiMn; else - dev_warn(&psy->dev, "%s unknown battery type\n", value); + dev_warn(dev, "%s unknown battery type\n", value); } fwnode_property_read_u32(fwnode, "energy-full-design-microwatt-hours", @@ -724,7 +721,7 @@ int power_supply_get_battery_info(struct power_supply *psy, err = len; goto out_put_node; } else if (len > POWER_SUPPLY_OCV_TEMP_MAX) { - dev_err(&psy->dev, "Too many temperature values\n"); + dev_err(dev, "Too many temperature values\n"); err = -EINVAL; goto out_put_node; } else if (len > 0) { @@ -739,28 +736,28 @@ int power_supply_get_battery_info(struct power_supply *psy, char *propname __free(kfree) = kasprintf(GFP_KERNEL, "ocv-capacity-table-%d", index); if (!propname) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = -ENOMEM; goto out_put_node; } proplen = fwnode_property_count_u32(fwnode, propname); if (proplen < 0 || proplen % 2 != 0) { - dev_err(&psy->dev, "failed to get %s\n", propname); - power_supply_put_battery_info(psy, info); + dev_err(dev, "failed to get %s\n", propname); + power_supply_put_battery_info_from_dev(dev, info); err = -EINVAL; goto out_put_node; } u32 *propdata __free(kfree) = kcalloc(proplen, sizeof(*propdata), GFP_KERNEL); if (!propdata) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = -EINVAL; goto out_put_node; } err = fwnode_property_read_u32_array(fwnode, propname, propdata, proplen); if (err < 0) { - dev_err(&psy->dev, "failed to get %s\n", propname); - power_supply_put_battery_info(psy, info); + dev_err(dev, "failed to get %s\n", propname); + power_supply_put_battery_info_from_dev(dev, info); goto out_put_node; } @@ -768,9 +765,9 @@ int power_supply_get_battery_info(struct power_supply *psy, info->ocv_table_size[index] = tab_len; info->ocv_table[index] = table = - devm_kcalloc(&psy->dev, tab_len, sizeof(*table), GFP_KERNEL); + devm_kcalloc(dev, tab_len, sizeof(*table), GFP_KERNEL); if (!info->ocv_table[index]) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = -ENOMEM; goto out_put_node; } @@ -786,14 +783,14 @@ int power_supply_get_battery_info(struct power_supply *psy, err = 0; goto out_ret_pointer; } else if (proplen < 0 || proplen % 2 != 0) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = (proplen < 0) ? proplen : -EINVAL; goto out_put_node; } propdata = kcalloc(proplen, sizeof(*propdata), GFP_KERNEL); if (!propdata) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = -ENOMEM; goto out_put_node; } @@ -801,17 +798,17 @@ int power_supply_get_battery_info(struct power_supply *psy, err = fwnode_property_read_u32_array(fwnode, "resistance-temp-table", propdata, proplen); if (err < 0) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); goto out_put_node; } info->resist_table_size = proplen / 2; - info->resist_table = resist_table = devm_kcalloc(&psy->dev, + info->resist_table = resist_table = devm_kcalloc(dev, info->resist_table_size, sizeof(*resist_table), GFP_KERNEL); if (!info->resist_table) { - power_supply_put_battery_info(psy, info); + power_supply_put_battery_info_from_dev(dev, info); err = -ENOMEM; goto out_put_node; } @@ -829,22 +826,69 @@ int power_supply_get_battery_info(struct power_supply *psy, fwnode_handle_put(fwnode); return err; } + +int power_supply_get_battery_info(struct power_supply *psy, + struct power_supply_battery_info **info_out) +{ + struct fwnode_handle *srcnode; + + srcnode = dev_fwnode(&psy->dev); + if (!srcnode && psy->dev.parent) + srcnode = dev_fwnode(psy->dev.parent); + + return __power_supply_get_battery_info(&psy->dev, srcnode, info_out); +} EXPORT_SYMBOL_GPL(power_supply_get_battery_info); -void power_supply_put_battery_info(struct power_supply *psy, - struct power_supply_battery_info *info) +/** + * power_supply_get_battery_info_from_dev() - Get battery info without a supply + * @dev: Device holding the "monitored-battery" reference, which also owns the + * devres allocations made for the returned info + * @info_out: Pointer to store the resulting battery info + * + * Same as power_supply_get_battery_info(), but keyed off a plain device rather + * than a registered power supply. Chargers that program hardware limits taken + * from the battery node need those values *before* they can safely register + * their power supply: registering makes the supply callable, so a later probe + * failure would free driver data underneath a running callback. + * + * Release the result with power_supply_put_battery_info_from_dev(). + * + * Return: 0 on success or an error code on failure. + */ +int power_supply_get_battery_info_from_dev(struct device *dev, + struct power_supply_battery_info **info_out) +{ + return __power_supply_get_battery_info(dev, dev_fwnode(dev), info_out); +} +EXPORT_SYMBOL_GPL(power_supply_get_battery_info_from_dev); + +/** + * power_supply_put_battery_info_from_dev() - Release battery info + * @dev: Device passed to power_supply_get_battery_info_from_dev() + * @info: Battery info to release + */ +void power_supply_put_battery_info_from_dev(struct device *dev, + struct power_supply_battery_info *info) { int i; for (i = 0; i < POWER_SUPPLY_OCV_TEMP_MAX; i++) { if (info->ocv_table[i]) - devm_kfree(&psy->dev, info->ocv_table[i]); + devm_kfree(dev, info->ocv_table[i]); } if (info->resist_table) - devm_kfree(&psy->dev, info->resist_table); + devm_kfree(dev, info->resist_table); + + devm_kfree(dev, info); +} +EXPORT_SYMBOL_GPL(power_supply_put_battery_info_from_dev); - devm_kfree(&psy->dev, info); +void power_supply_put_battery_info(struct power_supply *psy, + struct power_supply_battery_info *info) +{ + power_supply_put_battery_info_from_dev(&psy->dev, info); } EXPORT_SYMBOL_GPL(power_supply_put_battery_info); diff --git a/include/linux/power_supply.h b/include/linux/power_supply.h index 383943a236d47b..544ccb52c27f29 100644 --- a/include/linux/power_supply.h +++ b/include/linux/power_supply.h @@ -832,6 +832,10 @@ extern int power_supply_get_battery_info(struct power_supply *psy, struct power_supply_battery_info **info_out); extern void power_supply_put_battery_info(struct power_supply *psy, struct power_supply_battery_info *info); +extern int power_supply_get_battery_info_from_dev(struct device *dev, + struct power_supply_battery_info **info_out); +extern void power_supply_put_battery_info_from_dev(struct device *dev, + struct power_supply_battery_info *info); extern bool power_supply_battery_info_has_prop(struct power_supply_battery_info *info, enum power_supply_property psp); extern int power_supply_battery_info_get_prop(struct power_supply_battery_info *info, From 26b8b9f674c1ac3c0403d5325444d9f36f335fa3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 17 Aug 2026 17:38:48 +0400 Subject: [PATCH 231/258] power: supply: bq257xx: Parse battery info before registering power supply Switch to a dev-based battery get/put interface to parse battery info before registering the power supply, so that nobody tries to access the power supply until we finish programming the device parameters. Signed-off-by: Alexey Charkov --- drivers/power/supply/bq257xx_charger.c | 50 +++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/drivers/power/supply/bq257xx_charger.c b/drivers/power/supply/bq257xx_charger.c index 02e66cbc4f2711..2e00c932de8541 100644 --- a/drivers/power/supply/bq257xx_charger.c +++ b/drivers/power/supply/bq257xx_charger.c @@ -1165,38 +1165,38 @@ static const struct bq257xx_chip_info bq25792_chip_info = { /** * bq257xx_parse_dt() - Parse the device tree for required properties * @pdata: driver platform data - * @psy_cfg: power supply config data * @dev: device struct * * Read the device tree to identify the minimum system voltage, the * maximum charge current, the maximum charge voltage, and the maximum - * input current. + * input current. Deliberately keyed off @dev rather than the charger power + * supply, so that it can run before the supply is registered. * * Return: Returns 0 on success or error code on error. */ -static int bq257xx_parse_dt(struct bq257xx_chg *pdata, - struct power_supply_config *psy_cfg, struct device *dev) +static int bq257xx_parse_dt(struct bq257xx_chg *pdata, struct device *dev) { struct power_supply_battery_info *bat_info; int ret; - ret = power_supply_get_battery_info(pdata->charger, - &bat_info); + ret = power_supply_get_battery_info_from_dev(dev, &bat_info); if (ret) return dev_err_probe(dev, ret, "Unable to get battery info\n"); if ((bat_info->voltage_min_design_uv <= 0) || (bat_info->constant_charge_voltage_max_uv <= 0) || - (bat_info->constant_charge_current_max_ua <= 0)) + (bat_info->constant_charge_current_max_ua <= 0)) { + power_supply_put_battery_info_from_dev(dev, bat_info); return dev_err_probe(dev, -EINVAL, "Required bat info missing or invalid\n"); + } pdata->vsys_min = bat_info->voltage_min_design_uv; pdata->vbat_max = bat_info->constant_charge_voltage_max_uv; pdata->ichg_max = bat_info->constant_charge_current_max_ua; - power_supply_put_battery_info(pdata->charger, bat_info); + power_supply_put_battery_info_from_dev(dev, bat_info); ret = device_property_read_u32(dev, "input-current-limit-microamp", @@ -1212,9 +1212,14 @@ static int bq257xx_parse_dt(struct bq257xx_chg *pdata, * @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. + * appropriate chip-specific function pointers, parse device tree properties + * for battery limits, initialize hardware, register the power supply, and set + * up the interrupt handler if available. + * + * The power supply is registered only once the hardware is up, because + * registering it lets the core call ->external_power_changed() at any time. A + * probe failure after that point would have devres free @pdata while such a + * callback is still running on it. * * Return: Returns 0 on success or error code on failure. */ @@ -1247,6 +1252,14 @@ static int bq257xx_charger_probe(struct platform_device *pdev) platform_set_drvdata(pdev, pdata); + ret = bq257xx_parse_dt(pdata, dev); + if (ret) + return ret; + + ret = pdata->chip->bq257xx_hw_init(pdata); + if (ret) + return dev_err_probe(dev, ret, "Cannot initialize the charger\n"); + psy_cfg.drv_data = pdata; psy_cfg.fwnode = dev_fwnode(dev); @@ -1257,16 +1270,11 @@ static int bq257xx_charger_probe(struct platform_device *pdev) return dev_err_probe(dev, PTR_ERR(pdata->charger), "Power supply register charger failed\n"); - ret = bq257xx_parse_dt(pdata, &psy_cfg, dev); - if (ret) - return ret; - - ret = pdata->chip->bq257xx_hw_init(pdata); - if (ret) - return dev_err_probe(dev, ret, "Cannot initialize the charger\n"); - - platform_set_drvdata(pdev, pdata); - + /* + * Requested after the supply is registered so that devres tears it down + * first, quiescing the interrupt before the supply it reports on goes + * away. + */ if (bq->client->irq) { ret = devm_request_threaded_irq(dev, bq->client->irq, NULL, bq257xx_irq_handler_thread, From fb8a2cf4c73516c6f2cbfa70fd6efc8301319682 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Thu, 20 Aug 2026 12:30:32 +0400 Subject: [PATCH 232/258] arm64: dts: rockchip: rk3576: flipper-one: fix F0B1C2 Type-C2 regulator The upper Type-C is enabled by a combination of low levels of both MUX_EN and MUX_ID (via an onboard NOR gate), so the regulator should be enable-active-low, not high. Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts | 1 - 1 file changed, 1 deletion(-) 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 index f7a0f8dc005696..afeca09a0ae600 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -26,7 +26,6 @@ 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>; From 22c4cc5b850d1180ea0815d8c4082f9ac10a8080 Mon Sep 17 00:00:00 2001 From: Cole Munz Date: Wed, 29 Jul 2026 07:29:37 -0500 Subject: [PATCH 233/258] arm64: dts: rockchip: flipper-one: wire Type-C up port VBUS to the connector The hd3ss3220 driver reads the "vbus" regulator from the connector fwnode, not from its own node. With no "connector" child present it walks the graph instead: ep = fwnode_graph_get_next_endpoint(dev_fwnode(dev), NULL); connector = fwnode_graph_get_remote_port_parent(ep); ... vbus = devm_of_regulator_get_optional(dev, to_of_node(connector), "vbus"); so on F0B1C2 it lands on &typec_up_con. vbus-supply sits on &usbmux instead, the lookup returns -ENODEV, hd3ss3220->vbus stays NULL and hd3ss3220_regulator_control() is never reached. VBUS on the up port is only ever present because the regulator is marked always-on. The property is not valid where it currently sits either: usb-mux@47 (ti,hd3ss3220): 'vbus-supply' does not match any of the regexes: '^pinctrl-[0-9]+$' from schema $id: http://devicetree.org/schemas/usb/ti,hd3ss3220.yaml Move it to &typec_up_con, which usb-connector.yaml already describes as carrying vbus-supply, and drop regulator-always-on now that the driver switches the regulator with the port role. Signed-off-by: Cole Munz --- .../boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index afeca09a0ae600..22285e7b5e7f96 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -27,7 +27,6 @@ vusb_typec_up: regulator-vcc5v2-vusb-typec-up { compatible = "regulator-fixed"; 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>; @@ -68,6 +67,10 @@ }; }; +&typec_up_con { + vbus-supply = <&vusb_typec_up>; +}; + &uart1 { pinctrl-names = "default"; pinctrl-0 = <&uart1m1_xfer>; @@ -84,7 +87,6 @@ &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 { From 804fa73ee4325418e455592688b32610a8680105 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 21 Aug 2026 16:40:07 +0400 Subject: [PATCH 234/258] dt-bindings: usb: ti,hd3ss3220: Add support for supply regulators HD3SS3220 requires two supply regulators to operate. It also strictly requires that 5V is present before 3.3V, otherwise it gets backpowered in a non-functional state through the 3.3V rail and kills the I2C bus. Add both supply regulators to enable their explicit description in board device trees. Signed-off-by: Alexey Charkov --- .../devicetree/bindings/usb/ti,hd3ss3220.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Documentation/devicetree/bindings/usb/ti,hd3ss3220.yaml b/Documentation/devicetree/bindings/usb/ti,hd3ss3220.yaml index 06099e93c6c30d..654a982666ddb5 100644 --- a/Documentation/devicetree/bindings/usb/ti,hd3ss3220.yaml +++ b/Documentation/devicetree/bindings/usb/ti,hd3ss3220.yaml @@ -25,6 +25,15 @@ properties: interrupts: maxItems: 1 + vcc33-supply: + description: 3.3V supply (VCC33 pin), powering the SuperSpeed 2:1 MUX. + + vdd5-supply: + description: + 5V supply (VDD5 pin), powering the CC controller and sourcing VCONN. + VDD5 has to be stable for at least tVDD5V_PG (2ms) before VCC33 starts + ramping up, unless ENn_CC is held high while both rails ramp up. + id-gpios: description: An input gpio for USB ID pin. Upon detecting a UFP device, HD3SS3220 @@ -68,6 +77,8 @@ examples: reg = <0x47>; interrupt-parent = <&gpio6>; interrupts = <3>; + vcc33-supply = <&vcc3v3_control>; + vdd5-supply = <&vcc5v0_device_s0>; ports { #address-cells = <1>; From 6166033c87aa163ba0225fff94e1340a9f704eb3 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 21 Aug 2026 16:55:18 +0400 Subject: [PATCH 235/258] usb: typec: hd3ss3220: Add support for supply regulators HD3SS3220 requires VDD5 input to be present 2ms before VCC33 is applied, or else it gets backpowered via the 3.3V rail in a non-functional state and wedges the I2C bus, bringing down all devices on it. Enable both regulators in the datasheet prescribed sequence if provided. Signed-off-by: Alexey Charkov --- drivers/usb/typec/hd3ss3220.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/drivers/usb/typec/hd3ss3220.c b/drivers/usb/typec/hd3ss3220.c index 3e39b800e6b5f4..65e5e2e9705a09 100644 --- a/drivers/usb/typec/hd3ss3220.c +++ b/drivers/usb/typec/hd3ss3220.c @@ -49,6 +49,9 @@ #define HD3SS3220_REG_GEN_CTRL_MODE_SELECT_UFP BIT(4) #define HD3SS3220_REG_GEN_CTRL_MODE_SELECT_DRP (BIT(5) | BIT(4)) +/* Minimum time VDD5 has to be stable before VCC33 starts ramping up */ +#define HD3SS3220_TVDD5V_PG_US 2000 + struct hd3ss3220 { struct device *dev; struct regmap *regmap; @@ -358,6 +361,31 @@ static irqreturn_t hd3ss3220_id_isr(int irq, void *dev_id) return IRQ_HANDLED; } +/* + * Bring both supplies up in the order the datasheet asks for. Whenever the + * non-failsafe pins - I2C among them - are pulled up to a rail other than + * VDD5, powering VCC33 first lets them back-drive the device, which grounds + * the I2C bus and takes both this device and any others on the same bus down + */ +static int hd3ss3220_power_up(struct device *dev) +{ + int ret; + + ret = devm_regulator_get_enable_optional(dev, "vdd5"); + if (ret < 0 && ret != -ENODEV) + return dev_err_probe(dev, ret, "failed to enable VDD5\n"); + + /* Nothing to stagger against unless the board describes VCC33 too */ + if (!ret && device_property_present(dev, "vcc33-supply")) + fsleep(HD3SS3220_TVDD5V_PG_US); + + ret = devm_regulator_get_enable_optional(dev, "vcc33"); + if (ret < 0 && ret != -ENODEV) + return dev_err_probe(dev, ret, "failed to enable VCC33\n"); + + return 0; +} + static int hd3ss3220_probe(struct i2c_client *client) { struct typec_capability typec_cap = { }; @@ -379,6 +407,10 @@ static int hd3ss3220_probe(struct i2c_client *client) if (IS_ERR(hd3ss3220->regmap)) return PTR_ERR(hd3ss3220->regmap); + ret = hd3ss3220_power_up(hd3ss3220->dev); + if (ret) + return ret; + /* For backward compatibility check the connector child node first */ connector = device_get_named_child_node(hd3ss3220->dev, "connector"); if (connector) { From 99a245cb1a6681d9d3496876243aeb247640afdb Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 21 Aug 2026 17:23:58 +0400 Subject: [PATCH 236/258] arm64: dts: rockchip: flipper-one: Add Type-C mux supply regulators Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi index 73dba78035c9b2..562cc263dd9d09 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi @@ -1174,6 +1174,8 @@ usbmux: usb-mux@47 { compatible = "ti,hd3ss3220"; reg = <0x47>; + vcc33-supply = <&vcc3v3_control>; + vdd5-supply = <&vcc5v0_device_s0>; ports { #address-cells = <1>; From 0d222eaee6efa65348712ef8fcbf7913edbd9c68 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 21 Aug 2026 18:46:02 +0400 Subject: [PATCH 237/258] fixup! usb: typec: hd3ss3220: Add support for supply regulators --- drivers/usb/typec/hd3ss3220.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/drivers/usb/typec/hd3ss3220.c b/drivers/usb/typec/hd3ss3220.c index 65e5e2e9705a09..50903d9a45028c 100644 --- a/drivers/usb/typec/hd3ss3220.c +++ b/drivers/usb/typec/hd3ss3220.c @@ -371,16 +371,17 @@ static int hd3ss3220_power_up(struct device *dev) { int ret; - ret = devm_regulator_get_enable_optional(dev, "vdd5"); - if (ret < 0 && ret != -ENODEV) + ret = devm_regulator_get_enable(dev, "vdd5"); + if (ret) return dev_err_probe(dev, ret, "failed to enable VDD5\n"); - /* Nothing to stagger against unless the board describes VCC33 too */ - if (!ret && device_property_present(dev, "vcc33-supply")) + /* Nothing to stagger against unless the board describes both rails */ + if (device_property_present(dev, "vdd5-supply") && + device_property_present(dev, "vcc33-supply")) fsleep(HD3SS3220_TVDD5V_PG_US); - ret = devm_regulator_get_enable_optional(dev, "vcc33"); - if (ret < 0 && ret != -ENODEV) + ret = devm_regulator_get_enable(dev, "vcc33"); + if (ret) return dev_err_probe(dev, ret, "failed to enable VCC33\n"); return 0; From 38996835378db7304cc3bf525ed11585295ac8fb Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Fri, 21 Aug 2026 18:59:48 +0400 Subject: [PATCH 238/258] fixup! arm64: dts: rockchip: flipper-one: Add Type-C mux supply regulators --- .../rockchip/rk3576-flipper-one-rev-f0b0c1.dts | 1 + .../rockchip/rk3576-flipper-one-rev-f0b1c2.dts | 17 +++++++++++++++++ .../boot/dts/rockchip/rk3576-flipper-one.dtsi | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) 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 index 0b9648298be8b0..7f20474239b16e 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b0c1.dts @@ -46,6 +46,7 @@ interrupts = ; pinctrl-0 = <&usb_mux_int>; pinctrl-names = "default"; + vcc33-supply = <&vcc3v3_control>; }; &vcc5v0_device_s0 { 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 index 22285e7b5e7f96..f7c7c903cb7a98 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -11,6 +11,22 @@ model = "Flipper One rev. F0B1C2"; compatible = "flipper,one-rev-f0b1c2", "rockchip,rk3576"; + /* + * Gated off vcc5v0_device_s0 by a MOSFET whose gate is RC delayed, so + * that the mux sees VCC33 rise the prescribed time after VDD5. Shares + * the enable line with vcc5v0_device_s0 as that is what drives it. + */ + vcc3v3_usb_mux: regulator-vcc3v3-usb-mux { + compatible = "regulator-fixed"; + enable-active-high; + regulator-name = "vcc3v3_usb_mux"; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + gpios = <&gpio_expander 0xb GPIO_ACTIVE_HIGH>; + startup-delay-us = <200000>; + vin-supply = <&vcc3v3_control>; + }; + vcc3v3_wifi: regulator-vcc3v3-wifi { compatible = "regulator-fixed"; enable-active-high; @@ -87,6 +103,7 @@ &usbmux { interrupt-parent = <&gpio_expander>; interrupts = <4 IRQ_TYPE_LEVEL_LOW>; + vcc33-supply = <&vcc3v3_usb_mux>; }; &vcc5v0_device_s0 { diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi index 562cc263dd9d09..517fd0e9748962 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi @@ -1174,7 +1174,6 @@ usbmux: usb-mux@47 { compatible = "ti,hd3ss3220"; reg = <0x47>; - vcc33-supply = <&vcc3v3_control>; vdd5-supply = <&vcc5v0_device_s0>; ports { From 7a4e34eb2b3cc1e26e517e7ce74648c695f23924 Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Mon, 24 Aug 2026 16:04:23 +0400 Subject: [PATCH 239/258] arm64: dts: rockchip: rk3576-flipper-one: Enable overlays for F0B1C2 Signed-off-by: Alexey Charkov --- arch/arm64/boot/dts/rockchip/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/Makefile b/arch/arm64/boot/dts/rockchip/Makefile index f334196fe00356..53a4c03ef40b14 100644 --- a/arch/arm64/boot/dts/rockchip/Makefile +++ b/arch/arm64/boot/dts/rockchip/Makefile @@ -335,8 +335,12 @@ 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 \ +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b0c1-sata.dtb +rk3576-flipper-one-rev-f0b0c1-sata-dtbs := rk3576-flipper-one-rev-f0b0c1.dtb \ + rk3576-flipper-one-sata.dtbo + +dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-rev-f0b1c2-sata.dtb +rk3576-flipper-one-rev-f0b1c2-sata-dtbs := rk3576-flipper-one-rev-f0b1c2.dtb \ rk3576-flipper-one-sata.dtbo dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3576-flipper-one-uart2.dtb From 4503959d322d684d4138fda1fec8f8a58cd5a58b Mon Sep 17 00:00:00 2001 From: Alexey Charkov Date: Wed, 26 Aug 2026 14:14:01 +0400 Subject: [PATCH 240/258] arm64: dts: rockchip: flipper-one: Force physical SIM on F0B1C2 Signed-off-by: Alexey Charkov --- .../rk3576-flipper-one-rev-f0b1c2.dts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 index f7c7c903cb7a98..84b71d5888a85d 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one-rev-f0b1c2.dts @@ -62,6 +62,23 @@ vin-supply = <&vcc8v4_sys>; }; +&gpio1 { + pinctrl-names = "default"; + pinctrl-0 = <&sim_sel>; + + sim-sel-hog { + gpios = ; + output-high; + line-name = "Force physical SIM instead of eSIM"; + gpio-hog; + }; +}; + +&gpio2 { + pinctrl-names = "default"; + pinctrl-0 = <&sim_cd>; +}; + &hub_2_0 { vdd3v3-supply = <&vcc_3v3_s3>; }; @@ -76,6 +93,16 @@ }; &pinctrl { + sim { + sim_cd: sim-cd { + rockchip,pins = <2 RK_PB2 RK_FUNC_GPIO &pcfg_pull_none>; + }; + + sim_sel: sim-sel { + rockchip,pins = <1 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>; + }; + }; + wifi { wifi_pwr_en: wifi-pwr-en { rockchip,pins = <2 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>; From 18b6acd44373746927a32ef7862c77ddfe254950 Mon Sep 17 00:00:00 2001 From: Shuvam Pandey Date: Wed, 1 Jul 2026 10:15:52 -0700 Subject: [PATCH 241/258] accel/rocket: initialize job domain before cleanup paths rocket_ioctl_submit_job() releases rjob through rocket_job_put() on allocation error paths. rocket_job_cleanup() unconditionally calls rocket_iommu_domain_put(job->domain), but job->domain is assigned only after task copying and BO lookups. A failure before that assignment can therefore clean up a job with a NULL domain pointer. Take the per-file domain reference before the first error path can release rjob. Also clear rjob->tasks after freeing it in rocket_copy_tasks(), so the common cleanup path cannot free the task array again after a task-copy error. Fixes: 0810d5ad88a1 ("accel/rocket: Add job submission IOCTL") Cc: stable@vger.kernel.org Signed-off-by: Shuvam Pandey Link: https://lore.kernel.org/r/6a454b48.6a8fa39a.27019b.984b@mx.google.com Signed-off-by: Tomeu Vizoso --- drivers/accel/rocket/rocket_job.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index ac51bff39833fa..998fb8199c003c 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -102,6 +102,7 @@ rocket_copy_tasks(struct drm_device *dev, fail: kvfree(rjob->tasks); + rjob->tasks = NULL; return ret; } @@ -549,6 +550,7 @@ static int rocket_ioctl_submit_job(struct drm_device *dev, struct drm_file *file kref_init(&rjob->refcount); rjob->rdev = rdev; + rjob->domain = rocket_iommu_domain_get(file_priv); ret = drm_sched_job_init(&rjob->base, &file_priv->sched_entity, @@ -574,8 +576,6 @@ static int rocket_ioctl_submit_job(struct drm_device *dev, struct drm_file *file rjob->out_bo_count = job->out_bo_handle_count; - rjob->domain = rocket_iommu_domain_get(file_priv); - ret = rocket_job_push(rjob); if (ret) goto out_cleanup_job; From 4ac54d3d166e2c2ce6e4cccc5386189c05f399a7 Mon Sep 17 00:00:00 2001 From: Muhammad Bilal Date: Sun, 24 May 2026 15:57:16 +0000 Subject: [PATCH 242/258] accel/rocket: fix NULL dereference and integer overflow in rocket_job_push() rocket_job_push() allocates a temporary array to hold all input and output GEM object pointers: bos = kvmalloc_array(job->in_bo_count + job->out_bo_count, sizeof(void *), GFP_KERNEL); memcpy(bos, job->in_bos, job->in_bo_count * sizeof(void *)); memcpy(&bos[job->in_bo_count], job->out_bos, ...); Two bugs exist: 1. Missing NULL check: if kvmalloc_array() fails, bos is NULL and the subsequent memcpy() dereferences it, causing a kernel NULL pointer dereference. 2. Integer overflow: in_bo_count and out_bo_count are both u32, set directly from userspace-supplied in_bo_handle_count and out_bo_handle_count with no prior validation. Their sum is computed in u32 arithmetic and can wrap to a smaller value, causing the allocation count passed to kvmalloc_array() to be smaller than intended. Subsequent uses still operate on the original counts when copying and locking objects, which may lead to out-of-bounds accesses on the temporary array. Fix by using check_add_overflow() to detect count overflow before the allocation, and adding a NULL check on the allocation result. Fixes: 0810d5ad88a1 ("accel/rocket: Add job submission IOCTL") Cc: stable@vger.kernel.org Signed-off-by: Muhammad Bilal Link: https://lore.kernel.org/r/20260524155716.90955-1-meatuni001@gmail.com Signed-off-by: Tomeu Vizoso --- drivers/accel/rocket/rocket_job.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index 998fb8199c003c..e32386b5d26ae3 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -189,14 +190,19 @@ static int rocket_job_push(struct rocket_job *job) struct rocket_device *rdev = job->rdev; struct drm_gem_object **bos; struct ww_acquire_ctx acquire_ctx; + u32 bo_count; int ret = 0; - bos = kvmalloc_array(job->in_bo_count + job->out_bo_count, sizeof(void *), - GFP_KERNEL); + if (check_add_overflow(job->in_bo_count, job->out_bo_count, &bo_count)) + return -EINVAL; + + bos = kvmalloc_array(bo_count, sizeof(*bos), GFP_KERNEL); + if (!bos) + return -ENOMEM; memcpy(bos, job->in_bos, job->in_bo_count * sizeof(void *)); memcpy(&bos[job->in_bo_count], job->out_bos, job->out_bo_count * sizeof(void *)); - ret = drm_gem_lock_reservations(bos, job->in_bo_count + job->out_bo_count, &acquire_ctx); + ret = drm_gem_lock_reservations(bos, bo_count, &acquire_ctx); if (ret) goto err; @@ -221,7 +227,7 @@ static int rocket_job_push(struct rocket_job *job) rocket_attach_object_fences(job->out_bos, job->out_bo_count, job->inference_done_fence); err_unlock: - drm_gem_unlock_reservations(bos, job->in_bo_count + job->out_bo_count, &acquire_ctx); + drm_gem_unlock_reservations(bos, bo_count, &acquire_ctx); err: kvfree(bos); From ac54abee1fbcb6548dbff105767f7137a3c1ac56 Mon Sep 17 00:00:00 2001 From: ZhaoJinming Date: Wed, 10 Jun 2026 15:10:44 +0800 Subject: [PATCH 243/258] accel/rocket: Fix error path handling in rocket_job_run() In rocket_job_run(), after taking an extra fence reference for job->done_fence via dma_fence_get(), the error paths have three bugs: - The dma_fence reference held by job->done_fence is never released, causing a reference leak. - pm_runtime_get_sync() increments the usage counter even on failure, but the error path does not decrement it, leaking the runtime PM reference and preventing the NPU from suspending. - A valid but unsignaled fence is returned to the DRM scheduler, which triggers WARN("Fence ... released with pending signals!") when the scheduler drops its reference. Fix by replacing pm_runtime_get_sync() with pm_runtime_resume_and_get() which auto-balances the usage counter on failure, releasing both fence references on error, and returning ERR_PTR(ret) instead of the unsignaled fence. Cc: stable@vger.kernel.org Fixes: 0810d5ad88a1 ("accel/rocket: Add job submission IOCTL") Signed-off-by: ZhaoJinming Link: https://lore.kernel.org/r/20260610071045.3414828-1-zhaojinming@uniontech.com [tomeu: Refactored error paths to use consolidated goto labels] Signed-off-by: Tomeu Vizoso --- drivers/accel/rocket/rocket_job.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index e32386b5d26ae3..3141f210fcd1b5 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -317,13 +317,13 @@ static struct dma_fence *rocket_job_run(struct drm_sched_job *sched_job) dma_fence_put(job->done_fence); job->done_fence = dma_fence_get(fence); - ret = pm_runtime_get_sync(core->dev); + ret = pm_runtime_resume_and_get(core->dev); if (ret < 0) - return fence; + goto err_put_fences; ret = iommu_attach_group(job->domain->domain, core->iommu_group); if (ret < 0) - return fence; + goto err_put_pm; scoped_guard(mutex, &core->job_lock) { core->in_flight_job = job; @@ -331,6 +331,14 @@ static struct dma_fence *rocket_job_run(struct drm_sched_job *sched_job) } return fence; + +err_put_pm: + pm_runtime_put(core->dev); +err_put_fences: + dma_fence_put(job->done_fence); + job->done_fence = NULL; + dma_fence_put(fence); + return ERR_PTR(ret); } static void rocket_job_handle_irq(struct rocket_core *core) From 66b4ca1a494716f158144b1e67a397dfe5242176 Mon Sep 17 00:00:00 2001 From: Midgy BALON Date: Thu, 9 Jul 2026 01:46:14 +0200 Subject: [PATCH 244/258] pmdomain: rockchip: Add a regulator to the RK3568 NPU power domain The RK3568 NPU rail (vdd_npu) needs to be enabled before the domain is powered on and disabled after it is powered off. Give DOMAIN_RK3568 a regulator parameter (like DOMAIN_RK3588 already has) so the NPU domain can set need_regulator, letting genpd manage the rail wired up as the domain's domain-supply instead of marking it always-on in DT. Suggested-by: Chaoyi Chen Signed-off-by: Midgy BALON Reviewed-by: Sebastian Reichel Reviewed-by: Heiko Stuebner Signed-off-by: Ulf Hansson --- drivers/pmdomain/rockchip/pm-domains.c | 36 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/drivers/pmdomain/rockchip/pm-domains.c b/drivers/pmdomain/rockchip/pm-domains.c index 490bbb1d1d8e8f..ba66ae71942892 100644 --- a/drivers/pmdomain/rockchip/pm-domains.c +++ b/drivers/pmdomain/rockchip/pm-domains.c @@ -204,6 +204,20 @@ struct rockchip_pmu { .active_wakeup = wakeup, \ } +#define DOMAIN_M_R(_name, pwr, status, req, idle, ack, wakeup, regulator) \ +{ \ + .name = _name, \ + .pwr_w_mask = (pwr) << 16, \ + .pwr_mask = (pwr), \ + .status_mask = (status), \ + .req_w_mask = (req) << 16, \ + .req_mask = (req), \ + .idle_mask = (idle), \ + .ack_mask = (ack), \ + .active_wakeup = wakeup, \ + .need_regulator = regulator, \ +} + #define DOMAIN_RK3036(_name, req, ack, idle, wakeup) \ { \ .name = _name, \ @@ -241,8 +255,8 @@ struct rockchip_pmu { #define DOMAIN_RK3562(name, pwr, req, g_mask, mem, wakeup) \ DOMAIN_M_G_SD(name, pwr, pwr, req, req, req, g_mask, mem, wakeup, false) -#define DOMAIN_RK3568(name, pwr, req, wakeup) \ - DOMAIN_M(name, pwr, pwr, req, req, req, wakeup) +#define DOMAIN_RK3568(name, pwr, req, wakeup, regulator) \ + DOMAIN_M_R(name, pwr, pwr, req, req, req, wakeup, regulator) #define DOMAIN_RK3576(name, p_offset, pwr, status, r_status, r_offset, req, idle, g_mask, wakeup) \ DOMAIN_M_O_R_G(name, p_offset, pwr, status, 0, r_status, r_status, r_offset, req, idle, idle, g_mask, wakeup) @@ -1274,15 +1288,15 @@ static const struct rockchip_domain_info rk3562_pm_domains[] = { }; static const struct rockchip_domain_info rk3568_pm_domains[] = { - [RK3568_PD_NPU] = DOMAIN_RK3568("npu", BIT(1), BIT(2), false), - [RK3568_PD_GPU] = DOMAIN_RK3568("gpu", BIT(0), BIT(1), false), - [RK3568_PD_VI] = DOMAIN_RK3568("vi", BIT(6), BIT(3), false), - [RK3568_PD_VO] = DOMAIN_RK3568("vo", BIT(7), BIT(4), false), - [RK3568_PD_RGA] = DOMAIN_RK3568("rga", BIT(5), BIT(5), false), - [RK3568_PD_VPU] = DOMAIN_RK3568("vpu", BIT(2), BIT(6), false), - [RK3568_PD_RKVDEC] = DOMAIN_RK3568("vdec", BIT(4), BIT(8), false), - [RK3568_PD_RKVENC] = DOMAIN_RK3568("venc", BIT(3), BIT(7), false), - [RK3568_PD_PIPE] = DOMAIN_RK3568("pipe", BIT(8), BIT(11), false), + [RK3568_PD_NPU] = DOMAIN_RK3568("npu", BIT(1), BIT(2), false, true), + [RK3568_PD_GPU] = DOMAIN_RK3568("gpu", BIT(0), BIT(1), false, false), + [RK3568_PD_VI] = DOMAIN_RK3568("vi", BIT(6), BIT(3), false, false), + [RK3568_PD_VO] = DOMAIN_RK3568("vo", BIT(7), BIT(4), false, false), + [RK3568_PD_RGA] = DOMAIN_RK3568("rga", BIT(5), BIT(5), false, false), + [RK3568_PD_VPU] = DOMAIN_RK3568("vpu", BIT(2), BIT(6), false, false), + [RK3568_PD_RKVDEC] = DOMAIN_RK3568("vdec", BIT(4), BIT(8), false, false), + [RK3568_PD_RKVENC] = DOMAIN_RK3568("venc", BIT(3), BIT(7), false, false), + [RK3568_PD_PIPE] = DOMAIN_RK3568("pipe", BIT(8), BIT(11), false, false), }; static const struct rockchip_domain_info rk3576_pm_domains[] = { From 2730490d5ef1968ce64bb84a576362cb6cc0a6e6 Mon Sep 17 00:00:00 2001 From: Igor Paunovic Date: Wed, 29 Jul 2026 15:07:43 +0200 Subject: [PATCH 245/258] accel/rocket: request the core clocks by name rocket_core_init() hands core->clks to devm_clk_bulk_get() without ever setting the .id members. The rocket_core array is allocated with devm_kcalloc() in rocket_device_init(), and rocket_probe() only fills in .rdev, .dev and .index, so all four clk_bulk_data entries are requested with a NULL con_id (unlike core->resets, whose ids are set a few lines above). clk_get(dev, NULL) ends up in of_clk_get_hw(np, 0, NULL), and of_parse_clkspec() only consults "clock-names" when a name was passed, so the index stays 0 for all four entries. Every entry therefore ends up holding a handle to the *first* clock of the DT "clocks" property, i.e. ACLK_NPUn. Nothing fails: probe succeeds and the driver believes it owns four different clocks. The consequence is that rocket_device_runtime_resume() prepares and enables the AXI clock four times, while hclk, pclk and - most importantly - the NPU compute clock ("npu", SCMI_CLK_NPU on RK3588) are never prepared or enabled by this driver at all. The NPU still works only because the Rockchip power-domain driver sets GENPD_FLAG_PM_CLK and its attach_dev() callback walks the device node with of_clk_get() and adds every clock to the pm_clk list, so genpd happens to keep the remaining clocks running. The bug is therefore latent today, but it means the driver holds no reference to the clock that actually feeds the NPU, which stands in the way of any future frequency scaling (OPP/devfreq) work. Found on an Orange Pi 5 Plus (RK3588) by reading the live clock tree: /sys/kernel/debug/clk/clk_summary shows four "fdab0000.npu" consumer handles on aclk_npu0 (and likewise on aclk_npu1/aclk_npu2 for the other two cores), while hclk_npu0, pclk_npu_root and scmi_clk_npu have no "fdab0000.npu" consumer at all - their only consumers are the "npu@fdab0000" handles created by the power-domain driver via of_clk_get(). Set the ids explicitly, in the order mandated by the binding (Documentation/devicetree/bindings/npu/rockchip,rk3588-rknn-core.yaml): aclk, hclk, npu, pclk. After the change the driver holds one handle per distinct clock and clk_bulk_prepare_enable() covers all four. Note that this is a user-visible tightening for out-of-tree DTs: the old NULL-id requests resolved by index and succeeded no matter what "clock-names" contained, while the named requests fail probe with -ENOENT when one of the four names is missing. That is the right outcome for in-tree users - the binding requires exactly these four clock-names and rk3588-base.dtsi carries them on all three cores - but a DT that relied on the permissive lookup goes from silently running on the wrong clock handles to not probing at all, so record the change here where git log will find it. Fixes: ed98261b4168 ("accel/rocket: Add a new driver for Rockchip's NPU") Signed-off-by: Igor Paunovic Reviewed-by: Jiaxing Hu --- drivers/accel/rocket/rocket_core.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/accel/rocket/rocket_core.c b/drivers/accel/rocket/rocket_core.c index b3b2fa9ba645a6..5dd260bacbff61 100644 --- a/drivers/accel/rocket/rocket_core.c +++ b/drivers/accel/rocket/rocket_core.c @@ -28,6 +28,10 @@ int rocket_core_init(struct rocket_core *core) if (err) return dev_err_probe(dev, err, "failed to get resets for core %d\n", core->index); + core->clks[0].id = "aclk"; + core->clks[1].id = "hclk"; + core->clks[2].id = "npu"; + core->clks[3].id = "pclk"; err = devm_clk_bulk_get(dev, ARRAY_SIZE(core->clks), core->clks); if (err) return dev_err_probe(dev, err, "failed to get clocks for core %d\n", core->index); From 62da95c8a32fd69d6801640990f592ff9d2b9707 Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Wed, 12 Aug 2026 21:14:24 +1200 Subject: [PATCH 246/258] accel/rocket: take the completion register writes under job_lock rocket_job_handle_irq() writes OPERATION_ENABLE and INTERRUPT_CLEAR before taking job_lock, while rocket_job_hw_submit() writes OPERATION_ENABLE from inside it. The two can therefore race: a completion being handled on one core can write its zero after a submit on the same core has written its one, and stop a task that has only just started. Nothing in tree hits this often, because the interrupt is the only completion path and it does not overlap its own submit, but the ordering is wrong on its own terms. Move both writes inside the existing scoped_guard() rather than adding a second critical section, so stopping the block and deciding what to start next are one atomic step. Fixes: 0810d5ad88a1 ("accel/rocket: Add job submission IOCTL") Signed-off-by: Jiaxing Hu Tested-by: Igor Paunovic # RK3588, three cores --- drivers/accel/rocket/rocket_job.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index 3141f210fcd1b5..5f0f9682e57ca0 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -345,10 +345,15 @@ static void rocket_job_handle_irq(struct rocket_core *core) { pm_runtime_mark_last_busy(core->dev); - rocket_pc_writel(core, OPERATION_ENABLE, 0x0); - rocket_pc_writel(core, INTERRUPT_CLEAR, 0x1ffff); + scoped_guard(mutex, &core->job_lock) { + /* + * Stopping the block belongs under the lock. hw_submit() writes + * OPERATION_ENABLE too, and outside the lock this zero can land + * after that one and stop a task that has only just started. + */ + rocket_pc_writel(core, OPERATION_ENABLE, 0x0); + rocket_pc_writel(core, INTERRUPT_CLEAR, 0x1ffff); - scoped_guard(mutex, &core->job_lock) if (core->in_flight_job) { if (core->in_flight_job->next_task_idx < core->in_flight_job->task_count) { rocket_job_hw_submit(core, core->in_flight_job); @@ -360,6 +365,7 @@ static void rocket_job_handle_irq(struct rocket_core *core) pm_runtime_put_autosuspend(core->dev); core->in_flight_job = NULL; } + } } static void From 1c8263aa8dc59a21e8987d4a21c4de465605050c Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Mon, 17 Aug 2026 22:53:32 +1200 Subject: [PATCH 247/258] accel/rocket: wait for a running IRQ handler before resetting a core rocket_reset() calls drm_sched_stop(), which stops the scheduler and returns. It does not wait for a threaded handler that is already running, so the comment that follows, "Remaining interrupts have been handled", states an assumption rather than something the code arranges. Call synchronize_irq(core->irq) after drm_sched_stop() and reword the comment to say what holds afterwards. It has to go before the scoped_guard(mutex, &core->job_lock) rather than inside it. rocket_job_handle_irq() takes job_lock, so waiting for the handler while holding that lock would be waiting for a handler that is waiting for us. Nothing is held at that point, and both callers, rocket_job_timedout() and rocket_reset_work(), run in process context, so sleeping there is allowed. This does not stop a handler that has already read in_flight_job from finishing its work on the job the reset is about to drop. That window needs the check and the register writes to be one step under the lock, which is what the previous patch does; the two are complementary. Mask the block before the sync as well. INTERRUPT_MASK is armed by hw_submit() on every submit and cleared only by the hardirq, so on an ordinary timeout it is still live and a completion can arrive after synchronize_irq() returns. Nothing is lost by clearing it, since the next submit arms it again. That write is the first register access this function has ever made, and it is guarded, because the function holds no runtime PM reference of its own. The only reference in the window belongs to in_flight_job, and the completion path can have put it and cleared the pointer before the timeout worker arrives: drm_sched_stop() sits in between and can block on cancel_work_sync() and on a dma_fence_wait(), and it subtracts every pending job's credits, so rocket_job_is_idle() is true and rocket_device_runtime_suspend() will not refuse. With the autosuspend delay elapsed the clocks are off and both NPU domains are down. A register access in that state takes an async SError on this hardware, which is the failure two later patches in this series describe from the power-on side. pm_runtime_get_if_active() resumes nothing and allocates nothing; if the core is already down there is no live interrupt to mask and the following synchronize_irq() is all that is needed. Igor Paunovic asked the general form of this on v8 -- whether rocket_reset() should hold a reference -- and it was deferred then because nothing in the path touched a register. This patch is what makes it matter. The deadlock this placement avoids would not have been reported. The wait is on desc->wait_for_threads rather than on a lock, so lockdep does not model it and it would have hung silently. Suggested-by: Igor Paunovic Signed-off-by: Jiaxing Hu --- drivers/accel/rocket/rocket_job.c | 34 ++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index 5f0f9682e57ca0..3c0ed460506699 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -377,9 +377,37 @@ rocket_reset(struct rocket_core *core, struct drm_sched_job *bad) drm_sched_stop(&core->sched, bad); /* - * Remaining interrupts have been handled, but we might still have - * stuck jobs. Let's make sure the PM counters stay balanced by - * manually calling pm_runtime_put_noidle(). + * Mask the block before waiting. hw_submit() arms INTERRUPT_MASK on + * every submit and only the hardirq clears it, so on an ordinary + * timeout it is still live and a completion can arrive after the sync + * returns. The next submit re-arms it, so nothing is lost here. + * + * Only when the device is already awake, though. This function holds no + * runtime PM reference of its own: the only one in the window belongs to + * in_flight_job, and the completion path may have put it and cleared the + * pointer before the timeout worker got here. drm_sched_stop() above can + * block for a long time, and it drops every pending job's credits, so + * rocket_job_is_idle() is true and nothing keeps the core resumed. On + * this hardware a register access with the domain down takes an async + * SError, so a reset must not be the thing that causes one. + */ + if (pm_runtime_get_if_active(core->dev) > 0) { + rocket_pc_writel(core, INTERRUPT_MASK, 0x0); + pm_runtime_put_autosuspend(core->dev); + } + + /* + * drm_sched_stop() returns without waiting for a threaded handler that + * is already running, so wait for one here. This has to stay outside + * job_lock: the handler takes that lock, so waiting for it while + * holding it would deadlock instead of fencing anything. + */ + synchronize_irq(core->irq); + + /* + * No handler is running now, but we might still have stuck jobs. Let's + * make sure the PM counters stay balanced by manually calling + * pm_runtime_put_noidle(). */ scoped_guard(mutex, &core->job_lock) { if (core->in_flight_job) From 5b3430983ea265a9fef7fc7d31ef7462b0a35e6a Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Wed, 19 Aug 2026 19:11:26 +1200 Subject: [PATCH 248/258] accel/rocket: let the core suspend after a reset rocket_reset() drops the in-flight job's runtime PM reference with pm_runtime_put_noidle(), a bare decrement that requests nothing. The core is left at usage_count 0 but still runtime-active with no idle request pending, so it does not suspend until something else asks, and on a platform whose power domain does work on power-on that work never happens. On RK3576 that work is a bus interface reset the domain cycles when it comes up. Without it the NPU's IOMMU stops answering, and the job after a timeout returns a surface of the output zero point with rk_iommu reporting that MMU_DTE_ADDR is not functioning. Measured on a ROCK 4D in one boot, three runs, one variable between them. With the bare put the core reads runtime-active with its rail still up after the reset, the IOMMU reports the failure on the next attach and the inference returns 0 of 128 channels. With the reference put back through pm_runtime_put_autosuspend() the core reads suspended with the rail down, there is no IOMMU message, and the same inference returns 128 of 128. A third run repeating the first failed the same way. It also matches the put in the completion path a few lines away, so the reset path no longer leaves the device in a state the rest of the driver never produces. The remaining put, on the error path in rocket_job_run(), is a plain pm_runtime_put() and is left alone here: it unwinds a get_sync() that never reached the hardware, and changing it belongs in its own patch. Igor Paunovic ran the differential on RK3588: 45 induced resets across three cores, with and without the two preceding patches, and the domain dropped every single time with no MMU message on either kernel. So this is not rocket-wide. His conditions cross a healthy block with a lowered timeout rather than a hung one, which he was careful to say his protocol cannot settle, but it is what scopes the change to RK3576. Link: https://lore.kernel.org/all/20260819073530.6087-1-royalnet026@gmail.com/ Fixes: 0810d5ad88a1 ("accel/rocket: Add job submission IOCTL") Signed-off-by: Jiaxing Hu --- drivers/accel/rocket/rocket_job.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index 3c0ed460506699..a89ab49e17e529 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -406,12 +406,12 @@ rocket_reset(struct rocket_core *core, struct drm_sched_job *bad) /* * No handler is running now, but we might still have stuck jobs. Let's - * make sure the PM counters stay balanced by manually calling - * pm_runtime_put_noidle(). + * make sure the PM counters stay balanced by putting the reference the + * job took, and request idle while doing it so the core can suspend. */ scoped_guard(mutex, &core->job_lock) { if (core->in_flight_job) - pm_runtime_put_noidle(core->dev); + pm_runtime_put_autosuspend(core->dev); iommu_detach_group(NULL, core->iommu_group); From 8493b8b0e24bb348cba402e6ddfd0e7f8ee873e4 Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Mon, 17 Aug 2026 20:47:52 +1200 Subject: [PATCH 249/258] accel/rocket: factor the completion tail out of the IRQ handler rocket_job_handle_irq() stops the block and then either starts the job's next task or retires the job. The second half is a step of its own and reads better with a name, now that taking the register writes under job_lock has moved it a level deeper inside the scoped guard. Move it to rocket_job_next_locked(). The early return that used to leave the handler now leaves the helper, which is the same thing here: the scoped guard drops job_lock either way and nothing follows it. Doing it as its own patch keeps the locking fix at the head of the series minimal, so a bisect that stops before this one gets that fix and nothing else. There is one caller, and no functional change. Signed-off-by: Jiaxing Hu Reviewed-by: Igor Paunovic --- drivers/accel/rocket/rocket_job.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index a89ab49e17e529..69e29f40f27a08 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -341,6 +341,25 @@ static struct dma_fence *rocket_job_run(struct drm_sched_job *sched_job) return ERR_PTR(ret); } +/* Start the job's next task, or retire it. Caller holds job_lock. */ +static void rocket_job_next_locked(struct rocket_core *core) +{ + lockdep_assert_held(&core->job_lock); + + if (!core->in_flight_job) + return; + + if (core->in_flight_job->next_task_idx < core->in_flight_job->task_count) { + rocket_job_hw_submit(core, core->in_flight_job); + return; + } + + iommu_detach_group(NULL, iommu_group_get(core->dev)); + dma_fence_signal(core->in_flight_job->done_fence); + pm_runtime_put_autosuspend(core->dev); + core->in_flight_job = NULL; +} + static void rocket_job_handle_irq(struct rocket_core *core) { pm_runtime_mark_last_busy(core->dev); @@ -354,17 +373,7 @@ static void rocket_job_handle_irq(struct rocket_core *core) rocket_pc_writel(core, OPERATION_ENABLE, 0x0); rocket_pc_writel(core, INTERRUPT_CLEAR, 0x1ffff); - if (core->in_flight_job) { - if (core->in_flight_job->next_task_idx < core->in_flight_job->task_count) { - rocket_job_hw_submit(core, core->in_flight_job); - return; - } - - iommu_detach_group(NULL, iommu_group_get(core->dev)); - dma_fence_signal(core->in_flight_job->done_fence); - pm_runtime_put_autosuspend(core->dev); - core->in_flight_job = NULL; - } + rocket_job_next_locked(core); } } From ab4b974a9d924f39ceb0a220abd25d2597f9f87d Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Thu, 6 Aug 2026 17:41:12 +1200 Subject: [PATCH 250/258] dt-bindings: npu: rockchip: add rockchip,rk3576-rknn-core The RK3576 NPU has two cores of the same RKNN block the RK3588 binding already describes, but it wires them up differently: two extra CBUF clocks, two power domains per core, and a single reset instead of two. It also has no NPU SRAM supply. Widen the property ranges to cover both, then pin each SoC back to its own shape in allOf so nothing loosens for RK3588, and keep sram-supply required for rockchip,rk3588-rknn-core only. Signed-off-by: Jiaxing Hu Reviewed-by: Krzysztof Kozlowski --- .../npu/rockchip,rk3588-rknn-core.yaml | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/Documentation/devicetree/bindings/npu/rockchip,rk3588-rknn-core.yaml b/Documentation/devicetree/bindings/npu/rockchip,rk3588-rknn-core.yaml index caca2a4903cd15..3b611b64c8aa01 100644 --- a/Documentation/devicetree/bindings/npu/rockchip,rk3588-rknn-core.yaml +++ b/Documentation/devicetree/bindings/npu/rockchip,rk3588-rknn-core.yaml @@ -21,6 +21,7 @@ properties: compatible: enum: + - rockchip,rk3576-rknn-core - rockchip,rk3588-rknn-core reg: @@ -33,14 +34,18 @@ properties: - const: core # Main NPU core processing unit registers clocks: - maxItems: 4 + minItems: 4 + maxItems: 6 clock-names: + minItems: 4 items: - const: aclk - const: hclk - const: npu - const: pclk + - const: aclk_cbuf + - const: hclk_cbuf interrupts: maxItems: 1 @@ -51,12 +56,15 @@ properties: npu-supply: true power-domains: - maxItems: 1 + minItems: 1 + maxItems: 2 resets: + minItems: 1 maxItems: 2 reset-names: + minItems: 1 items: - const: srst_a - const: srst_h @@ -75,7 +83,40 @@ required: - resets - reset-names - npu-supply - - sram-supply + +allOf: + - if: + properties: + compatible: + contains: + const: rockchip,rk3588-rknn-core + then: + properties: + clocks: + maxItems: 4 + clock-names: + maxItems: 4 + power-domains: + maxItems: 1 + resets: + minItems: 2 + reset-names: + minItems: 2 + required: + - sram-supply + else: + properties: + clocks: + minItems: 6 + clock-names: + minItems: 6 + power-domains: + minItems: 2 + resets: + maxItems: 1 + reset-names: + maxItems: 1 + sram-supply: false additionalProperties: false From 4e77c28d88c3e74b649b4745ac13bdebbc825be7 Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Mon, 17 Aug 2026 20:47:04 +1200 Subject: [PATCH 251/258] dt-bindings: power: rockchip: allow resets in a power domain node Some domains do not come up in a usable state on their own and need their resets cycled once power is on. The RK3576 NPU domains are one case: without it the first access after power-on takes an async SError. Signed-off-by: Jiaxing Hu --- .../bindings/power/rockchip,power-controller.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/power/rockchip,power-controller.yaml b/Documentation/devicetree/bindings/power/rockchip,power-controller.yaml index b41db576f95de1..83741f04871600 100644 --- a/Documentation/devicetree/bindings/power/rockchip,power-controller.yaml +++ b/Documentation/devicetree/bindings/power/rockchip,power-controller.yaml @@ -136,6 +136,13 @@ $defs: A number of phandles to clocks that need to be enabled while power domain switches state. + resets: + maxItems: 1 + description: + A phandle to a reset that needs to be cycled once the power domain has + been switched on, for domains whose logic does not come up in a usable + state by itself. + domain-supply: description: domain regulator supply. @@ -216,6 +223,7 @@ examples: reg = ; clocks = <&cru ACLK_IEP>, <&cru HCLK_IEP>; + resets = <&cru SRST_A_IEP>; pm_qos = <&qos_iep>; #power-domain-cells = <0>; }; From 9e3933adf7ef5c72379240028497c6173e50642b Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Mon, 17 Aug 2026 20:48:34 +1200 Subject: [PATCH 252/258] dt-bindings: iommu: rockchip: describe the RK3576 NPU MMU The RK3576 NPU MMUs are rk3568-iommu compatible but take five clocks where every other Rockchip MMU takes two, the extra three being the compute clock and the two convolution buffer clocks. Give them a compatible of their own and pin both sides with an allOf, so that an rk3568-iommu cannot carry five clocks and an NPU MMU cannot carry two. Describing the extra clocks as belonging to one SoC without saying so in the schema, which is what a comment on a description does, leaves both of those spellings valid. Signed-off-by: Jiaxing Hu --- .../bindings/iommu/rockchip,iommu.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Documentation/devicetree/bindings/iommu/rockchip,iommu.yaml b/Documentation/devicetree/bindings/iommu/rockchip,iommu.yaml index 6ce41d11ff5e57..83d7e7c8e0c225 100644 --- a/Documentation/devicetree/bindings/iommu/rockchip,iommu.yaml +++ b/Documentation/devicetree/bindings/iommu/rockchip,iommu.yaml @@ -26,6 +26,7 @@ properties: - items: - enum: - rockchip,rk3576-iommu + - rockchip,rk3576-npu-iommu - rockchip,rk3588-iommu - const: rockchip,rk3568-iommu @@ -42,14 +43,22 @@ properties: minItems: 1 clocks: + minItems: 2 items: - description: Core clock - description: Interface clock + - description: Compute clock + - description: Convolution buffer core clock + - description: Convolution buffer interface clock clock-names: + minItems: 2 items: - const: aclk - const: iface + - const: npu + - const: aclk_cbuf + - const: hclk_cbuf "#iommu-cells": const: 0 @@ -72,6 +81,25 @@ required: - clock-names - "#iommu-cells" +allOf: + - if: + properties: + compatible: + contains: + const: rockchip,rk3576-npu-iommu + then: + properties: + clocks: + minItems: 5 + clock-names: + minItems: 5 + else: + properties: + clocks: + maxItems: 2 + clock-names: + maxItems: 2 + additionalProperties: false examples: From d36e48dc1247c527d92b64cbc3b8afd37c82013c Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Fri, 31 Jul 2026 14:31:40 +1200 Subject: [PATCH 253/258] pmdomain/rockchip: add optional per-domain power-on settle delay The RK3576 NPU domains need a short settle time after the idle request is released before the registers behind the domain answer. Without it the QoS writes that rockchip_pmu_restore_qos() issues land while the domain is still coming up, and the NPU throws an async SError on the first cold power-on. Give rockchip_domain_info an optional delay_us and wait for it between releasing idle and restoring QoS. Rename DOMAIN_M_O_R_G to DOMAIN_M_O_R_G_W, since the suffixes name the fields the macro sets and this one now also carries a wakeup delay; RK3576 is its only user, so the old spelling is not kept around. While the macro is being rewritten, give it the regulator argument that DOMAIN_M_O_R and DOMAIN_M_R already take. Without .need_regulator set, rockchip_pd_regulator_enable() returns early for every RK3576 domain, so a domain-supply in the device tree is never looked up and never enabled. Add a DOMAIN_RK3576_R spelling that passes true and use it for RK3576_PD_NPU, which is the one RK3576 domain with a rail of its own; every other domain passes false and is unchanged. Signed-off-by: Jiaxing Hu --- drivers/pmdomain/rockchip/pm-domains.c | 56 ++++++++++++++++---------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/drivers/pmdomain/rockchip/pm-domains.c b/drivers/pmdomain/rockchip/pm-domains.c index ba66ae71942892..39988efd86aaab 100644 --- a/drivers/pmdomain/rockchip/pm-domains.c +++ b/drivers/pmdomain/rockchip/pm-domains.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ struct rockchip_domain_info { u32 pwr_offset; u32 mem_offset; u32 req_offset; + u32 delay_us; }; struct rockchip_pmu_info { @@ -185,7 +187,7 @@ struct rockchip_pmu { .need_regulator = regulator, \ } -#define DOMAIN_M_O_R_G(_name, p_offset, pwr, status, m_offset, m_status, r_status, r_offset, req, idle, ack, g_mask, wakeup) \ +#define DOMAIN_M_O_R_G_W(_name, p_offset, pwr, status, m_offset, m_status, r_status, r_offset, req, idle, ack, g_mask, delay, wakeup, regulator) \ { \ .name = _name, \ .pwr_offset = p_offset, \ @@ -200,8 +202,10 @@ struct rockchip_pmu { .req_mask = (req), \ .idle_mask = (idle), \ .clk_ungate_mask = (g_mask), \ + .delay_us = (delay), \ .ack_mask = (ack), \ .active_wakeup = wakeup, \ + .need_regulator = regulator, \ } #define DOMAIN_M_R(_name, pwr, status, req, idle, ack, wakeup, regulator) \ @@ -258,8 +262,11 @@ struct rockchip_pmu { #define DOMAIN_RK3568(name, pwr, req, wakeup, regulator) \ DOMAIN_M_R(name, pwr, pwr, req, req, req, wakeup, regulator) -#define DOMAIN_RK3576(name, p_offset, pwr, status, r_status, r_offset, req, idle, g_mask, wakeup) \ - DOMAIN_M_O_R_G(name, p_offset, pwr, status, 0, r_status, r_status, r_offset, req, idle, idle, g_mask, wakeup) +#define DOMAIN_RK3576(name, p_offset, pwr, status, r_status, r_offset, req, idle, g_mask, delay, wakeup) \ + DOMAIN_M_O_R_G_W(name, p_offset, pwr, status, 0, r_status, r_status, r_offset, req, idle, idle, g_mask, delay, wakeup, false) + +#define DOMAIN_RK3576_R(name, p_offset, pwr, status, r_status, r_offset, req, idle, g_mask, delay, wakeup) \ + DOMAIN_M_O_R_G_W(name, p_offset, pwr, status, 0, r_status, r_status, r_offset, req, idle, idle, g_mask, delay, wakeup, true) /* * Dynamic Memory Controller may need to coordinate with us -- see @@ -681,6 +688,10 @@ static int rockchip_pd_power(struct rockchip_pm_domain *pd, bool power_on) if (ret < 0) goto out; + /* Some domains need to settle before the QoS registers answer. */ + if (pd->info->delay_us) + udelay(pd->info->delay_us); + rockchip_pmu_restore_qos(pd); } @@ -1300,25 +1311,26 @@ static const struct rockchip_domain_info rk3568_pm_domains[] = { }; static const struct rockchip_domain_info rk3576_pm_domains[] = { - [RK3576_PD_NPU] = DOMAIN_RK3576("npu", 0x0, BIT(0), BIT(0), 0, 0x0, 0, 0, 0, false), - [RK3576_PD_NVM] = DOMAIN_RK3576("nvm", 0x0, BIT(6), 0, BIT(6), 0x4, BIT(2), BIT(18), BIT(2), false), - [RK3576_PD_SDGMAC] = DOMAIN_RK3576("sdgmac", 0x0, BIT(7), 0, BIT(7), 0x4, BIT(1), BIT(17), 0x6, false), - [RK3576_PD_AUDIO] = DOMAIN_RK3576("audio", 0x0, BIT(8), 0, BIT(8), 0x4, BIT(0), BIT(16), BIT(0), false), - [RK3576_PD_PHP] = DOMAIN_RK3576("php", 0x0, BIT(9), 0, BIT(9), 0x0, BIT(15), BIT(15), BIT(15), false), - [RK3576_PD_SUBPHP] = DOMAIN_RK3576("subphp", 0x0, BIT(10), 0, BIT(10), 0x0, 0, 0, 0, false), - [RK3576_PD_VOP] = DOMAIN_RK3576("vop", 0x0, BIT(11), 0, BIT(11), 0x0, 0x6000, 0x6000, 0x6000, false), - [RK3576_PD_VO1] = DOMAIN_RK3576("vo1", 0x0, BIT(14), 0, BIT(14), 0x0, BIT(12), BIT(12), 0x7000, false), - [RK3576_PD_VO0] = DOMAIN_RK3576("vo0", 0x0, BIT(15), 0, BIT(15), 0x0, BIT(11), BIT(11), 0x6800, false), - [RK3576_PD_USB] = DOMAIN_RK3576("usb", 0x4, BIT(0), 0, BIT(16), 0x0, BIT(10), BIT(10), 0x6400, true), - [RK3576_PD_VI] = DOMAIN_RK3576("vi", 0x4, BIT(1), 0, BIT(17), 0x0, BIT(9), BIT(9), BIT(9), false), - [RK3576_PD_VEPU0] = DOMAIN_RK3576("vepu0", 0x4, BIT(2), 0, BIT(18), 0x0, BIT(7), BIT(7), 0x280, false), - [RK3576_PD_VEPU1] = DOMAIN_RK3576("vepu1", 0x4, BIT(3), 0, BIT(19), 0x0, BIT(8), BIT(8), BIT(8), false), - [RK3576_PD_VDEC] = DOMAIN_RK3576("vdec", 0x4, BIT(4), 0, BIT(20), 0x0, BIT(6), BIT(6), BIT(6), false), - [RK3576_PD_VPU] = DOMAIN_RK3576("vpu", 0x4, BIT(5), 0, BIT(21), 0x0, BIT(5), BIT(5), BIT(5), false), - [RK3576_PD_NPUTOP] = DOMAIN_RK3576("nputop", 0x4, BIT(6), 0, BIT(22), 0x0, 0x18, 0x18, 0x18, false), - [RK3576_PD_NPU0] = DOMAIN_RK3576("npu0", 0x4, BIT(7), 0, BIT(23), 0x0, BIT(1), BIT(1), 0x1a, false), - [RK3576_PD_NPU1] = DOMAIN_RK3576("npu1", 0x4, BIT(8), 0, BIT(24), 0x0, BIT(2), BIT(2), 0x1c, false), - [RK3576_PD_GPU] = DOMAIN_RK3576("gpu", 0x4, BIT(9), 0, BIT(25), 0x0, BIT(0), BIT(0), BIT(0), false), + /* name p_offset pwr status r_status r_offset req idle g_mask delay wakeup */ + [RK3576_PD_NPU] = DOMAIN_RK3576_R("npu", 0x0, BIT(0), BIT(0), 0, 0x0, 0, 0, 0, 0, false), + [RK3576_PD_NVM] = DOMAIN_RK3576("nvm", 0x0, BIT(6), 0, BIT(6), 0x4, BIT(2), BIT(18), BIT(2), 0, false), + [RK3576_PD_SDGMAC] = DOMAIN_RK3576("sdgmac", 0x0, BIT(7), 0, BIT(7), 0x4, BIT(1), BIT(17), 0x6, 0, false), + [RK3576_PD_AUDIO] = DOMAIN_RK3576("audio", 0x0, BIT(8), 0, BIT(8), 0x4, BIT(0), BIT(16), BIT(0), 0, false), + [RK3576_PD_PHP] = DOMAIN_RK3576("php", 0x0, BIT(9), 0, BIT(9), 0x0, BIT(15), BIT(15), BIT(15), 0, false), + [RK3576_PD_SUBPHP] = DOMAIN_RK3576("subphp", 0x0, BIT(10), 0, BIT(10), 0x0, 0, 0, 0, 0, false), + [RK3576_PD_VOP] = DOMAIN_RK3576("vop", 0x0, BIT(11), 0, BIT(11), 0x0, 0x6000, 0x6000, 0x6000, 0, false), + [RK3576_PD_VO1] = DOMAIN_RK3576("vo1", 0x0, BIT(14), 0, BIT(14), 0x0, BIT(12), BIT(12), 0x7000, 0, false), + [RK3576_PD_VO0] = DOMAIN_RK3576("vo0", 0x0, BIT(15), 0, BIT(15), 0x0, BIT(11), BIT(11), 0x6800, 0, false), + [RK3576_PD_USB] = DOMAIN_RK3576("usb", 0x4, BIT(0), 0, BIT(16), 0x0, BIT(10), BIT(10), 0x6400, 0, true), + [RK3576_PD_VI] = DOMAIN_RK3576("vi", 0x4, BIT(1), 0, BIT(17), 0x0, BIT(9), BIT(9), BIT(9), 0, false), + [RK3576_PD_VEPU0] = DOMAIN_RK3576("vepu0", 0x4, BIT(2), 0, BIT(18), 0x0, BIT(7), BIT(7), 0x280, 0, false), + [RK3576_PD_VEPU1] = DOMAIN_RK3576("vepu1", 0x4, BIT(3), 0, BIT(19), 0x0, BIT(8), BIT(8), BIT(8), 0, false), + [RK3576_PD_VDEC] = DOMAIN_RK3576("vdec", 0x4, BIT(4), 0, BIT(20), 0x0, BIT(6), BIT(6), BIT(6), 0, false), + [RK3576_PD_VPU] = DOMAIN_RK3576("vpu", 0x4, BIT(5), 0, BIT(21), 0x0, BIT(5), BIT(5), BIT(5), 0, false), + [RK3576_PD_NPUTOP] = DOMAIN_RK3576("nputop", 0x4, BIT(6), 0, BIT(22), 0x0, 0x18, 0x18, 0x18, 15, false), + [RK3576_PD_NPU0] = DOMAIN_RK3576("npu0", 0x4, BIT(7), 0, BIT(23), 0x0, BIT(1), BIT(1), 0x1a, 15, false), + [RK3576_PD_NPU1] = DOMAIN_RK3576("npu1", 0x4, BIT(8), 0, BIT(24), 0x0, BIT(2), BIT(2), 0x1c, 15, false), + [RK3576_PD_GPU] = DOMAIN_RK3576("gpu", 0x4, BIT(9), 0, BIT(25), 0x0, BIT(0), BIT(0), BIT(0), 0, false), }; static const struct rockchip_domain_info rk3588_pm_domains[] = { From f31bd3bdbefab3d8ed1e2e534334d21cb578ef2b Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Thu, 6 Aug 2026 17:41:29 +1200 Subject: [PATCH 254/258] pmdomain/rockchip: cycle optional power-domain resets on power-on Some Rockchip domains come out of power-on with their bus interface in an undefined state. On the RK3576 NPU this shows up as a hang on the first register access after the domain is switched on, and pulsing the domain's resets at this point clears it. Take the domain node's resets if it has any, and pulse them between releasing idle and restoring QoS. The resets are optional, so domains that do not list any are unaffected. Signed-off-by: Jiaxing Hu --- drivers/pmdomain/rockchip/pm-domains.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/drivers/pmdomain/rockchip/pm-domains.c b/drivers/pmdomain/rockchip/pm-domains.c index 39988efd86aaab..8f2fd8a83e8e9a 100644 --- a/drivers/pmdomain/rockchip/pm-domains.c +++ b/drivers/pmdomain/rockchip/pm-domains.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +104,7 @@ struct rockchip_pm_domain { struct clk_bulk_data *clks; struct device_node *node; struct regulator *supply; + struct reset_control *resets; }; struct rockchip_pmu { @@ -692,6 +694,13 @@ static int rockchip_pd_power(struct rockchip_pm_domain *pd, bool power_on) if (pd->info->delay_us) udelay(pd->info->delay_us); + /* Optional: some domains need their resets cycled after power-on. */ + if (pd->resets) { + reset_control_assert(pd->resets); + usleep_range(10, 20); + reset_control_deassert(pd->resets); + } + rockchip_pmu_restore_qos(pd); } @@ -861,6 +870,14 @@ static int rockchip_pm_add_one_domain(struct rockchip_pmu *pmu, if (error) goto err_put_clocks; + pd->resets = of_reset_control_array_get_optional_exclusive(node); + if (IS_ERR(pd->resets)) { + error = dev_err_probe(pmu->dev, PTR_ERR(pd->resets), + "%pOFn: failed to get resets\n", node); + pd->resets = NULL; + goto err_unprepare_clocks; + } + pd->num_qos = of_count_phandle_with_args(node, "pm_qos", NULL); @@ -931,6 +948,7 @@ static int rockchip_pm_add_one_domain(struct rockchip_pmu *pmu, clk_bulk_unprepare(pd->num_clks, pd->clks); err_put_clocks: clk_bulk_put(pd->num_clks, pd->clks); + reset_control_put(pd->resets); return error; } @@ -949,6 +967,7 @@ static void rockchip_pm_remove_one_domain(struct rockchip_pm_domain *pd) clk_bulk_unprepare(pd->num_clks, pd->clks); clk_bulk_put(pd->num_clks, pd->clks); + reset_control_put(pd->resets); /* protect the zeroing of pm->num_clks */ mutex_lock(&pd->pmu->mutex); From c7f9420857a585525d5ce1bde8012e34d03c225e Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Thu, 6 Aug 2026 17:41:38 +1200 Subject: [PATCH 255/258] accel/rocket: select the per-core clock and reset counts from match data The RK3576 carries the same RKNN block with a different set of clocks and resets, so the counts cannot stay compile-time constants. Add a soc_data struct to the of_device_id match data and take the bulk counts from it. RK3588 keeps four clocks and two resets, so nothing changes for it, and the arrays keep their present sizes: the SoC that needs a longer one grows it in the patch that adds the names. rocket_core_reset() is switched over as well. It is the same array, and leaving it on ARRAY_SIZE() would walk entries that were never acquired once a SoC asks for fewer. Signed-off-by: Jiaxing Hu --- drivers/accel/rocket/rocket_core.c | 8 ++++---- drivers/accel/rocket/rocket_core.h | 7 +++++++ drivers/accel/rocket/rocket_drv.c | 12 +++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/drivers/accel/rocket/rocket_core.c b/drivers/accel/rocket/rocket_core.c index 5dd260bacbff61..b202d15816a399 100644 --- a/drivers/accel/rocket/rocket_core.c +++ b/drivers/accel/rocket/rocket_core.c @@ -23,7 +23,7 @@ int rocket_core_init(struct rocket_core *core) core->resets[0].id = "srst_a"; core->resets[1].id = "srst_h"; - err = devm_reset_control_bulk_get_exclusive(&pdev->dev, ARRAY_SIZE(core->resets), + err = devm_reset_control_bulk_get_exclusive(&pdev->dev, core->soc->num_resets, core->resets); if (err) return dev_err_probe(dev, err, "failed to get resets for core %d\n", core->index); @@ -32,7 +32,7 @@ int rocket_core_init(struct rocket_core *core) core->clks[1].id = "hclk"; core->clks[2].id = "npu"; core->clks[3].id = "pclk"; - err = devm_clk_bulk_get(dev, ARRAY_SIZE(core->clks), core->clks); + err = devm_clk_bulk_get(dev, core->soc->num_clks, core->clks); if (err) return dev_err_probe(dev, err, "failed to get clocks for core %d\n", core->index); @@ -109,9 +109,9 @@ void rocket_core_fini(struct rocket_core *core) void rocket_core_reset(struct rocket_core *core) { - reset_control_bulk_assert(ARRAY_SIZE(core->resets), core->resets); + reset_control_bulk_assert(core->soc->num_resets, core->resets); udelay(10); - reset_control_bulk_deassert(ARRAY_SIZE(core->resets), core->resets); + reset_control_bulk_deassert(core->soc->num_resets, core->resets); } diff --git a/drivers/accel/rocket/rocket_core.h b/drivers/accel/rocket/rocket_core.h index f6d7382854ca9e..ba74c5339b94e7 100644 --- a/drivers/accel/rocket/rocket_core.h +++ b/drivers/accel/rocket/rocket_core.h @@ -27,9 +27,16 @@ #define rocket_core_writel(core, reg, value) \ writel(value, (core)->core_iomem + (REG_CORE_##reg) - REG_CORE_S_STATUS) +/* Per-SoC differences, selected by the of_device_id match data. */ +struct rocket_soc_data { + unsigned int num_clks; /* clk_bulk count */ + unsigned int num_resets; /* reset_bulk count */ +}; + struct rocket_core { struct device *dev; struct rocket_device *rdev; + const struct rocket_soc_data *soc; unsigned int index; int irq; diff --git a/drivers/accel/rocket/rocket_drv.c b/drivers/accel/rocket/rocket_drv.c index 8bbbce594883ef..6e7dc91c5faacb 100644 --- a/drivers/accel/rocket/rocket_drv.c +++ b/drivers/accel/rocket/rocket_drv.c @@ -176,6 +176,7 @@ static int rocket_probe(struct platform_device *pdev) rdev->cores[core].rdev = rdev; rdev->cores[core].dev = &pdev->dev; + rdev->cores[core].soc = of_device_get_match_data(&pdev->dev); rdev->cores[core].index = core; rdev->num_cores++; @@ -213,8 +214,13 @@ static void rocket_remove(struct platform_device *pdev) } } +static const struct rocket_soc_data rk3588_soc_data = { + .num_clks = 4, + .num_resets = 2, +}; + static const struct of_device_id dt_match[] = { - { .compatible = "rockchip,rk3588-rknn-core" }, + { .compatible = "rockchip,rk3588-rknn-core", .data = &rk3588_soc_data }, {} }; MODULE_DEVICE_TABLE(of, dt_match); @@ -240,7 +246,7 @@ static int rocket_device_runtime_resume(struct device *dev) if (core < 0) return -ENODEV; - err = clk_bulk_prepare_enable(ARRAY_SIZE(rdev->cores[core].clks), rdev->cores[core].clks); + err = clk_bulk_prepare_enable(rdev->cores[core].soc->num_clks, rdev->cores[core].clks); if (err) { dev_err(dev, "failed to enable (%d) clocks for core %d\n", err, core); return err; @@ -260,7 +266,7 @@ static int rocket_device_runtime_suspend(struct device *dev) if (!rocket_job_is_idle(&rdev->cores[core])) return -EBUSY; - clk_bulk_disable_unprepare(ARRAY_SIZE(rdev->cores[core].clks), rdev->cores[core].clks); + clk_bulk_disable_unprepare(rdev->cores[core].soc->num_clks, rdev->cores[core].clks); return 0; } From 1ddfbdadd57076b981e1bb69a929f5b38cc5e5c2 Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Thu, 6 Aug 2026 18:30:48 +1200 Subject: [PATCH 256/258] accel/rocket: add RK3576 NPU (RKNN) support The RK3576 has two cores of the same RKNN block and a few platform differences: - the CBUF (convolution buffer) has its own clock domain, so the core needs six clocks rather than four; - there is no per-core hclk reset. The CRU has SRST_A_RKNN0 and SRST_A_RKNN1 but no SRST_H_RKNN0 or SRST_H_RKNN1, so a core takes one reset where RK3588 takes two; - the NPU spans two power domains, and a device with more than one is skipped by the driver-core single-domain auto-attach, so the list has to be attached explicitly; - PC_TASK_CON packs the task number with sixteen bits rather than twelve, moving the three controls above it up by four. That last one is the reason this series has been reporting, since v3, that the block accepts exactly one task per reset. rocket_registers.h is generated from the RK3588 description, so writing it unchanged to an RK3576 asks for task_number 0x7001, which is 28673 tasks, and puts TASK_COUNT_CLEAR on a bit that does nothing. The counter is then only ever cleared by a reset. The layout was confirmed by Chaoyi Chen of Rockchip, including a fourth control at BIT(18), task_last_layer_clear, which belongs on every submit alongside the count clear: https://lore.kernel.org/all/4f300b78-d96d-4d98-8819-dc292b0c9b97@rock-chips.com/ With that written correctly a job of several tasks runs to completion, the completion interrupt arrives, and /proc/interrupts counts up. A convolution submitted three times with three different inputs is byte exact against the CPU reference each time, with no reset in between and with nothing retiring the job but the interrupt. Counting the cores now walks the driver's own match table instead of a second, hand-kept list of compatibles. The array sized from that count is indexed by every core that goes on to probe, so the two lists cannot be allowed to disagree. All of it hangs off the soc_data added earlier, so the RK3588 path keeps its existing counts and behaviour. The match table moves to rocket_drv.h so rocket_device.c can walk it with for_each_matching_node() rather than repeating a for_each_compatible_node() loop per SoC, which also keeps num_cores in step with the table that sizes the array it counts into. The declaration needs struct of_device_id, taken from rather than , which carries every subsystem's tables with it. Signed-off-by: Jiaxing Hu --- drivers/accel/rocket/rocket_core.c | 20 +++++++++++++++ drivers/accel/rocket/rocket_core.h | 8 +++--- drivers/accel/rocket/rocket_device.c | 7 ++++- drivers/accel/rocket/rocket_drv.c | 16 +++++++++--- drivers/accel/rocket/rocket_drv.h | 2 ++ drivers/accel/rocket/rocket_job.c | 38 +++++++++++++++++++++++++--- 6 files changed, 80 insertions(+), 11 deletions(-) diff --git a/drivers/accel/rocket/rocket_core.c b/drivers/accel/rocket/rocket_core.c index b202d15816a399..91f6901764f959 100644 --- a/drivers/accel/rocket/rocket_core.c +++ b/drivers/accel/rocket/rocket_core.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,7 @@ int rocket_core_init(struct rocket_core *core) u32 version; int err = 0; + /* RK3576 has no per-core hclk reset, so it takes srst_a alone. */ core->resets[0].id = "srst_a"; core->resets[1].id = "srst_h"; err = devm_reset_control_bulk_get_exclusive(&pdev->dev, core->soc->num_resets, @@ -32,6 +34,9 @@ int rocket_core_init(struct rocket_core *core) core->clks[1].id = "hclk"; core->clks[2].id = "npu"; core->clks[3].id = "pclk"; + /* RK3576 clocks the CBUF separately; the compute path stalls without these. */ + core->clks[4].id = "aclk_cbuf"; + core->clks[5].id = "hclk_cbuf"; err = devm_clk_bulk_get(dev, core->soc->num_clks, core->clks); if (err) return dev_err_probe(dev, err, "failed to get clocks for core %d\n", core->index); @@ -60,6 +65,21 @@ int rocket_core_init(struct rocket_core *core) if (err) return err; + /* + * RK3576 spans two power domains, and a multi-domain device is skipped + * by the driver-core single-domain auto-attach, so attach the list here. + * This goes before the first thing that would have to be unwound, so a + * failure can simply return. + */ + if (core->soc->multi_power_domain) { + struct dev_pm_domain_list *pd_list; + + err = devm_pm_domain_attach_list(dev, NULL, &pd_list); + if (err < 0) + return dev_err_probe(dev, err, + "failed to attach NPU power domains\n"); + } + core->iommu_group = iommu_group_get(dev); err = rocket_job_init(core); diff --git a/drivers/accel/rocket/rocket_core.h b/drivers/accel/rocket/rocket_core.h index ba74c5339b94e7..8c8d1f4532e008 100644 --- a/drivers/accel/rocket/rocket_core.h +++ b/drivers/accel/rocket/rocket_core.h @@ -29,8 +29,10 @@ /* Per-SoC differences, selected by the of_device_id match data. */ struct rocket_soc_data { - unsigned int num_clks; /* clk_bulk count */ - unsigned int num_resets; /* reset_bulk count */ + unsigned int num_clks; /* clk_bulk count: 4 base, 6 with CBUF */ + unsigned int num_resets; /* reset_bulk count: 2 base, 1 on RK3576 */ + bool multi_power_domain; /* device spans more than one PM domain */ + bool task_con_16bit; /* PC_TASK_CON uses the 16-bit task number */ }; struct rocket_core { @@ -43,7 +45,7 @@ struct rocket_core { void __iomem *pc_iomem; void __iomem *cna_iomem; void __iomem *core_iomem; - struct clk_bulk_data clks[4]; + struct clk_bulk_data clks[6]; struct reset_control_bulk_data resets[2]; struct iommu_group *iommu_group; diff --git a/drivers/accel/rocket/rocket_device.c b/drivers/accel/rocket/rocket_device.c index 46e6ee1e72c5f2..923add5bdc87ef 100644 --- a/drivers/accel/rocket/rocket_device.c +++ b/drivers/accel/rocket/rocket_device.c @@ -9,6 +9,7 @@ #include #include "rocket_device.h" +#include "rocket_drv.h" struct rocket_device *rocket_device_init(struct platform_device *pdev, const struct drm_driver *rocket_drm_driver) @@ -27,7 +28,11 @@ struct rocket_device *rocket_device_init(struct platform_device *pdev, ddev = &rdev->ddev; dev_set_drvdata(dev, rdev); - for_each_compatible_node(core_node, NULL, "rockchip,rk3588-rknn-core") + /* + * Count over the same match table the platform driver binds with, so + * that a core added there is counted here without a second edit. + */ + for_each_matching_node(core_node, rocket_dt_match) if (of_device_is_available(core_node)) num_cores++; diff --git a/drivers/accel/rocket/rocket_drv.c b/drivers/accel/rocket/rocket_drv.c index 6e7dc91c5faacb..e469629499fdbe 100644 --- a/drivers/accel/rocket/rocket_drv.c +++ b/drivers/accel/rocket/rocket_drv.c @@ -217,13 +217,23 @@ static void rocket_remove(struct platform_device *pdev) static const struct rocket_soc_data rk3588_soc_data = { .num_clks = 4, .num_resets = 2, + .multi_power_domain = false, + .task_con_16bit = false, }; -static const struct of_device_id dt_match[] = { +static const struct rocket_soc_data rk3576_soc_data = { + .num_clks = 6, + .num_resets = 1, + .multi_power_domain = true, + .task_con_16bit = true, +}; + +const struct of_device_id rocket_dt_match[] = { { .compatible = "rockchip,rk3588-rknn-core", .data = &rk3588_soc_data }, + { .compatible = "rockchip,rk3576-rknn-core", .data = &rk3576_soc_data }, {} }; -MODULE_DEVICE_TABLE(of, dt_match); +MODULE_DEVICE_TABLE(of, rocket_dt_match); static int find_core_for_dev(struct device *dev) { @@ -282,7 +292,7 @@ static struct platform_driver rocket_driver = { .driver = { .name = "rocket", .pm = pm_ptr(&rocket_pm_ops), - .of_match_table = dt_match, + .of_match_table = rocket_dt_match, }, }; diff --git a/drivers/accel/rocket/rocket_drv.h b/drivers/accel/rocket/rocket_drv.h index 2c673bb99ccc1d..0cd692a66b858b 100644 --- a/drivers/accel/rocket/rocket_drv.h +++ b/drivers/accel/rocket/rocket_drv.h @@ -6,10 +6,12 @@ #include #include +#include #include "rocket_device.h" extern const struct dev_pm_ops rocket_pm_ops; +extern const struct of_device_id rocket_dt_match[]; struct rocket_iommu_domain { struct iommu_domain *domain; diff --git a/drivers/accel/rocket/rocket_job.c b/drivers/accel/rocket/rocket_job.c index 69e29f40f27a08..2a272c2ef6bed6 100644 --- a/drivers/accel/rocket/rocket_job.c +++ b/drivers/accel/rocket/rocket_job.c @@ -21,6 +21,29 @@ #define JOB_TIMEOUT_MS 500 +/* + * PC_TASK_CON packs the task number with three controls, and the field widths + * are not the same on every SoC. rocket_registers.h is generated from the + * RK3588 description, where the task number is twelve bits: + * + * RK3588 BIT[11:0] task_number, BIT[12] pp_en, BIT[13] count_clear + * RK3576 BIT[15:0] task_number, BIT[16] pp_en, BIT[17] count_clear, + * BIT[18] last_layer_clear + * + * The RK3576 layout was confirmed by Chaoyi Chen of Rockchip: + * https://lore.kernel.org/all/4f300b78-d96d-4d98-8819-dc292b0c9b97@rock-chips.com/ + * + * Writing the RK3588 layout to an RK3576 therefore asks for task_number + * 0x7001, that is 28673 tasks, and lands the count clear on a bit that does + * nothing. The task counter is then only ever cleared by a reset, which is + * exactly the "one task per reset" behaviour this series has been reporting + * since v3. + */ +#define RK3576_PC_TASK_CON_TASK_NUMBER(n) ((n) & 0xffff) +#define RK3576_PC_TASK_CON_PP_EN BIT(16) +#define RK3576_PC_TASK_CON_COUNT_CLEAR BIT(17) +#define RK3576_PC_TASK_CON_LAST_LAYER_CLEAR BIT(18) + static struct rocket_job * to_rocket_job(struct drm_sched_job *sched_job) { @@ -142,10 +165,17 @@ static void rocket_job_hw_submit(struct rocket_core *core, struct rocket_job *jo rocket_pc_writel(core, INTERRUPT_MASK, PC_INTERRUPT_MASK_DPU_0 | PC_INTERRUPT_MASK_DPU_1); rocket_pc_writel(core, INTERRUPT_CLEAR, PC_INTERRUPT_CLEAR_DPU_0 | PC_INTERRUPT_CLEAR_DPU_1); - rocket_pc_writel(core, TASK_CON, PC_TASK_CON_RESERVED_0(1) | - PC_TASK_CON_TASK_COUNT_CLEAR(1) | - PC_TASK_CON_TASK_NUMBER(1) | - PC_TASK_CON_TASK_PP_EN(1)); + if (core->soc->task_con_16bit) + rocket_pc_writel(core, TASK_CON, + RK3576_PC_TASK_CON_LAST_LAYER_CLEAR | + RK3576_PC_TASK_CON_COUNT_CLEAR | + RK3576_PC_TASK_CON_PP_EN | + RK3576_PC_TASK_CON_TASK_NUMBER(1)); + else + rocket_pc_writel(core, TASK_CON, PC_TASK_CON_RESERVED_0(1) | + PC_TASK_CON_TASK_COUNT_CLEAR(1) | + PC_TASK_CON_TASK_NUMBER(1) | + PC_TASK_CON_TASK_PP_EN(1)); rocket_pc_writel(core, TASK_DMA_BASE_ADDR, PC_TASK_DMA_BASE_ADDR_DMA_BASE_ADDR(0x0)); From dc039b0fc3b324533a6a527682de8a7b0111bdd9 Mon Sep 17 00:00:00 2001 From: Jiaxing Hu Date: Thu, 6 Aug 2026 18:30:48 +1200 Subject: [PATCH 257/258] arm64: dts: rockchip: rk3576: add NPU (RKNN) nodes Add the two RKNN cores and their IOMMUs. Both cores are disabled by default; boards enable what they wire up. PD_NPU0 and PD_NPU1 are siblings under PD_NPUTOP and hold one core each, but the convolution buffer and the DSU sit above them: ACLK_RKNN_CBUF, HCLK_RKNN_CBUF and CLK_RKNN_DSU0 belong to the block rather than to either core, and PD_NPUTOP already lists all three. Add them to both core domains as well, so a core domain switching state has the clocks of the path it shares running, and give each core domain the BIU reset that the pmdomain driver now cycles once power is on. Each core lists both core domains, its own first, so that a core in use has the whole block powered. Whether a single core can reach the shared path with the sibling domain off is not something this series establishes; listing both is the description that has been tested here. The IOMMU in front of each core lists that core's domain only. Label the outer PD_NPU node so a board can attach the NPU rail to the domain that gates the block. Signed-off-by: Jiaxing Hu --- arch/arm64/boot/dts/rockchip/rk3576.dtsi | 82 +++++++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/arch/arm64/boot/dts/rockchip/rk3576.dtsi b/arch/arm64/boot/dts/rockchip/rk3576.dtsi index 0490a879cce471..bfc099d93d4353 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576.dtsi @@ -1157,7 +1157,7 @@ #address-cells = <1>; #size-cells = <0>; - power-domain@RK3576_PD_NPU { + pd_npu: power-domain@RK3576_PD_NPU { reg = ; #power-domain-cells = <1>; #address-cells = <1>; @@ -1185,14 +1185,22 @@ power-domain@RK3576_PD_NPU0 { reg = ; clocks = <&cru HCLK_RKNN_ROOT>, - <&cru ACLK_RKNN0>; + <&cru ACLK_RKNN0>, + <&cru CLK_RKNN_DSU0>, + <&cru ACLK_RKNN_CBUF>, + <&cru HCLK_RKNN_CBUF>; + resets = <&cru SRST_A_RKNN0_BIU>; pm_qos = <&qos_npu_m0>; #power-domain-cells = <0>; }; power-domain@RK3576_PD_NPU1 { reg = ; clocks = <&cru HCLK_RKNN_ROOT>, - <&cru ACLK_RKNN1>; + <&cru ACLK_RKNN1>, + <&cru CLK_RKNN_DSU0>, + <&cru ACLK_RKNN_CBUF>, + <&cru HCLK_RKNN_CBUF>; + resets = <&cru SRST_A_RKNN1_BIU>; pm_qos = <&qos_npu_m1>; #power-domain-cells = <0>; }; @@ -1376,6 +1384,74 @@ }; }; + rknn_core_0: npu@27700000 { + compatible = "rockchip,rk3576-rknn-core"; + reg = <0x0 0x27700000 0x0 0x1000>, + <0x0 0x27701000 0x0 0x1000>, + <0x0 0x27703000 0x0 0x1000>; + reg-names = "pc", "cna", "core"; + interrupts = ; + clocks = <&cru ACLK_RKNN0>, <&cru HCLK_RKNN_ROOT>, + <&cru CLK_RKNN_DSU0>, <&cru PCLK_NPUTOP_ROOT>, + <&cru ACLK_RKNN_CBUF>, <&cru HCLK_RKNN_CBUF>; + clock-names = "aclk", "hclk", "npu", "pclk", + "aclk_cbuf", "hclk_cbuf"; + resets = <&cru SRST_A_RKNN0>; + reset-names = "srst_a"; + power-domains = <&power RK3576_PD_NPU0>, <&power RK3576_PD_NPU1>; + iommus = <&rknn_mmu_0>; + status = "disabled"; + }; + + rknn_mmu_0: iommu@27702000 { + compatible = "rockchip,rk3576-npu-iommu", "rockchip,rk3568-iommu"; + reg = <0x0 0x27702000 0x0 0x100>, + <0x0 0x27702100 0x0 0x100>; + interrupts = ; + clocks = <&cru ACLK_RKNN0>, <&cru HCLK_RKNN_ROOT>, + <&cru CLK_RKNN_DSU0>, <&cru ACLK_RKNN_CBUF>, + <&cru HCLK_RKNN_CBUF>; + clock-names = "aclk", "iface", "npu", + "aclk_cbuf", "hclk_cbuf"; + #iommu-cells = <0>; + power-domains = <&power RK3576_PD_NPU0>; + status = "disabled"; + }; + + rknn_core_1: npu@27708000 { + compatible = "rockchip,rk3576-rknn-core"; + reg = <0x0 0x27708000 0x0 0x1000>, + <0x0 0x27709000 0x0 0x1000>, + <0x0 0x2770b000 0x0 0x1000>; + reg-names = "pc", "cna", "core"; + interrupts = ; + clocks = <&cru ACLK_RKNN1>, <&cru HCLK_RKNN_ROOT>, + <&cru CLK_RKNN_DSU0>, <&cru PCLK_NPUTOP_ROOT>, + <&cru ACLK_RKNN_CBUF>, <&cru HCLK_RKNN_CBUF>; + clock-names = "aclk", "hclk", "npu", "pclk", + "aclk_cbuf", "hclk_cbuf"; + resets = <&cru SRST_A_RKNN1>; + reset-names = "srst_a"; + power-domains = <&power RK3576_PD_NPU1>, <&power RK3576_PD_NPU0>; + iommus = <&rknn_mmu_1>; + status = "disabled"; + }; + + rknn_mmu_1: iommu@2770a000 { + compatible = "rockchip,rk3576-npu-iommu", "rockchip,rk3568-iommu"; + reg = <0x0 0x2770a000 0x0 0x100>, + <0x0 0x2770a100 0x0 0x100>; + interrupts = ; + clocks = <&cru ACLK_RKNN1>, <&cru HCLK_RKNN_ROOT>, + <&cru CLK_RKNN_DSU0>, <&cru ACLK_RKNN_CBUF>, + <&cru HCLK_RKNN_CBUF>; + clock-names = "aclk", "iface", "npu", + "aclk_cbuf", "hclk_cbuf"; + #iommu-cells = <0>; + power-domains = <&power RK3576_PD_NPU1>; + status = "disabled"; + }; + gpu: gpu@27800000 { compatible = "rockchip,rk3576-mali", "arm,mali-bifrost"; reg = <0x0 0x27800000 0x0 0x20000>; From 5d00ea3c937729c438812337b80cbd10bda92f08 Mon Sep 17 00:00:00 2001 From: Alex Brunton Date: Wed, 26 Aug 2026 14:58:36 -0600 Subject: [PATCH 258/258] arm64: dts: rockchip: flipper-one: Enable the NPU Wire vdd_npu_s0 (PMIC dcdc-reg2) into the NPU power domain and enable the first RKNN core with its MMU. Without domain-supply on pd_npu the domain cannot be powered, and the core's npu-supply is what keeps the regulator from being switched off as unused during late init. Only core 0 is enabled. The RK3576 has two, but single-core is the supported bring-up configuration and core 1 needs its own validation before it is turned on. Both board revisions inherit this from the shared dtsi. --- .../arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi index 517fd0e9748962..12996e63f741c8 100644 --- a/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi +++ b/arch/arm64/boot/dts/rockchip/rk3576-flipper-one.dtsi @@ -1322,6 +1322,10 @@ status = "okay"; }; +&pd_npu { + domain-supply = <&vdd_npu_s0>; +}; + &pinctrl { camera { cam_pdn: cam-pdn { @@ -1470,6 +1474,15 @@ }; }; +&rknn_core_0 { + npu-supply = <&vdd_npu_s0>; + status = "okay"; +}; + +&rknn_mmu_0 { + status = "okay"; +}; + &sai0 { /* Serial audio for M.2 WWAN */ pinctrl-names = "default";