Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions drivers/usb/typec/tipd/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions drivers/usb/typec/tipd/tps6598x.h
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down