From f73467461746b47c61849e64895b43cac184c98d Mon Sep 17 00:00:00 2001 From: nekolyanich Date: Tue, 13 Oct 2026 08:06:46 +0300 Subject: [PATCH] usb: typec: tipd: put an empty CD321x port to sleep, as macOS On an M2 Pro (j416s), replugs of a USB-C to HDMI adapter in DP alt mode often never reached the display: 26 of 56 natively, over three days. In a trace of the controller's status (4 cycles), after the unplug of a session that had DP it reported the cable power path (PP_CABLE) "in" and kept the session's data status (DATA_CONNECTION, USB_DATA_ROLE); the next plug got no VBUS, never turned on 5 V and never reached PD (2 of 2). Unplugging that failed plug cleared the state, so the next one worked. macOS, traced under the m1n1 hypervisor with the same TV and adapter, writes 0x2000 to register 0x50 and sends SSPS 2 on every disconnect (8 of 8 in two traces), and after it writes 0x2000 and 0 to 0x50 in 5 of 8. The port then reads back system power state 2 and stays in it while unplugged (3 of 3, the trace that decodes reads). On the next connect, as soon as STATUS shows the plug, macOS sends SSPS 0, before the display gets HPD. At boot it puts the MagSafe port's controller, which has nothing plugged in, into state 2 as well (2 of 2); no empty USB-C port was traced at boot. We have no documentation for register 0x50; it is 6 bytes long, and macOS also writes 0x2000 to it after each DATA_STATUS HPD_IRQ (11 of 11), so the bit is named by number only. Do the same from the cable work: whenever it finds no plug and the port is not known to be asleep (a disconnect, also a retried one, or an empty port at probe), write 0x2000 to 0x50, SSPS 2, 0x2000 and 0 to 0x50 (at probe too, where macOS's order differs slightly); before a new partner is registered, SSPS 0. STATUS is read again right before the sleep, and a plug present skips it: state 2 was never seen entered with a partner attached. So a plug within the debounce of an unplug, or one landing while the cable work runs, leaves the port awake, as before this change. Both go through switch_power_state(), which also reads the state before SSPS (macOS doesn't) and skips a state already set. A failure only warns and leaves the port state unknown, as does a resume, so the next pass tries again. Only the CD321x does this (tipd_cd321x_data sets port_sleep): nothing was traced for the SN201202x, which shares the cable work, so it keeps its behaviour. Testing: TBD Assisted-by: Claude:claude-opus-5-5 checkpatch Signed-off-by: nekolyanich --- drivers/usb/typec/tipd/core.c | 86 +++++++++++++++++++++++++++++++ drivers/usb/typec/tipd/tps6598x.h | 15 ++++++ 2 files changed, 101 insertions(+) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index 36a6a0abc349e..bf4aa02330b40 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -52,6 +52,15 @@ #define TPS_REG_DATA_STATUS 0x5f #define TPS_REG_SLEEP_CONF 0x70 +/* + * CD321x register 0x50, for which we have no documentation: 6 bytes, of + * which macOS reads and writes 4. It writes BIT(13) after each DATA_STATUS + * HPD_IRQ and around a disconnect, then clears it; what the bit does is not + * known. + */ +#define CD321X_REG_0X50 0x50 +#define CD321X_0X50_BIT13 BIT(13) + /* TPS_REG_SYSTEM_CONF bits */ #define TPS_SYSCONF_PORTINFO(c) ((c) & 7) @@ -711,6 +720,75 @@ static void cd321x_retry_revalidation(struct cd321x *cd321x, bool reconnect) CD321X_RESUME_ATTEMPTS); } +static void cd321x_write_0x50(struct tps6598x *tps, u32 val) +{ + __le32 v = cpu_to_le32(val); + int ret; + + ret = tps6598x_block_write(tps, CD321X_REG_0X50, &v, sizeof(v)); + if (ret) + dev_warn(tps->dev, "failed to write 0x%x to register 0x50: %d\n", + val, ret); +} + +/* + * After a session in DP alt mode, the controller reports the cable power + * path "in" and the session's data status once the partner is gone, and + * then gives the next partner no VBUS. macOS puts the port into system + * power state 2 on disconnect (and its empty MagSafe controller at boot) + * and back into S0 on the next connect; do the same, here also for a + * USB-C port found empty at probe. State 2 was never seen entered with a + * plug present, so STATUS is read again first and a plug skips the sleep. + * switch_power_state() reads the state before and after SSPS (macOS only + * after) and skips a state already set. A failure only warns, and leaves + * the port state unknown so that the next pass tries again: the cable + * state above is already applied, and a later event still reaches the port. + */ +static void cd321x_port_sleep(struct cd321x *cd321x) +{ + struct tps6598x *tps = &cd321x->tps; + u32 status; + int ret; + + if (!tps->data->port_sleep) + return; + + ret = tps6598x_read32(tps, TPS_REG_STATUS, &status); + if (ret || (status & TPS_STATUS_PLUG_PRESENT)) { + cd321x->port_power = CD321X_PORT_UNKNOWN; + return; + } + + cd321x_write_0x50(tps, CD321X_0X50_BIT13); + ret = tps->data->switch_power_state(tps, CD321X_SYSTEM_POWER_STATE_UNPLUGGED); + if (ret) { + dev_warn(tps->dev, "failed to enter the unplugged power state: %d\n", + ret); + cd321x->port_power = CD321X_PORT_UNKNOWN; + } else { + cd321x->port_power = CD321X_PORT_ASLEEP; + } + cd321x_write_0x50(tps, CD321X_0X50_BIT13); + cd321x_write_0x50(tps, 0); +} + +static void cd321x_port_wake(struct cd321x *cd321x) +{ + struct tps6598x *tps = &cd321x->tps; + int ret; + + if (!tps->data->port_sleep) + return; + + ret = tps->data->switch_power_state(tps, TPS_SYSTEM_POWER_STATE_S0); + if (ret) { + dev_warn(tps->dev, "failed to return to S0 on connect: %d\n", ret); + cd321x->port_power = CD321X_PORT_UNKNOWN; + } else { + cd321x->port_power = CD321X_PORT_AWAKE; + } +} + static void cd321x_update_work(struct work_struct *work) { struct cd321x *cd321x = container_of(to_delayed_work(work), @@ -850,6 +928,9 @@ static void cd321x_update_work(struct work_struct *work) /* If the plug is disconnected, we are done */ if (!new_connected) { + /* also an empty port at probe, and a disconnect retried */ + if (cd321x->port_power != CD321X_PORT_ASLEEP) + cd321x_port_sleep(cd321x); cd321x_pm_complete(&cd321x->pm); return; } @@ -858,6 +939,8 @@ static void cd321x_update_work(struct work_struct *work) if (!tps->partner) { struct typec_partner_desc desc; + cd321x_port_wake(cd321x); + desc.usb_pd = is_pd; desc.accessory = TYPEC_ACCESSORY_NONE; /* XXX: handle accessories */ desc.identity = NULL; @@ -1022,6 +1105,8 @@ static void cd321x_resume_reverify(struct tps6598x *tps) struct cd321x *cd321x = container_of(tps, struct cd321x, tps); guard(mutex)(&tps->lock); + /* the controller may have been re-initialised over sleep */ + cd321x->port_power = CD321X_PORT_UNKNOWN; if (cd321x_pm_resume(&cd321x->pm)) mod_delayed_work(system_freezable_wq, &cd321x->resume_work, 0); } @@ -2244,6 +2329,7 @@ const struct tipd_data tipd_cd321x_data = { .read_data_status = cd321x_read_data_status, .reset = cd321x_reset, .switch_power_state = cd321x_switch_power_state, + .port_sleep = true, .connect = cd321x_connect, .suspend_prepare = cd321x_suspend_prepare, .resume_reverify = cd321x_resume_reverify, diff --git a/drivers/usb/typec/tipd/tps6598x.h b/drivers/usb/typec/tipd/tps6598x.h index 3a807f1def6b2..ba7b1e1d703da 100644 --- a/drivers/usb/typec/tipd/tps6598x.h +++ b/drivers/usb/typec/tipd/tps6598x.h @@ -150,6 +150,8 @@ #define TPS_SYSTEM_POWER_STATE_S3 0x03 #define TPS_SYSTEM_POWER_STATE_S4 0x04 #define TPS_SYSTEM_POWER_STATE_S5 0x05 +/* CD321x: what macOS sets on a port with nothing plugged in */ +#define CD321X_SYSTEM_POWER_STATE_UNPLUGGED 0x02 /* TPS_REG_POWER_STATUS bits */ #define TPS_POWER_STATUS_CONNECTION(x) TPS_FIELD_GET(BIT(0), (x)) @@ -305,6 +307,11 @@ struct tipd_data { int (*apply_patch)(struct tps6598x *tps); int (*init)(struct tps6598x *tps); int (*switch_power_state)(struct tps6598x *tps, u8 target_state); + /* + * CD321x: sleep an empty port and wake it on connect, as macOS does + * (traced on CD321x only) + */ + bool port_sleep; bool (*read_data_status)(struct tps6598x *tps); int (*reset)(struct tps6598x *tps); int (*connect)(struct tps6598x *tps, u32 status); @@ -378,8 +385,16 @@ struct cd321x_status { struct tps6598x_usb4_status_reg usb4_status; }; +/* CD321x port power, as last set by the driver (UNKNOWN after probe) */ +enum cd321x_port_power { + CD321X_PORT_UNKNOWN, + CD321X_PORT_AWAKE, + CD321X_PORT_ASLEEP, +}; + struct cd321x { struct tps6598x tps; + enum cd321x_port_power port_power; struct tps6598x_dp_sid_status_reg dp_sid_status; struct tps6598x_intel_vid_status_reg intel_vid_status;