Re: [PATCH v3 3/5] phy: qualcomm: qmp-combo: Add preliminary USB4 support
From: Manivannan Sadhasivam
Date: Fri Aug 07 2026 - 08:48:34 EST
On Thu, Jul 30, 2026 at 04:19:15PM +0200, Konrad Dybcio wrote:
> From: Konrad Dybcio <konrad.dybcio@xxxxxxxxxxxxxxxx>
>
> Some Combo PHYs (so far only on SC8280XP, X1E80100 and Glymur), come in
> a flavor called USB43DP, which as the name implies, features USB4, USB3
> and DP signal processing capabilities. In that architecture, USB3 and
> USB4 PHYs share the same USB_PLL while featuring separate logic spaces.
> The DP part is roughly the same as on the instances without USB4.
>
> The USB4 and USB3/DP operation modes of the PHY are mutually exclusive.
> Only one USB protocol (and flavor of pipe clock) can be active at a
> given moment (not to be confused with USB3 not being able to be
> tunneled as USB4 packets - that of course remains possible).
> The DP PLL is still used for clocking tunneled DP links. It may be
> turned off to save power when no tunnels are active, but that's left as
> a TODO item for now.
>
> Due to the nature of USB4, the Type-C handling happens entirely inside
> the Host Router, and as such the QMPPHY's mux_set() function is
> nullified for the period when USB4 PHY remains active. This is strictly
> necessary, as the Host Router driver is going to excercise manual
> control over the USB4 PHY's power state, which is needed by the suspend
> and resume flows. Failure to control that synchronously with other
> parts of the code results in a SoC crash by unlocked access.
>
> Because of that, a new struct phy is spawned to expose the USB4 mode,
> along with a .set_mode callback to allow toggling between USB4 and TBT3
> submodes.
>
> Thunderbolt 3, having a number of differences vs USB4, requires a
> couple specific overrides, pertaining to electrical characteristics,
> which are easily accommodated for.
>
> Signed-off-by: Konrad Dybcio <konrad.dybcio@xxxxxxxxxxxxxxxx>
> ---
> drivers/phy/qualcomm/phy-qcom-qmp-combo.c | 392 ++++++++++++++++++++++++------
> 1 file changed, 322 insertions(+), 70 deletions(-)
>
> diff --git a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
> index cdcfad2e86b1..b80eac9d7140 100644
> --- a/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
> +++ b/drivers/phy/qualcomm/phy-qcom-qmp-combo.c
> @@ -22,6 +22,7 @@
> #include <linux/usb/typec.h>
> #include <linux/usb/typec_dp.h>
> #include <linux/usb/typec_mux.h>
> +#include <linux/usb/typec_tbt.h>
>
> #include <drm/bridge/aux-bridge.h>
>
> @@ -61,10 +62,14 @@
> #define SW_USB3PHY_RESET BIT(2)
> /* mux to select USB3 PHY reset control, 0:HW control, 1: software reset */
> #define SW_USB3PHY_RESET_MUX BIT(3)
> +#define SW_USB4PHY_RESET BIT(4)
> +#define SW_USB4PHY_RESET_MUX BIT(5)
>
> /* QPHY_V3_DP_COM_PHY_MODE_CTRL register bits */
> #define USB3_MODE BIT(0) /* enables USB3 mode */
> #define DP_MODE BIT(1) /* enables DP mode */
> +#define USB4_MODE BIT(2) /* mutually exclusive with the above */
> +#define DP_TUNNELING_CLOCK_GEN_EN BIT(3)
>
> /* QPHY_V3_DP_COM_TYPEC_CTRL register bits */
> #define SW_PORTSELECT_VAL BIT(0)
> @@ -77,6 +82,8 @@ enum qmpphy_mode {
> QMPPHY_MODE_USB3DP = 0,
> QMPPHY_MODE_DP_ONLY,
> QMPPHY_MODE_USB3_ONLY,
> + /* USB4 QMPPHY mode refers to both USB4 and TBT3 */
> + QMPPHY_MODE_USB4,
> };
>
> /* set of registers with offsets different per-PHY */
> @@ -89,6 +96,7 @@ enum qphy_reg_layout {
> QPHY_PCS_LFPS_RXTERM_IRQ_CLEAR,
> QPHY_PCS_POWER_DOWN_CONTROL,
> QPHY_PCS_CLAMP_ENABLE,
> + QPHY_PCS_USB4_CLAMP_ENABLE,
>
> QPHY_COM_RESETSM_CNTRL,
> QPHY_COM_C_READY_STATUS,
> @@ -2198,6 +2206,8 @@ struct qmp_combo_offsets {
> u16 dp_txa;
> u16 dp_txb;
> u16 dp_dp_phy;
> + u16 usb4_serdes;
> + u16 usb4_pcs;
> };
>
> struct qmp_phy_cfg {
> @@ -2245,6 +2255,18 @@ struct qmp_phy_cfg {
> int (*calibrate_dp_phy)(struct qmp_combo *qmp);
> void (*dp_aux_init)(struct qmp_combo *qmp);
>
> + /* USB4 specifics */
> + const struct qmp_phy_init_tbl *usb4_serdes_tbl;
> + int usb4_serdes_tbl_num;
> + const struct qmp_phy_init_tbl *usb4_serdes_tb3_ovrd_tbl;
> + int usb4_serdes_tb3_ovrd_num;
> + const struct qmp_phy_init_tbl *usb4_tx_tbl;
> + int usb4_tx_tbl_num;
> + const struct qmp_phy_init_tbl *usb4_rx_tbl;
> + int usb4_rx_tbl_num;
> + const struct qmp_phy_init_tbl *usb4_pcs_tbl;
> + int usb4_pcs_tbl_num;
> +
> /* resets to be requested */
> const char * const *reset_list;
> int num_resets;
> @@ -2286,8 +2308,12 @@ struct qmp_combo {
> void __iomem *dp_tx2;
> void __iomem *dp_dp_phy;
>
> + void __iomem *usb4_serdes;
> + void __iomem *usb4_pcs;
> +
> struct clk *pipe_clk;
> struct clk_bulk_data *clks;
> + struct clk *p2rr2p_pipe_clk;
> int num_clks;
> struct reset_control_bulk_data *resets;
> struct regulator_bulk_data *vregs;
> @@ -2306,6 +2332,10 @@ struct qmp_combo {
> unsigned int dp_init_count;
> bool dp_powered_on;
>
> + struct phy *usb4_phy;
> + enum tbt_phy_submode usb4_phy_submode;
> + unsigned int usb4phy_init_count;
There is already 'usb_init_count', so can this be named as 'usb4_init_count'?
> +
> struct clk_fixed_rate pipe_clk_fixed;
> struct clk_hw dp_link_hw;
> struct clk_hw dp_pixel_hw;
[...]
> +static int qmp_combo_reconfigure_phy(struct qmp_combo *qmp, enum qmpphy_mode new_mode)
> +{
> + dev_dbg(qmp->dev, "qmp_combo_reconfigure_phy: switching from qmpphy mode %d to %d\n",
> + qmp->qmpphy_mode, new_mode);
> +
Looks like a downstream debug leftover.
> + if (qmp->usb_init_count || qmp->usb4phy_init_count)
> + qmp_combo_usb_power_off(qmp->usb_phy);
> +
> + if (qmp->dp_init_count)
> + writel(DP_PHY_PD_CTL_PSR_PWRDN, qmp->dp_dp_phy + QSERDES_DP_PHY_PD_CTL);
> +
> + qmp_combo_com_exit(qmp, true);
> +
No delay required between power off/on?
> + qmp->qmpphy_mode = new_mode;
> +
> + /* Now everything's powered down, power up the right PHYs */
> + qmp_combo_com_init(qmp, true);
> +
> + if ((qmp->usb_init_count || qmp->usb4phy_init_count) &&
> + new_mode != QMPPHY_MODE_DP_ONLY)
> + qmp_combo_usb_power_on(qmp->usb_phy);
> +
> + if ((new_mode == QMPPHY_MODE_USB3DP || new_mode == QMPPHY_MODE_DP_ONLY) &&
> + qmp->dp_init_count)
> + qmp->cfg->dp_aux_init(qmp);
> +
> + return 0;
> +}
> +
> +static int qmp_combo_usb4_init(struct phy *phy)
> +{
> + struct qmp_combo *qmp = phy_get_drvdata(phy);
> + int ret;
> +
> + guard(mutex)(&qmp->phy_mutex);
> +
> + if (!qmp->p2rr2p_pipe_clk) {
> + dev_err(qmp->dev, "missing p2rr2p_pipe clock handle. Update your Device Tree.\n");
> + return -EINVAL;
> + }
> +
> + ret = clk_prepare_enable(qmp->p2rr2p_pipe_clk);
> + if (ret) {
> + dev_err(qmp->dev, "p2rr2p_pipe enable failed: %d\n", ret);
> + return ret;
> + }
> +
> + ret = qmp_combo_com_init(qmp, false);
> + if (ret)
> + return ret;
Missing clk_disable_unprepare() here and below?
> +
> + /* USB4 mode takes precedence to USB3(+DP), force reconfigure the PHY */
> + ret = qmp_combo_reconfigure_phy(qmp, QMPPHY_MODE_USB4);
> + if (ret)
Missing qmp_combo_com_exit().
> + return ret;
> +
> + ret = qmp_combo_usb_power_on(phy);
> + if (ret) {
> + qmp_combo_com_exit(qmp, false);
> + return ret;
> + }
> +
> + /*
> + * Due to the SoC design, the PHY only has a single valid consumer and
> + * preventing it from having sole ownership of the PHY's power state
> + * makes suspending/resuming the router impossible.
> + */
> + WARN_ON(qmp->usb4phy_init_count++);
> +
> + return 0;
> +}
> +
> +static int qmp_combo_usb4_exit(struct phy *phy)
> +{
> + struct qmp_combo *qmp = phy_get_drvdata(phy);
> + int ret;
> +
> + guard(mutex)(&qmp->phy_mutex);
> +
> + ret = qmp_combo_usb_power_off(qmp->usb_phy);
> + if (ret)
> + return ret;
> +
> + ret = qmp_combo_com_exit(qmp, false);
> + if (ret)
> + return ret;
> +
> + /*
> + * Mark the USB4 PHY uninitialized and wait for a mux_set event to determine the correct
> + * setting. This will always be possible because USB4 requires Type-C.
> + */
> + qmp->usb4phy_init_count--;
> +
> + clk_disable_unprepare(qmp->p2rr2p_pipe_clk);
> +
> + return 0;
> +}
> +
> +static int qmp_combo_usb4_set_mode(struct phy *phy, enum phy_mode mode, int submode)
> +{
> + struct qmp_combo *qmp = phy_get_drvdata(phy);
> +
> + if (mode != PHY_MODE_TBT)
> + return -EINVAL;
> +
> + if (submode == PHY_SUBMODE_USB4 || submode == PHY_SUBMODE_TBT3) {
> + qmp->usb4_phy_submode = submode;
> + return 0;
> + }
Idiomatic style is to error out if the condition is not met and return success
otherwise:
if (submode != PHY_SUBMODE_USB4 && submode != PHY_SUBMODE_TBT3)
return -EINVAL;
qmp->usb4_phy_submode = submode;
return 0;
- Mani
--
மணிவண்ணன் சதாசிவம்