[PATCH v7 2/7] usb: mux: add generic code for dual role port mux
From: Lu Baolu
Date: Fri Apr 29 2016 - 02:27:17 EST
Several Intel platforms implement USB dual role by having completely
separate xHCI and dwc3 IPs in PCH or SOC silicons. These two IPs share
a single USB port. There is another external port mux which controls
where the data lines should go. While the USB controllers are part of
the silicon, the port mux design are platform specific.
This patch adds the generic code to handle such usb port mux. It listens
to the USB HOST extcon cable, and switch the port by calling the port
switch ops provided by the individual port mux driver. It also registers
the mux device with sysfs, so that users can control the port mux from
user space.
Some other archs (e.g. Renesas R-Car gen2 SoCs) need an external mux to
swap usb roles as well. This code could be leveraged for those archs
as well.
Signed-off-by: Lu Baolu <baolu.lu@xxxxxxxxxxxxxxx>
Reviewed-by: Heikki Krogerus <heikki.krogerus@xxxxxxxxxxxxxxx>
Reviewed-by: Felipe Balbi <balbi@xxxxxxxxxx>
Reviewed-by: Chanwoo Choi <cw00.choi@xxxxxxxxxxx>
[baolu: extcon usage reviewed by Chanwoo Choi]
---
Documentation/ABI/testing/sysfs-bus-platform | 17 +++
drivers/usb/Kconfig | 2 +
drivers/usb/Makefile | 1 +
drivers/usb/mux/Kconfig | 11 ++
drivers/usb/mux/Makefile | 4 +
drivers/usb/mux/portmux-core.c | 217 +++++++++++++++++++++++++++
include/linux/usb/portmux.h | 78 ++++++++++
7 files changed, 330 insertions(+)
create mode 100644 drivers/usb/mux/Kconfig
create mode 100644 drivers/usb/mux/Makefile
create mode 100644 drivers/usb/mux/portmux-core.c
create mode 100644 include/linux/usb/portmux.h
diff --git a/Documentation/ABI/testing/sysfs-bus-platform b/Documentation/ABI/testing/sysfs-bus-platform
index 5172a61..f33f0a5 100644
--- a/Documentation/ABI/testing/sysfs-bus-platform
+++ b/Documentation/ABI/testing/sysfs-bus-platform
@@ -18,3 +18,20 @@ Description:
devices to opt-out of driver binding using a driver_override
name such as "none". Only a single driver may be specified in
the override, there is no support for parsing delimiters.
+
+What: /sys/bus/platform/devices/.../portmux.N/name
+ /sys/bus/platform/devices/.../portmux.N/state
+Date: April 2016
+Contact: Lu Baolu <baolu.lu@xxxxxxxxxxxxxxx>
+Description:
+ In some platforms, a single USB port is shared between a USB host
+ controller and a device controller. A USB mux driver is needed to
+ handle the port mux. Read-only attribute "name" shows the name of
+ the port mux device. "state" attribute shows and stores the mux
+ state.
+ For read:
+ 'peripheral' - mux switched to PERIPHERAL controller;
+ 'host' - mux switched to HOST controller.
+ For write:
+ 'peripheral' - mux will be switched to PERIPHERAL controller;
+ 'host' - mux will be switched to HOST controller.
diff --git a/drivers/usb/Kconfig b/drivers/usb/Kconfig
index 8689dcb..328916e 100644
--- a/drivers/usb/Kconfig
+++ b/drivers/usb/Kconfig
@@ -148,6 +148,8 @@ endif # USB
source "drivers/usb/phy/Kconfig"
+source "drivers/usb/mux/Kconfig"
+
source "drivers/usb/gadget/Kconfig"
config USB_LED_TRIG
diff --git a/drivers/usb/Makefile b/drivers/usb/Makefile
index dca7856..9a92338 100644
--- a/drivers/usb/Makefile
+++ b/drivers/usb/Makefile
@@ -6,6 +6,7 @@
obj-$(CONFIG_USB) += core/
obj-$(CONFIG_USB_SUPPORT) += phy/
+obj-$(CONFIG_USB_SUPPORT) += mux/
obj-$(CONFIG_USB_DWC3) += dwc3/
obj-$(CONFIG_USB_DWC2) += dwc2/
diff --git a/drivers/usb/mux/Kconfig b/drivers/usb/mux/Kconfig
new file mode 100644
index 0000000..d91909f
--- /dev/null
+++ b/drivers/usb/mux/Kconfig
@@ -0,0 +1,11 @@
+#
+# USB port mux driver configuration
+#
+
+menu "USB Port MUX drivers"
+config USB_PORTMUX
+ select EXTCON
+ def_bool n
+ help
+ Generic USB dual role port mux support.
+endmenu
diff --git a/drivers/usb/mux/Makefile b/drivers/usb/mux/Makefile
new file mode 100644
index 0000000..f85df92
--- /dev/null
+++ b/drivers/usb/mux/Makefile
@@ -0,0 +1,4 @@
+#
+# Makefile for USB port mux drivers
+#
+obj-$(CONFIG_USB_PORTMUX) += portmux-core.o
diff --git a/drivers/usb/mux/portmux-core.c b/drivers/usb/mux/portmux-core.c
new file mode 100644
index 0000000..0e3548b
--- /dev/null
+++ b/drivers/usb/mux/portmux-core.c
@@ -0,0 +1,217 @@
+/**
+ * intel_mux.c - USB Port Mux support
+ *
+ * Copyright (C) 2016 Intel Corporation
+ *
+ * Author: Lu Baolu <baolu.lu@xxxxxxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+#include <linux/slab.h>
+#include <linux/notifier.h>
+#include <linux/extcon.h>
+#include <linux/err.h>
+#include <linux/usb/portmux.h>
+
+static int usb_mux_change_state(struct portmux_dev *pdev, int state)
+{
+ int ret;
+ struct device *dev = &pdev->dev;
+
+ dev_WARN_ONCE(dev,
+ !mutex_is_locked(&pdev->mux_mutex),
+ "mutex is unlocked\n");
+
+ pdev->mux_state = state;
+
+ if (pdev->mux_state)
+ ret = pdev->desc->ops->cable_set_cb(pdev->dev.parent);
+ else
+ ret = pdev->desc->ops->cable_unset_cb(pdev->dev.parent);
+
+ return ret;
+}
+
+static int usb_mux_notifier(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct portmux_dev *pdev;
+ int state;
+ int ret = NOTIFY_DONE;
+
+ pdev = container_of(nb, struct portmux_dev, nb);
+
+ state = extcon_get_cable_state_(pdev->edev, EXTCON_USB_HOST);
+ if (state < 0)
+ return state;
+
+ mutex_lock(&pdev->mux_mutex);
+ ret = usb_mux_change_state(pdev, state);
+ mutex_unlock(&pdev->mux_mutex);
+
+ return ret;
+}
+
+static ssize_t state_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct portmux_dev *pdev = dev_get_drvdata(dev);
+
+ return sprintf(buf, "%s\n", pdev->mux_state ? "host" : "peripheral");
+}
+
+static ssize_t state_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct portmux_dev *pdev = dev_get_drvdata(dev);
+ int state;
+
+ if (sysfs_streq(buf, "peripheral"))
+ state = 0;
+ else if (sysfs_streq(buf, "host"))
+ state = 1;
+ else
+ return -EINVAL;
+
+ mutex_lock(&pdev->mux_mutex);
+ usb_mux_change_state(pdev, state);
+ mutex_unlock(&pdev->mux_mutex);
+
+ return count;
+}
+static DEVICE_ATTR_RW(state);
+
+static ssize_t name_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct portmux_dev *pdev = dev_get_drvdata(dev);
+
+ return sprintf(buf, "%s\n", pdev->desc->name);
+}
+static DEVICE_ATTR_RO(name);
+
+static struct attribute *portmux_attrs[] = {
+ &dev_attr_state.attr,
+ &dev_attr_name.attr,
+ NULL,
+};
+
+static struct attribute_group portmux_attr_grp = {
+ .attrs = portmux_attrs,
+};
+
+static const struct attribute_group *portmux_group[] = {
+ &portmux_attr_grp,
+ NULL,
+};
+
+/**
+ * portmux_register - register a port mux
+ * @dev: device the mux belongs to
+ * @desc: the descriptor of this port mux
+ *
+ * Called by port mux drivers to register a mux.
+ * Returns a valid pointer to struct portmux_dev on success
+ * or an ERR_PTR() on error.
+ */
+struct portmux_dev *portmux_register(struct portmux_desc *desc)
+{
+ static atomic_t portmux_no = ATOMIC_INIT(-1);
+ struct portmux_dev *pdev;
+ struct extcon_dev *edev = NULL;
+ struct device *dev;
+ int ret;
+
+ /* parameter sanity check */
+ if (!desc || !desc->name || !desc->ops || !desc->dev ||
+ !desc->ops->cable_set_cb || !desc->ops->cable_unset_cb)
+ return ERR_PTR(-EINVAL);
+
+ dev = desc->dev;
+
+ if (desc->extcon_name) {
+ edev = extcon_get_extcon_dev(desc->extcon_name);
+ if (IS_ERR_OR_NULL(edev))
+ return ERR_PTR(-EPROBE_DEFER);
+ }
+
+ pdev = kzalloc(sizeof(*pdev), GFP_KERNEL);
+ if (!pdev)
+ return ERR_PTR(-ENOMEM);
+
+ pdev->desc = desc;
+ pdev->edev = edev;
+ pdev->nb.notifier_call = usb_mux_notifier;
+ mutex_init(&pdev->mux_mutex);
+
+ pdev->dev.parent = dev;
+ dev_set_name(&pdev->dev, "portmux.%lu",
+ (unsigned long)atomic_inc_return(&portmux_no));
+ pdev->dev.groups = portmux_group;
+ ret = device_register(&pdev->dev);
+ if (ret)
+ goto cleanup_mem;
+
+ dev_set_drvdata(&pdev->dev, pdev);
+
+ if (edev) {
+ ret = extcon_register_notifier(edev, EXTCON_USB_HOST,
+ &pdev->nb);
+ if (ret < 0) {
+ dev_err(dev, "failed to register extcon notifier\n");
+ goto cleanup_dev;
+ }
+ }
+
+ if (desc->initial_state == -1) {
+ usb_mux_notifier(&pdev->nb, 0, NULL);
+ } else {
+ mutex_lock(&pdev->mux_mutex);
+ ret = usb_mux_change_state(pdev, !!desc->initial_state);
+ mutex_unlock(&pdev->mux_mutex);
+ }
+
+ return pdev;
+
+cleanup_dev:
+ device_unregister(&pdev->dev);
+cleanup_mem:
+ kfree(pdev);
+
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_GPL(portmux_register);
+
+/**
+ * portmux_unregister - unregister a port mux
+ * @pdev: the port mux device
+ *
+ * Called by port mux drivers to release a mux.
+ */
+void portmux_unregister(struct portmux_dev *pdev)
+{
+ extcon_unregister_notifier(pdev->edev, EXTCON_USB_HOST, &pdev->nb);
+ device_unregister(&pdev->dev);
+ kfree(pdev);
+}
+EXPORT_SYMBOL_GPL(portmux_unregister);
+
+#ifdef CONFIG_PM_SLEEP
+/**
+ * portmux_complete - refresh port state during system resumes back
+ * @pdev: the port mux device
+ *
+ * Called by port mux drivers to refresh port state during system
+ * resumes back.
+ */
+void portmux_complete(struct portmux_dev *pdev)
+{
+ usb_mux_notifier(&pdev->nb, 0, NULL);
+}
+EXPORT_SYMBOL_GPL(portmux_complete);
+#endif
diff --git a/include/linux/usb/portmux.h b/include/linux/usb/portmux.h
new file mode 100644
index 0000000..9250028
--- /dev/null
+++ b/include/linux/usb/portmux.h
@@ -0,0 +1,78 @@
+/**
+ * intel_mux.h - USB Port Mux definitions
+ *
+ * Copyright (C) 2016 Intel Corporation
+ *
+ * Author: Lu Baolu <baolu.lu@xxxxxxxxxxxxxxx>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#ifndef __LINUX_USB_PORTMUX_H
+#define __LINUX_USB_PORTMUX_H
+
+/**
+ * struct portmux_ops - ops two switch the port
+ *
+ * @cable_set_cb: function to switch port to host
+ * @cable_unset_cb: function to switch port to device
+ */
+struct portmux_ops {
+ int (*cable_set_cb)(struct device *dev);
+ int (*cable_unset_cb)(struct device *dev);
+};
+
+/**
+ * struct portmux_desc - port mux device descriptor
+ *
+ * @name: the name of the mux device
+ * @extcon_name: the name of extcon device, set to NULL if the mux
+ * is not connected to any extcon cable and control
+ * purely by user through sysfs.
+ * @dev: the parent of the mux device
+ * @ops: ops to switch the port role
+ * @initial_state: the initial state of the mux, set to -1 if the
+ * initial state is unknown, set to 0 for device
+ * and 1 for host.
+ */
+struct portmux_desc {
+ const char *name;
+ const char *extcon_name;
+ struct device *dev;
+ const struct portmux_ops *ops;
+ int initial_state;
+};
+
+/**
+ * struct portmux_dev - A mux device
+ *
+ * @desc: the descriptor of the mux
+ * @dev: device of this mux
+ * @edev: the extcon device bound to this mux
+ * @nb: notifier of extcon state change
+ * @mux_mutex: lock to serialize port switch operation
+ * @mux_state: state of the mux, could be set to below values
+ * -1: before initialization
+ * 0: port switched to device
+ * 1: port switched to host
+ */
+struct portmux_dev {
+ const struct portmux_desc *desc;
+ struct device dev;
+ struct extcon_dev *edev;
+ struct notifier_block nb;
+
+ /* lock for mux_state */
+ struct mutex mux_mutex;
+ int mux_state;
+};
+
+struct portmux_dev *portmux_register(struct portmux_desc *desc);
+void portmux_unregister(struct portmux_dev *pdev);
+#ifdef CONFIG_PM_SLEEP
+void portmux_complete(struct portmux_dev *pdev);
+#endif
+
+#endif /* __LINUX_USB_PORTMUX_H */
--
2.1.4