USB: implement non-tree resume ordering constraints for PCI host controllers
[linux-2.6.git] / drivers / usb / core / hcd-pci.c
index 2dcf906..1528653 100644 (file)
@@ -19,6 +19,7 @@
 #include <linux/kernel.h>
 #include <linux/module.h>
 #include <linux/pci.h>
+#include <linux/pm_runtime.h>
 #include <linux/usb.h>
 
 #include <asm/io.h>
 
 /* PCI-based HCs are common, but plenty of non-PCI HCs are used too */
 
+#ifdef CONFIG_PM_SLEEP
+
+/* Coordinate handoffs between EHCI and companion controllers
+ * during system resume
+ */
+
+static DEFINE_MUTEX(companions_mutex);
+
+#define CL_UHCI                PCI_CLASS_SERIAL_USB_UHCI
+#define CL_OHCI                PCI_CLASS_SERIAL_USB_OHCI
+#define CL_EHCI                PCI_CLASS_SERIAL_USB_EHCI
+
+enum companion_action {
+       SET_HS_COMPANION, CLEAR_HS_COMPANION, WAIT_FOR_COMPANIONS
+};
+
+static void companion_common(struct pci_dev *pdev, struct usb_hcd *hcd,
+               enum companion_action action)
+{
+       struct pci_dev          *companion;
+       struct usb_hcd          *companion_hcd;
+       unsigned int            slot = PCI_SLOT(pdev->devfn);
+
+       /* Iterate through other PCI functions in the same slot.
+        * If pdev is OHCI or UHCI then we are looking for EHCI, and
+        * vice versa.
+        */
+       companion = NULL;
+       for (;;) {
+               companion = pci_get_device(PCI_ANY_ID, PCI_ANY_ID, companion);
+               if (!companion)
+                       break;
+               if (companion->bus != pdev->bus ||
+                               PCI_SLOT(companion->devfn) != slot)
+                       continue;
+
+               companion_hcd = pci_get_drvdata(companion);
+               if (!companion_hcd)
+                       continue;
+
+               /* For SET_HS_COMPANION, store a pointer to the EHCI bus in
+                * the OHCI/UHCI companion bus structure.
+                * For CLEAR_HS_COMPANION, clear the pointer to the EHCI bus
+                * in the OHCI/UHCI companion bus structure.
+                * For WAIT_FOR_COMPANIONS, wait until the OHCI/UHCI
+                * companion controllers have fully resumed.
+                */
+
+               if ((pdev->class == CL_OHCI || pdev->class == CL_UHCI) &&
+                               companion->class == CL_EHCI) {
+                       /* action must be SET_HS_COMPANION */
+                       dev_dbg(&companion->dev, "HS companion for %s\n",
+                                       dev_name(&pdev->dev));
+                       hcd->self.hs_companion = &companion_hcd->self;
+
+               } else if (pdev->class == CL_EHCI &&
+                               (companion->class == CL_OHCI ||
+                               companion->class == CL_UHCI)) {
+                       switch (action) {
+                       case SET_HS_COMPANION:
+                               dev_dbg(&pdev->dev, "HS companion for %s\n",
+                                               dev_name(&companion->dev));
+                               companion_hcd->self.hs_companion = &hcd->self;
+                               break;
+                       case CLEAR_HS_COMPANION:
+                               companion_hcd->self.hs_companion = NULL;
+                               break;
+                       case WAIT_FOR_COMPANIONS:
+                               device_pm_wait_for_dev(&pdev->dev,
+                                               &companion->dev);
+                               break;
+                       }
+               }
+       }
+}
+
+static void set_hs_companion(struct pci_dev *pdev, struct usb_hcd *hcd)
+{
+       mutex_lock(&companions_mutex);
+       dev_set_drvdata(&pdev->dev, hcd);
+       companion_common(pdev, hcd, SET_HS_COMPANION);
+       mutex_unlock(&companions_mutex);
+}
+
+static void clear_hs_companion(struct pci_dev *pdev, struct usb_hcd *hcd)
+{
+       mutex_lock(&companions_mutex);
+       dev_set_drvdata(&pdev->dev, NULL);
+
+       /* If pdev is OHCI or UHCI, just clear its hs_companion pointer */
+       if (pdev->class == CL_OHCI || pdev->class == CL_UHCI)
+               hcd->self.hs_companion = NULL;
+
+       /* Otherwise search for companion buses and clear their pointers */
+       else
+               companion_common(pdev, hcd, CLEAR_HS_COMPANION);
+       mutex_unlock(&companions_mutex);
+}
+
+static void wait_for_companions(struct pci_dev *pdev, struct usb_hcd *hcd)
+{
+       /* Only EHCI controllers need to wait.
+        * No locking is needed because a controller cannot be resumed
+        * while one of its companions is getting unbound.
+        */
+       if (pdev->class == CL_EHCI)
+               companion_common(pdev, hcd, WAIT_FOR_COMPANIONS);
+}
+
+#else /* !CONFIG_PM_SLEEP */
+
+static inline void set_hs_companion(struct pci_dev *d, struct usb_hcd *h) {}
+static inline void clear_hs_companion(struct pci_dev *d, struct usb_hcd *h) {}
+static inline void wait_for_companions(struct pci_dev *d, struct usb_hcd *h) {}
+
+#endif /* !CONFIG_PM_SLEEP */
 
 /*-------------------------------------------------------------------------*/
 
@@ -123,7 +240,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
                if (region == PCI_ROM_RESOURCE) {
                        dev_dbg(&dev->dev, "no i/o regions available\n");
                        retval = -EBUSY;
-                       goto err1;
+                       goto err2;
                }
        }
 
@@ -132,6 +249,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
        retval = usb_add_hcd(hcd, dev->irq, IRQF_DISABLED | IRQF_SHARED);
        if (retval != 0)
                goto err4;
+       set_hs_companion(dev, hcd);
        return retval;
 
  err4:
@@ -142,6 +260,7 @@ int usb_hcd_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
        } else
                release_region(hcd->rsrc_start, hcd->rsrc_len);
  err2:
+       clear_hs_companion(dev, hcd);
        usb_put_hcd(hcd);
  err1:
        pci_disable_device(dev);
@@ -180,6 +299,7 @@ void usb_hcd_pci_remove(struct pci_dev *dev)
        } else {
                release_region(hcd->rsrc_start, hcd->rsrc_len);
        }
+       clear_hs_companion(dev, hcd);
        usb_put_hcd(hcd);
        pci_disable_device(dev);
 }
@@ -344,6 +464,11 @@ static int resume_common(struct device *dev, bool hibernated)
        clear_bit(HCD_FLAG_SAW_IRQ, &hcd->flags);
 
        if (hcd->driver->pci_resume) {
+               /* This call should be made only during system resume,
+                * not during runtime resume.
+                */
+               wait_for_companions(pci_dev, hcd);
+
                retval = hcd->driver->pci_resume(hcd, hibernated);
                if (retval) {
                        dev_err(dev, "PCI post-resume error %d!\n", retval);