| /* |
| * Copyright 2017, Data61 |
| * Commonwealth Scientific and Industrial Research Organisation (CSIRO) |
| * ABN 41 687 119 230. |
| * |
| * This software may be distributed and modified according to the terms of |
| * the BSD 2-Clause license. Note that NO WARRANTY is provided. |
| * See "LICENSE_BSD2.txt" for details. |
| * |
| * @TAG(DATA61_BSD) |
| */ |
| |
| /** |
| * @brief EHCI asynchronous schedule implementation. |
| */ |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include <usb/drivers/usbhub.h> |
| #include "ehci.h" |
| #include "../services.h" |
| |
| enum usb_xact_status qtd_get_status(volatile struct TD* qtd) |
| { |
| uint32_t t = qtd->token; |
| if (t & TDTOK_SACTIVE) { |
| /* Note that we have already returned an error code |
| * if this TD is still pending due to an error in |
| * a previous TD */ |
| return XACTSTAT_PENDING; |
| } else if (t & TDTOK_SHALTED) { |
| if (t & TDTOK_SXACTERR) { |
| return XACTSTAT_ERROR; |
| } else if (t & TDTOK_ERROR) { |
| return XACTSTAT_HOSTERROR; |
| } |
| ZF_LOGF("EHCI: Unknown QTD error code 0x%x\n", t); |
| return XACTSTAT_HOSTERROR; |
| } else { |
| return XACTSTAT_SUCCESS; |
| } |
| } |
| |
| enum usb_xact_status qhn_get_status(struct QHn * qhn) |
| { |
| struct TDn *tdn; |
| |
| tdn = qhn->tdns; |
| while (tdn) { |
| enum usb_xact_status stat; |
| stat = qtd_get_status(tdn->td); |
| if (stat != XACTSTAT_SUCCESS) { |
| return stat; |
| } |
| tdn = tdn->next; |
| } |
| /* All TDs complete, return the status of the QH */ |
| return qtd_get_status(&qhn->qh->td_overlay); |
| } |
| |
| /**************************** |
| **** Queue manipulation **** |
| ****************************/ |
| /* |
| * TODO: The current data structure assumes one xact per TD, which means the |
| * length of an xact can not exceed 20KB. |
| */ |
| struct TDn* |
| qtd_alloc(struct ehci_host *edev, enum usb_speed speed, struct endpoint *ep, |
| struct xact *xact, int nxact, usb_cb_t cb, void *token) |
| { |
| struct TDn *head_tdn = NULL, *prev_tdn, *tdn = NULL; |
| int buf_filled, cnt, total_bytes = 0; |
| int xact_stage = 0; |
| |
| if (!xact || nxact <=0) { |
| ZF_LOGF("Invalid arguments\n"); |
| } |
| |
| prev_tdn = NULL; |
| for (int i = 0; i < nxact; i++) { |
| tdn = calloc(1, sizeof(struct TDn)); |
| if (!tdn) { |
| ZF_LOGE("Out of memory"); |
| return NULL; |
| } |
| |
| /* Allocate TD overlay */ |
| tdn->td = ps_dma_alloc_pinned(edev->dman, sizeof(*tdn->td), 32, 0, |
| PS_MEM_NORMAL, &tdn->ptd); |
| if (!tdn->td) { |
| ZF_LOGE("Out of DMA memory"); |
| return NULL; |
| } |
| |
| memset((void*)tdn->td, 0, sizeof(*tdn->td)); |
| |
| /* Fill in the TD */ |
| if (prev_tdn) { |
| prev_tdn->td->next = tdn->ptd; |
| } else { |
| head_tdn = tdn; |
| } |
| tdn->td->alt = TDLP_INVALID; |
| |
| /* The Control endpoint manages its own data toggle */ |
| if (ep->type == EP_CONTROL) { |
| if (ep->max_pkt & (ep->max_pkt + total_bytes - 1)) { |
| tdn->td->token = TDTOK_DT; |
| } |
| } |
| tdn->td->token |= TDTOK_BYTES(xact[i].len); |
| tdn->td->token |= TDTOK_C_ERR(0x3); //Maximize retries |
| |
| switch (xact[i].type) { |
| case PID_SETUP: |
| tdn->td->token |= TDTOK_PID_SETUP; |
| xact_stage |= TDTOK_PID_SETUP; |
| break; |
| case PID_IN: |
| tdn->td->token |= TDTOK_PID_IN; |
| xact_stage |= TDTOK_PID_IN; |
| break; |
| case PID_OUT: |
| tdn->td->token |= TDTOK_PID_OUT; |
| xact_stage |= TDTOK_PID_OUT; |
| break; |
| default: |
| ZF_LOGF("Invalid PID!\n"); |
| break; |
| } |
| |
| tdn->td->token |= TDTOK_SHALTED; |
| |
| /* Ping control */ |
| if (speed == USBSPEED_HIGH && xact[i].type == PID_OUT) { |
| tdn->td->token |= TDTOK_PINGSTATE; |
| } |
| |
| /* Fill in the buffer */ |
| cnt = 0; |
| tdn->td->buf[cnt] = xact[i].paddr; //First buffer has offset |
| buf_filled = 0x1000 - (xact[i].paddr & 0xFFF); |
| /* All following buffers are page aligned */ |
| while (buf_filled < xact[i].len) { |
| cnt++; |
| tdn->td->buf[cnt] = (xact[i].paddr + 0x1000 * cnt) & ~0xFFF; |
| buf_filled += 0x1000; |
| } |
| |
| /* We only have 5 page-sized buffers */ |
| if (cnt > 4) { |
| ZF_LOGF("Too many buffers\n"); |
| } |
| |
| /* Total data transferred */ |
| total_bytes += xact[i].len; |
| |
| if (prev_tdn) { |
| prev_tdn->next = tdn; |
| } |
| prev_tdn = tdn; |
| } |
| |
| /* |
| * Zero length packet |
| * XXX: It is unclear that the exact condition of when the zero length |
| * packet is required. The following implementation is partially based |
| * on observation. According to USB 2.0 spec(5.5.3), a zero length |
| * packet shouldn't be needed under some of the situations below, but |
| * apparently, some devices don't always follow the spec. |
| */ |
| if (((xact_stage & TDTOK_PID_OUT) && !(total_bytes % ep->max_pkt)) || |
| ep->type == EP_CONTROL) { |
| /* Allocate TD for the zero length packet */ |
| tdn = calloc(1, sizeof(struct TDn)); |
| |
| /* Allocate TD overlay */ |
| tdn->td = ps_dma_alloc_pinned(edev->dman, sizeof(*tdn->td), |
| 32, 0, PS_MEM_NORMAL, &tdn->ptd); |
| if (!tdn->td) { |
| ZF_LOGF("Out of DMA memory\n"); |
| } |
| memset((void*)tdn->td, 0, sizeof(*tdn->td)); |
| |
| /* Fill in the TD */ |
| tdn->td->alt = TDLP_INVALID; |
| tdn->td->token = TDTOK_C_ERR(0x3) | TDTOK_SHALTED; |
| |
| if (xact_stage & TDTOK_PID_SETUP) { |
| /* Flip the PID, if there is no data stage, then IN */ |
| if (xact_stage & TDTOK_PID_IN) { |
| tdn->td->token |= TDTOK_PID_OUT; |
| } else { |
| tdn->td->token |= TDTOK_PID_IN; |
| } |
| |
| /* Force DATA1 */ |
| tdn->td->token |= TDTOK_DT; |
| } else { |
| /* Bulk out transfer */ |
| tdn->td->token |= TDTOK_PID_OUT; |
| |
| /* XXX: Flip the data toggle? */ |
| if (!(prev_tdn->td->token & TDTOK_DT)) { |
| tdn->td->token |= TDTOK_DT; |
| } |
| } |
| |
| /* Add to the list */ |
| prev_tdn->td->next = tdn->ptd; |
| prev_tdn->next = tdn; |
| } |
| |
| /* Send IRQ when finished processing the last TD */ |
| tdn->td->token |= TDTOK_IOC; //TODO: Maybe disable IRQ when cb == NULL |
| tdn->cb = cb; |
| tdn->token = token; |
| |
| /* Mark the last TD as terminate TD */ |
| tdn->td->next |= TDLP_INVALID; |
| |
| return head_tdn; |
| } |
| |
| /* |
| * Allocate generic queue head for both periodic and asynchronous schedule |
| * Note that the link pointer and reclamation flag bit are set when inserting |
| * the queue head to asynchronous schedule. |
| * |
| * XXX: For some unknown reason, we cannot initialize the link pointer and head |
| * type here, it seems that the register can be only written once. So we'll fill |
| * the register when adding the queue head to the schedule. |
| */ |
| struct QHn* |
| qhn_alloc(struct ehci_host *edev, uint8_t address, uint8_t hub_addr, |
| uint8_t hub_port, enum usb_speed speed, struct endpoint *ep) |
| { |
| struct QHn *qhn; |
| volatile struct QH *qh; |
| |
| qhn = calloc(1, sizeof(struct QHn)); |
| if (!qhn) { |
| ZF_LOGF("Out of memory\n"); |
| } |
| |
| /* Allocate queue head */ |
| qhn->qh = ps_dma_alloc_pinned(edev->dman, sizeof(*qh), 32, 0, |
| PS_MEM_NORMAL, &qhn->pqh); |
| if (!qhn->qh) { |
| ZF_LOGF("Out of DMA memory\n"); |
| } |
| memset((void*)qhn->qh, 0, sizeof(*qh)); |
| |
| /* Fill in the queue head */ |
| qh = qhn->qh; |
| |
| qh->qhlptr = QHLP_INVALID; |
| /* epc0 */ |
| switch (speed) { |
| case USBSPEED_HIGH: |
| qh->epc[0] = QHEPC0_HSPEED; |
| break; |
| case USBSPEED_FULL: |
| qh->epc[0] = QHEPC0_FSPEED; |
| break; |
| case USBSPEED_LOW : |
| qh->epc[0] = QHEPC0_LSPEED; |
| break; |
| default: |
| ZF_LOGF("Invalid speed\n"); |
| } |
| |
| qh->epc[0] |= QHEPC0_MAXPKTLEN(ep->max_pkt) | QHEPC0_ADDR(address) | |
| QHEPC0_EP(ep->num); |
| |
| /* |
| * Nak counter must NOT be used for interrupt endpoint |
| * EHCI spec chapter 4.9(Nak "Not Used" mode) |
| */ |
| if (ep->type == EP_INTERRUPT) { |
| qh->epc[0] |= QHEPC0_NAKCNT_RL(0); |
| } else { |
| qh->epc[0] |= QHEPC0_NAKCNT_RL(0x8); |
| } |
| |
| /* Control endpoint manages its own data toggle */ |
| if (ep->type == EP_CONTROL) { |
| qh->epc[0] |= QHEPC0_DTC; |
| |
| /* For full/low speed control endpoint */ |
| if (speed != USBSPEED_HIGH) { |
| qh->epc[0] |= QHEPC0_C; |
| } |
| } |
| |
| /* epc1 */ |
| qh->epc[1] = QHEPC1_MULT(1); |
| if (speed != USBSPEED_HIGH) { |
| qh->epc[1] |= QHEPC1_HUB_ADDR(hub_addr) | QHEPC1_PORT(hub_port); |
| } |
| |
| /* TODO: Check CMASK and SMASK */ |
| if ((ep->type == EP_INTERRUPT || ep->type == EP_ISOCHRONOUS) && |
| speed != USBSPEED_HIGH) { |
| qh->epc[1] |= QHEPC1_UFRAME_CMASK(0x1C); |
| } |
| |
| if (ep->type == EP_INTERRUPT) { |
| qh->epc[1] |= QHEPC1_UFRAME_SMASK(1); |
| } |
| |
| qh->td_overlay.next = TDLP_INVALID; |
| qh->td_overlay.alt = TDLP_INVALID; |
| |
| return qhn; |
| } |
| |
| void |
| qhn_update(struct QHn *qhn, uint8_t address, struct endpoint *ep) |
| { |
| uint32_t epc0; |
| |
| if (!qhn || !ep) { |
| ZF_LOGF("Invalid arguments\n"); |
| } |
| |
| /* |
| * We only care about the control endpoint, because all other |
| * endpoints' info is extracted from the endpoint descriptor, and by the |
| * time the core driver reads the descriptors, the device's address |
| * should have settled already. |
| */ |
| if (ep->type != EP_CONTROL) { |
| return; |
| } |
| |
| /* Update maximum packet size */ |
| epc0 = qhn->qh->epc[0]; |
| if (unlikely(QHEPC0_GET_MAXPKT(epc0) != ep->max_pkt)) { |
| epc0 &= ~QHEPC0_MAXPKT_MASK; |
| epc0 |= QHEPC0_MAXPKTLEN(ep->max_pkt); |
| } |
| |
| /* Update device address */ |
| if (unlikely(QHEPC0_GET_ADDR(epc0) != address)) { |
| epc0 &= ~QHEPC0_ADDR_MASK; |
| epc0 |= QHEPC0_ADDR(address); |
| } |
| |
| qhn->qh->epc[0] = epc0; |
| } |
| |
| void |
| qtd_enqueue(struct ehci_host *edev, struct QHn *qhn, struct TDn *tdn) |
| { |
| struct TDn *last_tdn; |
| |
| if (!qhn || !tdn) { |
| ZF_LOGF("Invalid arguments\n"); |
| } |
| |
| /* If the queue is empty, point the TD overlay to the first TD */ |
| if (!qhn->tdns) { |
| qhn->qh->td_overlay.next = tdn->ptd; |
| qhn->tdns = tdn; |
| } else { |
| ps_mutex_lock(edev->sync, qhn->lock); |
| |
| /* Find the last TD */ |
| last_tdn = qhn->tdns; |
| while (last_tdn->next) { |
| last_tdn = last_tdn->next; |
| } |
| |
| if (qhn->qh->td_cur == last_tdn->ptd && |
| qhn->qh->td_overlay.next == TDLP_INVALID) { |
| qhn->qh->td_overlay.next = tdn->ptd; |
| } |
| |
| /* Add new TD to the queue and update the termination bit */ |
| last_tdn->next = tdn; |
| last_tdn->td->next = tdn->ptd & ~TDLP_INVALID; |
| |
| ps_mutex_unlock(edev->sync, qhn->lock); |
| } |
| |
| /* Enable all TDs */ |
| while (tdn) { |
| tdn->td->token &= ~TDTOK_SHALTED; |
| tdn->td->token |= TDTOK_SACTIVE; |
| tdn = tdn->next; |
| } |
| |
| /* Make sure the controller sees all the active TDs */ |
| dsb(); |
| } |
| |
| void qhn_destroy(ps_dma_man_t* dman, struct QHn* qhn) |
| { |
| struct TDn *tdn, *tmp; |
| |
| tdn = qhn->tdns; |
| while (tdn) { |
| tmp = tdn; |
| tdn = tdn->next; |
| if (tmp->cb) { |
| tmp->cb(tmp->token, XACTSTAT_CANCELLED, 0); |
| } |
| ps_dma_free_pinned(dman, (void*)tmp->td, sizeof(struct TD)); |
| usb_free(tmp); |
| } |
| |
| ps_dma_free_pinned(dman, (void*)qhn->qh, sizeof(struct QH)); |
| usb_free(qhn); |
| } |
| |
| void ehci_async_complete(struct ehci_host *edev) |
| { |
| struct QHn *qhn; |
| struct TDn *tdn, *head, *tmp; |
| int sum; |
| |
| qhn = edev->alist_tail; |
| |
| /* Nothing to do if the queue is empty */ |
| if (!qhn) { |
| return; |
| } |
| |
| do { |
| ps_mutex_lock(edev->sync, qhn->lock); |
| |
| tdn = qhn->tdns; |
| sum = 0; |
| head = tdn; |
| while (tdn != NULL && |
| qtd_get_status(tdn->td) == XACTSTAT_SUCCESS) { |
| sum += TDTOK_GET_BYTES(tdn->td->token); |
| if (tdn->td->token & TDTOK_IOC) { |
| if (tdn->cb) { |
| tdn->cb(tdn->token, XACTSTAT_SUCCESS, sum); |
| sum = 0; |
| } |
| |
| qhn->tdns = tdn->next; |
| |
| /* |
| * Update the QH if we are about to dequeue the |
| * "previous" last TD in the queue. This happens |
| * when the last TD gets partially processed |
| * while we enqueue new TDs. |
| */ |
| if (qhn->tdns && |
| qhn->qh->td_cur == tdn->ptd && |
| qhn->qh->td_overlay.next == TDLP_INVALID) { |
| qhn->qh->td_overlay.next = tdn->next->ptd; |
| dsb(); |
| } |
| |
| /* Free */ |
| while (head != tdn->next) { |
| tmp = head; |
| head = head->next; |
| ps_dma_free_pinned(edev->dman, |
| (void*)tmp->td, |
| sizeof(struct TD)); |
| usb_free(tmp); |
| } |
| } |
| tdn = tdn->next; |
| } |
| |
| ps_mutex_unlock(edev->sync, qhn->lock); |
| qhn = qhn->next; |
| } while (qhn != edev->alist_tail); |
| } |
| |
| void ehci_add_qhn_async(struct ehci_host *edev, struct QHn *qhn) |
| { |
| /* Add new queue head to async queue */ |
| if (edev->alist_tail) { |
| /* Update the hardware queue */ |
| qhn->qh->qhlptr = edev->alist_tail->qh->qhlptr; |
| edev->alist_tail->qh->qhlptr = qhn->pqh | QHLP_TYPE_QH; |
| |
| /* Update the Software queue */ |
| qhn->next = edev->alist_tail->next; |
| edev->alist_tail->next = qhn; |
| edev->alist_tail = qhn; |
| } else { |
| edev->alist_tail = qhn; |
| edev->alist_tail->next = qhn; |
| |
| qhn->qh->qhlptr = qhn->pqh | QHLP_TYPE_QH; |
| } |
| |
| dsb(); |
| } |
| |
| void ehci_del_qhn_async(struct ehci_host *edev, struct QHn *qhn) |
| { |
| struct TDn *tdn; |
| struct QHn *prev; |
| |
| /* |
| * The EHCI spec(section 4.8.2) gives the instructions of removing a QH |
| * from the active async queue. However, the instructions have been |
| * proved racy and impractical. See the following link for more |
| * information, |
| * |
| * https://marc.info/?l=linux-usb&m=144554811526940&w=2 |
| * |
| * This implementation combines part of the spec instructions and |
| * Linux's double IAA cycle approach. Hope it would minimize the chances |
| * of trigging serious problems. Good Luck! |
| */ |
| |
| /* |
| * Deactivate all active TDs |
| * |
| * This is the racy part of the EHCI spec. The host may have already in |
| * the middle of processing the TD overlay when we change the token. |
| * But we've got no choice otherwise the host would follow the TD list |
| * and would cause further errors. |
| */ |
| tdn = qhn->tdns; |
| while (tdn) { |
| if (tdn->td->token & TDTOK_SACTIVE) { |
| tdn->td->token &= ~TDTOK_SACTIVE; |
| tdn->td->token |= TDTOK_SHALTED; |
| } |
| tdn = tdn->next; |
| } |
| |
| dsb(); |
| |
| /* Select another queue head to set its H-bit */ |
| if ((qhn->qh->epc[0] & QHEPC0_H) && qhn->next) { |
| qhn->next->qh->epc[0] |= QHEPC0_H; |
| } |
| |
| /* Remove the queue head from async list */ |
| prev = edev->alist_tail; |
| while (prev->next != qhn) { |
| prev = prev->next; |
| } |
| |
| prev->qh->qhlptr = qhn->qh->qhlptr; |
| prev->next = qhn->next; |
| |
| if (edev->alist_tail == qhn) { |
| edev->alist_tail = qhn->next; |
| } |
| |
| /* Put the queue head to the recycle queue */ |
| if (edev->db_pending) { |
| prev = edev->db_pending; |
| while (prev->next != NULL) { |
| prev = prev->next; |
| } |
| prev->next = qhn; |
| } else { |
| edev->db_pending = qhn; |
| } |
| qhn->next = NULL; |
| qhn->was_cancelled = 1; |
| |
| /* Ring the doorbell */ |
| edev->op_regs->usbcmd |= EHCICMD_ASYNC_DB; |
| } |
| |
| int ehci_wait_for_completion(struct TDn *tdn) |
| { |
| int status; |
| int cnt, sum = 0; |
| |
| while (tdn) { |
| cnt = 3000; |
| do { |
| status = tdn->td->token & 0xFF; |
| if (status == 0 || status == 1) { |
| sum += TDTOK_GET_BYTES(tdn->td->token); |
| break; |
| } |
| if (cnt <= 0) { |
| ZF_LOGF("Timeout(%p, %p)\n", tdn->td, |
| (void*)tdn->ptd); |
| return -1; |
| } |
| ps_mdelay(1); |
| cnt--; |
| } while (status); |
| tdn = tdn->next; |
| } |
| |
| return sum; |
| } |
| |
| /* TODO: Is it okay to use alist_tail and remove qhn */ |
| void ehci_schedule_async(struct ehci_host* edev, struct QHn* qhn) |
| { |
| /* Make sure we are safe to write to the register */ |
| while (((edev->op_regs->usbsts & EHCISTS_ASYNC_EN) >> 15) |
| ^ ((edev->op_regs->usbcmd & EHCICMD_ASYNC_EN) >> 5)); |
| |
| /* Enable the async scheduling */ |
| if (!(edev->op_regs->usbsts & EHCISTS_ASYNC_EN)) { |
| qhn->qh->epc[0] |= QHEPC0_H; |
| edev->op_regs->asynclistaddr = qhn->pqh; |
| edev->op_regs->usbcmd |= EHCICMD_ASYNC_EN; |
| while (edev->op_regs->usbsts & EHCISTS_ASYNC_EN) break; |
| } |
| } |
| |
| void check_doorbell(struct ehci_host* edev) |
| { |
| int again = 0; |
| struct QHn *qhn, *tmp, *tail = NULL; |
| |
| qhn = edev->db_pending; |
| edev->db_pending = NULL; |
| while (qhn) { |
| tmp = qhn; |
| qhn = qhn->next; |
| |
| /* Two IAA cycles have passed, safe to remove */ |
| if (tmp->was_cancelled > 1) { |
| qhn_destroy(edev->dman, tmp); |
| } else { |
| tmp->was_cancelled++; |
| if (!edev->db_pending) { |
| edev->db_pending = tmp; |
| } else { |
| tail->next = tmp; |
| } |
| tail = tmp; |
| |
| again = 1; |
| } |
| } |
| |
| if (again) { |
| edev->op_regs->usbcmd |= EHCICMD_ASYNC_DB; |
| } |
| } |
| |