aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/usb/typec/ucsi/ucsi.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/usb/typec/ucsi/ucsi.c')
-rw-r--r--drivers/usb/typec/ucsi/ucsi.c337
1 files changed, 165 insertions, 172 deletions
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
index 5ef5bd0e87cf..6aa28384f77f 100644
--- a/drivers/usb/typec/ucsi/ucsi.c
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -128,8 +128,10 @@ static int ucsi_exec_command(struct ucsi *ucsi, u64 cmd)
if (ret)
return ret;
- if (cci & UCSI_CCI_BUSY)
+ if (cci & UCSI_CCI_BUSY) {
+ ucsi->ops->async_write(ucsi, UCSI_CANCEL, NULL, 0);
return -EBUSY;
+ }
if (!(cci & UCSI_CCI_COMMAND_COMPLETE))
return -EIO;
@@ -189,6 +191,64 @@ int ucsi_resume(struct ucsi *ucsi)
EXPORT_SYMBOL_GPL(ucsi_resume);
/* -------------------------------------------------------------------------- */
+struct ucsi_work {
+ struct delayed_work work;
+ unsigned long delay;
+ unsigned int count;
+ struct ucsi_connector *con;
+ int (*cb)(struct ucsi_connector *);
+};
+
+static void ucsi_poll_worker(struct work_struct *work)
+{
+ struct ucsi_work *uwork = container_of(work, struct ucsi_work, work.work);
+ struct ucsi_connector *con = uwork->con;
+ int ret;
+
+ mutex_lock(&con->lock);
+
+ if (!con->partner) {
+ mutex_unlock(&con->lock);
+ kfree(uwork);
+ return;
+ }
+
+ ret = uwork->cb(con);
+
+ if (uwork->count-- && (ret == -EBUSY || ret == -ETIMEDOUT))
+ queue_delayed_work(con->wq, &uwork->work, uwork->delay);
+ else
+ kfree(uwork);
+
+ mutex_unlock(&con->lock);
+}
+
+static int ucsi_partner_task(struct ucsi_connector *con,
+ int (*cb)(struct ucsi_connector *),
+ int retries, unsigned long delay)
+{
+ struct ucsi_work *uwork;
+
+ if (!con->partner)
+ return 0;
+
+ uwork = kzalloc(sizeof(*uwork), GFP_KERNEL);
+ if (!uwork)
+ return -ENOMEM;
+
+ INIT_DELAYED_WORK(&uwork->work, ucsi_poll_worker);
+ uwork->count = retries;
+ uwork->delay = delay;
+ uwork->con = con;
+ uwork->cb = cb;
+
+ queue_delayed_work(con->wq, &uwork->work, delay);
+
+ return 0;
+}
+
+/* -------------------------------------------------------------------------- */
+
void ucsi_altmode_update_active(struct ucsi_connector *con)
{
const struct typec_altmode *altmode = NULL;
@@ -435,6 +495,8 @@ static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
command |= UCSI_GET_ALTMODE_CONNECTOR_NUMBER(con->num);
command |= UCSI_GET_ALTMODE_OFFSET(i);
len = ucsi_send_command(con->ucsi, command, alt, sizeof(alt));
+ if (len == -EBUSY)
+ continue;
if (len <= 0)
return len;
@@ -509,7 +571,7 @@ static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner,
command |= UCSI_GET_PDOS_SRC_PDOS;
ret = ucsi_send_command(ucsi, command, pdos + offset,
num_pdos * sizeof(u32));
- if (ret < 0)
+ if (ret < 0 && ret != -ETIMEDOUT)
dev_err(ucsi->dev, "UCSI_GET_PDOS failed (%d)\n", ret);
if (ret == 0 && offset == 0)
dev_warn(ucsi->dev, "UCSI_GET_PDOS returned 0 bytes\n");
@@ -517,26 +579,49 @@ static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner,
return ret;
}
-static void ucsi_get_src_pdos(struct ucsi_connector *con, int is_partner)
+static int ucsi_get_src_pdos(struct ucsi_connector *con)
{
int ret;
/* UCSI max payload means only getting at most 4 PDOs at a time */
ret = ucsi_get_pdos(con, 1, con->src_pdos, 0, UCSI_MAX_PDOS);
if (ret < 0)
- return;
+ return ret;
con->num_pdos = ret / sizeof(u32); /* number of bytes to 32-bit PDOs */
if (con->num_pdos < UCSI_MAX_PDOS)
- return;
+ return 0;
/* get the remaining PDOs, if any */
ret = ucsi_get_pdos(con, 1, con->src_pdos, UCSI_MAX_PDOS,
PDO_MAX_OBJECTS - UCSI_MAX_PDOS);
if (ret < 0)
- return;
+ return ret;
con->num_pdos += ret / sizeof(u32);
+
+ ucsi_port_psy_changed(con);
+
+ return 0;
+}
+
+static int ucsi_check_altmodes(struct ucsi_connector *con)
+{
+ int ret;
+
+ ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_SOP);
+ if (ret && ret != -ETIMEDOUT)
+ dev_err(con->ucsi->dev,
+ "con%d: failed to register partner alt modes (%d)\n",
+ con->num, ret);
+
+ /* Ignoring the errors in this case. */
+ if (con->partner_altmode[0]) {
+ ucsi_altmode_update_active(con);
+ return 0;
+ }
+
+ return ret;
}
static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
@@ -545,7 +630,8 @@ static void ucsi_pwr_opmode_change(struct ucsi_connector *con)
case UCSI_CONSTAT_PWR_OPMODE_PD:
con->rdo = con->status.request_data_obj;
typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_PD);
- ucsi_get_src_pdos(con, 1);
+ ucsi_partner_task(con, ucsi_get_src_pdos, 30, 0);
+ ucsi_partner_task(con, ucsi_check_altmodes, 30, 0);
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
con->rdo = 0;
@@ -614,9 +700,6 @@ static void ucsi_partner_change(struct ucsi_connector *con)
enum usb_role u_role = USB_ROLE_NONE;
int ret;
- if (!con->partner)
- return;
-
switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
case UCSI_CONSTAT_PARTNER_TYPE_UFP:
case UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP:
@@ -633,10 +716,6 @@ static void ucsi_partner_change(struct ucsi_connector *con)
break;
}
- /* Complete pending data role swap */
- if (!completion_done(&con->complete))
- complete(&con->complete);
-
/* Only notify USB controller if partner supports USB data */
if (!(UCSI_CONSTAT_PARTNER_FLAGS(con->status.flags) & UCSI_CONSTAT_PARTNER_FLAG_USB))
u_role = USB_ROLE_NONE;
@@ -645,15 +724,31 @@ static void ucsi_partner_change(struct ucsi_connector *con)
if (ret)
dev_err(con->ucsi->dev, "con:%d: failed to set usb role:%d\n",
con->num, u_role);
+}
- /* Can't rely on Partner Flags field. Always checking the alt modes. */
- ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_SOP);
- if (ret)
- dev_err(con->ucsi->dev,
- "con%d: failed to register partner alternate modes\n",
- con->num);
- else
- ucsi_altmode_update_active(con);
+static int ucsi_check_connection(struct ucsi_connector *con)
+{
+ u64 command;
+ int ret;
+
+ command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
+ ret = ucsi_send_command(con->ucsi, command, &con->status, sizeof(con->status));
+ if (ret < 0) {
+ dev_err(con->ucsi->dev, "GET_CONNECTOR_STATUS failed (%d)\n", ret);
+ return ret;
+ }
+
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED) {
+ if (UCSI_CONSTAT_PWR_OPMODE(con->status.flags) ==
+ UCSI_CONSTAT_PWR_OPMODE_PD)
+ ucsi_partner_task(con, ucsi_check_altmodes, 30, 0);
+ } else {
+ ucsi_partner_change(con);
+ ucsi_port_psy_changed(con);
+ ucsi_unregister_partner(con);
+ }
+
+ return 0;
}
static void ucsi_handle_connector_change(struct work_struct *work)
@@ -661,122 +756,24 @@ static void ucsi_handle_connector_change(struct work_struct *work)
struct ucsi_connector *con = container_of(work, struct ucsi_connector,
work);
struct ucsi *ucsi = con->ucsi;
- struct ucsi_connector_status pre_ack_status;
- struct ucsi_connector_status post_ack_status;
enum typec_role role;
- enum usb_role u_role = USB_ROLE_NONE;
- u16 inferred_changes;
- u16 changed_flags;
u64 command;
int ret;
mutex_lock(&con->lock);
- /*
- * Some/many PPMs have an issue where all fields in the change bitfield
- * are cleared when an ACK is send. This will causes any change
- * between GET_CONNECTOR_STATUS and ACK to be lost.
- *
- * We work around this by re-fetching the connector status afterwards.
- * We then infer any changes that we see have happened but that may not
- * be represented in the change bitfield.
- *
- * Also, even though we don't need to know the currently supported alt
- * modes, we run the GET_CAM_SUPPORTED command to ensure the PPM does
- * not get stuck in case it assumes we do.
- * Always do this, rather than relying on UCSI_CONSTAT_CAM_CHANGE to be
- * set in the change bitfield.
- *
- * We end up with the following actions:
- * 1. UCSI_GET_CONNECTOR_STATUS, store result, update unprocessed_changes
- * 2. UCSI_GET_CAM_SUPPORTED, discard result
- * 3. ACK connector change
- * 4. UCSI_GET_CONNECTOR_STATUS, store result
- * 5. Infere lost changes by comparing UCSI_GET_CONNECTOR_STATUS results
- * 6. If PPM reported a new change, then restart in order to ACK
- * 7. Process everything as usual.
- *
- * We may end up seeing a change twice, but we can only miss extremely
- * short transitional changes.
- */
-
- /* 1. First UCSI_GET_CONNECTOR_STATUS */
command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
- ret = ucsi_send_command(ucsi, command, &pre_ack_status,
- sizeof(pre_ack_status));
- if (ret < 0) {
- dev_err(ucsi->dev, "%s: GET_CONNECTOR_STATUS failed (%d)\n",
- __func__, ret);
- goto out_unlock;
- }
- con->unprocessed_changes |= pre_ack_status.change;
-
- /* 2. Run UCSI_GET_CAM_SUPPORTED and discard the result. */
- command = UCSI_GET_CAM_SUPPORTED;
- command |= UCSI_CONNECTOR_NUMBER(con->num);
- ucsi_send_command(con->ucsi, command, NULL, 0);
-
- /* 3. ACK connector change */
- ret = ucsi_acknowledge_connector_change(ucsi);
- clear_bit(EVENT_PENDING, &ucsi->flags);
- if (ret) {
- dev_err(ucsi->dev, "%s: ACK failed (%d)", __func__, ret);
- goto out_unlock;
- }
-
- /* 4. Second UCSI_GET_CONNECTOR_STATUS */
- command = UCSI_GET_CONNECTOR_STATUS | UCSI_CONNECTOR_NUMBER(con->num);
- ret = ucsi_send_command(ucsi, command, &post_ack_status,
- sizeof(post_ack_status));
+ ret = ucsi_send_command(ucsi, command, &con->status, sizeof(con->status));
if (ret < 0) {
dev_err(ucsi->dev, "%s: GET_CONNECTOR_STATUS failed (%d)\n",
__func__, ret);
goto out_unlock;
}
- /* 5. Inferre any missing changes */
- changed_flags = pre_ack_status.flags ^ post_ack_status.flags;
- inferred_changes = 0;
- if (UCSI_CONSTAT_PWR_OPMODE(changed_flags) != 0)
- inferred_changes |= UCSI_CONSTAT_POWER_OPMODE_CHANGE;
-
- if (changed_flags & UCSI_CONSTAT_CONNECTED)
- inferred_changes |= UCSI_CONSTAT_CONNECT_CHANGE;
-
- if (changed_flags & UCSI_CONSTAT_PWR_DIR)
- inferred_changes |= UCSI_CONSTAT_POWER_DIR_CHANGE;
-
- if (UCSI_CONSTAT_PARTNER_FLAGS(changed_flags) != 0)
- inferred_changes |= UCSI_CONSTAT_PARTNER_CHANGE;
-
- if (UCSI_CONSTAT_PARTNER_TYPE(changed_flags) != 0)
- inferred_changes |= UCSI_CONSTAT_PARTNER_CHANGE;
-
- /* Mask out anything that was correctly notified in the later call. */
- inferred_changes &= ~post_ack_status.change;
- if (inferred_changes)
- dev_dbg(ucsi->dev, "%s: Inferred changes that would have been lost: 0x%04x\n",
- __func__, inferred_changes);
-
- con->unprocessed_changes |= inferred_changes;
-
- /* 6. If PPM reported a new change, then restart in order to ACK */
- if (post_ack_status.change)
- goto out_unlock;
-
- /* 7. Continue as if nothing happened */
- con->status = post_ack_status;
- con->status.change = con->unprocessed_changes;
- con->unprocessed_changes = 0;
+ trace_ucsi_connector_change(con->num, &con->status);
role = !!(con->status.flags & UCSI_CONSTAT_PWR_DIR);
- if (con->status.change & UCSI_CONSTAT_POWER_OPMODE_CHANGE ||
- con->status.change & UCSI_CONSTAT_POWER_LEVEL_CHANGE) {
- ucsi_pwr_opmode_change(con);
- ucsi_port_psy_changed(con);
- }
-
if (con->status.change & UCSI_CONSTAT_POWER_DIR_CHANGE) {
typec_set_pwr_role(con->port, role);
@@ -787,54 +784,39 @@ static void ucsi_handle_connector_change(struct work_struct *work)
if (con->status.change & UCSI_CONSTAT_CONNECT_CHANGE) {
typec_set_pwr_role(con->port, role);
+ ucsi_port_psy_changed(con);
+ ucsi_partner_change(con);
- switch (UCSI_CONSTAT_PARTNER_TYPE(con->status.flags)) {
- case UCSI_CONSTAT_PARTNER_TYPE_UFP:
- case UCSI_CONSTAT_PARTNER_TYPE_CABLE_AND_UFP:
- u_role = USB_ROLE_HOST;
- fallthrough;
- case UCSI_CONSTAT_PARTNER_TYPE_CABLE:
- typec_set_data_role(con->port, TYPEC_HOST);
- break;
- case UCSI_CONSTAT_PARTNER_TYPE_DFP:
- u_role = USB_ROLE_DEVICE;
- typec_set_data_role(con->port, TYPEC_DEVICE);
- break;
- default:
- break;
- }
-
- if (con->status.flags & UCSI_CONSTAT_CONNECTED)
+ if (con->status.flags & UCSI_CONSTAT_CONNECTED) {
ucsi_register_partner(con);
- else
+ ucsi_partner_task(con, ucsi_check_connection, 1, HZ);
+ } else {
ucsi_unregister_partner(con);
+ }
+ }
- ucsi_port_psy_changed(con);
+ if (con->status.change & UCSI_CONSTAT_POWER_OPMODE_CHANGE ||
+ con->status.change & UCSI_CONSTAT_POWER_LEVEL_CHANGE)
+ ucsi_pwr_opmode_change(con);
- /* Only notify USB controller if partner supports USB data */
- if (!(UCSI_CONSTAT_PARTNER_FLAGS(con->status.flags) &
- UCSI_CONSTAT_PARTNER_FLAG_USB))
- u_role = USB_ROLE_NONE;
+ if (con->partner && con->status.change & UCSI_CONSTAT_PARTNER_CHANGE) {
+ ucsi_partner_change(con);
- ret = usb_role_switch_set_role(con->usb_role_sw, u_role);
- if (ret)
- dev_err(ucsi->dev, "con:%d: failed to set usb role:%d\n",
- con->num, u_role);
+ /* Complete pending data role swap */
+ if (!completion_done(&con->complete))
+ complete(&con->complete);
}
- if (con->status.change & UCSI_CONSTAT_PARTNER_CHANGE)
- ucsi_partner_change(con);
+ if (con->status.change & UCSI_CONSTAT_CAM_CHANGE)
+ ucsi_partner_task(con, ucsi_check_altmodes, 1, 0);
- trace_ucsi_connector_change(con->num, &con->status);
+ clear_bit(EVENT_PENDING, &con->ucsi->flags);
-out_unlock:
- if (test_and_clear_bit(EVENT_PENDING, &ucsi->flags)) {
- schedule_work(&con->work);
- mutex_unlock(&con->lock);
- return;
- }
+ ret = ucsi_acknowledge_connector_change(ucsi);
+ if (ret)
+ dev_err(ucsi->dev, "%s: ACK failed (%d)", __func__, ret);
- clear_bit(EVENT_PROCESSING, &ucsi->flags);
+out_unlock:
mutex_unlock(&con->lock);
}
@@ -852,9 +834,7 @@ void ucsi_connector_change(struct ucsi *ucsi, u8 num)
return;
}
- set_bit(EVENT_PENDING, &ucsi->flags);
-
- if (!test_and_set_bit(EVENT_PROCESSING, &ucsi->flags))
+ if (!test_and_set_bit(EVENT_PENDING, &ucsi->flags))
schedule_work(&con->work);
}
EXPORT_SYMBOL_GPL(ucsi_connector_change);
@@ -1041,8 +1021,18 @@ static int ucsi_register_port(struct ucsi *ucsi, int index)
enum typec_accessory *accessory = cap->accessory;
enum usb_role u_role = USB_ROLE_NONE;
u64 command;
+ char *name;
int ret;
+ name = kasprintf(GFP_KERNEL, "%s-con%d", dev_name(ucsi->dev), con->num);
+ if (!name)
+ return -ENOMEM;
+
+ con->wq = create_singlethread_workqueue(name);
+ kfree(name);
+ if (!con->wq)
+ return -ENOMEM;
+
INIT_WORK(&con->work, ucsi_handle_connector_change);
init_completion(&con->complete);
mutex_init(&con->lock);
@@ -1160,16 +1150,9 @@ static int ucsi_register_port(struct ucsi *ucsi, int index)
ret = 0;
}
- if (con->partner) {
- ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_SOP);
- if (ret) {
- dev_err(ucsi->dev,
- "con%d: failed to register alternate modes\n",
- con->num);
- ret = 0;
- } else {
- ucsi_altmode_update_active(con);
- }
+ if (UCSI_CONSTAT_PWR_OPMODE(con->status.flags) == UCSI_CONSTAT_PWR_OPMODE_PD) {
+ ucsi_get_src_pdos(con);
+ ucsi_check_altmodes(con);
}
trace_ucsi_register_port(con->num, &con->status);
@@ -1178,6 +1161,12 @@ out:
fwnode_handle_put(cap->fwnode);
out_unlock:
mutex_unlock(&con->lock);
+
+ if (ret && con->wq) {
+ destroy_workqueue(con->wq);
+ con->wq = NULL;
+ }
+
return ret;
}
@@ -1248,6 +1237,8 @@ err_unregister:
ucsi_unregister_partner(con);
ucsi_unregister_altmodes(con, UCSI_RECIPIENT_CON);
ucsi_unregister_port_psy(con);
+ if (con->wq)
+ destroy_workqueue(con->wq);
typec_unregister_port(con->port);
con->port = NULL;
}
@@ -1370,6 +1361,8 @@ void ucsi_unregister(struct ucsi *ucsi)
ucsi_unregister_altmodes(&ucsi->connector[i],
UCSI_RECIPIENT_CON);
ucsi_unregister_port_psy(&ucsi->connector[i]);
+ if (ucsi->connector[i].wq)
+ destroy_workqueue(ucsi->connector[i].wq);
typec_unregister_port(ucsi->connector[i].port);
}