xhci: process isoc TD properly when there was a transaction error mid TD.
[platform/kernel/linux-starfive.git] / drivers / usb / host / xhci-ring.c
index 3e5dc07..bd7b541 100644 (file)
@@ -2377,6 +2377,9 @@ static int process_isoc_td(struct xhci_hcd *xhci, struct xhci_virt_ep *ep,
        /* handle completion code */
        switch (trb_comp_code) {
        case COMP_SUCCESS:
+               /* Don't overwrite status if TD had an error, see xHCI 4.9.1 */
+               if (td->error_mid_td)
+                       break;
                if (remaining) {
                        frame->status = short_framestatus;
                        if (xhci->quirks & XHCI_TRUST_TX_LENGTH)
@@ -2402,8 +2405,9 @@ static int process_isoc_td(struct xhci_hcd *xhci, struct xhci_virt_ep *ep,
                break;
        case COMP_USB_TRANSACTION_ERROR:
                frame->status = -EPROTO;
+               sum_trbs_for_length = true;
                if (ep_trb != td->last_trb)
-                       return 0;
+                       td->error_mid_td = true;
                break;
        case COMP_STOPPED:
                sum_trbs_for_length = true;
@@ -2423,6 +2427,9 @@ static int process_isoc_td(struct xhci_hcd *xhci, struct xhci_virt_ep *ep,
                break;
        }
 
+       if (td->urb_length_set)
+               goto finish_td;
+
        if (sum_trbs_for_length)
                frame->actual_length = sum_trb_lengths(xhci, ep->ring, ep_trb) +
                        ep_trb_len - remaining;
@@ -2431,6 +2438,14 @@ static int process_isoc_td(struct xhci_hcd *xhci, struct xhci_virt_ep *ep,
 
        td->urb->actual_length += frame->actual_length;
 
+finish_td:
+       /* Don't give back TD yet if we encountered an error mid TD */
+       if (td->error_mid_td && ep_trb != td->last_trb) {
+               xhci_dbg(xhci, "Error mid isoc TD, wait for final completion event\n");
+               td->urb_length_set = true;
+               return 0;
+       }
+
        return finish_td(xhci, ep, ep_ring, td, trb_comp_code);
 }
 
@@ -2809,17 +2824,51 @@ static int handle_tx_event(struct xhci_hcd *xhci,
                }
 
                if (!ep_seg) {
-                       if (!ep->skip ||
-                           !usb_endpoint_xfer_isoc(&td->urb->ep->desc)) {
-                               /* Some host controllers give a spurious
-                                * successful event after a short transfer.
-                                * Ignore it.
-                                */
-                               if ((xhci->quirks & XHCI_SPURIOUS_SUCCESS) &&
-                                               ep_ring->last_td_was_short) {
-                                       ep_ring->last_td_was_short = false;
-                                       goto cleanup;
+
+                       if (ep->skip && usb_endpoint_xfer_isoc(&td->urb->ep->desc)) {
+                               skip_isoc_td(xhci, td, ep, status);
+                               goto cleanup;
+                       }
+
+                       /*
+                        * Some hosts give a spurious success event after a short
+                        * transfer. Ignore it.
+                        */
+                       if ((xhci->quirks & XHCI_SPURIOUS_SUCCESS) &&
+                           ep_ring->last_td_was_short) {
+                               ep_ring->last_td_was_short = false;
+                               goto cleanup;
+                       }
+
+                       /*
+                        * xhci 4.10.2 states isoc endpoints should continue
+                        * processing the next TD if there was an error mid TD.
+                        * So host like NEC don't generate an event for the last
+                        * isoc TRB even if the IOC flag is set.
+                        * xhci 4.9.1 states that if there are errors in mult-TRB
+                        * TDs xHC should generate an error for that TRB, and if xHC
+                        * proceeds to the next TD it should genete an event for
+                        * any TRB with IOC flag on the way. Other host follow this.
+                        * So this event might be for the next TD.
+                        */
+                       if (td->error_mid_td &&
+                           !list_is_last(&td->td_list, &ep_ring->td_list)) {
+                               struct xhci_td *td_next = list_next_entry(td, td_list);
+
+                               ep_seg = trb_in_td(xhci, td_next->start_seg, td_next->first_trb,
+                                                  td_next->last_trb, ep_trb_dma, false);
+                               if (ep_seg) {
+                                       /* give back previous TD, start handling new */
+                                       xhci_dbg(xhci, "Missing TD completion event after mid TD error\n");
+                                       ep_ring->dequeue = td->last_trb;
+                                       ep_ring->deq_seg = td->last_trb_seg;
+                                       inc_deq(xhci, ep_ring);
+                                       xhci_td_cleanup(xhci, td, ep_ring, td->status);
+                                       td = td_next;
                                }
+                       }
+
+                       if (!ep_seg) {
                                /* HC is busted, give up! */
                                xhci_err(xhci,
                                        "ERROR Transfer event TRB DMA ptr not "
@@ -2831,9 +2880,6 @@ static int handle_tx_event(struct xhci_hcd *xhci,
                                          ep_trb_dma, true);
                                return -ESHUTDOWN;
                        }
-
-                       skip_isoc_td(xhci, td, ep, status);
-                       goto cleanup;
                }
                if (trb_comp_code == COMP_SHORT_PACKET)
                        ep_ring->last_td_was_short = true;