[PATCH 2/4] usb: chipidea: add otg id switch and vbus connect/disconnect detect

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



The main design flow is the same with msm otg driver, that is the id and
vbus interrupt are handled at core driver, others are handled by
individual drivers.

- At former design, when switch usb role from device->host, it will call
udc_stop, it will remove the gadget driver, so when switch role
from host->device, it can't add gadget driver any more.
At new design, when role switch occurs, the gadget just calls
usb_gadget_vbus_disconnect/usb_gadget_vbus_connect as well as
reset controller, it will not free any device/gadget structure

- Add vbus connect and disconnect to core interrupt handler, it can
notify udc driver by calling usb_gadget_vbus_disconnect/usb_gadget_vbus_connect.

- Add otg.c to implement struct usb_otg, in that case, calling otg_set_peripheral
will not be failed at udc.c. Besides, we enable id interrupt at
ci_hdrc_otg_init.

- Add special case handling, like connecting to host during boot up at device
mode, usb device at the MicroB to A cable at host mode, etc.

Signed-off-by: Peter Chen <peter.chen@xxxxxxxxxxxxx>
---
 drivers/usb/chipidea/Makefile |    2 +-
 drivers/usb/chipidea/bits.h   |   10 ++
 drivers/usb/chipidea/ci.h     |    6 +
 drivers/usb/chipidea/core.c   |  217 +++++++++++++++++++++++++++++++++++++----
 drivers/usb/chipidea/otg.c    |   60 +++++++++++
 drivers/usb/chipidea/otg.h    |    6 +
 drivers/usb/chipidea/udc.c    |    2 +
 7 files changed, 284 insertions(+), 19 deletions(-)

diff --git a/drivers/usb/chipidea/Makefile b/drivers/usb/chipidea/Makefile
index d92ca32..11f513c 100644
--- a/drivers/usb/chipidea/Makefile
+++ b/drivers/usb/chipidea/Makefile
@@ -2,7 +2,7 @@ ccflags-$(CONFIG_USB_CHIPIDEA_DEBUG) := -DDEBUG
 
 obj-$(CONFIG_USB_CHIPIDEA)		+= ci_hdrc.o
 
-ci_hdrc-y				:= core.o
+ci_hdrc-y				:= core.o otg.o
 ci_hdrc-$(CONFIG_USB_CHIPIDEA_UDC)	+= udc.o
 ci_hdrc-$(CONFIG_USB_CHIPIDEA_HOST)	+= host.o
 ci_hdrc-$(CONFIG_USB_CHIPIDEA_DEBUG)	+= debug.o
diff --git a/drivers/usb/chipidea/bits.h b/drivers/usb/chipidea/bits.h
index 050de85..4b6ae3e 100644
--- a/drivers/usb/chipidea/bits.h
+++ b/drivers/usb/chipidea/bits.h
@@ -65,11 +65,21 @@
 #define OTGSC_ASVIS	      BIT(18)
 #define OTGSC_BSVIS	      BIT(19)
 #define OTGSC_BSEIS	      BIT(20)
+#define OTGSC_1MSIS	      BIT(21)
+#define OTGSC_DPIS	      BIT(22)
 #define OTGSC_IDIE	      BIT(24)
 #define OTGSC_AVVIE	      BIT(25)
 #define OTGSC_ASVIE	      BIT(26)
 #define OTGSC_BSVIE	      BIT(27)
 #define OTGSC_BSEIE	      BIT(28)
+#define OTGSC_1MSIE	      BIT(29)
+#define OTGSC_DPIE	      BIT(30)
+#define OTGSC_INT_EN_BITS	(BIT(24) | BIT(25) | BIT(26)	\
+				| BIT(27) | BIT(28) | BIT(29)	\
+				| BIT(30))
+#define OTGSC_INT_STATUS_BITS	(BIT(16) | BIT(17) | BIT(18)	\
+				| BIT(19) | BIT(20) | BIT(21)	\
+				| BIT(22))
 
 /* USBMODE */
 #define USBMODE_CM            (0x03UL <<  0)
