389 lines
11 KiB
Diff
389 lines
11 KiB
Diff
From: Ondrej Jirman <megous@megous.com>
|
|
Date: Sun, 7 Nov 2021 19:24:40 +0100
|
|
Subject: [PATCH 35/36] usb: typec: typec-extcon: Add typec -> extcon bridge
|
|
driver
|
|
|
|
This bridge connects standard Type C port interfaces for controling
|
|
muxes, switches and usb roles to muxes, switches and usb role
|
|
drivers controlled via extcon interface.
|
|
|
|
Signed-off-by: Ondrej Jirman <megous@megous.com>
|
|
---
|
|
drivers/usb/typec/Kconfig | 7 +
|
|
drivers/usb/typec/Makefile | 1 +
|
|
drivers/usb/typec/typec-extcon.c | 337 +++++++++++++++++++++++++++++++++++++++
|
|
3 files changed, 345 insertions(+)
|
|
create mode 100644 drivers/usb/typec/typec-extcon.c
|
|
|
|
diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
|
|
index ab480f3..01ecc5e 100644
|
|
--- a/drivers/usb/typec/Kconfig
|
|
+++ b/drivers/usb/typec/Kconfig
|
|
@@ -88,6 +88,13 @@ config TYPEC_QCOM_PMIC
|
|
It will also enable the VBUS output to connected devices when a
|
|
DFP connection is made.
|
|
|
|
+config TYPEC_EXTCON
|
|
+ tristate "Type-C switch/mux -> extcon interface bridge driver"
|
|
+ depends on USB_ROLE_SWITCH
|
|
+ help
|
|
+ Say Y or M here if your system needs bridging between typec class
|
|
+ and extcon interfaces.
|
|
+
|
|
source "drivers/usb/typec/mux/Kconfig"
|
|
|
|
source "drivers/usb/typec/altmodes/Kconfig"
|
|
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
|
|
index a0adb89..d9d8293 100644
|
|
--- a/drivers/usb/typec/Makefile
|
|
+++ b/drivers/usb/typec/Makefile
|
|
@@ -8,4 +8,5 @@ obj-$(CONFIG_TYPEC_TPS6598X) += tipd/
|
|
obj-$(CONFIG_TYPEC_HD3SS3220) += hd3ss3220.o
|
|
obj-$(CONFIG_TYPEC_QCOM_PMIC) += qcom-pmic-typec.o
|
|
obj-$(CONFIG_TYPEC_STUSB160X) += stusb160x.o
|
|
+obj-$(CONFIG_TYPEC_EXTCON) += typec-extcon.o
|
|
obj-$(CONFIG_TYPEC) += mux/
|
|
diff --git a/drivers/usb/typec/typec-extcon.c b/drivers/usb/typec/typec-extcon.c
|
|
new file mode 100644
|
|
index 00000000..143ff24
|
|
--- /dev/null
|
|
+++ b/drivers/usb/typec/typec-extcon.c
|
|
@@ -0,0 +1,337 @@
|
|
+/*
|
|
+ * typec -> extcon bridge
|
|
+ * Copyright (c) 2021 Ondřej Jirman <megi@xff.cz>
|
|
+ *
|
|
+ * This driver bridges standard type-c interfaces to drivers that
|
|
+ * expect extcon interface.
|
|
+ */
|
|
+
|
|
+#include <linux/delay.h>
|
|
+#include <linux/kernel.h>
|
|
+#include <linux/module.h>
|
|
+#include <linux/power_supply.h>
|
|
+#include <linux/platform_device.h>
|
|
+#include <linux/usb/pd.h>
|
|
+#include <linux/usb/role.h>
|
|
+#include <linux/usb/typec.h>
|
|
+#include <linux/usb/typec_dp.h>
|
|
+#include <linux/usb/typec_mux.h>
|
|
+#include <linux/extcon-provider.h>
|
|
+
|
|
+struct typec_extcon {
|
|
+ struct device *dev;
|
|
+
|
|
+ /* consumers */
|
|
+ struct usb_role_switch *role_sw;
|
|
+ struct typec_switch *sw;
|
|
+ struct typec_mux *mux;
|
|
+
|
|
+ /* providers */
|
|
+ struct extcon_dev *extcon;
|
|
+ struct notifier_block extcon_nb;
|
|
+
|
|
+ /* cached state from typec controller */
|
|
+ enum usb_role role;
|
|
+ enum typec_orientation orientation;
|
|
+ struct typec_altmode alt;
|
|
+ unsigned long mode;
|
|
+ bool has_alt;
|
|
+ struct mutex lock;
|
|
+};
|
|
+
|
|
+static const unsigned int typec_extcon_cable[] = {
|
|
+ EXTCON_DISP_DP,
|
|
+
|
|
+ EXTCON_USB,
|
|
+ EXTCON_USB_HOST,
|
|
+
|
|
+ EXTCON_CHG_USB_SDP,
|
|
+ EXTCON_CHG_USB_CDP,
|
|
+ EXTCON_CHG_USB_DCP,
|
|
+ EXTCON_CHG_USB_ACA,
|
|
+
|
|
+ EXTCON_NONE,
|
|
+};
|
|
+
|
|
+static void typec_extcon_set_cable(struct typec_extcon *tce, int id, bool on,
|
|
+ union extcon_property_value prop_ss,
|
|
+ union extcon_property_value prop_or)
|
|
+{
|
|
+ union extcon_property_value cur_ss, cur_or;
|
|
+ bool prop_diff = false;
|
|
+ int ret;
|
|
+
|
|
+ ret = extcon_get_property(tce->extcon, id,
|
|
+ EXTCON_PROP_USB_SS, &cur_ss);
|
|
+ if (ret || cur_ss.intval != prop_ss.intval)
|
|
+ prop_diff = true;
|
|
+
|
|
+ ret = extcon_get_property(tce->extcon, id,
|
|
+ EXTCON_PROP_USB_TYPEC_POLARITY, &cur_or);
|
|
+ if (ret || cur_or.intval != prop_or.intval)
|
|
+ prop_diff = true;
|
|
+
|
|
+ if (!on && extcon_get_state(tce->extcon, id)) {
|
|
+ extcon_set_state_sync(tce->extcon, id, false);
|
|
+ } else if (on && (!extcon_get_state(tce->extcon, id) || prop_diff)) {
|
|
+ extcon_set_state(tce->extcon, id, true);
|
|
+ extcon_set_property(tce->extcon, id,
|
|
+ EXTCON_PROP_USB_SS, prop_ss);
|
|
+ extcon_set_property(tce->extcon, id,
|
|
+ EXTCON_PROP_USB_TYPEC_POLARITY, prop_or);
|
|
+ extcon_sync(tce->extcon, id);
|
|
+ }
|
|
+}
|
|
+
|
|
+static int typec_extcon_sync_extcon(struct typec_extcon *tce)
|
|
+{
|
|
+ union extcon_property_value prop_ss, prop_or;
|
|
+ bool has_dp = false;
|
|
+
|
|
+ mutex_lock(&tce->lock);
|
|
+
|
|
+ /* connector is disconnected */
|
|
+ if (tce->orientation == TYPEC_ORIENTATION_NONE) {
|
|
+ typec_extcon_set_cable(tce, EXTCON_USB, false, prop_ss, prop_or);
|
|
+ typec_extcon_set_cable(tce, EXTCON_USB_HOST, false, prop_ss, prop_or);
|
|
+ typec_extcon_set_cable(tce, EXTCON_DISP_DP, false, prop_ss, prop_or);
|
|
+
|
|
+ extcon_set_state_sync(tce->extcon, EXTCON_CHG_USB_SDP, false);
|
|
+ extcon_set_state_sync(tce->extcon, EXTCON_CHG_USB_DCP, false);
|
|
+ extcon_set_state_sync(tce->extcon, EXTCON_CHG_USB_CDP, false);
|
|
+ extcon_set_state_sync(tce->extcon, EXTCON_CHG_USB_ACA, false);
|
|
+
|
|
+ goto out_unlock;
|
|
+ }
|
|
+
|
|
+ prop_or.intval = tce->orientation == TYPEC_ORIENTATION_NORMAL ? 0 : 1;
|
|
+ prop_ss.intval = 0;
|
|
+
|
|
+ if (tce->has_alt && tce->alt.svid == USB_TYPEC_DP_SID) {
|
|
+ switch (tce->mode) {
|
|
+ case TYPEC_STATE_SAFE:
|
|
+ break;
|
|
+ case TYPEC_DP_STATE_C:
|
|
+ case TYPEC_DP_STATE_E:
|
|
+ has_dp = true;
|
|
+ break;
|
|
+ case TYPEC_DP_STATE_D:
|
|
+ has_dp = true;
|
|
+ fallthrough;
|
|
+ case TYPEC_STATE_USB:
|
|
+ prop_ss.intval = 1;
|
|
+ break;
|
|
+ default:
|
|
+ dev_err(tce->dev, "unhandled mux mode=%lu\n", tce->mode);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ typec_extcon_set_cable(tce, EXTCON_USB,
|
|
+ tce->role == USB_ROLE_DEVICE, prop_ss, prop_or);
|
|
+ typec_extcon_set_cable(tce, EXTCON_USB_HOST,
|
|
+ tce->role == USB_ROLE_HOST, prop_ss, prop_or);
|
|
+
|
|
+ typec_extcon_set_cable(tce, EXTCON_DISP_DP, has_dp, prop_ss, prop_or);
|
|
+
|
|
+out_unlock:
|
|
+ mutex_unlock(&tce->lock);
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int typec_extcon_sw_set(struct typec_switch *sw,
|
|
+ enum typec_orientation orientation)
|
|
+{
|
|
+ struct typec_extcon *tce = typec_switch_get_drvdata(sw);
|
|
+
|
|
+ dev_dbg(tce->dev, "SW SET: orientation=%d\n", orientation);
|
|
+
|
|
+ mutex_lock(&tce->lock);
|
|
+ tce->orientation = orientation;
|
|
+ mutex_unlock(&tce->lock);
|
|
+
|
|
+ typec_extcon_sync_extcon(tce);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int typec_extcon_mux_set(struct typec_mux *mux,
|
|
+ struct typec_mux_state *state)
|
|
+{
|
|
+ struct typec_extcon *tce = typec_mux_get_drvdata(mux);
|
|
+ struct typec_altmode *alt = state->alt;
|
|
+
|
|
+ dev_dbg(tce->dev, "MUX SET: state->mode=%lu\n", state->mode);
|
|
+ if (alt)
|
|
+ dev_dbg(tce->dev, " ...alt: svid=%04hx mode=%d vdo=%08x active=%u\n",
|
|
+ alt->svid, alt->mode, alt->vdo, alt->active);
|
|
+
|
|
+ mutex_lock(&tce->lock);
|
|
+ tce->mode = state->mode;
|
|
+ tce->has_alt = alt != NULL;
|
|
+ if (alt)
|
|
+ tce->alt = *alt;
|
|
+ mutex_unlock(&tce->lock);
|
|
+
|
|
+ typec_extcon_sync_extcon(tce);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int typec_extcon_usb_set_role(struct usb_role_switch *sw,
|
|
+ enum usb_role role)
|
|
+{
|
|
+ struct typec_extcon *tce = usb_role_switch_get_drvdata(sw);
|
|
+
|
|
+ dev_dbg(tce->dev, "ROLE SET: role=%d\n", role);
|
|
+
|
|
+ mutex_lock(&tce->lock);
|
|
+ tce->role = role;
|
|
+ mutex_unlock(&tce->lock);
|
|
+
|
|
+ typec_extcon_sync_extcon(tce);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int typec_extcon_notifier(struct notifier_block *nb,
|
|
+ unsigned long action, void *data)
|
|
+{
|
|
+ struct typec_extcon *tce = container_of(nb, struct typec_extcon, extcon_nb);
|
|
+
|
|
+ bool sdp = extcon_get_state(tce->extcon, EXTCON_CHG_USB_SDP);
|
|
+ bool cdp = extcon_get_state(tce->extcon, EXTCON_CHG_USB_CDP);
|
|
+ bool dcp = extcon_get_state(tce->extcon, EXTCON_CHG_USB_DCP);
|
|
+ bool usb = extcon_get_state(tce->extcon, EXTCON_USB);
|
|
+ bool usb_host = extcon_get_state(tce->extcon, EXTCON_USB_HOST);
|
|
+ bool dp = extcon_get_state(tce->extcon, EXTCON_DISP_DP);
|
|
+
|
|
+ dev_info(tce->dev, "extcon changed sdp=%d cdp=%d dcp=%d usb=%d usb_host=%d dp=%d\n",
|
|
+ sdp, cdp, dcp, usb, usb_host, dp);
|
|
+
|
|
+ return NOTIFY_OK;
|
|
+}
|
|
+
|
|
+static int typec_extcon_probe(struct platform_device *pdev)
|
|
+{
|
|
+ struct typec_switch_desc sw_desc = { };
|
|
+ struct typec_mux_desc mux_desc = { };
|
|
+ struct usb_role_switch_desc role_desc = { };
|
|
+ struct device *dev = &pdev->dev;
|
|
+ struct typec_extcon *tce;
|
|
+ int ret = 0;
|
|
+
|
|
+ tce = devm_kzalloc(dev, sizeof(*tce), GFP_KERNEL);
|
|
+ if (!tce)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ tce->dev = &pdev->dev;
|
|
+ mutex_init(&tce->lock);
|
|
+ tce->mode = TYPEC_STATE_SAFE;
|
|
+
|
|
+ sw_desc.drvdata = tce;
|
|
+ sw_desc.fwnode = dev->fwnode;
|
|
+ sw_desc.set = typec_extcon_sw_set;
|
|
+
|
|
+ tce->sw = typec_switch_register(dev, &sw_desc);
|
|
+ if (IS_ERR(tce->sw))
|
|
+ return dev_err_probe(dev, PTR_ERR(tce->sw),
|
|
+ "Error registering typec switch\n");
|
|
+
|
|
+ mux_desc.drvdata = tce;
|
|
+ mux_desc.fwnode = dev->fwnode;
|
|
+ mux_desc.set = typec_extcon_mux_set;
|
|
+
|
|
+ tce->mux = typec_mux_register(dev, &mux_desc);
|
|
+ if (IS_ERR(tce->mux)) {
|
|
+ ret = dev_err_probe(dev, PTR_ERR(tce->mux),
|
|
+ "Error registering typec mux\n");
|
|
+ goto err_sw;
|
|
+ }
|
|
+
|
|
+ role_desc.driver_data = tce;
|
|
+ role_desc.fwnode = dev->fwnode;
|
|
+ role_desc.name = fwnode_get_name(dev->fwnode);
|
|
+ role_desc.set = typec_extcon_usb_set_role;
|
|
+
|
|
+ tce->role_sw = usb_role_switch_register(dev, &role_desc);
|
|
+ if (IS_ERR(tce->role_sw)) {
|
|
+ ret = dev_err_probe(dev, PTR_ERR(tce->role_sw),
|
|
+ "Error registering USB role switch\n");
|
|
+ goto err_mux;
|
|
+ }
|
|
+
|
|
+ tce->extcon = devm_extcon_dev_allocate(dev, typec_extcon_cable);
|
|
+ if (IS_ERR(tce->extcon)) {
|
|
+ ret = PTR_ERR(tce->extcon);
|
|
+ goto err_role;
|
|
+ }
|
|
+
|
|
+ ret = devm_extcon_dev_register(dev, tce->extcon);
|
|
+ if (ret) {
|
|
+ ret = dev_err_probe(dev, ret, "failed to register extcon device\n");
|
|
+ goto err_role;
|
|
+ }
|
|
+
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_USB,
|
|
+ EXTCON_PROP_USB_SS);
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_USB,
|
|
+ EXTCON_PROP_USB_TYPEC_POLARITY);
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_USB_HOST,
|
|
+ EXTCON_PROP_USB_SS);
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_USB_HOST,
|
|
+ EXTCON_PROP_USB_TYPEC_POLARITY);
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_DISP_DP,
|
|
+ EXTCON_PROP_USB_SS);
|
|
+ extcon_set_property_capability(tce->extcon, EXTCON_DISP_DP,
|
|
+ EXTCON_PROP_USB_TYPEC_POLARITY);
|
|
+
|
|
+ tce->extcon_nb.notifier_call = typec_extcon_notifier;
|
|
+ ret = devm_extcon_register_notifier_all(dev, tce->extcon, &tce->extcon_nb);
|
|
+ if (ret) {
|
|
+ dev_err_probe(dev, ret, "Failed to register extcon notifier\n");
|
|
+ goto err_role;
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+
|
|
+err_role:
|
|
+ usb_role_switch_unregister(tce->role_sw);
|
|
+err_mux:
|
|
+ typec_mux_unregister(tce->mux);
|
|
+err_sw:
|
|
+ typec_switch_unregister(tce->sw);
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+static int typec_extcon_remove(struct platform_device *pdev)
|
|
+{
|
|
+ struct typec_extcon *tce = platform_get_drvdata(pdev);
|
|
+
|
|
+ usb_role_switch_unregister(tce->role_sw);
|
|
+ typec_mux_unregister(tce->mux);
|
|
+ typec_switch_unregister(tce->sw);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static struct of_device_id typec_extcon_of_match_table[] = {
|
|
+ { .compatible = "linux,typec-extcon-bridge" },
|
|
+ { },
|
|
+};
|
|
+MODULE_DEVICE_TABLE(of, typec_extcon_of_match_table);
|
|
+
|
|
+static struct platform_driver typec_extcon_driver = {
|
|
+ .driver = {
|
|
+ .name = "typec-extcon",
|
|
+ .of_match_table = typec_extcon_of_match_table,
|
|
+ },
|
|
+ .probe = typec_extcon_probe,
|
|
+ .remove = typec_extcon_remove,
|
|
+};
|
|
+
|
|
+module_platform_driver(typec_extcon_driver);
|
|
+
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_AUTHOR("Ondrej Jirman <megous@megous.com>");
|
|
+MODULE_DESCRIPTION("typec -> extcon bridge driver");
|