]> git.kernelconcepts.de Git - karo-tx-linux.git/commitdiff
USB: OHCI: add I/O watchdog for orphan TDs
authorAlan Stern <stern@rowland.harvard.edu>
Fri, 18 Jul 2014 20:26:12 +0000 (16:26 -0400)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Fri, 18 Jul 2014 23:34:07 +0000 (16:34 -0700)
Some OHCI controllers have a bug: They fail to add completed TDs to
the done queue.  Examining this queue is the only method ohci-hcd has
for telling when a transfer is complete; failure to add a TD can
result in an URB that never completes and cannot be unlinked.

This patch adds a watchdog routine to ohci-hcd.  The routine
periodically scans the active ED and TD lists, looking for TDs which
are finished but not on the done queue.  When one is found, and it is
certain that the controller hardware will never add the TD to the done
queue, the watchdog routine manually puts the TD on the done list so
that it can be handled normally.

The watchdog routine also checks for a condition indicating the
controller has died.  If the done queue is non-empty but the
HccaDoneHead pointer hasn't been updated for a few hundred
milliseconds, we assume the controller will never update it and
therefore is dead.

Signed-off-by: Alan Stern <stern@rowland.harvard.edu>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/usb/host/ohci-hcd.c
drivers/usb/host/ohci-hub.c
drivers/usb/host/ohci-mem.c
drivers/usb/host/ohci-q.c
drivers/usb/host/ohci.h

index ad588538e2e794c32c5329cecd2206c4a27c530f..aba8f19eae4d4e0fc2394c6aca357f954494421e 100644 (file)
 static const char      hcd_name [] = "ohci_hcd";
 
 #define        STATECHANGE_DELAY       msecs_to_jiffies(300)
+#define IO_WATCHDOG_DELAY      msecs_to_jiffies(250)
 
 #include "ohci.h"
 #include "pci-quirks.h"
 
 static void ohci_dump(struct ohci_hcd *ohci);
 static void ohci_stop(struct usb_hcd *hcd);
+static void io_watchdog_func(unsigned long _ohci);
 
 #include "ohci-hub.c"
 #include "ohci-dbg.c"
@@ -225,6 +227,14 @@ static int ohci_urb_enqueue (
                        usb_hcd_unlink_urb_from_ep(hcd, urb);
                        goto fail;
                }
+
+               /* Start up the I/O watchdog timer, if it's not running */
+               if (!timer_pending(&ohci->io_watchdog) &&
+                               list_empty(&ohci->eds_in_use))
+                       mod_timer(&ohci->io_watchdog,
+                                       jiffies + IO_WATCHDOG_DELAY);
+               list_add(&ed->in_use_list, &ohci->eds_in_use);
+
                if (ed->type == PIPE_ISOCHRONOUS) {
                        u16     frame = ohci_frame_no(ohci);
 
@@ -416,6 +426,7 @@ ohci_shutdown (struct usb_hcd *hcd)
        udelay(10);
 
        ohci_writel(ohci, ohci->fminterval, &ohci->regs->fminterval);
+       ohci->rh_state = OHCI_RH_HALTED;
 }
 
 /*-------------------------------------------------------------------------*
@@ -484,6 +495,10 @@ static int ohci_init (struct ohci_hcd *ohci)
        if (ohci->hcca)
                return 0;
 
+       setup_timer(&ohci->io_watchdog, io_watchdog_func,
+                       (unsigned long) ohci);
+       set_timer_slack(&ohci->io_watchdog, msecs_to_jiffies(20));
+
        ohci->hcca = dma_alloc_coherent (hcd->self.controller,
                        sizeof(*ohci->hcca), &ohci->hcca_dma, GFP_KERNEL);
        if (!ohci->hcca)
@@ -694,6 +709,112 @@ static int ohci_start(struct usb_hcd *hcd)
 
 /*-------------------------------------------------------------------------*/
 
