blob: 2dcb90c40c266756264bc943903b5dfc789c6f36 [file] [log] [blame]
/*
* 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;
}
}