[PATCH v4 3/3] usb: dwc3: Add dual-role support

From: Roger Quadros
Date: Tue Apr 04 2017 - 03:47:16 EST


If dr_mode is "otg" then support dual role mode of operation.
Currently this mode is only supported when an extcon handle is
present in the dwc3 device tree node. This is needed to
get the ID status events of the port.

We're using a workqueue to manage the dual-role state transitions
as the extcon notifier (dwc3_drd_notifier) is called in an atomic
context by extcon_sync() and this doesn't go well with
usb_del_gadget_udc() causing a lockdep and softirq warning.

Signed-off-by: Roger Quadros <rogerq@xxxxxx>
---
v4:
-fix build error if CONFIG_USB_DWC3_DUAL_ROLE is not set

drivers/usb/dwc3/Makefile | 4 ++
drivers/usb/dwc3/core.c | 15 +----
drivers/usb/dwc3/core.h | 19 ++++++
drivers/usb/dwc3/drd.c | 167 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 193 insertions(+), 12 deletions(-)
create mode 100644 drivers/usb/dwc3/drd.c

diff --git a/drivers/usb/dwc3/Makefile b/drivers/usb/dwc3/Makefile
index ffca340..f15fabb 100644
--- a/drivers/usb/dwc3/Makefile
+++ b/drivers/usb/dwc3/Makefile
@@ -17,6 +17,10 @@ ifneq ($(filter y,$(CONFIG_USB_DWC3_GADGET) $(CONFIG_USB_DWC3_DUAL_ROLE)),)
dwc3-y += gadget.o ep0.o
endif