+/*
+ * Some OHCI controllers are known to lose track of completed TDs.  They
+ * don't add the TDs to the hardware done queue, which means we never see
+ * them as being completed.
+ *
+ * This watchdog routine checks for such problems.  Without some way to
+ * tell when those TDs have completed, we would never take their EDs off
+ * the unlink list.  As a result, URBs could never be dequeued and
+ * endpoints could never be released.
+ */
+static void io_watchdog_func(unsigned long _ohci)
+{
+       struct ohci_hcd *ohci = (struct ohci_hcd *) _ohci;
+       bool            takeback_all_pending = false;
+       u32             status;
+       u32             head;
+       struct ed       *ed;
+       struct td       *td, *td_start, *td_next;
+       unsigned long   flags;
+
+       spin_lock_irqsave(&ohci->lock, flags);
+
+       /*
+        * One way to lose track of completed TDs is if the controller
+        * never writes back the done queue head.  If it hasn't been
+        * written back since the last time this function ran and if it
+        * was non-empty at that time, something is badly wrong with the
+        * hardware.
+        */
+       status = ohci_readl(ohci, &ohci->regs->intrstatus);
+       if (!(status & OHCI_INTR_WDH) && ohci->wdh_cnt == ohci->prev_wdh_cnt) {
+               if (ohci->prev_donehead) {
+                       ohci_err(ohci, "HcDoneHead not written back; disabled\n");
+                       usb_hc_died(ohci_to_hcd(ohci));
+                       ohci_dump(ohci);
+                       ohci_shutdown(ohci_to_hcd(ohci));
+                       goto done;
+               } else {
+                       /* No write back because the done queue was empty */
+                       takeback_all_pending = true;
+               }
+       }
+
+       /* Check every ED which might have pending TDs */
+       list_for_each_entry(ed, &ohci->eds_in_use, in_use_list) {
+               if (ed->pending_td) {
+                       if (takeback_all_pending ||
+                                       OKAY_TO_TAKEBACK(ohci, ed)) {
+                               unsigned tmp = hc32_to_cpu(ohci, ed->hwINFO);
+
+                               ohci_dbg(ohci, "takeback pending TD for dev %d ep 0x%x\n",
+                                               0x007f & tmp,
+                                               (0x000f & (tmp >> 7)) +
+                                                       ((tmp & ED_IN) >> 5));
+                               add_to_done_list(ohci, ed->pending_td);
+                       }
+               }
+
+               /* Starting from the latest pending TD, */
+               td = ed->pending_td;
+
+               /* or the last TD on the done list, */
+               if (!td) {
+                       list_for_each_entry(td_next, &ed->td_list, td_list) {
+                               if (!td_next->next_dl_td)
+                                       break;
+                               td = td_next;
+                       }
+               }
+
+               /* find the last TD processed by the controller. */
+               head = hc32_to_cpu(ohci, ACCESS_ONCE(ed->hwHeadP)) & TD_MASK;
+               td_start = td;
+               td_next = list_prepare_entry(td, &ed->td_list, td_list);
+               list_for_each_entry_continue(td_next, &ed->td_list, td_list) {
+                       if (head == (u32) td_next->td_dma)
+                               break;
+                       td = td_next;   /* head pointer has passed this TD */
+               }
+               if (td != td_start) {
+                       /*
+                        * In case a WDH cycle is in progress, we will wait
+                        * for the next two cycles to complete before assuming
+                        * this TD will never get on the done queue.
+                        */
+                       ed->takeback_wdh_cnt = ohci->wdh_cnt + 2;
+                       ed->pending_td = td;
+               }
+       }
+
+       ohci_work(ohci);
+
+       if (ohci->rh_state == OHCI_RH_RUNNING) {
+               if (!list_empty(&ohci->eds_in_use)) {
+                       ohci->prev_wdh_cnt = ohci->wdh_cnt;
+                       ohci->prev_donehead = ohci_readl(ohci,
+                                       &ohci->regs->donehead);
+                       mod_timer(&ohci->io_watchdog,
+                                       jiffies + IO_WATCHDOG_DELAY);
+               }
+       }
+
+ done:
+       spin_unlock_irqrestore(&ohci->lock, flags);
+}
+
 /* an interrupt happens */
 
 static irqreturn_t ohci_irq (struct usb_hcd *hcd)
