| /* |
| * 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) |
| */ |
| #include "../services.h" |
| #include "ehci.h" |
| |
| /************************** |
| **** Queue scheduling **** |
| **************************/ |
| /* |
| * TODO: We only support interrupt endpoint at the moment, this function is |
| * subject to change when we add isochronous endpoint support. |
| */ |
| void ehci_add_qhn_periodic(struct ehci_host *edev, struct QHn *qhn) |
| { |
| struct QHn *cur; |
| |
| /* Allocate the frame list */ |
| if (!edev->flist) { |
| /* XXX: The frame list size is default to 1024 */ |
| edev->flist_size = 1024; |
| edev->flist = ps_dma_alloc_pinned(edev->dman, |
| edev->flist_size * sizeof(uint32_t*), 0x1000, 0, |
| PS_MEM_NORMAL, &edev->pflist); |
| if (!edev->flist) { |
| ZF_LOGF("Out of DMA memory\n"); |
| } |
| |
| /* Mark all frames as disabled */ |
| for (int i = 0; i < edev->flist_size; i++) { |
| edev->flist[i] = TDLP_INVALID; |
| } |
| |
| /* |
| * Allocate shadow frame list to keep track of the virtual |
| * address of the queue heads. |
| */ |
| edev->shadow_flist = (struct QHn**)usb_malloc( |
| sizeof(struct QHn*) * edev->flist_size); |
| if (!edev->shadow_flist) { |
| ZF_LOGF("Out of memory\n"); |
| } |
| } |
| |
| /* Check if the queue head has already been scheduled */ |
| cur = edev->intn_list; |
| while (cur) { |
| if (cur == qhn) { |
| return; |
| } |
| cur = cur->next; |
| } |
| |
| /* |
| * Insert the queue head into the list. The queue heads are sorted, from |
| * low rate to high rate. |
| */ |
| cur = edev->intn_list; |
| qhn->qh->qhlptr = QHLP_INVALID; |
| if (!cur) { |
| edev->intn_list = qhn; |
| qhn->next = NULL; |
| } else { |
| while (cur->next && cur->next->rate >= qhn->rate) { |
| cur = cur->next; |
| } |
| qhn->next = cur->next; |
| cur->next = qhn; |
| |
| cur->qh->qhlptr = qhn->pqh | QHLP_TYPE_QH; |
| if (qhn->next) { |
| qhn->qh->qhlptr = qhn->next->pqh | QHLP_TYPE_QH; |
| } |
| } |
| |
| /* Update the frame list */ |
| for (int i = qhn->rate - 1; i < edev->flist_size; i += qhn->rate) { |
| cur = edev->shadow_flist[i]; |
| if (!cur || cur->rate < qhn->rate) { |
| edev->shadow_flist[i] = qhn; |
| edev->flist[i] = qhn->pqh | QHLP_TYPE_QH; |
| } |
| } |
| |
| dsb(); |
| } |
| |
| /* |
| * FIXME: If the queue head is for a full/low speed device. Simply removing it |
| * could cause problems with ongoing split transaction. We cannot wait until the |
| * current TD returns, because the interrupt could never happen, i.e the TD |
| * remains active. The correct way is to use the "Inactive on Next Transaction" |
| * bit in the queue head. Read EHCI spec 4.12.2.5. |
| */ |
| void ehci_del_qhn_periodic(struct ehci_host *edev, struct QHn *qhn) |
| { |
| struct QHn *cur; |
| struct TDn *tdn; |
| |
| /* Remove the active bit from the TD */ |
| tdn = qhn->tdns; |
| tdn->td->token &= ~TDTOK_SACTIVE; |
| |
| /* Remove from the software list */ |
| cur = edev->intn_list; |
| if (cur == qhn) { |
| edev->intn_list = qhn->next; |
| } else { |
| while (cur->next && cur->next != qhn) { |
| cur = cur->next; |
| } |
| cur->next = qhn->next; |
| if (qhn->next) { |
| cur->qh->qhlptr = qhn->next->pqh | QHLP_TYPE_QH; |
| } else { |
| cur->qh->qhlptr = QHLP_INVALID; |
| } |
| } |
| |
| /* Remove from the periodic schedule table */ |
| for (int i = qhn->rate - 1; i < edev->flist_size; i += qhn->rate) { |
| cur = edev->shadow_flist[i]; |
| |
| /* |
| * If we are removing the first element, we need to update the |
| * hardware frame list. There's no need to check if removing |
| * queue head points to another valid queue head, because even |
| * if this queue head points to NULL, its horizontal link pointer |
| * should have already had the terminate bit set to 1. |
| */ |
| if (cur == qhn) { |
| edev->shadow_flist[i] = qhn->next; |
| edev->flist[i] = qhn->qh->qhlptr; |
| } |
| } |
| |
| dsb(); |
| |
| /* Free */ |
| ps_dma_free_pinned(edev->dman, qhn->tdns, sizeof(struct TDn)); |
| usb_free(qhn->tdns); |
| qhn->tdns = NULL; |
| |
| ps_dma_free_pinned(edev->dman, qhn, sizeof(struct QHn)); |
| usb_free(qhn); |
| } |
| |
| int ehci_schedule_periodic_root(struct ehci_host *edev, struct xact *xact, |
| int nxact, usb_cb_t cb, void *t) |
| { |
| int port; |
| |
| if (!xact->vaddr || !cb) { |
| ZF_LOGF("Invalid arguments\n"); |
| } |
| edev->irq_xact = *xact; |
| edev->irq_cb = cb; |
| edev->irq_token = t; |
| /* Enable IRQS */ |
| for (port = 1; port <= EHCI_HCS_N_PORTS(edev->cap_regs->hcsparams); port++) { |
| volatile uint32_t *ps_reg = _get_portsc(edev, port); |
| uint32_t v; |
| v = *ps_reg & ~(EHCI_PORT_CHANGE); |
| v |= (EHCI_PORT_WO_OCURRENT | EHCI_PORT_WO_DCONNECT | |
| EHCI_PORT_WO_CONNECT); |
| *ps_reg = v; |
| } |
| edev->op_regs->usbintr |= EHCIINTR_PORTC_DET; |
| return 0; |
| } |
| |
| int ehci_schedule_periodic(struct ehci_host *edev) |
| { |
| /* Make sure we are safe to write to the register */ |
| while (((edev->op_regs->usbsts & EHCISTS_PERI_EN) >> 14) |
| ^ ((edev->op_regs->usbcmd & EHCICMD_PERI_EN) >> 4)); |
| |
| /* Enable the periodic schedule */ |
| if (!(edev->op_regs->usbsts & EHCISTS_PERI_EN)) { |
| edev->op_regs->periodiclistbase= edev->pflist; |
| |
| /* TODO: Check FRINDEX, FLIST_SIZE, IRQTHRES_MASK */ |
| edev->op_regs->usbcmd |= EHCICMD_PERI_EN; |
| while (edev->op_regs->usbsts & EHCISTS_PERI_EN) break; |
| } |
| |
| return 0; |
| } |
| |
| void ehci_periodic_complete(struct ehci_host *edev) |
| { |
| struct QHn *qhn; |
| struct TDn *tdn; |
| int sum; |
| |
| /* Interrupt endpoints would never queue multiple TDs */ |
| qhn = edev->intn_list; |
| while (qhn) { |
| tdn = qhn->tdns; |
| if (tdn && qtd_get_status(tdn->td) == XACTSTAT_SUCCESS) { |
| /* Restore the queue head to its initial state */ |
| qhn->tdns = NULL; |
| |
| sum = TDTOK_GET_BYTES(tdn->td->token); |
| if (tdn->cb) { |
| tdn->cb(tdn->token, XACTSTAT_SUCCESS, sum); |
| } |
| |
| ps_dma_free_pinned(edev->dman, (void*)tdn->td, |
| sizeof(struct TD)); |
| usb_free(tdn); |
| } |
| qhn = qhn->next; |
| } |
| } |
| |