diff --git a/drivers/usb/chipidea/ci.h b/drivers/usb/chipidea/ci.h
index d738603..d32f932 100644
--- a/drivers/usb/chipidea/ci.h
+++ b/drivers/usb/chipidea/ci.h
@@ -139,6 +139,7 @@ struct ci13xxx {
 	enum ci_role			role;
 	bool				is_otg;
 	struct work_struct		work;
+	struct delayed_work		dwork;
 	struct workqueue_struct		*wq;
 
 	struct dma_pool			*qh_pool;
@@ -164,6 +165,11 @@ struct ci13xxx {
 	bool				global_phy;
 	struct usb_phy			*transceiver;
 	struct usb_hcd			*hcd;
+	/* events handled at ci_role_work */
+#define ID		0
+#define B_SESS_VALID	1
+	unsigned long events;
+	struct usb_otg			otg;
 };
 
 static inline struct ci_role_driver *ci_role(struct ci13xxx *ci)
diff --git a/drivers/usb/chipidea/core.c b/drivers/usb/chipidea/core.c
index f69d029..b50b77a 100644
--- a/drivers/usb/chipidea/core.c
+++ b/drivers/usb/chipidea/core.c
@@ -73,6 +73,7 @@
 #include "bits.h"
 #include "host.h"
 #include "debug.h"
+#include "otg.h"
 
 /* Controller register map */
 static uintptr_t ci_regs_nolpm[] = {
@@ -264,25 +265,138 @@ static enum ci_role ci_otg_role(struct ci13xxx *ci)
 	return role;
 }
 
+#define CI_WAIT_VBUS_STABLE_TIMEOUT 500
 /**
- * ci_role_work - perform role changing based on ID pin
- * @work: work struct
+ * ci_wait_vbus_stable
+ * When usb role switches, we need to turn on/off internal vbus
+ * regulaor, sometimes, the real vbus value may not lower fast
+ * enough due to capacitance, and we do want the vbus lower
+ * than 0.8v if it is device mode, and higher than 4.4v, if it
+ * is host mode.
+ *
+ * @low: true, Wait vbus lower than B_SESSION_VALID
+ *     : false, Wait vbus higher than A_VBUS_VALID
  */
-static void ci_role_work(struct work_struct *work)
+static void ci_wait_vbus_stable(struct ci13xxx *ci, bool low)
+{
+	unsigned long timeout;
+	u32 otgsc = hw_read(ci, OP_OTGSC, ~0);
+
+	timeout = jiffies + CI_WAIT_VBUS_STABLE_TIMEOUT;
+
+	if (low) {
+		while (otgsc & OTGSC_BSV) {
+			if (time_after(jiffies, timeout)) {
+				dev_err(ci->dev, "wait vbus lower than\
+						B_SESS_VALID timeout!\n");
+				return;
+			}
+			msleep(20);
+			otgsc = hw_read(ci, OP_OTGSC, ~0);
+		}
+	} else {
+		while (!(otgsc & OTGSC_AVV)) {
+			if (time_after(jiffies, timeout)) {
+				dev_err(ci->dev, "wait vbus higher than\
+						A_VBUS_VALID timeout!\n");
+				return;
+			}
+			msleep(20);
+			otgsc = hw_read(ci, OP_OTGSC, ~0);
+		}
+
+	}
+}
+
+static void ci_handle_id_switch(struct ci13xxx *ci)
 {
-	struct ci13xxx *ci = container_of(work, struct ci13xxx, work);
 	enum ci_role role = ci_otg_role(ci);
 
 	if (role != ci->role) {
 		dev_dbg(ci->dev, "switching from %s to %s\n",
 			ci_role(ci)->name, ci->roles[role]->name);
 
-		ci_role_stop(ci);
-		ci_role_start(ci, role);
-		enable_irq(ci->irq);
+		/* 1. Finish the current role */
+		if (ci->role == CI_ROLE_GADGET) {
+			usb_gadget_vbus_disconnect(&ci->gadget);
+			/* host doesn't care B_SESSION_VALID event */
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIE, ~OTGSC_BSVIE);
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIS, OTGSC_BSVIS);
+			ci->role = CI_ROLE_END;
+			/* reset controller */
+			hw_write(ci, OP_USBCMD, USBCMD_RST, USBCMD_RST);
+			while (hw_read(ci, OP_USBCMD, USBCMD_RST))
+				udelay(10);
+		} else if (ci->role == CI_ROLE_HOST) {
+			ci_role_stop(ci);
+			/* reset controller */
+			hw_write(ci, OP_USBCMD, USBCMD_RST, USBCMD_RST);
+			while (hw_read(ci, OP_USBCMD, USBCMD_RST))
+				udelay(10);
+		}
+
+		/* 2. Turn on/off vbus according to coming role */
+		if (ci_otg_role(ci) == CI_ROLE_GADGET) {
+			otg_set_vbus(&ci->otg, false);
+			ci_wait_vbus_stable(ci, true);
+		} else if (ci_otg_role(ci) == CI_ROLE_HOST) {
+			otg_set_vbus(&ci->otg, true);
+			ci_wait_vbus_stable(ci, false);
+		}
+
+		/* 3. Begin the new role */
+		if (ci_otg_role(ci) == CI_ROLE_GADGET) {
+			ci->role = role;
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIS, OTGSC_BSVIS);
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIE, OTGSC_BSVIE);
+		} else if (ci_otg_role(ci) == CI_ROLE_HOST) {
+			ci_role_start(ci, role);
+		}
 	}
 }
 
