diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:49:45 +0000 |
commit | 2c3c1048746a4622d8c89a29670120dc8fab93c4 (patch) | |
tree | 848558de17fb3008cdf4d861b01ac7781903ce39 /drivers/usb/typec/mux | |
parent | Initial commit. (diff) | |
download | linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.tar.xz linux-2c3c1048746a4622d8c89a29670120dc8fab93c4.zip |
Adding upstream version 6.1.76.upstream/6.1.76
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/usb/typec/mux')
-rw-r--r-- | drivers/usb/typec/mux/Kconfig | 32 | ||||
-rw-r--r-- | drivers/usb/typec/mux/Makefile | 5 | ||||
-rw-r--r-- | drivers/usb/typec/mux/fsa4480.c | 216 | ||||
-rw-r--r-- | drivers/usb/typec/mux/intel_pmc_mux.c | 745 | ||||
-rw-r--r-- | drivers/usb/typec/mux/pi3usb30532.c | 190 |
5 files changed, 1188 insertions, 0 deletions
diff --git a/drivers/usb/typec/mux/Kconfig b/drivers/usb/typec/mux/Kconfig new file mode 100644 index 000000000..5eb2c17d7 --- /dev/null +++ b/drivers/usb/typec/mux/Kconfig @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0 + +menu "USB Type-C Multiplexer/DeMultiplexer Switch support" + +config TYPEC_MUX_FSA4480 + tristate "ON Semi FSA4480 Analog Audio Switch driver" + depends on I2C + select REGMAP_I2C + help + Driver for the ON Semiconductor FSA4480 Analog Audio Switch, which + provides support for muxing analog audio and sideband signals on a + common USB Type-C connector. + If compiled as a module, the module will be named fsa4480. + +config TYPEC_MUX_PI3USB30532 + tristate "Pericom PI3USB30532 Type-C cross switch driver" + depends on I2C + help + Say Y or M if your system has a Pericom PI3USB30532 Type-C cross + switch / mux chip found on some devices with a Type-C port. + +config TYPEC_MUX_INTEL_PMC + tristate "Intel PMC mux control" + depends on ACPI + depends on INTEL_SCU_IPC + select USB_ROLE_SWITCH + help + Driver for USB muxes controlled by Intel PMC FW. Intel PMC FW can + control the USB role switch and also the multiplexer/demultiplexer + switches used with USB Type-C Alternate Modes. + +endmenu diff --git a/drivers/usb/typec/mux/Makefile b/drivers/usb/typec/mux/Makefile new file mode 100644 index 000000000..e52a56c16 --- /dev/null +++ b/drivers/usb/typec/mux/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 + +obj-$(CONFIG_TYPEC_MUX_FSA4480) += fsa4480.o +obj-$(CONFIG_TYPEC_MUX_PI3USB30532) += pi3usb30532.o +obj-$(CONFIG_TYPEC_MUX_INTEL_PMC) += intel_pmc_mux.o diff --git a/drivers/usb/typec/mux/fsa4480.c b/drivers/usb/typec/mux/fsa4480.c new file mode 100644 index 000000000..d6495e533 --- /dev/null +++ b/drivers/usb/typec/mux/fsa4480.c @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2021-2022 Linaro Ltd. + * Copyright (C) 2018-2020 The Linux Foundation + */ + +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/regmap.h> +#include <linux/usb/typec_dp.h> +#include <linux/usb/typec_mux.h> + +#define FSA4480_SWITCH_ENABLE 0x04 +#define FSA4480_SWITCH_SELECT 0x05 +#define FSA4480_SWITCH_STATUS1 0x07 +#define FSA4480_SLOW_L 0x08 +#define FSA4480_SLOW_R 0x09 +#define FSA4480_SLOW_MIC 0x0a +#define FSA4480_SLOW_SENSE 0x0b +#define FSA4480_SLOW_GND 0x0c +#define FSA4480_DELAY_L_R 0x0d +#define FSA4480_DELAY_L_MIC 0x0e +#define FSA4480_DELAY_L_SENSE 0x0f +#define FSA4480_DELAY_L_AGND 0x10 +#define FSA4480_RESET 0x1e +#define FSA4480_MAX_REGISTER 0x1f + +#define FSA4480_ENABLE_DEVICE BIT(7) +#define FSA4480_ENABLE_SBU GENMASK(6, 5) +#define FSA4480_ENABLE_USB GENMASK(4, 3) + +#define FSA4480_SEL_SBU_REVERSE GENMASK(6, 5) +#define FSA4480_SEL_USB GENMASK(4, 3) + +struct fsa4480 { + struct i2c_client *client; + + /* used to serialize concurrent change requests */ + struct mutex lock; + + struct typec_switch_dev *sw; + struct typec_mux_dev *mux; + + struct regmap *regmap; + + u8 cur_enable; + u8 cur_select; +}; + +static const struct regmap_config fsa4480_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = FSA4480_MAX_REGISTER, + /* Accesses only done under fsa4480->lock */ + .disable_locking = true, +}; + +static int fsa4480_switch_set(struct typec_switch_dev *sw, + enum typec_orientation orientation) +{ + struct fsa4480 *fsa = typec_switch_get_drvdata(sw); + u8 new_sel; + + mutex_lock(&fsa->lock); + new_sel = FSA4480_SEL_USB; + if (orientation == TYPEC_ORIENTATION_REVERSE) + new_sel |= FSA4480_SEL_SBU_REVERSE; + + if (new_sel == fsa->cur_select) + goto out_unlock; + + if (fsa->cur_enable & FSA4480_ENABLE_SBU) { + /* Disable SBU output while re-configuring the switch */ + regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, + fsa->cur_enable & ~FSA4480_ENABLE_SBU); + + /* 35us to allow the SBU switch to turn off */ + usleep_range(35, 1000); + } + + regmap_write(fsa->regmap, FSA4480_SWITCH_SELECT, new_sel); + fsa->cur_select = new_sel; + + if (fsa->cur_enable & FSA4480_ENABLE_SBU) { + regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, fsa->cur_enable); + + /* 15us to allow the SBU switch to turn on again */ + usleep_range(15, 1000); + } + +out_unlock: + mutex_unlock(&fsa->lock); + + return 0; +} + +static int fsa4480_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +{ + struct fsa4480 *fsa = typec_mux_get_drvdata(mux); + u8 new_enable; + + mutex_lock(&fsa->lock); + + new_enable = FSA4480_ENABLE_DEVICE | FSA4480_ENABLE_USB; + if (state->mode >= TYPEC_DP_STATE_A) + new_enable |= FSA4480_ENABLE_SBU; + + if (new_enable == fsa->cur_enable) + goto out_unlock; + + regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, new_enable); + fsa->cur_enable = new_enable; + + if (new_enable & FSA4480_ENABLE_SBU) { + /* 15us to allow the SBU switch to turn off */ + usleep_range(15, 1000); + } + +out_unlock: + mutex_unlock(&fsa->lock); + + return 0; +} + +static int fsa4480_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct typec_switch_desc sw_desc = { }; + struct typec_mux_desc mux_desc = { }; + struct fsa4480 *fsa; + + fsa = devm_kzalloc(dev, sizeof(*fsa), GFP_KERNEL); + if (!fsa) + return -ENOMEM; + + fsa->client = client; + mutex_init(&fsa->lock); + + fsa->regmap = devm_regmap_init_i2c(client, &fsa4480_regmap_config); + if (IS_ERR(fsa->regmap)) + return dev_err_probe(dev, PTR_ERR(fsa->regmap), "failed to initialize regmap\n"); + + fsa->cur_enable = FSA4480_ENABLE_DEVICE | FSA4480_ENABLE_USB; + fsa->cur_select = FSA4480_SEL_USB; + + /* set default settings */ + regmap_write(fsa->regmap, FSA4480_SLOW_L, 0x00); + regmap_write(fsa->regmap, FSA4480_SLOW_R, 0x00); + regmap_write(fsa->regmap, FSA4480_SLOW_MIC, 0x00); + regmap_write(fsa->regmap, FSA4480_SLOW_SENSE, 0x00); + regmap_write(fsa->regmap, FSA4480_SLOW_GND, 0x00); + regmap_write(fsa->regmap, FSA4480_DELAY_L_R, 0x00); + regmap_write(fsa->regmap, FSA4480_DELAY_L_MIC, 0x00); + regmap_write(fsa->regmap, FSA4480_DELAY_L_SENSE, 0x00); + regmap_write(fsa->regmap, FSA4480_DELAY_L_AGND, 0x09); + regmap_write(fsa->regmap, FSA4480_SWITCH_SELECT, fsa->cur_select); + regmap_write(fsa->regmap, FSA4480_SWITCH_ENABLE, fsa->cur_enable); + + sw_desc.drvdata = fsa; + sw_desc.fwnode = dev_fwnode(dev); + sw_desc.set = fsa4480_switch_set; + + fsa->sw = typec_switch_register(dev, &sw_desc); + if (IS_ERR(fsa->sw)) + return dev_err_probe(dev, PTR_ERR(fsa->sw), "failed to register typec switch\n"); + + mux_desc.drvdata = fsa; + mux_desc.fwnode = dev_fwnode(dev); + mux_desc.set = fsa4480_mux_set; + + fsa->mux = typec_mux_register(dev, &mux_desc); + if (IS_ERR(fsa->mux)) { + typec_switch_unregister(fsa->sw); + return dev_err_probe(dev, PTR_ERR(fsa->mux), "failed to register typec mux\n"); + } + + i2c_set_clientdata(client, fsa); + return 0; +} + +static void fsa4480_remove(struct i2c_client *client) +{ + struct fsa4480 *fsa = i2c_get_clientdata(client); + + typec_mux_unregister(fsa->mux); + typec_switch_unregister(fsa->sw); +} + +static const struct i2c_device_id fsa4480_table[] = { + { "fsa4480" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, fsa4480_table); + +static const struct of_device_id fsa4480_of_table[] = { + { .compatible = "fcs,fsa4480" }, + { } +}; +MODULE_DEVICE_TABLE(of, fsa4480_of_table); + +static struct i2c_driver fsa4480_driver = { + .driver = { + .name = "fsa4480", + .of_match_table = fsa4480_of_table, + }, + .probe_new = fsa4480_probe, + .remove = fsa4480_remove, + .id_table = fsa4480_table, +}; +module_i2c_driver(fsa4480_driver); + +MODULE_DESCRIPTION("ON Semiconductor FSA4480 driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/typec/mux/intel_pmc_mux.c b/drivers/usb/typec/mux/intel_pmc_mux.c new file mode 100644 index 000000000..87e2c9130 --- /dev/null +++ b/drivers/usb/typec/mux/intel_pmc_mux.c @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for Intel PMC USB mux control + * + * Copyright (C) 2020 Intel Corporation + * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> + */ + +#include <linux/acpi.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/usb/pd.h> +#include <linux/usb/role.h> +#include <linux/usb/typec_mux.h> +#include <linux/usb/typec_dp.h> +#include <linux/usb/typec_tbt.h> + +#include <asm/intel_scu_ipc.h> + +#define PMC_USBC_CMD 0xa7 + +/* Response status bits */ +#define PMC_USB_RESP_STATUS_FAILURE BIT(0) +#define PMC_USB_RESP_STATUS_FATAL BIT(1) + +/* "Usage" OOB Message field values */ +enum { + PMC_USB_CONNECT, + PMC_USB_DISCONNECT, + PMC_USB_SAFE_MODE, + PMC_USB_ALT_MODE, + PMC_USB_DP_HPD, +}; + +#define PMC_USB_MSG_USB2_PORT_SHIFT 0 +#define PMC_USB_MSG_USB3_PORT_SHIFT 4 +#define PMC_USB_MSG_UFP_SHIFT 4 +#define PMC_USB_MSG_ORI_HSL_SHIFT 5 +#define PMC_USB_MSG_ORI_AUX_SHIFT 6 + +/* Alt Mode Request */ +struct altmode_req { + u8 usage; + u8 mode_type; + u8 mode_id; + u8 reserved; + u32 mode_data; +} __packed; + +#define PMC_USB_MODE_TYPE_SHIFT 4 + +enum { + PMC_USB_MODE_TYPE_USB, + PMC_USB_MODE_TYPE_DP, + PMC_USB_MODE_TYPE_TBT, +}; + +/* Common Mode Data bits */ +#define PMC_USB_ALTMODE_ACTIVE_CABLE BIT(2) + +#define PMC_USB_ALTMODE_ORI_SHIFT 1 +#define PMC_USB_ALTMODE_UFP_SHIFT 3 + +/* DP specific Mode Data bits */ +#define PMC_USB_ALTMODE_DP_MODE_SHIFT 8 + +/* TBT specific Mode Data bits */ +#define PMC_USB_ALTMODE_TBT_TYPE BIT(17) +#define PMC_USB_ALTMODE_CABLE_TYPE BIT(18) +#define PMC_USB_ALTMODE_ACTIVE_LINK BIT(20) +#define PMC_USB_ALTMODE_FORCE_LSR BIT(23) +#define PMC_USB_ALTMODE_CABLE_SPD(_s_) (((_s_) & GENMASK(2, 0)) << 25) +#define PMC_USB_ALTMODE_CABLE_USB31 1 +#define PMC_USB_ALTMODE_CABLE_10GPS 2 +#define PMC_USB_ALTMODE_CABLE_20GPS 3 +#define PMC_USB_ALTMODE_TBT_GEN(_g_) (((_g_) & GENMASK(1, 0)) << 28) + +/* Display HPD Request bits */ +#define PMC_USB_DP_HPD_LVL BIT(4) +#define PMC_USB_DP_HPD_IRQ BIT(5) + +/* + * Input Output Manager (IOM) PORT STATUS + */ +#define IOM_PORT_STATUS_ACTIVITY_TYPE_MASK GENMASK(9, 6) +#define IOM_PORT_STATUS_ACTIVITY_TYPE_SHIFT 6 +#define IOM_PORT_STATUS_ACTIVITY_TYPE_USB 0x03 +/* activity type: Safe Mode */ +#define IOM_PORT_STATUS_ACTIVITY_TYPE_SAFE_MODE 0x04 +/* activity type: Display Port */ +#define IOM_PORT_STATUS_ACTIVITY_TYPE_DP 0x05 +/* activity type: Display Port Multi Function Device */ +#define IOM_PORT_STATUS_ACTIVITY_TYPE_DP_MFD 0x06 +/* activity type: Thunderbolt */ +#define IOM_PORT_STATUS_ACTIVITY_TYPE_TBT 0x07 +#define IOM_PORT_STATUS_ACTIVITY_TYPE_ALT_MODE_USB 0x0c +#define IOM_PORT_STATUS_ACTIVITY_TYPE_ALT_MODE_TBT_USB 0x0d +/* Upstream Facing Port Information */ +#define IOM_PORT_STATUS_UFP BIT(10) +/* Display Port Hot Plug Detect status */ +#define IOM_PORT_STATUS_DHPD_HPD_STATUS_MASK GENMASK(13, 12) +#define IOM_PORT_STATUS_DHPD_HPD_STATUS_SHIFT 12 +#define IOM_PORT_STATUS_DHPD_HPD_STATUS_ASSERT 0x01 +#define IOM_PORT_STATUS_DHPD_HPD_SOURCE_TBT BIT(14) +#define IOM_PORT_STATUS_CONNECTED BIT(31) + +#define IOM_PORT_ACTIVITY_IS(_status_, _type_) \ + ((((_status_) & IOM_PORT_STATUS_ACTIVITY_TYPE_MASK) >> \ + IOM_PORT_STATUS_ACTIVITY_TYPE_SHIFT) == \ + (IOM_PORT_STATUS_ACTIVITY_TYPE_##_type_)) + +#define IOM_PORT_HPD_ASSERTED(_status_) \ + ((((_status_) & IOM_PORT_STATUS_DHPD_HPD_STATUS_MASK) >> \ + IOM_PORT_STATUS_DHPD_HPD_STATUS_SHIFT) & \ + IOM_PORT_STATUS_DHPD_HPD_STATUS_ASSERT) + +struct pmc_usb; + +struct pmc_usb_port { + int num; + u32 iom_status; + struct pmc_usb *pmc; + struct typec_mux_dev *typec_mux; + struct typec_switch_dev *typec_sw; + struct usb_role_switch *usb_sw; + + enum typec_orientation orientation; + enum usb_role role; + + u8 usb2_port; + u8 usb3_port; + + enum typec_orientation sbu_orientation; + enum typec_orientation hsl_orientation; +}; + +struct pmc_usb { + u8 num_ports; + struct device *dev; + struct intel_scu_ipc_dev *ipc; + struct pmc_usb_port *port; + struct acpi_device *iom_adev; + void __iomem *iom_base; + u32 iom_port_status_offset; +}; + +static void update_port_status(struct pmc_usb_port *port) +{ + u8 port_num; + + /* SoC expects the USB Type-C port numbers to start with 0 */ + port_num = port->usb3_port - 1; + + port->iom_status = readl(port->pmc->iom_base + + port->pmc->iom_port_status_offset + + port_num * sizeof(u32)); +} + +static int sbu_orientation(struct pmc_usb_port *port) +{ + if (port->sbu_orientation) + return port->sbu_orientation - 1; + + return port->orientation - 1; +} + +static int hsl_orientation(struct pmc_usb_port *port) +{ + if (port->hsl_orientation) + return port->hsl_orientation - 1; + + return port->orientation - 1; +} + +static int pmc_usb_send_command(struct intel_scu_ipc_dev *ipc, u8 *msg, u32 len) +{ + u8 response[4]; + u8 status_res; + int ret; + + /* + * Error bit will always be 0 with the USBC command. + * Status can be checked from the response message if the + * function intel_scu_ipc_dev_command succeeds. + */ + ret = intel_scu_ipc_dev_command(ipc, PMC_USBC_CMD, 0, msg, + len, response, sizeof(response)); + + if (ret) + return ret; + + status_res = (msg[0] & 0xf) < PMC_USB_SAFE_MODE ? + response[2] : response[1]; + + if (status_res & PMC_USB_RESP_STATUS_FAILURE) { + if (status_res & PMC_USB_RESP_STATUS_FATAL) + return -EIO; + + return -EBUSY; + } + + return 0; +} + +static int pmc_usb_command(struct pmc_usb_port *port, u8 *msg, u32 len) +{ + int retry_count = 3; + int ret; + + /* + * If PMC is busy then retry the command once again + */ + while (retry_count--) { + ret = pmc_usb_send_command(port->pmc->ipc, msg, len); + if (ret != -EBUSY) + break; + } + + return ret; +} + +static int +pmc_usb_mux_dp_hpd(struct pmc_usb_port *port, struct typec_displayport_data *dp) +{ + u8 msg[2] = { }; + int ret; + + msg[0] = PMC_USB_DP_HPD; + msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + + /* Configure HPD first if HPD,IRQ comes together */ + if (!IOM_PORT_HPD_ASSERTED(port->iom_status) && + dp->status & DP_STATUS_IRQ_HPD && + dp->status & DP_STATUS_HPD_STATE) { + msg[1] = PMC_USB_DP_HPD_LVL; + ret = pmc_usb_command(port, msg, sizeof(msg)); + if (ret) + return ret; + } + + if (dp->status & DP_STATUS_IRQ_HPD) + msg[1] = PMC_USB_DP_HPD_IRQ; + + if (dp->status & DP_STATUS_HPD_STATE) + msg[1] |= PMC_USB_DP_HPD_LVL; + + return pmc_usb_command(port, msg, sizeof(msg)); +} + +static int +pmc_usb_mux_dp(struct pmc_usb_port *port, struct typec_mux_state *state) +{ + struct typec_displayport_data *data = state->data; + struct altmode_req req = { }; + int ret; + + if (IOM_PORT_ACTIVITY_IS(port->iom_status, DP) || + IOM_PORT_ACTIVITY_IS(port->iom_status, DP_MFD)) { + if (IOM_PORT_HPD_ASSERTED(port->iom_status) && + (!(data->status & DP_STATUS_IRQ_HPD) && + data->status & DP_STATUS_HPD_STATE)) + return 0; + + return pmc_usb_mux_dp_hpd(port, state->data); + } + + req.usage = PMC_USB_ALT_MODE; + req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + req.mode_type = PMC_USB_MODE_TYPE_DP << PMC_USB_MODE_TYPE_SHIFT; + + req.mode_data = (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT; + req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT; + + req.mode_data |= (state->mode - TYPEC_STATE_MODAL) << + PMC_USB_ALTMODE_DP_MODE_SHIFT; + + ret = pmc_usb_command(port, (void *)&req, sizeof(req)); + if (ret) + return ret; + + if (data->status & (DP_STATUS_IRQ_HPD | DP_STATUS_HPD_STATE)) + return pmc_usb_mux_dp_hpd(port, state->data); + + return 0; +} + +static int +pmc_usb_mux_tbt(struct pmc_usb_port *port, struct typec_mux_state *state) +{ + struct typec_thunderbolt_data *data = state->data; + u8 cable_rounded = TBT_CABLE_ROUNDED_SUPPORT(data->cable_mode); + u8 cable_speed = TBT_CABLE_SPEED(data->cable_mode); + struct altmode_req req = { }; + + if (IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) || + IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB)) + return 0; + + req.usage = PMC_USB_ALT_MODE; + req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + req.mode_type = PMC_USB_MODE_TYPE_TBT << PMC_USB_MODE_TYPE_SHIFT; + + req.mode_data = (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT; + req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT; + + if (TBT_ADAPTER(data->device_mode) == TBT_ADAPTER_TBT3) + req.mode_data |= PMC_USB_ALTMODE_TBT_TYPE; + + if (data->cable_mode & TBT_CABLE_OPTICAL) + req.mode_data |= PMC_USB_ALTMODE_CABLE_TYPE; + + if (data->cable_mode & TBT_CABLE_LINK_TRAINING) + req.mode_data |= PMC_USB_ALTMODE_ACTIVE_LINK; + + if (data->enter_vdo & TBT_ENTER_MODE_ACTIVE_CABLE) + req.mode_data |= PMC_USB_ALTMODE_ACTIVE_CABLE; + + req.mode_data |= PMC_USB_ALTMODE_CABLE_SPD(cable_speed); + + req.mode_data |= PMC_USB_ALTMODE_TBT_GEN(cable_rounded); + + return pmc_usb_command(port, (void *)&req, sizeof(req)); +} + +static int +pmc_usb_mux_usb4(struct pmc_usb_port *port, struct typec_mux_state *state) +{ + struct enter_usb_data *data = state->data; + struct altmode_req req = { }; + u8 cable_speed; + + if (IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) || + IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB)) + return 0; + + req.usage = PMC_USB_ALT_MODE; + req.usage |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + req.mode_type = PMC_USB_MODE_TYPE_TBT << PMC_USB_MODE_TYPE_SHIFT; + + /* USB4 Mode */ + req.mode_data = PMC_USB_ALTMODE_FORCE_LSR; + + if (data->active_link_training) + req.mode_data |= PMC_USB_ALTMODE_ACTIVE_LINK; + + req.mode_data |= (port->orientation - 1) << PMC_USB_ALTMODE_ORI_SHIFT; + req.mode_data |= (port->role - 1) << PMC_USB_ALTMODE_UFP_SHIFT; + + switch ((data->eudo & EUDO_CABLE_TYPE_MASK) >> EUDO_CABLE_TYPE_SHIFT) { + case EUDO_CABLE_TYPE_PASSIVE: + break; + case EUDO_CABLE_TYPE_OPTICAL: + req.mode_data |= PMC_USB_ALTMODE_CABLE_TYPE; + fallthrough; + default: + req.mode_data |= PMC_USB_ALTMODE_ACTIVE_CABLE; + + /* Configure data rate to rounded in the case of Active TBT3 + * and USB4 cables. + */ + req.mode_data |= PMC_USB_ALTMODE_TBT_GEN(1); + break; + } + + cable_speed = (data->eudo & EUDO_CABLE_SPEED_MASK) >> EUDO_CABLE_SPEED_SHIFT; + req.mode_data |= PMC_USB_ALTMODE_CABLE_SPD(cable_speed); + + return pmc_usb_command(port, (void *)&req, sizeof(req)); +} + +static int pmc_usb_mux_safe_state(struct pmc_usb_port *port, + struct typec_mux_state *state) +{ + u8 msg; + + if (IOM_PORT_ACTIVITY_IS(port->iom_status, SAFE_MODE)) + return 0; + + if ((IOM_PORT_ACTIVITY_IS(port->iom_status, DP) || + IOM_PORT_ACTIVITY_IS(port->iom_status, DP_MFD)) && + state->alt && state->alt->svid == USB_TYPEC_DP_SID) + return 0; + + if ((IOM_PORT_ACTIVITY_IS(port->iom_status, TBT) || + IOM_PORT_ACTIVITY_IS(port->iom_status, ALT_MODE_TBT_USB)) && + state->alt && state->alt->svid == USB_TYPEC_TBT_SID) + return 0; + + msg = PMC_USB_SAFE_MODE; + msg |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + + return pmc_usb_command(port, &msg, sizeof(msg)); +} + +static int pmc_usb_disconnect(struct pmc_usb_port *port) +{ + struct typec_displayport_data data = { }; + u8 msg[2]; + + if (!(port->iom_status & IOM_PORT_STATUS_CONNECTED)) + return 0; + + /* Clear DisplayPort HPD if it's still asserted. */ + if (IOM_PORT_HPD_ASSERTED(port->iom_status)) + pmc_usb_mux_dp_hpd(port, &data); + + msg[0] = PMC_USB_DISCONNECT; + msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + + msg[1] = port->usb2_port << PMC_USB_MSG_USB2_PORT_SHIFT; + + return pmc_usb_command(port, msg, sizeof(msg)); +} + +static int pmc_usb_connect(struct pmc_usb_port *port, enum usb_role role) +{ + u8 ufp = role == USB_ROLE_DEVICE ? 1 : 0; + u8 msg[2]; + int ret; + + if (port->orientation == TYPEC_ORIENTATION_NONE) + return -EINVAL; + + if (port->iom_status & IOM_PORT_STATUS_CONNECTED) { + if (port->role == role || port->role == USB_ROLE_NONE) + return 0; + + /* Role swap */ + ret = pmc_usb_disconnect(port); + if (ret) + return ret; + } + + msg[0] = PMC_USB_CONNECT; + msg[0] |= port->usb3_port << PMC_USB_MSG_USB3_PORT_SHIFT; + + msg[1] = port->usb2_port << PMC_USB_MSG_USB2_PORT_SHIFT; + msg[1] |= ufp << PMC_USB_MSG_UFP_SHIFT; + msg[1] |= hsl_orientation(port) << PMC_USB_MSG_ORI_HSL_SHIFT; + msg[1] |= sbu_orientation(port) << PMC_USB_MSG_ORI_AUX_SHIFT; + + return pmc_usb_command(port, msg, sizeof(msg)); +} + +static int +pmc_usb_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +{ + struct pmc_usb_port *port = typec_mux_get_drvdata(mux); + + update_port_status(port); + + if (port->orientation == TYPEC_ORIENTATION_NONE || port->role == USB_ROLE_NONE) + return 0; + + if (state->mode == TYPEC_STATE_SAFE) + return pmc_usb_mux_safe_state(port, state); + if (state->mode == TYPEC_STATE_USB) + return pmc_usb_connect(port, port->role); + + if (state->alt) { + switch (state->alt->svid) { + case USB_TYPEC_TBT_SID: + return pmc_usb_mux_tbt(port, state); + case USB_TYPEC_DP_SID: + return pmc_usb_mux_dp(port, state); + } + } else { + switch (state->mode) { + case TYPEC_MODE_USB2: + /* REVISIT: Try with usb3_port set to 0? */ + break; + case TYPEC_MODE_USB3: + return pmc_usb_connect(port, port->role); + case TYPEC_MODE_USB4: + return pmc_usb_mux_usb4(port, state); + } + } + + return -EOPNOTSUPP; +} + +static int pmc_usb_set_orientation(struct typec_switch_dev *sw, + enum typec_orientation orientation) +{ + struct pmc_usb_port *port = typec_switch_get_drvdata(sw); + + update_port_status(port); + + port->orientation = orientation; + + return 0; +} + +static int pmc_usb_set_role(struct usb_role_switch *sw, enum usb_role role) +{ + struct pmc_usb_port *port = usb_role_switch_get_drvdata(sw); + int ret; + + update_port_status(port); + + if (role == USB_ROLE_NONE) + ret = pmc_usb_disconnect(port); + else + ret = pmc_usb_connect(port, role); + + port->role = role; + + return ret; +} + +static int pmc_usb_register_port(struct pmc_usb *pmc, int index, + struct fwnode_handle *fwnode) +{ + struct pmc_usb_port *port = &pmc->port[index]; + struct usb_role_switch_desc desc = { }; + struct typec_switch_desc sw_desc = { }; + struct typec_mux_desc mux_desc = { }; + const char *str; + int ret; + + ret = fwnode_property_read_u8(fwnode, "usb2-port-number", &port->usb2_port); + if (ret) + return ret; + + ret = fwnode_property_read_u8(fwnode, "usb3-port-number", &port->usb3_port); + if (ret) + return ret; + + ret = fwnode_property_read_string(fwnode, "sbu-orientation", &str); + if (!ret) + port->sbu_orientation = typec_find_orientation(str); + + ret = fwnode_property_read_string(fwnode, "hsl-orientation", &str); + if (!ret) + port->hsl_orientation = typec_find_orientation(str); + + port->num = index; + port->pmc = pmc; + + sw_desc.fwnode = fwnode; + sw_desc.drvdata = port; + sw_desc.name = fwnode_get_name(fwnode); + sw_desc.set = pmc_usb_set_orientation; + + port->typec_sw = typec_switch_register(pmc->dev, &sw_desc); + if (IS_ERR(port->typec_sw)) + return PTR_ERR(port->typec_sw); + + mux_desc.fwnode = fwnode; + mux_desc.drvdata = port; + mux_desc.name = fwnode_get_name(fwnode); + mux_desc.set = pmc_usb_mux_set; + + port->typec_mux = typec_mux_register(pmc->dev, &mux_desc); + if (IS_ERR(port->typec_mux)) { + ret = PTR_ERR(port->typec_mux); + goto err_unregister_switch; + } + + desc.fwnode = fwnode; + desc.driver_data = port; + desc.name = fwnode_get_name(fwnode); + desc.set = pmc_usb_set_role; + + port->usb_sw = usb_role_switch_register(pmc->dev, &desc); + if (IS_ERR(port->usb_sw)) { + ret = PTR_ERR(port->usb_sw); + goto err_unregister_mux; + } + + return 0; + +err_unregister_mux: + typec_mux_unregister(port->typec_mux); + +err_unregister_switch: + typec_switch_unregister(port->typec_sw); + + return ret; +} + +/* IOM ACPI IDs and IOM_PORT_STATUS_OFFSET */ +static const struct acpi_device_id iom_acpi_ids[] = { + /* TigerLake */ + { "INTC1072", 0x560, }, + + /* AlderLake */ + { "INTC1079", 0x160, }, + + /* Meteor Lake */ + { "INTC107A", 0x160, }, + {} +}; + +static int pmc_usb_probe_iom(struct pmc_usb *pmc) +{ + struct list_head resource_list; + struct resource_entry *rentry; + static const struct acpi_device_id *dev_id; + struct acpi_device *adev = NULL; + int ret; + + for (dev_id = &iom_acpi_ids[0]; dev_id->id[0]; dev_id++) { + if (acpi_dev_present(dev_id->id, NULL, -1)) { + pmc->iom_port_status_offset = (u32)dev_id->driver_data; + adev = acpi_dev_get_first_match_dev(dev_id->id, NULL, -1); + break; + } + } + + if (!adev) + return -ENODEV; + + INIT_LIST_HEAD(&resource_list); + ret = acpi_dev_get_memory_resources(adev, &resource_list); + if (ret < 0) { + acpi_dev_put(adev); + return ret; + } + + rentry = list_first_entry_or_null(&resource_list, struct resource_entry, node); + if (rentry) + pmc->iom_base = devm_ioremap_resource(pmc->dev, rentry->res); + + acpi_dev_free_resource_list(&resource_list); + + if (!pmc->iom_base) { + acpi_dev_put(adev); + return -ENOMEM; + } + + if (IS_ERR(pmc->iom_base)) { + acpi_dev_put(adev); + return PTR_ERR(pmc->iom_base); + } + + pmc->iom_adev = adev; + + return 0; +} + +static int pmc_usb_probe(struct platform_device *pdev) +{ + struct fwnode_handle *fwnode = NULL; + struct pmc_usb *pmc; + int i = 0; + int ret; + + pmc = devm_kzalloc(&pdev->dev, sizeof(*pmc), GFP_KERNEL); + if (!pmc) + return -ENOMEM; + + device_for_each_child_node(&pdev->dev, fwnode) + pmc->num_ports++; + + /* The IOM microcontroller has a limitation of max 4 ports. */ + if (pmc->num_ports > 4) { + dev_err(&pdev->dev, "driver limited to 4 ports\n"); + return -ERANGE; + } + + pmc->port = devm_kcalloc(&pdev->dev, pmc->num_ports, + sizeof(struct pmc_usb_port), GFP_KERNEL); + if (!pmc->port) + return -ENOMEM; + + pmc->ipc = devm_intel_scu_ipc_dev_get(&pdev->dev); + if (!pmc->ipc) + return -ENODEV; + + pmc->dev = &pdev->dev; + + ret = pmc_usb_probe_iom(pmc); + if (ret) + return ret; + + /* + * For every physical USB connector (USB2 and USB3 combo) there is a + * child ACPI device node under the PMC mux ACPI device object. + */ + for (i = 0; i < pmc->num_ports; i++) { + fwnode = device_get_next_child_node(pmc->dev, fwnode); + if (!fwnode) + break; + + ret = pmc_usb_register_port(pmc, i, fwnode); + if (ret) { + fwnode_handle_put(fwnode); + goto err_remove_ports; + } + } + + platform_set_drvdata(pdev, pmc); + + return 0; + +err_remove_ports: + for (i = 0; i < pmc->num_ports; i++) { + typec_switch_unregister(pmc->port[i].typec_sw); + typec_mux_unregister(pmc->port[i].typec_mux); + usb_role_switch_unregister(pmc->port[i].usb_sw); + } + + acpi_dev_put(pmc->iom_adev); + + return ret; +} + +static int pmc_usb_remove(struct platform_device *pdev) +{ + struct pmc_usb *pmc = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < pmc->num_ports; i++) { + typec_switch_unregister(pmc->port[i].typec_sw); + typec_mux_unregister(pmc->port[i].typec_mux); + usb_role_switch_unregister(pmc->port[i].usb_sw); + } + + acpi_dev_put(pmc->iom_adev); + + return 0; +} + +static const struct acpi_device_id pmc_usb_acpi_ids[] = { + { "INTC105C", }, + { } +}; +MODULE_DEVICE_TABLE(acpi, pmc_usb_acpi_ids); + +static struct platform_driver pmc_usb_driver = { + .driver = { + .name = "intel_pmc_usb", + .acpi_match_table = ACPI_PTR(pmc_usb_acpi_ids), + }, + .probe = pmc_usb_probe, + .remove = pmc_usb_remove, +}; + +module_platform_driver(pmc_usb_driver); + +MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("Intel PMC USB mux control"); diff --git a/drivers/usb/typec/mux/pi3usb30532.c b/drivers/usb/typec/mux/pi3usb30532.c new file mode 100644 index 000000000..1cd388b55 --- /dev/null +++ b/drivers/usb/typec/mux/pi3usb30532.c @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Pericom PI3USB30532 Type-C cross switch / mux driver + * + * Copyright (c) 2017-2018 Hans de Goede <hdegoede@redhat.com> + */ + +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/usb/typec_dp.h> +#include <linux/usb/typec_mux.h> + +#define PI3USB30532_CONF 0x00 + +#define PI3USB30532_CONF_OPEN 0x00 +#define PI3USB30532_CONF_SWAP 0x01 +#define PI3USB30532_CONF_4LANE_DP 0x02 +#define PI3USB30532_CONF_USB3 0x04 +#define PI3USB30532_CONF_USB3_AND_2LANE_DP 0x06 + +struct pi3usb30532 { + struct i2c_client *client; + struct mutex lock; /* protects the cached conf register */ + struct typec_switch_dev *sw; + struct typec_mux_dev *mux; + u8 conf; +}; + +static int pi3usb30532_set_conf(struct pi3usb30532 *pi, u8 new_conf) +{ + int ret = 0; + + if (pi->conf == new_conf) + return 0; + + ret = i2c_smbus_write_byte_data(pi->client, PI3USB30532_CONF, new_conf); + if (ret) { + dev_err(&pi->client->dev, "Error writing conf: %d\n", ret); + return ret; + } + + pi->conf = new_conf; + return 0; +} + +static int pi3usb30532_sw_set(struct typec_switch_dev *sw, + enum typec_orientation orientation) +{ + struct pi3usb30532 *pi = typec_switch_get_drvdata(sw); + u8 new_conf; + int ret; + + mutex_lock(&pi->lock); + new_conf = pi->conf; + + switch (orientation) { + case TYPEC_ORIENTATION_NONE: + new_conf = PI3USB30532_CONF_OPEN; + break; + case TYPEC_ORIENTATION_NORMAL: + new_conf &= ~PI3USB30532_CONF_SWAP; + break; + case TYPEC_ORIENTATION_REVERSE: + new_conf |= PI3USB30532_CONF_SWAP; + break; + } + + ret = pi3usb30532_set_conf(pi, new_conf); + mutex_unlock(&pi->lock); + + return ret; +} + +static int +pi3usb30532_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *state) +{ + struct pi3usb30532 *pi = typec_mux_get_drvdata(mux); + u8 new_conf; + int ret; + + mutex_lock(&pi->lock); + new_conf = pi->conf; + + switch (state->mode) { + case TYPEC_STATE_SAFE: + new_conf = (new_conf & PI3USB30532_CONF_SWAP) | + PI3USB30532_CONF_OPEN; + break; + case TYPEC_STATE_USB: + new_conf = (new_conf & PI3USB30532_CONF_SWAP) | + PI3USB30532_CONF_USB3; + break; + case TYPEC_DP_STATE_C: + case TYPEC_DP_STATE_E: + new_conf = (new_conf & PI3USB30532_CONF_SWAP) | + PI3USB30532_CONF_4LANE_DP; + break; + case TYPEC_DP_STATE_D: + new_conf = (new_conf & PI3USB30532_CONF_SWAP) | + PI3USB30532_CONF_USB3_AND_2LANE_DP; + break; + default: + break; + } + + ret = pi3usb30532_set_conf(pi, new_conf); + mutex_unlock(&pi->lock); + + return ret; +} + +static int pi3usb30532_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct typec_switch_desc sw_desc = { }; + struct typec_mux_desc mux_desc = { }; + struct pi3usb30532 *pi; + int ret; + + pi = devm_kzalloc(dev, sizeof(*pi), GFP_KERNEL); + if (!pi) + return -ENOMEM; + + pi->client = client; + mutex_init(&pi->lock); + + ret = i2c_smbus_read_byte_data(client, PI3USB30532_CONF); + if (ret < 0) { + dev_err(dev, "Error reading config register %d\n", ret); + return ret; + } + pi->conf = ret; + + sw_desc.drvdata = pi; + sw_desc.fwnode = dev->fwnode; + sw_desc.set = pi3usb30532_sw_set; + + pi->sw = typec_switch_register(dev, &sw_desc); + if (IS_ERR(pi->sw)) { + dev_err(dev, "Error registering typec switch: %ld\n", + PTR_ERR(pi->sw)); + return PTR_ERR(pi->sw); + } + + mux_desc.drvdata = pi; + mux_desc.fwnode = dev->fwnode; + mux_desc.set = pi3usb30532_mux_set; + + pi->mux = typec_mux_register(dev, &mux_desc); + if (IS_ERR(pi->mux)) { + typec_switch_unregister(pi->sw); + dev_err(dev, "Error registering typec mux: %ld\n", + PTR_ERR(pi->mux)); + return PTR_ERR(pi->mux); + } + + i2c_set_clientdata(client, pi); + return 0; +} + +static void pi3usb30532_remove(struct i2c_client *client) +{ + struct pi3usb30532 *pi = i2c_get_clientdata(client); + + typec_mux_unregister(pi->mux); + typec_switch_unregister(pi->sw); +} + +static const struct i2c_device_id pi3usb30532_table[] = { + { "pi3usb30532" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, pi3usb30532_table); + +static struct i2c_driver pi3usb30532_driver = { + .driver = { + .name = "pi3usb30532", + }, + .probe_new = pi3usb30532_probe, + .remove = pi3usb30532_remove, + .id_table = pi3usb30532_table, +}; + +module_i2c_driver(pi3usb30532_driver); + +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_DESCRIPTION("Pericom PI3USB30532 Type-C mux driver"); +MODULE_LICENSE("GPL"); |