+ifneq ($(CONFIG_USB_DWC3_DUAL_ROLE),)
+ dwc3-y += drd.o
+endif
+
ifneq ($(CONFIG_USB_DWC3_ULPI),)
dwc3-y += ulpi.o
endif
diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c
index bf917c2..15c05a1 100644
--- a/drivers/usb/dwc3/core.c
+++ b/drivers/usb/dwc3/core.c
@@ -864,12 +864,10 @@ static int dwc3_core_init_mode(struct dwc3 *dwc)
}
break;
case USB_DR_MODE_OTG:
- /* start in peripheral role by default */
- dwc3_set_mode(dwc, DWC3_GCTL_PRTCAP_DEVICE);
- ret = dwc3_gadget_init(dwc);
+ ret = dwc3_drd_init(dwc);
if (ret) {
if (ret != -EPROBE_DEFER)
- dev_err(dev, "failed to initialize gadget\n");
+ dev_err(dev, "failed to initialize dual-role\n");
return ret;
}
break;
@@ -891,14 +889,7 @@ static void dwc3_core_exit_mode(struct dwc3 *dwc)
dwc3_host_exit(dwc);
break;
case USB_DR_MODE_OTG:
- /* role might have changed since start */
- if (dwc->current_dr_role == DWC3_GCTL_PRTCAP_HOST) {
- dwc3_host_exit(dwc);
- /* Add back UDC to match dwc3_gadget_exit() */
- usb_add_gadget_udc(dwc->dev, &dwc->gadget);
- }
-
- dwc3_gadget_exit(dwc);
+ dwc3_drd_exit(dwc);
break;
default:
/* do nothing */
diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h
index adb04af..b1be4a3 100644
--- a/drivers/usb/dwc3/core.h
+++ b/drivers/usb/dwc3/core.h
@@ -786,6 +786,10 @@ struct dwc3_scratchpad_array {
* @revision: revision register contents
* @dr_mode: requested mode of operation
* @current_dr_role: current role of operation when in dual-role mode
+ * @edev: extcon handle
+ * @edev_nb: extcon notifier
+ * @drd_work: dual-role work
+ * @drd_prevent_change: flag to prevent dual-role state change
* @hsphy_mode: UTMI phy mode, one of following:
* - USBPHY_INTERFACE_MODE_UTMI
* - USBPHY_INTERFACE_MODE_UTMIW
@@ -903,6 +907,11 @@ struct dwc3 {

enum usb_dr_mode dr_mode;
u32 current_dr_role;
+ struct extcon_dev *edev;
+ struct notifier_block edev_nb;
+ bool drd_prevent_change;
+ struct work_struct drd_work;
+
enum usb_phy_interface hsphy_mode;

u32 fladj;
@@ -1225,6 +1234,16 @@ static inline int dwc3_send_gadget_generic_command(struct dwc3 *dwc,
{ return 0; }
#endif

+#if IS_ENABLED(CONFIG_USB_DWC3_DUAL_ROLE)
+int dwc3_drd_init(struct dwc3 *dwc);
+void dwc3_drd_exit(struct dwc3 *dwc);
+#else
+static inline int dwc3_drd_init(struct dwc3 *dwc)
+{ return 0; }
+static inline void dwc3_drd_exit(struct dwc3 *dwc)
+{ }
+#endif
+
/* power management interface */
#if !IS_ENABLED(CONFIG_USB_DWC3_HOST)
int dwc3_gadget_suspend(struct dwc3 *dwc);
diff --git a/drivers/usb/dwc3/drd.c b/drivers/usb/dwc3/drd.c
new file mode 100644
index 0000000..c9b02a3
--- /dev/null
+++ b/drivers/usb/dwc3/drd.c
@@ -0,0 +1,167 @@
+/**
+ * drd.c - DesignWare USB3 DRD Controller Dual-role support
+ *
+ * Copyright (C) 2017 Texas Instruments Incorporated - http://www.ti.com
+ *
+ * Authors: Roger Quadros <rogerq@xxxxxx>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 of
+ * the License as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <linux/extcon.h>
+
+#include "debug.h"
+#include "core.h"
+#include "gadget.h"
+
+static void dwc3_drd_update(struct dwc3 *dwc)
+{
+ int id;
+ int new_role;
+ unsigned long flags;
+
+ if (dwc->drd_prevent_change)
+ return;
+
+ id = extcon_get_state(dwc->edev, EXTCON_USB_HOST);
+ /* Host means ID is 0 */
+ id = !id;
+
+ if (!id)
+ new_role = DWC3_GCTL_PRTCAP_HOST;
+ else
+ new_role = DWC3_GCTL_PRTCAP_DEVICE;
+
+ if (dwc->current_dr_role == new_role)
+ return;
+
+ /* stop old role */
+ switch (dwc->current_dr_role) {
+ case DWC3_GCTL_PRTCAP_HOST:
+ dwc3_host_exit(dwc);
+ break;
+ case DWC3_GCTL_PRTCAP_DEVICE:
+ usb_del_gadget_udc(&dwc->gadget);
+ break;
+ default:
+ break;
+ }
+
+ /* switch PRTCAP mode. updates current_dr_role */
+ spin_lock_irqsave(&dwc->lock, flags);
+ dwc3_set_mode(dwc, new_role);
+ spin_unlock_irqrestore(&dwc->lock, flags);
+
+ /* start new role */
+ switch (dwc->current_dr_role) {
+ case DWC3_GCTL_PRTCAP_HOST:
+ dwc3_host_init(dwc);
+ break;
+ case DWC3_GCTL_PRTCAP_DEVICE:
+ dwc3_event_buffers_setup(dwc);
+ usb_add_gadget_udc(dwc->dev, &dwc->gadget);
+ break;
+ default:
+ break;
+ }
+}
+
+static void dwc3_drd_work(struct work_struct *work)
+{
+ struct dwc3 *dwc = container_of(work, struct dwc3,
+ drd_work);
+ dwc3_drd_update(dwc);
+}
+
+static int dwc3_drd_notifier(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct dwc3 *dwc = container_of(nb, struct dwc3, edev_nb);
+
+ queue_work(system_power_efficient_wq, &dwc->drd_work);
+
+ return NOTIFY_DONE;
+}
+
+int dwc3_drd_init(struct dwc3 *dwc)
+{
+ int ret;
+ int id;
+ struct device *dev = dwc->dev;
+
+ INIT_WORK(&dwc->drd_work, dwc3_drd_work);
+
+ if (dev->of_node) {
+ if (of_property_read_bool(dev->of_node, "extcon"))
+ dwc->edev = extcon_get_edev_by_phandle(dev, 0);
+
+ if (IS_ERR(dwc->edev))
+ return PTR_ERR(dwc->edev);
+ } else {
+ return -ENODEV;
+ }
+
+ dwc->edev_nb.notifier_call = dwc3_drd_notifier;
+ ret = extcon_register_notifier(dwc->edev, EXTCON_USB_HOST,
+ &dwc->edev_nb);
+ if (ret < 0) {
+ dev_err(dwc->dev, "Couldn't register USB-HOST cable notifier\n");
+ return -ENODEV;
+ }
+
+ /* sanity check id & vbus states */
+ id = extcon_get_state(dwc->edev, EXTCON_USB_HOST);
+ if (id < 0) {
+ dev_err(dwc->dev, "Invalid USB cable state. ID %d", id);
+ ret = -ENODEV;
+ goto fail;
+ }
+
+ /* start in peripheral role by default */
+ dwc3_set_mode(dwc, DWC3_GCTL_PRTCAP_DEVICE);
+ ret = dwc3_gadget_init(dwc);
+ if (ret)
+ goto fail;
+
+ /* check & update drd state */
+ dwc3_drd_update(dwc);
+
+ return 0;
+
+fail:
+ extcon_unregister_notifier(dwc->edev, EXTCON_USB_HOST,
+ &dwc->edev_nb);
+
+ return ret;
+}
+
+void dwc3_drd_exit(struct dwc3 *dwc)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&dwc->lock, flags);
+ dwc->drd_prevent_change = true;
+ spin_unlock_irqrestore(&dwc->lock, flags);
+
+ extcon_unregister_notifier(dwc->edev, EXTCON_USB_HOST,
+ &dwc->edev_nb);
+
+ /* role might have changed since start, stop active controller */
+ if (dwc->current_dr_role == DWC3_GCTL_PRTCAP_HOST) {
+ dwc3_host_exit(dwc);
+ /* Add back UDC to match dwc3_gadget_exit() */
+ usb_add_gadget_udc(dwc->dev, &dwc->gadget);
+ }
+
+ dwc3_gadget_exit(dwc);
+}
--
2.7.4