+static void ci_handle_vbus_change(struct ci13xxx *ci)
+{
+	u32 otgsc = hw_read(ci, OP_OTGSC, ~0);
+
+	if (otgsc & OTGSC_BSV)
+		usb_gadget_vbus_connect(&ci->gadget);
+	else
+		usb_gadget_vbus_disconnect(&ci->gadget);
+}
+
+/**
+ * ci_otg_work - perform otg (vbus/id) event handle
+ * @work: work struct
+ */
+static void ci_otg_work(struct work_struct *work)
+{
+	struct ci13xxx *ci = container_of(work, struct ci13xxx, work);
+
+	if (test_bit(ID, &ci->events)) {
+		clear_bit(ID, &ci->events);
+		ci_handle_id_switch(ci);
+	} else if (test_bit(B_SESS_VALID, &ci->events)) {
+		clear_bit(B_SESS_VALID, &ci->events);
+		ci_handle_vbus_change(ci);
+	} else
+		dev_err(ci->dev, "unexpected event occurs at %s\n", __func__);
+
+	enable_irq(ci->irq);
+}
+
+static void ci_delayed_work(struct work_struct *work)
+{
+	struct delayed_work *dwork = to_delayed_work(work);
+	struct ci13xxx *ci = container_of(dwork, struct ci13xxx, dwork);
+	u32 otgsc = hw_read(ci, OP_OTGSC, ~0);
+
+	/* Handle the situation that usb device at the MicroB to A cable */
+	if (ci->is_otg && !(otgsc & OTGSC_ID))
+		otg_set_vbus(&ci->otg, true);
+
+}
+
 static ssize_t show_role(struct device *dev, struct device_attribute *attr,
 			 char *buf)
 {
@@ -321,19 +435,36 @@ static irqreturn_t ci_irq(int irq, void *data)
 	irqreturn_t ret = IRQ_NONE;
 	u32 otgsc = 0;
 
-	if (ci->is_otg)
-		otgsc = hw_read(ci, OP_OTGSC, ~0);
+	otgsc = hw_read(ci, OP_OTGSC, ~0);
 
-	if (ci->role != CI_ROLE_END)
-		ret = ci_role(ci)->irq(ci);
-
-	if (ci->is_otg && (otgsc & OTGSC_IDIS)) {
+	/*
+	 * Handle id change interrupt, it indicates device/host function
+	 * switch.
+	 */
+	if (ci->is_otg && (otgsc & OTGSC_IDIE) && (otgsc & OTGSC_IDIS)) {
+		set_bit(ID, &ci->events);
 		hw_write(ci, OP_OTGSC, OTGSC_IDIS, OTGSC_IDIS);
 		disable_irq_nosync(ci->irq);
 		queue_work(ci->wq, &ci->work);
-		ret = IRQ_HANDLED;
+		return IRQ_HANDLED;
 	}
 
+	/*
+	 * Handle vbus change interrupt, it indicates device connection
+	 * and disconnection events.
+	 */
+	if ((otgsc & OTGSC_BSVIE) && (otgsc & OTGSC_BSVIS)) {
+		set_bit(B_SESS_VALID, &ci->events);
+		hw_write(ci, OP_OTGSC, OTGSC_BSVIS, OTGSC_BSVIS);
+		disable_irq_nosync(ci->irq);
+		queue_work(ci->wq, &ci->work);
+		return IRQ_HANDLED;
+	}
+
+	/* Handle device/host interrupt */
+	if (ci->role != CI_ROLE_END)
+		ret = ci_role(ci)->irq(ci);
+
 	return ret;
 }
 
@@ -397,6 +528,7 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 	struct resource	*res;
 	void __iomem	*base;
 	int		ret;
+	u32		otgsc;
 
 	if (!dev->platform_data) {
 		dev_err(dev, "platform data missing\n");
@@ -421,6 +553,7 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 		return -ENOMEM;
 	}
 
+	ci->events = 0;
 	ci->dev = dev;
 	ci->platdata = dev->platform_data;
 	if (ci->platdata->phy)
@@ -442,12 +575,20 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 		return -ENODEV;
 	}
 
