* QEMU USB EHCI Emulation
*
* Copyright(c) 2008 Emutex Ltd. (address@hidden)
+ * Copyright(c) 2011-2012 Red Hat, Inc.
+ *
+ * Red Hat Authors:
*
* EHCI project was started by Mark Burkley, with contributions by
* Niels de Vos. David S. Ahern continued working on it. Kevin Wolf,
uint32_t seen;
uint64_t ts;
int async;
- int revalidate;
/* cached data from guest - needs to be flushed
* when guest removes an entry (doorbell, handshake sequence)
[CONFIGFLAG] = "CONFIGFLAG",
};
+static int ehci_state_executing(EHCIQueue *q);
+static int ehci_state_writeback(EHCIQueue *q);
+
static const char *nr2str(const char **n, size_t len, uint32_t nr)
{
if (nr < len && n[nr] != NULL) {
(bool)(sitd->results & SITD_RESULTS_ACTIVE));
}
+static void ehci_trace_guest_bug(EHCIState *s, const char *message)
+{
+ trace_usb_ehci_guest_bug(message);
+ fprintf(stderr, "ehci warning: %s\n", message);
+}
+
static inline bool ehci_enabled(EHCIState *s)
{
return s->usbcmd & USBCMD_RUNSTOP;
static void ehci_free_packet(EHCIPacket *p)
{
+ if (p->async == EHCI_ASYNC_FINISHED) {
+ int state = ehci_get_state(p->queue->ehci, p->queue->async);
+ /* This is a normal, but rare condition (cancel racing completion) */
+ fprintf(stderr, "EHCI: Warning packet completed but not processed\n");
+ ehci_state_executing(p->queue);
+ ehci_state_writeback(p->queue);
+ ehci_set_state(p->queue->ehci, p->queue->async, state);
+ /* state_writeback recurses into us with async == EHCI_ASYNC_NONE!! */
+ return;
+ }
trace_usb_ehci_packet_action(p->queue, p, "free");
if (p->async == EHCI_ASYNC_INFLIGHT) {
usb_cancel_packet(&p->packet);
+ usb_packet_unmap(&p->packet, &p->sgl);
+ qemu_sglist_destroy(&p->sgl);
}
QTAILQ_REMOVE(&p->queue->packets, p, next);
usb_packet_cleanup(&p->packet);
return q;
}
-static void ehci_free_queue(EHCIQueue *q)
+static int ehci_cancel_queue(EHCIQueue *q)
{
- EHCIQueueHead *head = q->async ? &q->ehci->aqueues : &q->ehci->pqueues;
EHCIPacket *p;
+ int packets = 0;
- trace_usb_ehci_queue_action(q, "free");
- while ((p = QTAILQ_FIRST(&q->packets)) != NULL) {
+ p = QTAILQ_FIRST(&q->packets);
+ if (p == NULL) {
+ return 0;
+ }
+
+ trace_usb_ehci_queue_action(q, "cancel");
+ do {
ehci_free_packet(p);
+ packets++;
+ } while ((p = QTAILQ_FIRST(&q->packets)) != NULL);
+ return packets;
+}
+
+static int ehci_reset_queue(EHCIQueue *q)
+{
+ int packets;
+
+ trace_usb_ehci_queue_action(q, "reset");
+ packets = ehci_cancel_queue(q);
+ q->dev = NULL;
+ q->qtdaddr = 0;
+ return packets;
+}
+
+static void ehci_free_queue(EHCIQueue *q, const char *warn)
+{
+ EHCIQueueHead *head = q->async ? &q->ehci->aqueues : &q->ehci->pqueues;
+ int cancelled;
+
+ trace_usb_ehci_queue_action(q, "free");
+ cancelled = ehci_cancel_queue(q);
+ if (warn && cancelled > 0) {
+ ehci_trace_guest_bug(q->ehci, warn);
}
QTAILQ_REMOVE(head, q, next);
g_free(q);
return NULL;
}
-static void ehci_queues_tag_unused_async(EHCIState *ehci)
-{
- EHCIQueue *q;
-
- QTAILQ_FOREACH(q, &ehci->aqueues, next) {
- if (!q->seen) {
- q->revalidate = 1;
- }
- }
-}
-
-static void ehci_queues_rip_unused(EHCIState *ehci, int async)
+static void ehci_queues_rip_unused(EHCIState *ehci, int async, int flush)
{
EHCIQueueHead *head = async ? &ehci->aqueues : &ehci->pqueues;
+ const char *warn = (async && !flush) ? "guest unlinked busy QH" : NULL;
uint64_t maxage = FRAME_TIMER_NS * ehci->maxframes * 4;
EHCIQueue *q, *tmp;
q->ts = ehci->last_run_ns;
continue;
}
- if (ehci->last_run_ns < q->ts + maxage) {
+ if (!flush && ehci->last_run_ns < q->ts + maxage) {
continue;
}
- ehci_free_queue(q);
+ ehci_free_queue(q, warn);
}
}
if (q->dev != dev) {
continue;
}
- ehci_free_queue(q);
+ ehci_free_queue(q, NULL);
}
}
static void ehci_queues_rip_all(EHCIState *ehci, int async)
{
EHCIQueueHead *head = async ? &ehci->aqueues : &ehci->pqueues;
+ const char *warn = async ? "guest stopped busy async schedule" : NULL;
EHCIQueue *q, *tmp;
QTAILQ_FOREACH_SAFE(q, head, next, tmp) {
- ehci_free_queue(q);
+ ehci_free_queue(q, warn);
}
}
val &= ~USBCMD_FLS;
}
+ if (val & USBCMD_IAAD) {
+ /*
+ * Process IAAD immediately, otherwise the Linux IAAD watchdog may
+ * trigger and re-use a qh without us seeing the unlink.
+ */
+ s->async_stepdown = 0;
+ qemu_bh_schedule(s->async_bh);
+ trace_usb_ehci_doorbell_ring();
+ }
+
if (((USBCMD_RUNSTOP | USBCMD_PSE | USBCMD_ASE) & val) !=
((USBCMD_RUNSTOP | USBCMD_PSE | USBCMD_ASE) & s->usbcmd)) {
if (s->pstate == EST_INACTIVE) {
assert(0);
break;
}
- } else if ((p->usb_status > p->tbytes) && (p->pid == USB_TOKEN_IN)) {
- p->usb_status = USB_RET_BABBLE;
- q->qh.token |= (QTD_TOKEN_HALT | QTD_TOKEN_BABBLE);
- ehci_raise_irq(q->ehci, USBSTS_ERRINT);
} else {
// TODO check 4.12 for splits
p->tbytes = (p->qtd.token & QTD_TOKEN_TBYTES_MASK) >> QTD_TOKEN_TBYTES_SH;
if (p->tbytes > BUFF_SIZE) {
- fprintf(stderr, "Request for more bytes than allowed\n");
+ ehci_trace_guest_bug(p->queue->ehci,
+ "guest requested more bytes than allowed");
return USB_RET_PROCERR;
}
endp = get_field(p->queue->qh.epchar, QH_EPCHAR_EP);
ep = usb_ep_get(p->queue->dev, p->pid, endp);
- usb_packet_setup(&p->packet, p->pid, ep);
+ usb_packet_setup(&p->packet, p->pid, ep, p->qtdaddr);
usb_packet_map(&p->packet, &p->sgl);
trace_usb_ehci_packet_action(p->queue, p, action);
*/
static int ehci_process_itd(EHCIState *ehci,
- EHCIitd *itd)
+ EHCIitd *itd,
+ uint32_t addr)
{
USBDevice *dev;
USBEndpoint *ep;
dev = ehci_find_device(ehci, devaddr);
ep = usb_ep_get(dev, pid, endp);
- if (ep->type == USB_ENDPOINT_XFER_ISOC) {
- usb_packet_setup(&ehci->ipacket, pid, ep);
+ if (ep && ep->type == USB_ENDPOINT_XFER_ISOC) {
+ usb_packet_setup(&ehci->ipacket, pid, ep, addr);
usb_packet_map(&ehci->ipacket, &ehci->isgl);
ret = usb_handle_packet(dev, &ehci->ipacket);
assert(ret != USB_RET_ASYNC);
ehci_set_usbsts(ehci, USBSTS_REC);
}
- ehci_queues_rip_unused(ehci, async);
+ ehci_queues_rip_unused(ehci, async, 0);
/* Find the head of the list (4.9.1.1) */
for(i = 0; i < MAX_QH; i++) {
static EHCIQueue *ehci_state_fetchqh(EHCIState *ehci, int async)
{
EHCIPacket *p;
- uint32_t entry, devaddr;
+ uint32_t entry, devaddr, endp;
EHCIQueue *q;
EHCIqh qh;
get_dwords(ehci, NLPTR_GET(q->qhaddr),
(uint32_t *) &qh, sizeof(EHCIqh) >> 2);
- if (q->revalidate && (q->qh.epchar != qh.epchar ||
- q->qh.epcap != qh.epcap ||
- q->qh.current_qtd != qh.current_qtd)) {
- ehci_free_queue(q);
- q = ehci_alloc_queue(ehci, entry, async);
- q->seen++;
+ ehci_trace_qh(q, NLPTR_GET(q->qhaddr), &qh);
+
+ /*
+ * The overlay area of the qh should never be changed by the guest,
+ * except when idle, in which case the reset is a nop.
+ */
+ devaddr = get_field(qh.epchar, QH_EPCHAR_DEVADDR);
+ endp = get_field(qh.epchar, QH_EPCHAR_EP);
+ if ((devaddr != get_field(q->qh.epchar, QH_EPCHAR_DEVADDR)) ||
+ (endp != get_field(q->qh.epchar, QH_EPCHAR_EP)) ||
+ (memcmp(&qh.current_qtd, &q->qh.current_qtd,
+ 9 * sizeof(uint32_t)) != 0) ||
+ (q->dev != NULL && q->dev->addr != devaddr)) {
+ if (ehci_reset_queue(q) > 0) {
+ ehci_trace_guest_bug(ehci, "guest updated active QH");
+ }
p = NULL;
}
q->qh = qh;
- q->revalidate = 0;
- ehci_trace_qh(q, NLPTR_GET(q->qhaddr), &q->qh);
-
- devaddr = get_field(q->qh.epchar, QH_EPCHAR_DEVADDR);
- if (q->dev != NULL && q->dev->addr != devaddr) {
- if (!QTAILQ_EMPTY(&q->packets)) {
- /* should not happen (guest bug) */
- while ((p = QTAILQ_FIRST(&q->packets)) != NULL) {
- ehci_free_packet(p);
- }
- }
- q->dev = NULL;
- }
+
if (q->dev == NULL) {
q->dev = ehci_find_device(q->ehci, devaddr);
}
- if (p && p->async == EHCI_ASYNC_INFLIGHT) {
- /* I/O still in progress -- skip queue */
- ehci_set_state(ehci, async, EST_HORIZONTALQH);
- goto out;
- }
if (p && p->async == EHCI_ASYNC_FINISHED) {
/* I/O finished -- continue processing queue */
trace_usb_ehci_packet_action(p->queue, p, "complete");
sizeof(EHCIitd) >> 2);
ehci_trace_itd(ehci, entry, &itd);
- if (ehci_process_itd(ehci, &itd) != 0) {
+ if (ehci_process_itd(ehci, &itd, entry) != 0) {
return -1;
}
ehci_trace_qtd(q, NLPTR_GET(q->qtdaddr), &qtd);
p = QTAILQ_FIRST(&q->packets);
- while (p != NULL && p->qtdaddr != q->qtdaddr) {
- /* should not happen (guest bug) */
- ehci_free_packet(p);
- p = QTAILQ_FIRST(&q->packets);
- }
if (p != NULL) {
- ehci_qh_do_overlay(q);
- ehci_flush_qh(q);
- if (p->async == EHCI_ASYNC_INFLIGHT) {
- ehci_set_state(q->ehci, q->async, EST_HORIZONTALQH);
+ if (p->qtdaddr != q->qtdaddr ||
+ (!NLPTR_TBIT(p->qtd.next) && (p->qtd.next != qtd.next)) ||
+ (!NLPTR_TBIT(p->qtd.altnext) && (p->qtd.altnext != qtd.altnext)) ||
+ p->qtd.bufptr[0] != qtd.bufptr[0]) {
+ ehci_cancel_queue(q);
+ ehci_trace_guest_bug(q->ehci, "guest updated active QH or qTD");
+ p = NULL;
} else {
+ p->qtd = qtd;
+ ehci_qh_do_overlay(q);
+ }
+ }
+
+ if (!(qtd.token & QTD_TOKEN_ACTIVE)) {
+ if (p != NULL) {
+ /* transfer canceled by guest (clear active) */
+ ehci_cancel_queue(q);
+ p = NULL;
+ }
+ ehci_set_state(q->ehci, q->async, EST_HORIZONTALQH);
+ again = 1;
+ } else if (p != NULL) {
+ switch (p->async) {
+ case EHCI_ASYNC_NONE:
+ /* Previously nacked packet (likely interrupt ep) */
+ ehci_set_state(q->ehci, q->async, EST_EXECUTE);
+ break;
+ case EHCI_ASYNC_INFLIGHT:
+ /* Unfinyshed async handled packet, go horizontal */
+ ehci_set_state(q->ehci, q->async, EST_HORIZONTALQH);
+ break;
+ case EHCI_ASYNC_FINISHED:
+ /* Should never happen, as this case is caught by fetchqh */
ehci_set_state(q->ehci, q->async, EST_EXECUTING);
+ break;
}
again = 1;
- } else if (qtd.token & QTD_TOKEN_ACTIVE) {
+ } else {
p = ehci_alloc_packet(q);
p->qtdaddr = q->qtdaddr;
p->qtd = qtd;
ehci_set_state(q->ehci, q->async, EST_EXECUTE);
again = 1;
- } else {
- ehci_set_state(q->ehci, q->async, EST_HORIZONTALQH);
- again = 1;
}
return again;
static int ehci_state_executing(EHCIQueue *q)
{
EHCIPacket *p = QTAILQ_FIRST(&q->packets);
- int again = 0;
assert(p != NULL);
assert(p->qtdaddr == q->qtdaddr);
ehci_execute_complete(q);
- if (p->usb_status == USB_RET_ASYNC) {
- goto out;
- }
- if (p->usb_status == USB_RET_PROCERR) {
- again = -1;
- goto out;
- }
// 4.10.3
if (!q->async) {
ehci_set_state(q->ehci, q->async, EST_WRITEBACK);
}
- again = 1;
-
-out:
ehci_flush_qh(q);
- return again;
+ return 1;
}
* bit is clear.
*/
if (q->qh.token & QTD_TOKEN_HALT) {
+ /*
+ * We should not do any further processing on a halted queue!
+ * This is esp. important for bulk endpoints with pipelining enabled
+ * (redirection to a real USB device), where we must cancel all the
+ * transfers after this one so that:
+ * 1) If they've completed already, they are not processed further
+ * causing more stalls, originating from the same failed transfer
+ * 2) If still in flight, they are cancelled before the guest does
+ * a clear stall, otherwise the guest and device can loose sync!
+ */
+ while ((p = QTAILQ_FIRST(&q->packets)) != NULL) {
+ ehci_free_packet(p);
+ }
ehci_set_state(q->ehci, q->async, EST_HORIZONTALQH);
again = 1;
} else {
*/
if (ehci->usbcmd & USBCMD_IAAD) {
/* Remove all unseen qhs from the async qhs queue */
- ehci_queues_tag_unused_async(ehci);
- DPRINTF("ASYNC: doorbell request acknowledged\n");
+ ehci_queues_rip_unused(ehci, async, 1);
+ trace_usb_ehci_doorbell_ack();
ehci->usbcmd &= ~USBCMD_IAAD;
ehci_raise_irq(ehci, USBSTS_IAA);
}
ehci_set_fetch_addr(ehci, async,entry);
ehci_set_state(ehci, async, EST_FETCHENTRY);
ehci_advance_state(ehci, async);
- ehci_queues_rip_unused(ehci, async);
+ ehci_queues_rip_unused(ehci, async, 0);
break;
default: