usb: gadget: pch_udc: Detecting VBUS through GPIO

Problem:
 In USB Suspend, pch_udc handles 'disconnect'.

Root cause:
 The current pch_udc is not monitoring VBUS.
 When USB cable is disconnected, USB Device Controller generates
 an interrupt of USB Suspend.
 pch_udc cannot distinguish it is USB Suspend or disconnect.
 Therefore, pch_udc handles 'disconnect' after an interrupt of
 USB Suspend happend.

Solution:
 VBUS is detected through GPIO.
 After an interrupt produced USB Suspend, if VBUS is Low,
 pch_udc handles 'disconnect'.
 If VBUS is High, pch_udc handles 'suspend'.

Signed-off-by: Tomoya MORINAGA <tomoya.rohm@gmail.com>
Signed-off-by: Felipe Balbi <balbi@ti.com>
This commit is contained in:
Tomoya MORINAGA 2012-02-03 16:35:26 +09:00 committed by Felipe Balbi
parent 20edfbb6a1
commit dd63180b75
1 changed files with 142 additions and 3 deletions

View File

@ -15,6 +15,13 @@
#include <linux/interrupt.h>
#include <linux/usb/ch9.h>
#include <linux/usb/gadget.h>
#include <linux/gpio.h>
/* GPIO port for VBUS detecting */
static int vbus_gpio_port = -1; /* GPIO port number (-1:Not used) */
#define PCH_VBUS_PERIOD 3000 /* VBUS polling period (msec) */
#define PCH_VBUS_INTERVAL 10 /* VBUS polling interval (msec) */
/* Address offset of Registers */
#define UDC_EP_REG_SHIFT 0x20 /* Offset to next EP */
@ -295,6 +302,17 @@ struct pch_udc_ep {
unsigned long epsts;
};
/**
* struct pch_vbus_gpio_data - Structure holding GPIO informaton
* for detecting VBUS
* @port: gpio port number
* @irq_work_fall Structure for WorkQueue
*/
struct pch_vbus_gpio_data {
int port;
struct work_struct irq_work_fall;
};
/**
* struct pch_udc_dev - Structure holding complete information
* of the PCH USB device
@ -323,6 +341,7 @@ struct pch_udc_ep {
* @base_addr: for mapped device memory
* @irq: IRQ line for the device
* @cfg_data: current cfg, intf, and alt in use
* @vbus_gpio: GPIO informaton for detecting VBUS
*/
struct pch_udc_dev {
struct usb_gadget gadget;
@ -349,7 +368,8 @@ struct pch_udc_dev {
unsigned long phys_addr;
void __iomem *base_addr;
unsigned irq;
struct pch_udc_cfg_data cfg_data;
struct pch_udc_cfg_data cfg_data;
struct pch_vbus_gpio_data vbus_gpio;
};
#define PCH_UDC_PCI_BAR 1
@ -1225,6 +1245,115 @@ static const struct usb_gadget_ops pch_udc_ops = {
.stop = pch_udc_stop,
};
/**
* pch_vbus_gpio_get_value() - This API gets value of GPIO port as VBUS status.
* @dev: Reference to the driver structure
*
* Return value:
* 1: VBUS is high
* 0: VBUS is low
* -1: It is not enable to detect VBUS using GPIO
*/
static int pch_vbus_gpio_get_value(struct pch_udc_dev *dev)
{
int vbus = 0;
if (dev->vbus_gpio.port)
vbus = gpio_get_value(dev->vbus_gpio.port) ? 1 : 0;
else
vbus = -1;
return vbus;
}
/**
* pch_vbus_gpio_work_fall() - This API keeps watch on VBUS becoming Low.
* If VBUS is Low, disconnect is processed
* @irq_work: Structure for WorkQueue
*
*/
static void pch_vbus_gpio_work_fall(struct work_struct *irq_work)
{
struct pch_vbus_gpio_data *vbus_gpio = container_of(irq_work,
struct pch_vbus_gpio_data, irq_work_fall);
struct pch_udc_dev *dev =
container_of(vbus_gpio, struct pch_udc_dev, vbus_gpio);
int vbus_saved = -1;
int vbus;
int count;
if (!dev->vbus_gpio.port)
return;
for (count = 0; count < (PCH_VBUS_PERIOD / PCH_VBUS_INTERVAL);
count++) {
vbus = pch_vbus_gpio_get_value(dev);
if ((vbus_saved == vbus) && (vbus == 0)) {
dev_dbg(&dev->pdev->dev, "VBUS fell");
if (dev->driver
&& dev->driver->disconnect) {
dev->driver->disconnect(
&dev->gadget);
}
pch_udc_reconnect(dev);
dev_dbg(&dev->pdev->dev, "VBUS fell");
return;
}
vbus_saved = vbus;
mdelay(PCH_VBUS_INTERVAL);
}
}
/**
* pch_vbus_gpio_init() - This API initializes GPIO port detecting VBUS.
* @dev: Reference to the driver structure
* @vbus_gpio Number of GPIO port to detect gpio
*
* Return codes:
* 0: Success
* -EINVAL: GPIO port is invalid or can't be initialized.
*/
static int pch_vbus_gpio_init(struct pch_udc_dev *dev, int vbus_gpio_port)
{
int err;
dev->vbus_gpio.port = 0;
if (vbus_gpio_port <= -1)
return -EINVAL;
err = gpio_is_valid(vbus_gpio_port);
if (!err) {
pr_err("%s: gpio port %d is invalid\n",
__func__, vbus_gpio_port);
return -EINVAL;
}
err = gpio_request(vbus_gpio_port, "pch_vbus");
if (err) {
pr_err("%s: can't request gpio port %d, err: %d\n",
__func__, vbus_gpio_port, err);
return -EINVAL;
}
dev->vbus_gpio.port = vbus_gpio_port;
gpio_direction_input(vbus_gpio_port);
INIT_WORK(&dev->vbus_gpio.irq_work_fall, pch_vbus_gpio_work_fall);
return 0;
}
/**
* pch_vbus_gpio_free() - This API frees resources of GPIO port
* @dev: Reference to the driver structure
*/
static void pch_vbus_gpio_free(struct pch_udc_dev *dev)
{
if (dev->vbus_gpio.port)
gpio_free(dev->vbus_gpio.port);
}
/**
* complete_req() - This API is invoked from the driver when processing
* of a request is complete
@ -2510,6 +2639,8 @@ static void pch_udc_svc_cfg_interrupt(struct pch_udc_dev *dev)
*/
static void pch_udc_dev_isr(struct pch_udc_dev *dev, u32 dev_intr)
{
int vbus;
/* USB Reset Interrupt */
if (dev_intr & UDC_DEVINT_UR) {
pch_udc_svc_ur_interrupt(dev);
@ -2535,14 +2666,19 @@ static void pch_udc_dev_isr(struct pch_udc_dev *dev, u32 dev_intr)
spin_lock(&dev->lock);
}
if (dev->vbus_session == 0) {
vbus = pch_vbus_gpio_get_value(dev);
if ((dev->vbus_session == 0)
&& (vbus != 1)) {
if (dev->driver && dev->driver->disconnect) {
spin_unlock(&dev->lock);
dev->driver->disconnect(&dev->gadget);
spin_lock(&dev->lock);
}
pch_udc_reconnect(dev);
}
} else if ((dev->vbus_session == 0)
&& (vbus == 1))
schedule_work(&dev->vbus_gpio.irq_work_fall);
dev_dbg(&dev->pdev->dev, "USB_SUSPEND\n");
}
/* Clear the SOF interrupt, if enabled */
@ -2704,6 +2840,7 @@ static int pch_udc_pcd_init(struct pch_udc_dev *dev)
{
pch_udc_init(dev);
pch_udc_pcd_reinit(dev);
pch_vbus_gpio_init(dev, vbus_gpio_port);
return 0;
}
@ -2882,6 +3019,8 @@ static void pch_udc_remove(struct pci_dev *pdev)
UDC_EP0OUT_BUFF_SIZE * 4, DMA_FROM_DEVICE);
kfree(dev->ep0out_buf);
pch_vbus_gpio_free(dev);
pch_udc_exit(dev);
if (dev->irq_registered)