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;