summaryrefslogtreecommitdiffstats
path: root/drivers
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2022-10-05 19:14:48 +0200
committerLinus Torvalds <torvalds@linux-foundation.org>2022-10-05 19:14:48 +0200
commit1c2daf52185bbc91421f0e84e6bf2706bb350cce (patch)
tree9ea3cb05ced3148a46dfe6b729766ae8fd415766 /drivers
parentMerge tag 'mmc-v6.1' of git://git.kernel.org/pub/scm/linux/kernel/git/ulfh/mmc (diff)
parentplatform/chrome: cros_ec: Notify the PM of wake events during resume (diff)
downloadlinux-1c2daf52185bbc91421f0e84e6bf2706bb350cce.tar.xz
linux-1c2daf52185bbc91421f0e84e6bf2706bb350cce.zip
Merge tag 'tag-chrome-platform-for-v6.1' of git://git.kernel.org/pub/scm/linux/kernel/git/chrome-platform/linux
Pull chrome platform updates from Tzung-Bi Shih: "cros_ec_proto: - Fix protocol failure if EC firmware jumps to RO part cros_typec_switch: - Add USB Type-C switch driver for mode switches and retimers - Integrate to EC for retimers, status update, and mode switches - Clean-ups cros_ec_typec: - Clean-ups - Use partner PDOs to register USB PD capabilities chromeos_laptop: - Fix a double-free cros_ec_chardev: - Check data length from userland to avoid a memory corruption cros_ec: - Expose suspend_timeout_ms in debugfs - Notify the PM about wake events during resume" * tag 'tag-chrome-platform-for-v6.1' of git://git.kernel.org/pub/scm/linux/kernel/git/chrome-platform/linux: platform/chrome: cros_ec: Notify the PM of wake events during resume platform/chrome: cros_ec_typec: Register partner PDOs platform/chrome: cros_typec_switch: Inline DRV_NAME platform/chrome: cros_typec_switch: Use PTR_ERR_OR_ZERO() to simplify platform/chrome: cros_typec_switch: Remove impossible condition platform/chrome: cros_typec_switch: Add missing newline on printk platform/chrome: cros_ec_typec: Correct alt mode index platform/chrome: cros_ec_typec: Add bit offset for DP VDO platform/chrome: cros_ec: Expose suspend_timeout_ms in debugfs platform/chrome: fix memory corruption in ioctl platform/chrome: fix double-free in chromeos_laptop_prepare() platform/chrome: cros_ec_typec: Get retimer handle platform/chrome: cros_ec_typec: Cleanup switch handle return paths platform/chrome: cros_typec_switch: Register mode switches platform/chrome: cros_typec_switch: Add event check platform/chrome: cros_typec_switch: Set EC retimer platform/chrome: cros_typec_switch: Add switch driver platform/chrome: Add Type-C mux set command definitions platform/chrome: cros_ec_proto: Update version on GET_NEXT_EVENT failure
Diffstat (limited to 'drivers')
-rw-r--r--drivers/platform/chrome/Kconfig11
-rw-r--r--drivers/platform/chrome/Makefile1
-rw-r--r--drivers/platform/chrome/chromeos_laptop.c24
-rw-r--r--drivers/platform/chrome/cros_ec.c11
-rw-r--r--drivers/platform/chrome/cros_ec_chardev.c3
-rw-r--r--drivers/platform/chrome/cros_ec_debugfs.c3
-rw-r--r--drivers/platform/chrome/cros_ec_proto.c32
-rw-r--r--drivers/platform/chrome/cros_ec_typec.c110
-rw-r--r--drivers/platform/chrome/cros_typec_switch.c321
9 files changed, 494 insertions, 22 deletions
diff --git a/drivers/platform/chrome/Kconfig b/drivers/platform/chrome/Kconfig
index c45fb376d653..6b954c5acadb 100644
--- a/drivers/platform/chrome/Kconfig
+++ b/drivers/platform/chrome/Kconfig
@@ -265,6 +265,17 @@ config CHROMEOS_PRIVACY_SCREEN
this should probably always be built into the kernel to avoid or
minimize drm probe deferral.
+config CROS_TYPEC_SWITCH
+ tristate "ChromeOS EC Type-C Switch Control"
+ depends on MFD_CROS_EC_DEV && TYPEC && ACPI
+ default MFD_CROS_EC_DEV
+ help
+ If you say Y here, you get support for configuring the ChromeOS EC Type-C
+ muxes and retimers.
+
+ To compile this driver as a module, choose M here: the module will be
+ called cros_typec_switch.
+
source "drivers/platform/chrome/wilco_ec/Kconfig"
# Kunit test cases
diff --git a/drivers/platform/chrome/Makefile b/drivers/platform/chrome/Makefile
index f7e74a845afc..2950610101f1 100644
--- a/drivers/platform/chrome/Makefile
+++ b/drivers/platform/chrome/Makefile
@@ -12,6 +12,7 @@ obj-$(CONFIG_CHROMEOS_TBMC) += chromeos_tbmc.o
obj-$(CONFIG_CROS_EC) += cros_ec.o
obj-$(CONFIG_CROS_EC_I2C) += cros_ec_i2c.o
obj-$(CONFIG_CROS_EC_ISHTP) += cros_ec_ishtp.o
+obj-$(CONFIG_CROS_TYPEC_SWITCH) += cros_typec_switch.o
obj-$(CONFIG_CROS_EC_RPMSG) += cros_ec_rpmsg.o
obj-$(CONFIG_CROS_EC_SPI) += cros_ec_spi.o
cros_ec_lpcs-objs := cros_ec_lpc.o cros_ec_lpc_mec.o
diff --git a/drivers/platform/chrome/chromeos_laptop.c b/drivers/platform/chrome/chromeos_laptop.c
index 4e14b4d6635d..a2cdbfbaeae6 100644
--- a/drivers/platform/chrome/chromeos_laptop.c
+++ b/drivers/platform/chrome/chromeos_laptop.c
@@ -740,6 +740,7 @@ static int __init
chromeos_laptop_prepare_i2c_peripherals(struct chromeos_laptop *cros_laptop,
const struct chromeos_laptop *src)
{
+ struct i2c_peripheral *i2c_peripherals;
struct i2c_peripheral *i2c_dev;
struct i2c_board_info *info;
int i;
@@ -748,17 +749,15 @@ chromeos_laptop_prepare_i2c_peripherals(struct chromeos_laptop *cros_laptop,
if (!src->num_i2c_peripherals)
return 0;
- cros_laptop->i2c_peripherals = kmemdup(src->i2c_peripherals,
- src->num_i2c_peripherals *
- sizeof(*src->i2c_peripherals),
- GFP_KERNEL);
- if (!cros_laptop->i2c_peripherals)
+ i2c_peripherals = kmemdup(src->i2c_peripherals,
+ src->num_i2c_peripherals *
+ sizeof(*src->i2c_peripherals),
+ GFP_KERNEL);
+ if (!i2c_peripherals)
return -ENOMEM;
- cros_laptop->num_i2c_peripherals = src->num_i2c_peripherals;
-
- for (i = 0; i < cros_laptop->num_i2c_peripherals; i++) {
- i2c_dev = &cros_laptop->i2c_peripherals[i];
+ for (i = 0; i < src->num_i2c_peripherals; i++) {
+ i2c_dev = &i2c_peripherals[i];
info = &i2c_dev->board_info;
error = chromeos_laptop_setup_irq(i2c_dev);
@@ -775,16 +774,19 @@ chromeos_laptop_prepare_i2c_peripherals(struct chromeos_laptop *cros_laptop,
}
}
+ cros_laptop->i2c_peripherals = i2c_peripherals;
+ cros_laptop->num_i2c_peripherals = src->num_i2c_peripherals;
+
return 0;
err_out:
while (--i >= 0) {
- i2c_dev = &cros_laptop->i2c_peripherals[i];
+ i2c_dev = &i2c_peripherals[i];
info = &i2c_dev->board_info;
if (!IS_ERR_OR_NULL(info->fwnode))
fwnode_remove_software_node(info->fwnode);
}
- kfree(cros_laptop->i2c_peripherals);
+ kfree(i2c_peripherals);
return error;
}
diff --git a/drivers/platform/chrome/cros_ec.c b/drivers/platform/chrome/cros_ec.c
index 8aace50d446d..ec733f683f34 100644
--- a/drivers/platform/chrome/cros_ec.c
+++ b/drivers/platform/chrome/cros_ec.c
@@ -115,7 +115,7 @@ static int cros_ec_sleep_event(struct cros_ec_device *ec_dev, u8 sleep_event)
if (ec_dev->host_sleep_v1) {
buf.u.req1.sleep_event = sleep_event;
buf.u.req1.suspend_params.sleep_timeout_ms =
- EC_HOST_SLEEP_TIMEOUT_DEFAULT;
+ ec_dev->suspend_timeout_ms;
buf.msg.outsize = sizeof(buf.u.req1);
if ((sleep_event == HOST_SLEEP_EVENT_S3_RESUME) ||
@@ -188,6 +188,7 @@ int cros_ec_register(struct cros_ec_device *ec_dev)
ec_dev->max_passthru = 0;
ec_dev->ec = NULL;
ec_dev->pd = NULL;
+ ec_dev->suspend_timeout_ms = EC_HOST_SLEEP_TIMEOUT_DEFAULT;
ec_dev->din = devm_kzalloc(dev, ec_dev->din_size, GFP_KERNEL);
if (!ec_dev->din)
@@ -349,10 +350,16 @@ EXPORT_SYMBOL(cros_ec_suspend);
static void cros_ec_report_events_during_suspend(struct cros_ec_device *ec_dev)
{
+ bool wake_event;
+
while (ec_dev->mkbp_event_supported &&
- cros_ec_get_next_event(ec_dev, NULL, NULL) > 0)
+ cros_ec_get_next_event(ec_dev, &wake_event, NULL) > 0) {
blocking_notifier_call_chain(&ec_dev->event_notifier,
1, ec_dev);
+
+ if (wake_event && device_may_wakeup(ec_dev->dev))
+ pm_wakeup_event(ec_dev->dev, 0);
+ }
}
/**
diff --git a/drivers/platform/chrome/cros_ec_chardev.c b/drivers/platform/chrome/cros_ec_chardev.c
index fd33de546aee..0de7c255254e 100644
--- a/drivers/platform/chrome/cros_ec_chardev.c
+++ b/drivers/platform/chrome/cros_ec_chardev.c
@@ -327,6 +327,9 @@ static long cros_ec_chardev_ioctl_readmem(struct cros_ec_dev *ec,
if (copy_from_user(&s_mem, arg, sizeof(s_mem)))
return -EFAULT;
+ if (s_mem.bytes > sizeof(s_mem.buffer))
+ return -EINVAL;
+
num = ec_dev->cmd_readmem(ec_dev, s_mem.offset, s_mem.bytes,
s_mem.buffer);
if (num <= 0)
diff --git a/drivers/platform/chrome/cros_ec_debugfs.c b/drivers/platform/chrome/cros_ec_debugfs.c
index 0dbceee87a4b..4e63adf083ea 100644
--- a/drivers/platform/chrome/cros_ec_debugfs.c
+++ b/drivers/platform/chrome/cros_ec_debugfs.c
@@ -470,6 +470,9 @@ static int cros_ec_debugfs_probe(struct platform_device *pd)
debugfs_create_x32("last_resume_result", 0444, debug_info->dir,
&ec->ec_dev->last_resume_result);
+ debugfs_create_u16("suspend_timeout_ms", 0664, debug_info->dir,
+ &ec->ec_dev->suspend_timeout_ms);
+
ec->debug_info = debug_info;
dev_set_drvdata(&pd->dev, ec);
diff --git a/drivers/platform/chrome/cros_ec_proto.c b/drivers/platform/chrome/cros_ec_proto.c
index 05d2e8765a66..475a6dd72db6 100644
--- a/drivers/platform/chrome/cros_ec_proto.c
+++ b/drivers/platform/chrome/cros_ec_proto.c
@@ -773,6 +773,7 @@ int cros_ec_get_next_event(struct cros_ec_device *ec_dev,
u8 event_type;
u32 host_event;
int ret;
+ u32 ver_mask;
/*
* Default value for wake_event.
@@ -794,6 +795,37 @@ int cros_ec_get_next_event(struct cros_ec_device *ec_dev,
return get_keyboard_state_event(ec_dev);
ret = get_next_event(ec_dev);
+ /*
+ * -ENOPROTOOPT is returned when EC returns EC_RES_INVALID_VERSION.
+ * This can occur when EC based device (e.g. Fingerprint MCU) jumps to
+ * the RO image which doesn't support newer version of the command. In
+ * this case we will attempt to update maximum supported version of the
+ * EC_CMD_GET_NEXT_EVENT.
+ */
+ if (ret == -ENOPROTOOPT) {
+ dev_dbg(ec_dev->dev,
+ "GET_NEXT_EVENT returned invalid version error.\n");
+ ret = cros_ec_get_host_command_version_mask(ec_dev,
+ EC_CMD_GET_NEXT_EVENT,
+ &ver_mask);
+ if (ret < 0 || ver_mask == 0)
+ /*
+ * Do not change the MKBP supported version if we can't
+ * obtain supported version correctly. Please note that
+ * calling EC_CMD_GET_NEXT_EVENT returned
+ * EC_RES_INVALID_VERSION which means that the command
+ * is present.
+ */
+ return -ENOPROTOOPT;
+
+ ec_dev->mkbp_event_supported = fls(ver_mask);
+ dev_dbg(ec_dev->dev, "MKBP support version changed to %u\n",
+ ec_dev->mkbp_event_supported - 1);
+
+ /* Try to get next event with new MKBP support version set. */
+ ret = get_next_event(ec_dev);
+ }
+
if (ret <= 0)
return ret;
diff --git a/drivers/platform/chrome/cros_ec_typec.c b/drivers/platform/chrome/cros_ec_typec.c
index de6ee0f926a6..2a7ff14dc37e 100644
--- a/drivers/platform/chrome/cros_ec_typec.c
+++ b/drivers/platform/chrome/cros_ec_typec.c
@@ -20,12 +20,14 @@
#include <linux/usb/typec_altmode.h>
#include <linux/usb/typec_dp.h>
#include <linux/usb/typec_mux.h>
+#include <linux/usb/typec_retimer.h>
#include <linux/usb/typec_tbt.h>
#include <linux/usb/role.h>
#define DRV_NAME "cros-ec-typec"
-#define DP_PORT_VDO (BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) | DP_CAP_DFP_D)
+#define DP_PORT_VDO (DP_CONF_SET_PIN_ASSIGN(BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D)) | \
+ DP_CAP_DFP_D)
/* Supported alt modes. */
enum {
@@ -55,6 +57,7 @@ struct cros_typec_port {
struct usb_pd_identity c_identity;
struct typec_switch *ori_sw;
struct typec_mux *mux;
+ struct typec_retimer *retimer;
struct usb_role_switch *role_sw;
/* Variables keeping track of switch state. */
@@ -70,6 +73,11 @@ struct cros_typec_port {
struct ec_response_typec_discovery *disc_data;
struct list_head partner_mode_list;
struct list_head plug_mode_list;
+
+ /* PDO-related structs */
+ struct usb_power_delivery *partner_pd;
+ struct usb_power_delivery_capabilities *partner_src_caps;
+ struct usb_power_delivery_capabilities *partner_sink_caps;
};
/* Platform-specific data for the Chrome OS EC Type C controller. */
@@ -143,6 +151,12 @@ static int cros_typec_get_switch_handles(struct cros_typec_port *port,
goto mux_err;
}
+ port->retimer = fwnode_typec_retimer_get(fwnode);
+ if (IS_ERR(port->retimer)) {
+ dev_dbg(dev, "Retimer handle not found.\n");
+ goto retimer_sw_err;
+ }
+
port->ori_sw = fwnode_typec_switch_get(fwnode);
if (IS_ERR(port->ori_sw)) {
dev_dbg(dev, "Orientation switch handle not found.\n");
@@ -158,12 +172,12 @@ static int cros_typec_get_switch_handles(struct cros_typec_port *port,
return 0;
role_sw_err:
- usb_role_switch_put(port->role_sw);
-ori_sw_err:
typec_switch_put(port->ori_sw);
-mux_err:
+ori_sw_err:
+ typec_retimer_put(port->retimer);
+retimer_sw_err:
typec_mux_put(port->mux);
-
+mux_err:
return -ENODEV;
}
@@ -206,6 +220,21 @@ static void cros_typec_unregister_altmodes(struct cros_typec_data *typec, int po
}
}
+/*
+ * Map the Type-C Mux state to retimer state and call the retimer set function. We need this
+ * because we re-use the Type-C mux state for retimers.
+ */
+static int cros_typec_retimer_set(struct typec_retimer *retimer, struct typec_mux_state state)
+{
+ struct typec_retimer_state rstate = {
+ .alt = state.alt,
+ .mode = state.mode,
+ .data = state.data,
+ };
+
+ return typec_retimer_set(retimer, &rstate);
+}
+
static int cros_typec_usb_disconnect_state(struct cros_typec_port *port)
{
port->state.alt = NULL;
@@ -214,6 +243,7 @@ static int cros_typec_usb_disconnect_state(struct cros_typec_port *port)
usb_role_switch_set_role(port->role_sw, USB_ROLE_NONE);
typec_switch_set(port->ori_sw, TYPEC_ORIENTATION_NONE);
+ cros_typec_retimer_set(port->retimer, port->state);
return typec_mux_set(port->mux, &port->state);
}
@@ -228,6 +258,14 @@ static void cros_typec_remove_partner(struct cros_typec_data *typec,
cros_typec_unregister_altmodes(typec, port_num, true);
+ typec_partner_set_usb_power_delivery(port->partner, NULL);
+ usb_power_delivery_unregister_capabilities(port->partner_sink_caps);
+ port->partner_sink_caps = NULL;
+ usb_power_delivery_unregister_capabilities(port->partner_src_caps);
+ port->partner_src_caps = NULL;
+ usb_power_delivery_unregister(port->partner_pd);
+ port->partner_pd = NULL;
+
cros_typec_usb_disconnect_state(port);
port->mux_flags = USB_PD_MUX_NONE;
@@ -411,9 +449,14 @@ unregister_ports:
static int cros_typec_usb_safe_state(struct cros_typec_port *port)
{
+ int ret;
port->state.mode = TYPEC_STATE_SAFE;
- return typec_mux_set(port->mux, &port->state);
+ ret = cros_typec_retimer_set(port->retimer, port->state);
+ if (!ret)
+ ret = typec_mux_set(port->mux, &port->state);
+
+ return ret;
}
/*
@@ -510,7 +553,11 @@ static int cros_typec_enable_dp(struct cros_typec_data *typec,
port->state.data = &dp_data;
port->state.mode = TYPEC_MODAL_STATE(ffs(pd_ctrl->dp_mode));
- return typec_mux_set(port->mux, &port->state);
+ ret = cros_typec_retimer_set(port->retimer, port->state);
+ if (!ret)
+ ret = typec_mux_set(port->mux, &port->state);
+
+ return ret;
}
static int cros_typec_enable_usb4(struct cros_typec_data *typec,
@@ -599,7 +646,10 @@ static int cros_typec_configure_mux(struct cros_typec_data *typec, int port_num,
} else if (port->mux_flags & USB_PD_MUX_USB_ENABLED) {
port->state.alt = NULL;
port->state.mode = TYPEC_STATE_USB;
- ret = typec_mux_set(port->mux, &port->state);
+
+ ret = cros_typec_retimer_set(port->retimer, port->state);
+ if (!ret)
+ ret = typec_mux_set(port->mux, &port->state);
} else {
dev_dbg(typec->dev,
"Unrecognized mode requested, mux flags: %x\n",
@@ -697,7 +747,7 @@ static int cros_typec_register_altmodes(struct cros_typec_data *typec, int port_
for (j = 0; j < sop_disc->svids[i].mode_count; j++) {
memset(&desc, 0, sizeof(desc));
desc.svid = sop_disc->svids[i].svid;
- desc.mode = j;
+ desc.mode = j + 1;
desc.vdo = sop_disc->svids[i].mode_vdo[j];
if (is_partner)
@@ -902,6 +952,46 @@ static int cros_typec_send_clear_event(struct cros_typec_data *typec, int port_n
sizeof(req), NULL, 0);
}
+static void cros_typec_register_partner_pdos(struct cros_typec_data *typec,
+ struct ec_response_typec_status *resp, int port_num)
+{
+ struct usb_power_delivery_capabilities_desc caps_desc = {};
+ struct usb_power_delivery_desc desc = {
+ .revision = (le16_to_cpu(resp->sop_revision) & 0xff00) >> 4,
+ };
+ struct cros_typec_port *port = typec->ports[port_num];
+
+ if (!port->partner || port->partner_pd)
+ return;
+
+ /* If no caps are available, don't bother creating a device. */
+ if (!resp->source_cap_count && !resp->sink_cap_count)
+ return;
+
+ port->partner_pd = usb_power_delivery_register(NULL, &desc);
+ if (IS_ERR(port->partner_pd)) {
+ dev_warn(typec->dev, "Failed to register partner PD device, port: %d\n", port_num);
+ return;
+ }
+
+ typec_partner_set_usb_power_delivery(port->partner, port->partner_pd);
+
+ memcpy(caps_desc.pdo, resp->source_cap_pdos, sizeof(u32) * resp->source_cap_count);
+ caps_desc.role = TYPEC_SOURCE;
+ port->partner_src_caps = usb_power_delivery_register_capabilities(port->partner_pd,
+ &caps_desc);
+ if (IS_ERR(port->partner_src_caps))
+ dev_warn(typec->dev, "Failed to register source caps, port: %d\n", port_num);
+
+ memset(&caps_desc, 0, sizeof(caps_desc));
+ memcpy(caps_desc.pdo, resp->sink_cap_pdos, sizeof(u32) * resp->sink_cap_count);
+ caps_desc.role = TYPEC_SINK;
+ port->partner_sink_caps = usb_power_delivery_register_capabilities(port->partner_pd,
+ &caps_desc);
+ if (IS_ERR(port->partner_sink_caps))
+ dev_warn(typec->dev, "Failed to register sink caps, port: %d\n", port_num);
+}
+
static void cros_typec_handle_status(struct cros_typec_data *typec, int port_num)
{
struct ec_response_typec_status resp;
@@ -949,6 +1039,8 @@ static void cros_typec_handle_status(struct cros_typec_data *typec, int port_num
}
if (resp.sop_connected)
typec_set_pwr_opmode(typec->ports[port_num]->port, TYPEC_PWR_MODE_PD);
+
+ cros_typec_register_partner_pdos(typec, &resp, port_num);
}
if (resp.events & PD_STATUS_EVENT_SOP_PRIME_DISC_DONE &&
diff --git a/drivers/platform/chrome/cros_typec_switch.c b/drivers/platform/chrome/cros_typec_switch.c
new file mode 100644
index 000000000000..a26219e97c93
--- /dev/null
+++ b/drivers/platform/chrome/cros_typec_switch.c
@@ -0,0 +1,321 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2022 Google LLC
+ *
+ * This driver provides the ability to configure Type-C muxes and retimers which are controlled by
+ * the ChromeOS EC.
+ */
+
+#include <linux/acpi.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/platform_data/cros_ec_commands.h>
+#include <linux/platform_data/cros_ec_proto.h>
+#include <linux/platform_device.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+#include <linux/usb/typec_retimer.h>
+
+/* Handles and other relevant data required for each port's switches. */
+struct cros_typec_port {
+ int port_num;
+ struct typec_mux_dev *mode_switch;
+ struct typec_retimer *retimer;
+ struct cros_typec_switch_data *sdata;
+};
+
+/* Driver-specific data. */
+struct cros_typec_switch_data {
+ struct device *dev;
+ struct cros_ec_device *ec;
+ struct cros_typec_port *ports[EC_USB_PD_MAX_PORTS];
+};
+
+static int cros_typec_cmd_mux_set(struct cros_typec_switch_data *sdata, int port_num, u8 index,
+ u8 state)
+{
+ struct ec_params_typec_control req = {
+ .port = port_num,
+ .command = TYPEC_CONTROL_COMMAND_USB_MUX_SET,
+ .mux_params = {
+ .mux_index = index,
+ .mux_flags = state,
+ },
+ };
+
+ return cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_CONTROL, &req, sizeof(req), NULL, 0);
+}
+
+static int cros_typec_get_mux_state(unsigned long mode, struct typec_altmode *alt)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (mode == TYPEC_STATE_SAFE)
+ ret = USB_PD_MUX_SAFE_MODE;
+ else if (mode == TYPEC_STATE_USB)
+ ret = USB_PD_MUX_USB_ENABLED;
+ else if (alt && alt->svid == USB_TYPEC_DP_SID)
+ ret = USB_PD_MUX_DP_ENABLED;
+
+ return ret;
+}
+
+static int cros_typec_send_clear_event(struct cros_typec_switch_data *sdata, int port_num,
+ u32 events_mask)
+{
+ struct ec_params_typec_control req = {
+ .port = port_num,
+ .command = TYPEC_CONTROL_COMMAND_CLEAR_EVENTS,
+ .clear_events_mask = events_mask,
+ };
+
+ return cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_CONTROL, &req, sizeof(req), NULL, 0);
+}
+
+static bool cros_typec_check_event(struct cros_typec_switch_data *sdata, int port_num, u32 mask)
+{
+ struct ec_response_typec_status resp;
+ struct ec_params_typec_status req = {
+ .port = port_num,
+ };
+ int ret;
+
+ ret = cros_ec_cmd(sdata->ec, 0, EC_CMD_TYPEC_STATUS, &req, sizeof(req),
+ &resp, sizeof(resp));
+ if (ret < 0) {
+ dev_warn(sdata->dev, "EC_CMD_TYPEC_STATUS failed for port: %d\n", port_num);
+ return false;
+ }
+
+ if (resp.events & mask)
+ return true;
+
+ return false;
+}
+
+/*
+ * The ChromeOS EC treats both mode-switches and retimers as "muxes" for the purposes of the
+ * host command API. This common function configures and verifies the retimer/mode-switch
+ * according to the provided setting.
+ */
+static int cros_typec_configure_mux(struct cros_typec_switch_data *sdata, int port_num, int index,
+ unsigned long mode, struct typec_altmode *alt)
+{
+ unsigned long end;
+ u32 event_mask;
+ u8 mux_state;
+ int ret;
+
+ ret = cros_typec_get_mux_state(mode, alt);
+ if (ret < 0)
+ return ret;
+ mux_state = (u8)ret;
+
+ /* Clear any old mux set done event. */
+ if (index == 0)
+ event_mask = PD_STATUS_EVENT_MUX_0_SET_DONE;
+ else
+ event_mask = PD_STATUS_EVENT_MUX_1_SET_DONE;
+
+ ret = cros_typec_send_clear_event(sdata, port_num, event_mask);
+ if (ret < 0)
+ return ret;
+
+ /* Send the set command. */
+ ret = cros_typec_cmd_mux_set(sdata, port_num, index, mux_state);
+ if (ret < 0)
+ return ret;
+
+ /* Check for the mux set done event. */
+ end = jiffies + msecs_to_jiffies(1000);
+ do {
+ if (cros_typec_check_event(sdata, port_num, event_mask))
+ return 0;
+
+ usleep_range(500, 1000);
+ } while (time_before(jiffies, end));
+
+ dev_err(sdata->dev, "Timed out waiting for mux set done on index: %d, state: %d\n",
+ index, mux_state);
+
+ return -ETIMEDOUT;
+}
+
+static int cros_typec_mode_switch_set(struct typec_mux_dev *mode_switch,
+ struct typec_mux_state *state)
+{
+ struct cros_typec_port *port = typec_mux_get_drvdata(mode_switch);
+
+ /* Mode switches have index 0. */
+ return cros_typec_configure_mux(port->sdata, port->port_num, 0, state->mode, state->alt);
+}
+
+static int cros_typec_retimer_set(struct typec_retimer *retimer, struct typec_retimer_state *state)
+{
+ struct cros_typec_port *port = typec_retimer_get_drvdata(retimer);
+
+ /* Retimers have index 1. */
+ return cros_typec_configure_mux(port->sdata, port->port_num, 1, state->mode, state->alt);
+}
+
+static void cros_typec_unregister_switches(struct cros_typec_switch_data *sdata)
+{
+ int i;
+
+ for (i = 0; i < EC_USB_PD_MAX_PORTS; i++) {
+ if (!sdata->ports[i])
+ continue;
+ typec_retimer_unregister(sdata->ports[i]->retimer);
+ typec_mux_unregister(sdata->ports[i]->mode_switch);
+ }
+}
+
+static int cros_typec_register_mode_switch(struct cros_typec_port *port,
+ struct fwnode_handle *fwnode)
+{
+ struct typec_mux_desc mode_switch_desc = {
+ .fwnode = fwnode,
+ .drvdata = port,
+ .name = fwnode_get_name(fwnode),
+ .set = cros_typec_mode_switch_set,
+ };
+
+ port->mode_switch = typec_mux_register(port->sdata->dev, &mode_switch_desc);
+
+ return PTR_ERR_OR_ZERO(port->mode_switch);
+}
+
+static int cros_typec_register_retimer(struct cros_typec_port *port, struct fwnode_handle *fwnode)
+{
+ struct typec_retimer_desc retimer_desc = {
+ .fwnode = fwnode,
+ .drvdata = port,
+ .name = fwnode_get_name(fwnode),
+ .set = cros_typec_retimer_set,
+ };
+
+ port->retimer = typec_retimer_register(port->sdata->dev, &retimer_desc);
+
+ return PTR_ERR_OR_ZERO(port->retimer);
+}
+
+static int cros_typec_register_switches(struct cros_typec_switch_data *sdata)
+{
+ struct cros_typec_port *port;
+ struct device *dev = sdata->dev;
+ struct fwnode_handle *fwnode;
+ struct acpi_device *adev;
+ unsigned long long index;
+ int nports, ret;
+
+ nports = device_get_child_node_count(dev);
+ if (nports == 0) {
+ dev_err(dev, "No switch devices found.\n");
+ return -ENODEV;
+ }
+
+ device_for_each_child_node(dev, fwnode) {
+ port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
+ if (!port) {
+ ret = -ENOMEM;
+ goto err_switch;
+ }
+
+ adev = to_acpi_device_node(fwnode);
+ if (!adev) {
+ dev_err(fwnode->dev, "Couldn't get ACPI device handle\n");
+ ret = -ENODEV;
+ goto err_switch;
+ }
+
+ ret = acpi_evaluate_integer(adev->handle, "_ADR", NULL, &index);
+ if (ACPI_FAILURE(ret)) {
+ dev_err(fwnode->dev, "_ADR wasn't evaluated\n");
+ ret = -ENODATA;
+ goto err_switch;
+ }
+
+ if (index >= EC_USB_PD_MAX_PORTS) {
+ dev_err(fwnode->dev, "Invalid port index number: %llu\n", index);
+ ret = -EINVAL;
+ goto err_switch;
+ }
+ port->sdata = sdata;
+ port->port_num = index;
+ sdata->ports[index] = port;
+
+ ret = cros_typec_register_retimer(port, fwnode);
+ if (ret) {
+ dev_err(dev, "Retimer switch register failed\n");
+ goto err_switch;
+ }
+
+ dev_dbg(dev, "Retimer switch registered for index %llu\n", index);
+
+ if (!device_property_present(fwnode->dev, "mode-switch"))
+ continue;
+
+ ret = cros_typec_register_mode_switch(port, fwnode);
+ if (ret) {
+ dev_err(dev, "Mode switch register failed\n");
+ goto err_switch;
+ }
+
+ dev_dbg(dev, "Mode switch registered for index %llu\n", index);
+ }
+
+ return 0;
+err_switch:
+ cros_typec_unregister_switches(sdata);
+ return ret;
+}
+
+static int cros_typec_switch_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct cros_typec_switch_data *sdata;
+
+ sdata = devm_kzalloc(dev, sizeof(*sdata), GFP_KERNEL);
+ if (!sdata)
+ return -ENOMEM;
+
+ sdata->dev = dev;
+ sdata->ec = dev_get_drvdata(pdev->dev.parent);
+
+ platform_set_drvdata(pdev, sdata);
+
+ return cros_typec_register_switches(sdata);
+}
+
+static int cros_typec_switch_remove(struct platform_device *pdev)
+{
+ struct cros_typec_switch_data *sdata = platform_get_drvdata(pdev);
+
+ cros_typec_unregister_switches(sdata);
+ return 0;
+}
+
+#ifdef CONFIG_ACPI
+static const struct acpi_device_id cros_typec_switch_acpi_id[] = {
+ { "GOOG001A", 0 },
+ {}
+};
+MODULE_DEVICE_TABLE(acpi, cros_typec_switch_acpi_id);
+#endif
+
+static struct platform_driver cros_typec_switch_driver = {
+ .driver = {
+ .name = "cros-typec-switch",
+ .acpi_match_table = ACPI_PTR(cros_typec_switch_acpi_id),
+ },
+ .probe = cros_typec_switch_probe,
+ .remove = cros_typec_switch_remove,
+};
+
+module_platform_driver(cros_typec_switch_driver);
+
+MODULE_AUTHOR("Prashant Malani <pmalani@chromium.org>");
+MODULE_DESCRIPTION("ChromeOS EC Type-C Switch control");
+MODULE_LICENSE("GPL");