From c51b4cd25c8224ea7667def456cb55f18d8e5d5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Jirman?= <megi@xff.cz>
Date: Sun, 7 Nov 2021 19:24:40 +0100
Subject: 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 <megi@xff.cz>
---
 drivers/usb/typec/Kconfig        |   7 +
 drivers/usb/typec/Makefile       |   1 +
 drivers/usb/typec/typec-extcon.c | 330 +++++++++++++++++++++++++++++++
 3 files changed, 338 insertions(+)
 create mode 100644 drivers/usb/typec/typec-extcon.c

diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
index 982b7c444a1f..9913435a0fd9 100644
--- a/drivers/usb/typec/Kconfig
+++ b/drivers/usb/typec/Kconfig
@@ -122,6 +122,13 @@ config TYPEC_WUSB3801
 	  If you choose to build this driver as a dynamically linked module, the
 	  module will be called wusb3801.ko.
 
+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 3f8ff94ad294..0b2d1d251c53 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -12,4 +12,5 @@ obj-$(CONFIG_TYPEC_HD3SS3220)	+= hd3ss3220.o
 obj-$(CONFIG_TYPEC_STUSB160X) 	+= stusb160x.o
 obj-$(CONFIG_TYPEC_RT1719)	+= rt1719.o
 obj-$(CONFIG_TYPEC_WUSB3801)	+= wusb3801.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 000000000000..e09cd7a28c24
--- /dev/null
+++ b/drivers/usb/typec/typec-extcon.c
@@ -0,0 +1,330 @@
+/*
+ * 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_dev *sw;
+        struct typec_mux_dev *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);
+
+                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_dev *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_dev *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 void 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);
+}
+
+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 <megi@xff.cz>");
+MODULE_DESCRIPTION("typec -> extcon bridge driver");
-- 
2.35.3