@@ -796,6 +917,9 @@ static irqreturn_t ohci_irq (struct usb_hcd *hcd)
 
        if (ohci->rh_state == OHCI_RH_RUNNING) {
                ohci_writel (ohci, ints, &regs->intrstatus);
+               if (ints & OHCI_INTR_WDH)
+                       ++ohci->wdh_cnt;
+
                ohci_writel (ohci, OHCI_INTR_MIE, &regs->intrenable);
                // flush those writes
                (void) ohci_readl (ohci, &ohci->regs->control);
@@ -815,6 +939,7 @@ static void ohci_stop (struct usb_hcd *hcd)
 
        if (quirk_nec(ohci))
                flush_work(&ohci->nec_work);
+       del_timer_sync(&ohci->io_watchdog);
 
        ohci_writel (ohci, OHCI_INTR_MIE, &ohci->regs->intrdisable);
        ohci_usb_reset(ohci);
index 8991692bcfb8bef6c311d366b38d144bf4298a0c..17d32b0ea56583aae9d7a77a87ed0d54f82d932d 100644 (file)
@@ -309,6 +309,9 @@ static int ohci_bus_suspend (struct usb_hcd *hcd)
        else
                rc = ohci_rh_suspend (ohci, 0);
        spin_unlock_irq (&ohci->lock);
+
+       if (rc == 0)
+               del_timer_sync(&ohci->io_watchdog);
        return rc;
 }
 
index 2f20d3dc895b8b4a959459573f4089e2014f27f8..c9e315c6808aa77d1e491507d58e7612e14465cc 100644 (file)
@@ -28,6 +28,7 @@ static void ohci_hcd_init (struct ohci_hcd *ohci)
        ohci->next_statechange = jiffies;
        spin_lock_init (&ohci->lock);
        INIT_LIST_HEAD (&ohci->pending);
+       INIT_LIST_HEAD(&ohci->eds_in_use);
 }
 
 /*-------------------------------------------------------------------------*/
index 1974ddc68e455e71f8285d5643e71dfc303a17db..1463c398d32204cb063e1b8c72a4a57246b30b48 100644 (file)
@@ -921,6 +921,11 @@ static void add_to_done_list(struct ohci_hcd *ohci, struct td *td)
         * that td is on the done list.
         */
        ohci->dl_end = td->next_dl_td = td;
+
+       /* Did we just add the latest pending TD? */
+       td2 = ed->pending_td;
+       if (td2 && td2->next_dl_td)
+               ed->pending_td = NULL;
 }
 
 /* Get the entries on the hardware done queue and put them on our list */
@@ -1082,6 +1087,7 @@ rescan_this:
                if (list_empty(&ed->td_list)) {
                        *last = ed->ed_next;
                        ed->ed_next = NULL;
+                       list_del(&ed->in_use_list);
                } else if (ohci->rh_state == OHCI_RH_RUNNING) {
                        *last = ed->ed_next;
                        ed->ed_next = NULL;
index ef348c2e1e4b0f5a38f48d1cc0edc579d1060e7c..0548f5ca18e28027cbbd22381aec5dfaa9b72489 100644 (file)
@@ -47,6 +47,7 @@ struct ed {
        struct ed               *ed_next;       /* on schedule or rm_list */
        struct ed               *ed_prev;       /* for non-interrupt EDs */
        struct list_head        td_list;        /* "shadow list" of our TDs */
+       struct list_head        in_use_list;
 
        /* create --> IDLE --> OPER --> ... --> IDLE --> destroy
         * usually:  OPER --> UNLINK --> (IDLE | OPER) --> ...
@@ -66,6 +67,13 @@ struct ed {
 
        /* HC may see EDs on rm_list until next frame (frame_no == tick) */
        u16                     tick;
+
+       /* Detect TDs not added to the done queue */
+       unsigned                takeback_wdh_cnt;
+       struct td               *pending_td;
+#define        OKAY_TO_TAKEBACK(ohci, ed)                      \
+               ((int) (ohci->wdh_cnt - ed->takeback_wdh_cnt) >= 0)
+
 } __attribute__ ((aligned(16)));
 
 #define ED_MASK        ((u32)~0x0f)            /* strip hw status in low addr bits */
@@ -382,6 +390,7 @@ struct ohci_hcd {
        struct td               *td_hash [TD_HASH_SIZE];
        struct td               *dl_start, *dl_end;     /* the done list */
        struct list_head        pending;
+       struct list_head        eds_in_use;     /* all EDs with at least 1 TD */
 
        /*
         * driver state
@@ -412,6 +421,10 @@ struct ohci_hcd {
 
        // there are also chip quirks/bugs in init logic
 
+       unsigned                wdh_cnt, prev_wdh_cnt;
+       u32                     prev_donehead;
+       struct timer_list       io_watchdog;
+
        struct work_struct      nec_work;       /* Worker for NEC quirk */
 
        struct dentry           *debug_dir;