diff options
Diffstat (limited to 'drivers/usb/typec/ucsi/ucsi.c')
-rw-r--r-- | drivers/usb/typec/ucsi/ucsi.c | 197 |
1 files changed, 180 insertions, 17 deletions
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c index eabe519013e7..f632350f6dcb 100644 --- a/drivers/usb/typec/ucsi/ucsi.c +++ b/drivers/usb/typec/ucsi/ucsi.c @@ -187,6 +187,7 @@ EXPORT_SYMBOL_GPL(ucsi_send_command); struct ucsi_work { struct delayed_work work; + struct list_head node; unsigned long delay; unsigned int count; struct ucsi_connector *con; @@ -202,6 +203,7 @@ static void ucsi_poll_worker(struct work_struct *work) mutex_lock(&con->lock); if (!con->partner) { + list_del(&uwork->node); mutex_unlock(&con->lock); kfree(uwork); return; @@ -209,10 +211,12 @@ static void ucsi_poll_worker(struct work_struct *work) ret = uwork->cb(con); - if (uwork->count-- && (ret == -EBUSY || ret == -ETIMEDOUT)) + if (uwork->count-- && (ret == -EBUSY || ret == -ETIMEDOUT)) { queue_delayed_work(con->wq, &uwork->work, uwork->delay); - else + } else { + list_del(&uwork->node); kfree(uwork); + } mutex_unlock(&con->lock); } @@ -236,6 +240,7 @@ static int ucsi_partner_task(struct ucsi_connector *con, uwork->con = con; uwork->cb = cb; + list_add_tail(&uwork->node, &con->partner_tasks); queue_delayed_work(con->wq, &uwork->work, delay); return 0; @@ -562,8 +567,9 @@ static void ucsi_unregister_altmodes(struct ucsi_connector *con, u8 recipient) } } -static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner, - u32 *pdos, int offset, int num_pdos) +static int ucsi_read_pdos(struct ucsi_connector *con, + enum typec_role role, int is_partner, + u32 *pdos, int offset, int num_pdos) { struct ucsi *ucsi = con->ucsi; u64 command; @@ -573,7 +579,7 @@ static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner, command |= UCSI_GET_PDOS_PARTNER_PDO(is_partner); command |= UCSI_GET_PDOS_PDO_OFFSET(offset); command |= UCSI_GET_PDOS_NUM_PDOS(num_pdos - 1); - command |= UCSI_GET_PDOS_SRC_PDOS; + command |= is_source(role) ? UCSI_GET_PDOS_SRC_PDOS : 0; ret = ucsi_send_command(ucsi, command, pdos + offset, num_pdos * sizeof(u32)); if (ret < 0 && ret != -ETIMEDOUT) @@ -582,30 +588,43 @@ static int ucsi_get_pdos(struct ucsi_connector *con, int is_partner, return ret; } -static int ucsi_get_src_pdos(struct ucsi_connector *con) +static int ucsi_get_pdos(struct ucsi_connector *con, enum typec_role role, + int is_partner, u32 *pdos) { + u8 num_pdos; 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); + ret = ucsi_read_pdos(con, role, is_partner, pdos, 0, UCSI_MAX_PDOS); if (ret < 0) return ret; - con->num_pdos = ret / sizeof(u32); /* number of bytes to 32-bit PDOs */ - if (con->num_pdos < UCSI_MAX_PDOS) - return 0; + num_pdos = ret / sizeof(u32); /* number of bytes to 32-bit PDOs */ + if (num_pdos < UCSI_MAX_PDOS) + return num_pdos; /* get the remaining PDOs, if any */ - ret = ucsi_get_pdos(con, 1, con->src_pdos, UCSI_MAX_PDOS, - PDO_MAX_OBJECTS - UCSI_MAX_PDOS); + ret = ucsi_read_pdos(con, role, is_partner, pdos, UCSI_MAX_PDOS, + PDO_MAX_OBJECTS - UCSI_MAX_PDOS); if (ret < 0) return ret; - con->num_pdos += ret / sizeof(u32); + return ret / sizeof(u32) + num_pdos; +} + +static int ucsi_get_src_pdos(struct ucsi_connector *con) +{ + int ret; + + ret = ucsi_get_pdos(con, TYPEC_SOURCE, 1, con->src_pdos); + if (ret < 0) + return ret; + + con->num_pdos = ret; ucsi_port_psy_changed(con); - return 0; + return ret; } static int ucsi_check_altmodes(struct ucsi_connector *con) @@ -630,6 +649,72 @@ static int ucsi_check_altmodes(struct ucsi_connector *con) return ret; } +static int ucsi_register_partner_pdos(struct ucsi_connector *con) +{ + struct usb_power_delivery_desc desc = { con->ucsi->cap.pd_version }; + struct usb_power_delivery_capabilities_desc caps; + struct usb_power_delivery_capabilities *cap; + int ret; + + if (con->partner_pd) + return 0; + + con->partner_pd = usb_power_delivery_register(NULL, &desc); + if (IS_ERR(con->partner_pd)) + return PTR_ERR(con->partner_pd); + + ret = ucsi_get_pdos(con, TYPEC_SOURCE, 1, caps.pdo); + if (ret > 0) { + if (ret < PDO_MAX_OBJECTS) + caps.pdo[ret] = 0; + + caps.role = TYPEC_SOURCE; + cap = usb_power_delivery_register_capabilities(con->partner_pd, &caps); + if (IS_ERR(cap)) + return PTR_ERR(cap); + + con->partner_source_caps = cap; + + ret = typec_partner_set_usb_power_delivery(con->partner, con->partner_pd); + if (ret) { + usb_power_delivery_unregister_capabilities(con->partner_source_caps); + return ret; + } + } + + ret = ucsi_get_pdos(con, TYPEC_SINK, 1, caps.pdo); + if (ret > 0) { + if (ret < PDO_MAX_OBJECTS) + caps.pdo[ret] = 0; + + caps.role = TYPEC_SINK; + + cap = usb_power_delivery_register_capabilities(con->partner_pd, &caps); + if (IS_ERR(cap)) + return PTR_ERR(cap); + + con->partner_sink_caps = cap; + + ret = typec_partner_set_usb_power_delivery(con->partner, con->partner_pd); + if (ret) { + usb_power_delivery_unregister_capabilities(con->partner_sink_caps); + return ret; + } + } + + return 0; +} + +static void ucsi_unregister_partner_pdos(struct ucsi_connector *con) +{ + usb_power_delivery_unregister_capabilities(con->partner_sink_caps); + con->partner_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(con->partner_source_caps); + con->partner_source_caps = NULL; + usb_power_delivery_unregister(con->partner_pd); + con->partner_pd = NULL; +} + static void ucsi_pwr_opmode_change(struct ucsi_connector *con) { switch (UCSI_CONSTAT_PWR_OPMODE(con->status.flags)) { @@ -638,6 +723,7 @@ static void ucsi_pwr_opmode_change(struct ucsi_connector *con) typec_set_pwr_opmode(con->port, TYPEC_PWR_MODE_PD); ucsi_partner_task(con, ucsi_get_src_pdos, 30, 0); ucsi_partner_task(con, ucsi_check_altmodes, 30, 0); + ucsi_partner_task(con, ucsi_register_partner_pdos, 1, HZ); break; case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5: con->rdo = 0; @@ -696,6 +782,7 @@ static void ucsi_unregister_partner(struct ucsi_connector *con) if (!con->partner) return; + ucsi_unregister_partner_pdos(con); ucsi_unregister_altmodes(con, UCSI_RECIPIENT_SOP); typec_unregister_partner(con->partner); con->partner = NULL; @@ -800,6 +887,10 @@ static void ucsi_handle_connector_change(struct work_struct *work) if (con->status.flags & UCSI_CONSTAT_CONNECTED) { ucsi_register_partner(con); ucsi_partner_task(con, ucsi_check_connection, 1, HZ); + + if (UCSI_CONSTAT_PWR_OPMODE(con->status.flags) == + UCSI_CONSTAT_PWR_OPMODE_PD) + ucsi_partner_task(con, ucsi_register_partner_pdos, 1, HZ); } else { ucsi_unregister_partner(con); } @@ -1036,6 +1127,9 @@ static struct fwnode_handle *ucsi_find_fwnode(struct ucsi_connector *con) static int ucsi_register_port(struct ucsi *ucsi, int index) { + struct usb_power_delivery_desc desc = { ucsi->cap.pd_version}; + struct usb_power_delivery_capabilities_desc pd_caps; + struct usb_power_delivery_capabilities *pd_cap; struct ucsi_connector *con = &ucsi->connector[index]; struct typec_capability *cap = &con->typec_cap; enum typec_accessory *accessory = cap->accessory; @@ -1056,6 +1150,7 @@ static int ucsi_register_port(struct ucsi *ucsi, int index) INIT_WORK(&con->work, ucsi_handle_connector_change); init_completion(&con->complete); mutex_init(&con->lock); + INIT_LIST_HEAD(&con->partner_tasks); con->num = index + 1; con->ucsi = ucsi; @@ -1114,6 +1209,41 @@ static int ucsi_register_port(struct ucsi *ucsi, int index) goto out; } + con->pd = usb_power_delivery_register(ucsi->dev, &desc); + + ret = ucsi_get_pdos(con, TYPEC_SOURCE, 0, pd_caps.pdo); + if (ret > 0) { + if (ret < PDO_MAX_OBJECTS) + pd_caps.pdo[ret] = 0; + + pd_caps.role = TYPEC_SOURCE; + pd_cap = usb_power_delivery_register_capabilities(con->pd, &pd_caps); + if (IS_ERR(pd_cap)) { + ret = PTR_ERR(pd_cap); + goto out; + } + + con->port_source_caps = pd_cap; + typec_port_set_usb_power_delivery(con->port, con->pd); + } + + memset(&pd_caps, 0, sizeof(pd_caps)); + ret = ucsi_get_pdos(con, TYPEC_SINK, 0, pd_caps.pdo); + if (ret > 0) { + if (ret < PDO_MAX_OBJECTS) + pd_caps.pdo[ret] = 0; + + pd_caps.role = TYPEC_SINK; + pd_cap = usb_power_delivery_register_capabilities(con->pd, &pd_caps); + if (IS_ERR(pd_cap)) { + ret = PTR_ERR(pd_cap); + goto out; + } + + con->port_sink_caps = pd_cap; + typec_port_set_usb_power_delivery(con->port, con->pd); + } + /* Alternate modes */ ret = ucsi_register_altmodes(con, UCSI_RECIPIENT_CON); if (ret) { @@ -1152,8 +1282,8 @@ static int ucsi_register_port(struct ucsi *ucsi, int index) if (con->status.flags & UCSI_CONSTAT_CONNECTED) { typec_set_pwr_role(con->port, !!(con->status.flags & UCSI_CONSTAT_PWR_DIR)); - ucsi_pwr_opmode_change(con); ucsi_register_partner(con); + ucsi_pwr_opmode_change(con); ucsi_port_psy_changed(con); } @@ -1259,10 +1389,20 @@ err_unregister: ucsi_unregister_port_psy(con); if (con->wq) destroy_workqueue(con->wq); + + usb_power_delivery_unregister_capabilities(con->port_sink_caps); + con->port_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(con->port_source_caps); + con->port_source_caps = NULL; + usb_power_delivery_unregister(con->pd); + con->pd = NULL; typec_unregister_port(con->port); con->port = NULL; } + kfree(ucsi->connector); + ucsi->connector = NULL; + err_reset: memset(&ucsi->cap, 0, sizeof(ucsi->cap)); ucsi_reset_ppm(ucsi); @@ -1294,7 +1434,8 @@ static void ucsi_resume_work(struct work_struct *work) int ucsi_resume(struct ucsi *ucsi) { - queue_work(system_long_wq, &ucsi->resume_work); + if (ucsi->connector) + queue_work(system_long_wq, &ucsi->resume_work); return 0; } EXPORT_SYMBOL_GPL(ucsi_resume); @@ -1414,14 +1555,36 @@ void ucsi_unregister(struct ucsi *ucsi) /* Disable notifications */ ucsi->ops->async_write(ucsi, UCSI_CONTROL, &cmd, sizeof(cmd)); + if (!ucsi->connector) + return; + for (i = 0; i < ucsi->cap.num_connectors; i++) { cancel_work_sync(&ucsi->connector[i].work); ucsi_unregister_partner(&ucsi->connector[i]); ucsi_unregister_altmodes(&ucsi->connector[i], UCSI_RECIPIENT_CON); ucsi_unregister_port_psy(&ucsi->connector[i]); - if (ucsi->connector[i].wq) + + if (ucsi->connector[i].wq) { + struct ucsi_work *uwork; + + mutex_lock(&ucsi->connector[i].lock); + /* + * queue delayed items immediately so they can execute + * and free themselves before the wq is destroyed + */ + list_for_each_entry(uwork, &ucsi->connector[i].partner_tasks, node) + mod_delayed_work(ucsi->connector[i].wq, &uwork->work, 0); + mutex_unlock(&ucsi->connector[i].lock); destroy_workqueue(ucsi->connector[i].wq); + } + + usb_power_delivery_unregister_capabilities(ucsi->connector[i].port_sink_caps); + ucsi->connector[i].port_sink_caps = NULL; + usb_power_delivery_unregister_capabilities(ucsi->connector[i].port_source_caps); + ucsi->connector[i].port_source_caps = NULL; + usb_power_delivery_unregister(ucsi->connector[i].pd); + ucsi->connector[i].pd = NULL; typec_unregister_port(ucsi->connector[i].port); } |