-	INIT_WORK(&ci->work, ci_role_work);
+	INIT_WORK(&ci->work, ci_otg_work);
+	INIT_DELAYED_WORK(&ci->dwork, ci_delayed_work);
 	ci->wq = create_singlethread_workqueue("ci_otg");
 	if (!ci->wq) {
 		dev_err(dev, "can't create workqueue\n");
 		return -ENODEV;
 	}
+	/* Disable all interrupts bits */
+	hw_write(ci, OP_USBINTR, 0xffffffff, 0);
+	hw_write(ci, OP_OTGSC, OTGSC_INT_EN_BITS, ~OTGSC_INT_EN_BITS);
+
+	/* Clear all interrupts status bits*/
+	hw_write(ci, OP_USBSTS, 0xffffffff, 0xffffffff);
+	hw_write(ci, OP_OTGSC, OTGSC_INT_STATUS_BITS, OTGSC_INT_STATUS_BITS);
 
 	/* initialize role(s) before the interrupt is requested */
 	ret = ci_hdrc_host_init(ci);
@@ -465,6 +606,7 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 	}
 
 	if (ci->roles[CI_ROLE_HOST] && ci->roles[CI_ROLE_GADGET]) {
+		dev_info(dev, "support otg\n");
 		ci->is_otg = true;
 		/* ID pin needs 1ms debouce time, we delay 2ms for safe */
 		mdelay(2);
@@ -475,13 +617,53 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 			: CI_ROLE_GADGET;
 	}
 
+	if (ci->is_otg) {
+		/* if otg is supported, create struct usb_otg */
+		ci_hdrc_otg_init(ci);
+
+		/*
+		 * If it is host role at otg port, start gadget role first
+		 * as we need to allocate device struct.
+		 */
+		if (ci->role == CI_ROLE_HOST) {
+
+			ret = ci_role_start(ci, CI_ROLE_GADGET);
+			if (ret) {
+				dev_err(dev, "can't start gadget role, ret=%d\n",
+						ret);
+				ret = -ENODEV;
+				goto rm_wq;
+			}
+
+			usb_gadget_vbus_disconnect(&ci->gadget);
+			/* host doesn't care B_SESSION_VALID event */
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIE, ~OTGSC_BSVIE);
+			hw_write(ci, OP_OTGSC, OTGSC_BSVIS, OTGSC_BSVIS);
+			ci->role = CI_ROLE_HOST;
+		}
+		msleep(20);
+	}
+
 	ret = ci_role_start(ci, ci->role);
 	if (ret) {
-		dev_err(dev, "can't start %s role\n", ci_role(ci)->name);
+		dev_err(dev, "can't start %s role, ret=%d\n",
+				ci_role(ci)->name, ret);
 		ret = -ENODEV;
 		goto rm_wq;
 	}
 
+	otgsc = hw_read(ci, OP_OTGSC, ~0);
+	/*
+	 * if it is device mode:
+	 * - Enable vbus detect
+	 * - If it has already connected to host, notify udc
+	 */
+	if (ci->role == CI_ROLE_GADGET) {
+		hw_write(ci, OP_OTGSC, OTGSC_BSVIE, OTGSC_BSVIE);
+		if (otgsc & OTGSC_BSV)
+			usb_gadget_vbus_connect(&ci->gadget);
+	}
+
 	platform_set_drvdata(pdev, ci);
 	ret = request_irq(ci->irq, ci_irq, IRQF_SHARED, ci->platdata->name,
 			  ci);
