| From 34bbd13f252cde22dd1a472e4f61c2c8d5aeb74a Mon Sep 17 00:00:00 2001 |
| From: Pavan Holla <pholla@chromium.org> |
| Date: Fri, 15 Mar 2024 19:17:35 +0000 |
| Subject: [PATCH] CHROMIUM: usb: typec: Implement UCSI driver for ChromeOS |
| |
| Implementation of transport driver for UCSI. This driver will be loaded |
| if the ChromeOS EC implements a PPM. |
| Upstream review: |
| https://patchwork.kernel.org/project/linux-usb/patch/20240409-public-ucsi-h-v4-2-e770735222a2@chromium.org/ |
| |
| BUG=b:319125287 |
| TEST=emerge-brox chromeos-kernel-6_6 |
| UPSTREAM-TASK=b:335338337 |
| |
| Signed-off-by: Pavan Holla <pholla@chromium.org> |
| Change-Id: I1833b7655e4e7f0cdaea1cb1ab054e82c25b461a |
| Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/kernel/+/5302396 |
| Reviewed-by: Abhishek Pandit-Subedi <abhishekpandit@google.com> |
| Commit-Queue: Pavan Holla <pholla@google.com> |
| Tested-by: Pavan Holla <pholla@google.com> |
| --- |
| drivers/usb/typec/ucsi/Kconfig | 13 + |
| drivers/usb/typec/ucsi/Makefile | 1 + |
| drivers/usb/typec/ucsi/cros_ec_ucsi.c | 271 ++++++++++++++++++ |
| .../linux/platform_data/cros_ec_commands.h | 19 ++ |
| 4 files changed, 304 insertions(+) |
| create mode 100644 drivers/usb/typec/ucsi/cros_ec_ucsi.c |
| |
| diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig |
| index 680e1b87b15260fd10231c9109687a4b29315ceb..d33677bd434cd19b24b6bc953afca6d8113d6cea 100644 |
| --- a/drivers/usb/typec/ucsi/Kconfig |
| +++ b/drivers/usb/typec/ucsi/Kconfig |
| @@ -78,4 +78,17 @@ config UCSI_LENOVO_YOGA_C630 |
| To compile the driver as a module, choose M here: the module will be |
| called ucsi_yoga_c630. |
| |
| +config CROS_EC_UCSI |
| + tristate "UCSI Driver for ChromeOS EC" |
| + depends on MFD_CROS_EC_DEV |
| + depends on CROS_USBPD_NOTIFY |
| + depends on !EXTCON_TCSS_CROS_EC |
| + default MFD_CROS_EC_DEV |
| + help |
| + This driver enables UCSI support for a ChromeOS EC. The EC is |
| + expected to implement a PPM. |
| + |
| + To compile the driver as a module, choose M here: the module |
| + will be called cros_ec_ucsi. |
| + |
| endif |
| diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile |
| index aed41d23887b5f7db39ed4c47b248c4412504ce5..e3a27159fa0cd34cae9a79150a2a81b8295f84e5 100644 |
| --- a/drivers/usb/typec/ucsi/Makefile |
| +++ b/drivers/usb/typec/ucsi/Makefile |
| @@ -22,3 +22,4 @@ obj-$(CONFIG_UCSI_CCG) += ucsi_ccg.o |
| obj-$(CONFIG_UCSI_STM32G0) += ucsi_stm32g0.o |
| obj-$(CONFIG_UCSI_PMIC_GLINK) += ucsi_glink.o |
| obj-$(CONFIG_UCSI_LENOVO_YOGA_C630) += ucsi_yoga_c630.o |
| +obj-$(CONFIG_CROS_EC_UCSI) += cros_ec_ucsi.o |
| diff --git a/drivers/usb/typec/ucsi/cros_ec_ucsi.c b/drivers/usb/typec/ucsi/cros_ec_ucsi.c |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b369096d06099e99e5295c8f44e3903cfd30fb58 |
| --- /dev/null |
| +++ b/drivers/usb/typec/ucsi/cros_ec_ucsi.c |
| @@ -0,0 +1,271 @@ |
| +// SPDX-License-Identifier: GPL-2.0 |
| +/* |
| + * UCSI driver for ChromeOS EC |
| + * |
| + * Copyright 2024 Google LLC. |
| + */ |
| + |
| +#include <linux/container_of.h> |
| +#include <linux/dev_printk.h> |
| +#include <linux/mod_devicetable.h> |
| +#include <linux/module.h> |
| +#include <linux/platform_data/cros_ec_commands.h> |
| +#include <linux/platform_data/cros_usbpd_notify.h> |
| +#include <linux/platform_data/cros_ec_proto.h> |
| +#include <linux/platform_device.h> |
| +#include <linux/slab.h> |
| +#include <linux/wait.h> |
| + |
| +#include "ucsi.h" |
| + |
| +/* MAX_EC_DATA_SIZE is the number of bytes that can be read from or written to |
| + * in the UCSI data structure using a single host command to the EC. |
| + */ |
| +#define MAX_EC_DATA_SIZE 256 |
| + |
| +/* WRITE_TMO_MS is the time within which a cmd complete or ack notification must |
| + * arrive after a command is sent to the PPM. |
| + */ |
| +#define WRITE_TMO_MS 500 |
| + |
| +struct cros_ucsi_data { |
| + struct device *dev; |
| + struct ucsi *ucsi; |
| + |
| + struct cros_ec_device *ec; |
| + struct notifier_block nb; |
| + struct work_struct work; |
| + |
| + struct completion complete; |
| + unsigned long flags; |
| +}; |
| + |
| +static int cros_ucsi_read(struct ucsi *ucsi, unsigned int offset, void *val, |
| + size_t val_len) |
| +{ |
| + struct cros_ucsi_data *udata = ucsi_get_drvdata(ucsi); |
| + struct ec_params_ucsi_ppm_get req = { |
| + .offset = offset, |
| + .size = val_len, |
| + }; |
| + int ret; |
| + |
| + if (val_len > MAX_EC_DATA_SIZE) { |
| + dev_err(udata->dev, "Can't read %zu bytes. Too big.", val_len); |
| + return -EINVAL; |
| + } |
| + |
| + ret = cros_ec_cmd(udata->ec, 0, EC_CMD_UCSI_PPM_GET, |
| + &req, sizeof(req), val, val_len); |
| + if (ret < 0) { |
| + dev_warn(udata->dev, "Failed to send EC message UCSI_PPM_GET: error=%d", ret); |
| + return ret; |
| + } |
| + return 0; |
| +} |
| + |
| +static int cros_ucsi_async_write(struct ucsi *ucsi, unsigned int offset, |
| + const void *val, size_t val_len) |
| +{ |
| + struct cros_ucsi_data *udata = ucsi_get_drvdata(ucsi); |
| + struct ec_params_ucsi_ppm_set *req; |
| + size_t req_len; |
| + int ret; |
| + |
| + if (val_len > MAX_EC_DATA_SIZE) { |
| + dev_err(udata->dev, "Can't write %zu bytes. Too big.", val_len); |
| + return -EINVAL; |
| + } |
| + |
| + req_len = sizeof(struct ec_params_ucsi_ppm_set) + val_len; |
| + req = kzalloc(req_len, GFP_KERNEL); |
| + if (!req) |
| + return -ENOMEM; |
| + req->offset = offset; |
| + memcpy(req->data, val, val_len); |
| + ret = cros_ec_cmd(udata->ec, 0, EC_CMD_UCSI_PPM_SET, |
| + req, req_len, NULL, 0); |
| + kfree(req); |
| + |
| + if (ret < 0) { |
| + dev_warn(udata->dev, "Failed to send EC message UCSI_PPM_SET: error=%d", ret); |
| + return ret; |
| + } |
| + return 0; |
| +} |
| + |
| +static int cros_ucsi_sync_write(struct ucsi *ucsi, unsigned int offset, |
| + const void *val, size_t val_len) |
| +{ |
| + struct cros_ucsi_data *udata = ucsi_get_drvdata(ucsi); |
| + bool ack = UCSI_COMMAND(*(u64 *)val) == UCSI_ACK_CC_CI; |
| + int ret; |
| + |
| + if (ack) |
| + set_bit(ACK_PENDING, &udata->flags); |
| + else |
| + set_bit(COMMAND_PENDING, &udata->flags); |
| + |
| + ret = cros_ucsi_async_write(ucsi, offset, val, val_len); |
| + if (ret) |
| + goto err; |
| + |
| + if (!wait_for_completion_timeout(&udata->complete, WRITE_TMO_MS)) { |
| + ret = -ETIMEDOUT; |
| + goto err; |
| + } |
| + |
| + return 0; |
| +err: |
| + if (ack) |
| + clear_bit(ACK_PENDING, &udata->flags); |
| + else |
| + clear_bit(COMMAND_PENDING, &udata->flags); |
| + return ret; |
| +} |
| + |
| +struct ucsi_operations cros_ucsi_ops = { |
| + .read = cros_ucsi_read, |
| + .async_write = cros_ucsi_async_write, |
| + .sync_write = cros_ucsi_sync_write, |
| +}; |
| + |
| +static void cros_ucsi_work(struct work_struct *work) |
| +{ |
| + struct cros_ucsi_data *udata = container_of(work, struct cros_ucsi_data, work); |
| + u32 cci; |
| + |
| + if (cros_ucsi_read(udata->ucsi, UCSI_CCI, &cci, sizeof(cci))) |
| + return; |
| + |
| + if (UCSI_CCI_CONNECTOR(cci)) |
| + ucsi_connector_change(udata->ucsi, UCSI_CCI_CONNECTOR(cci)); |
| + |
| + if (cci & UCSI_CCI_ACK_COMPLETE && |
| + test_and_clear_bit(ACK_PENDING, &udata->flags)) |
| + complete(&udata->complete); |
| + if (cci & UCSI_CCI_COMMAND_COMPLETE && |
| + test_and_clear_bit(COMMAND_PENDING, &udata->flags)) |
| + complete(&udata->complete); |
| +} |
| + |
| +static int cros_ucsi_event(struct notifier_block *nb, |
| + unsigned long host_event, void *_notify) |
| +{ |
| + struct cros_ucsi_data *udata = container_of(nb, struct cros_ucsi_data, nb); |
| + |
| + if (!(host_event & PD_EVENT_PPM)) |
| + return NOTIFY_OK; |
| + |
| + dev_dbg(udata->dev, "UCSI notification received"); |
| + flush_work(&udata->work); |
| + schedule_work(&udata->work); |
| + |
| + return NOTIFY_OK; |
| +} |
| + |
| +static void cros_ucsi_destroy(struct cros_ucsi_data *udata) |
| +{ |
| + cros_usbpd_unregister_notify(&udata->nb); |
| + cancel_work_sync(&udata->work); |
| + ucsi_destroy(udata->ucsi); |
| +} |
| + |
| +static int cros_ucsi_probe(struct platform_device *pdev) |
| +{ |
| + struct device *dev = &pdev->dev; |
| + struct cros_ec_dev *ec_data = dev_get_drvdata(dev->parent); |
| + struct cros_ucsi_data *udata; |
| + int ret; |
| + |
| + udata = devm_kzalloc(dev, sizeof(*udata), GFP_KERNEL); |
| + if (!udata) |
| + return -ENOMEM; |
| + |
| + udata->dev = dev; |
| + |
| + udata->ec = ec_data->ec_dev; |
| + if (!udata->ec) { |
| + dev_err(dev, "couldn't find parent EC device"); |
| + return -ENODEV; |
| + } |
| + |
| + platform_set_drvdata(pdev, udata); |
| + |
| + INIT_WORK(&udata->work, cros_ucsi_work); |
| + init_completion(&udata->complete); |
| + |
| + udata->ucsi = ucsi_create(dev, &cros_ucsi_ops); |
| + if (IS_ERR(udata->ucsi)) { |
| + dev_err(dev, "failed to allocate UCSI instance"); |
| + return PTR_ERR(udata->ucsi); |
| + } |
| + |
| + ucsi_set_drvdata(udata->ucsi, udata); |
| + |
| + udata->nb.notifier_call = cros_ucsi_event; |
| + ret = cros_usbpd_register_notify(&udata->nb); |
| + if (ret) { |
| + dev_err(dev, "failed to register notifier: error=%d", ret); |
| + ucsi_destroy(udata->ucsi); |
| + return ret; |
| + } |
| + |
| + ret = ucsi_register(udata->ucsi); |
| + if (ret) { |
| + dev_err(dev, "failed to register UCSI: error=%d", ret); |
| + cros_ucsi_destroy(udata); |
| + return ret; |
| + } |
| + |
| + return 0; |
| +} |
| + |
| +static int cros_ucsi_remove(struct platform_device *dev) |
| +{ |
| + struct cros_ucsi_data *udata = platform_get_drvdata(dev); |
| + |
| + ucsi_unregister(udata->ucsi); |
| + cros_ucsi_destroy(udata); |
| + return 0; |
| +} |
| + |
| +static int __maybe_unused cros_ucsi_suspend(struct device *dev) |
| +{ |
| + struct cros_ucsi_data *udata = dev_get_drvdata(dev); |
| + |
| + cancel_work_sync(&udata->work); |
| + |
| + return 0; |
| +} |
| + |
| +static int __maybe_unused cros_ucsi_resume(struct device *dev) |
| +{ |
| + struct cros_ucsi_data *udata = dev_get_drvdata(dev); |
| + |
| + return ucsi_resume(udata->ucsi); |
| +} |
| + |
| +static SIMPLE_DEV_PM_OPS(cros_ucsi_pm_ops, cros_ucsi_suspend, |
| + cros_ucsi_resume); |
| + |
| +static const struct platform_device_id cros_ucsi_id[] = { |
| + { KBUILD_MODNAME, 0 }, |
| + {} |
| +}; |
| +MODULE_DEVICE_TABLE(platform, cros_ucsi_id); |
| + |
| +static struct platform_driver cros_ucsi_driver = { |
| + .driver = { |
| + .name = KBUILD_MODNAME, |
| + .pm = &cros_ucsi_pm_ops, |
| + }, |
| + .id_table = cros_ucsi_id, |
| + .probe = cros_ucsi_probe, |
| + .remove = cros_ucsi_remove, |
| +}; |
| + |
| +module_platform_driver(cros_ucsi_driver); |
| + |
| +MODULE_LICENSE("GPL"); |
| +MODULE_DESCRIPTION("UCSI driver for ChromeOS EC"); |
| diff --git a/include/linux/platform_data/cros_ec_commands.h b/include/linux/platform_data/cros_ec_commands.h |
| index e574b790be6fbc1fca47df05eb3232edec93e6bc..321594d22a8b29055d6965d1f33315661315a3ed 100644 |
| --- a/include/linux/platform_data/cros_ec_commands.h |
| +++ b/include/linux/platform_data/cros_ec_commands.h |
| @@ -5012,6 +5012,7 @@ struct ec_response_pd_status { |
| #define PD_EVENT_POWER_CHANGE BIT(1) |
| #define PD_EVENT_IDENTITY_RECEIVED BIT(2) |
| #define PD_EVENT_DATA_SWAP BIT(3) |
| +#define PD_EVENT_PPM BIT(5) |
| struct ec_response_host_event_status { |
| uint32_t status; /* PD MCU host event status */ |
| } __ec_align4; |
| @@ -6073,6 +6074,24 @@ struct ec_response_typec_vdm_response { |
| |
| #undef VDO_MAX_SIZE |
| |
| +/* |
| + * Read/write interface for UCSI OPM <-> PPM communication. |
| + */ |
| +#define EC_CMD_UCSI_PPM_SET 0x0140 |
| + |
| +/* The data size is stored in the host command protocol header. */ |
| +struct ec_params_ucsi_ppm_set { |
| + uint16_t offset; |
| + uint8_t data[]; |
| +} __ec_align2; |
| + |
| +#define EC_CMD_UCSI_PPM_GET 0x0141 |
| + |
| +struct ec_params_ucsi_ppm_get { |
| + uint16_t offset; |
| + uint8_t size; |
| +} __ec_align2; |
| + |
| /*****************************************************************************/ |
| /* The command range 0x200-0x2FF is reserved for Rotor. */ |
| |
| -- |
| 2.46.0.rc2.264.g509ed76dc8-goog |
| |