From 6d19c009cc780c63de25a046509ebc9473809fd6 Mon Sep 17 00:00:00 2001 From: Alan Stern Date: Fri, 12 Feb 2010 12:21:11 +0100 Subject: [PATCH] USB: implement non-tree resume ordering constraints for PCI host controllers This patch (as1331) adds non-tree ordering constraints needed for proper resume of PCI USB host controllers from hibernation. The main issue is that non-high-speed devices must not be resumed before the high-speed root hub, because it is the ehci_bus_resume() routine which takes care of handing the device connection over to the companion controller. If the device resume is attempted before the handover then the device won't be found and it will be treated as though it had disconnected. The patch adds a new field to the usb_bus structure; for each full/low-speed bus this field will contain a pointer to the companion high-speed bus (if one exists). It is used during normal device resume; if the hs_companion pointer isn't NULL then we wait for the root-hub device on the hs_companion bus. A secondary issue is that an EHCI controlller shouldn't be resumed before any of its companions. On some machines I have observed handovers failing if the companion controller is reinitialized after the handover. Thus, the EHCI resume routine must wait for the companion controllers to be resumed. The patch also fixes a small bug in usb_hcd_pci_probe(); an error path jumps to the wrong label, causing a memory leak. [rjw: Fixed compilation for CONFIG_PM_SLEEP unset.] Signed-off-by: Alan Stern Acked-by: Greg Kroah-Hartman Signed-off-by: Rafael J. Wysocki --- drivers/usb/core/driver.c | 8 +++ drivers/usb/core/hcd-pci.c | 127 ++++++++++++++++++++++++++++++++++++- include/linux/usb.h | 1 + 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c index 60a45f1e3a67..f2f055eb6831 100644 --- a/drivers/usb/core/driver.c +++ b/drivers/usb/core/driver.c @@ -1022,6 +1022,14 @@ static int usb_resume_device(struct usb_device *udev, pm_message_t msg) goto done; } + /* Non-root devices on a full/low-speed bus must wait for their + * companion high-speed root hub, in case a handoff is needed. + */ + if (!(msg.event & PM_EVENT_AUTO) && udev->parent && + udev->bus->hs_companion) + device_pm_wait_for_dev(&udev->dev, + &udev->bus->hs_companion->root_hub->dev); + if (udev->quirks & USB_QUIRK_RESET_RESUME) udev->reset_resume = 1; diff --git a/drivers/usb/core/hcd-pci.c b/drivers/usb/core/hcd-pci.c index 2dcf906df569..15286533c15a 100644 --- a/drivers/usb/core/hcd-pci.c +++ b/drivers/usb/core/hcd-pci.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,122 @@ /* 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); diff --git a/include/linux/usb.h b/include/linux/usb.h index d7ace1b80f09..332eaea61021 100644 --- a/include/linux/usb.h +++ b/include/linux/usb.h @@ -339,6 +339,7 @@ struct usb_bus { struct usb_devmap devmap; /* device address allocation map */ struct usb_device *root_hub; /* Root hub */ + struct usb_bus *hs_companion; /* Companion EHCI bus, if any */ struct list_head bus_list; /* list of busses */ int bandwidth_allocated; /* on this bus: how much of the time -- 2.20.1