@@ -492,8 +674,7 @@ static int __devinit ci_hdrc_probe(struct platform_device *pdev)
 	if (ret)
 		goto rm_attr;
 
-	if (ci->is_otg)
-		hw_write(ci, OP_OTGSC, OTGSC_IDIE, OTGSC_IDIE);
+	queue_delayed_work(ci->wq, &ci->dwork, msecs_to_jiffies(500));
 
 	return ret;
 
diff --git a/drivers/usb/chipidea/otg.c b/drivers/usb/chipidea/otg.c
new file mode 100644
index 0000000..cc2864f
--- /dev/null
+++ b/drivers/usb/chipidea/otg.c
@@ -0,0 +1,60 @@
+/*
+ * otg.c - ChipIdea USB IP core OTG driver
+ *
+ * Copyright 2012 Freescale Semiconductor, Inc.
+ *
+ * Author: Peter Chen
+ *
+ * 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/platform_device.h>
+#include <linux/module.h>
+#include <linux/io.h>
+#include <linux/irq.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/usb/gadget.h>
+#include <linux/usb/otg.h>
+#include <linux/usb/chipidea.h>
+
+#include "ci.h"
+#include "udc.h"
+#include "bits.h"
+#include "host.h"
+#include "debug.h"
+
+static int ci_otg_set_peripheral(struct usb_otg *otg,
+		struct usb_gadget *periph)
+{
+	otg->gadget = periph;
+
+	return 0;
+}
+
+static int ci_otg_set_host(struct usb_otg *otg, struct usb_bus *host)
+{
+	otg->host = host;
+
+	return 0;
+}
+
+/**
+ * ci_hdrc_otg_init - initialize device related bits
+ * ci: the controller
+ *
+ * This function create otg struct, if the device can switch between
+ * device and host.
+ */
+int ci_hdrc_otg_init(struct ci13xxx *ci)
+{
+	/* Useless at current */
+	ci->otg.set_peripheral = ci_otg_set_peripheral;
+	ci->otg.set_host = ci_otg_set_host;
+	ci->transceiver->otg = &ci->otg;
+	hw_write(ci, OP_OTGSC, OTGSC_IDIE, OTGSC_IDIE);
+
+	return 0;
+}
diff --git a/drivers/usb/chipidea/otg.h b/drivers/usb/chipidea/otg.h
new file mode 100644
index 0000000..b4c6b3e
--- /dev/null
+++ b/drivers/usb/chipidea/otg.h
@@ -0,0 +1,6 @@
+#ifndef __DRIVERS_USB_CHIPIDEA_OTG_H
+#define __DRIVERS_USB_CHIPIDEA_OTG_H
+
+int ci_hdrc_otg_init(struct ci13xxx *ci);
+
+#endif /* __DRIVERS_USB_CHIPIDEA_OTG_H */
diff --git a/drivers/usb/chipidea/udc.c b/drivers/usb/chipidea/udc.c
index d214448..b52cb10 100644
--- a/drivers/usb/chipidea/udc.c
+++ b/drivers/usb/chipidea/udc.c
@@ -1371,6 +1371,7 @@ static int ci13xxx_vbus_session(struct usb_gadget *_gadget, int is_active)
 			pm_runtime_get_sync(&_gadget->dev);
 			hw_device_reset(ci, USBMODE_CM_DC);
 			hw_device_state(ci, ci->ep0out->qh.dma);
+			dev_dbg(ci->dev, "Connected to host\n");
 		} else {
 			hw_device_state(ci, 0);
 			if (ci->platdata->notify_event)
@@ -1378,6 +1379,7 @@ static int ci13xxx_vbus_session(struct usb_gadget *_gadget, int is_active)
 				CI13XXX_CONTROLLER_STOPPED_EVENT);
 			_gadget_stop_activity(&ci->gadget);
 			pm_runtime_put_sync(&_gadget->dev);
+			dev_dbg(ci->dev, "disconnected from host\n");
 		}
 	}
 
-- 
1.7.0.4


--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux