diff options
author | Don Dugger <n0ano@n0ano.com> | 2016-06-03 03:33:22 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@172.30.200.206> | 2016-06-03 03:33:23 +0000 |
commit | da27230f80795d0028333713f036d44c53cb0e68 (patch) | |
tree | b3d379eaf000adf72b36cb01cdf4d79c3e3f064c /qemu/roms/ipxe/src/drivers/usb/uhci.c | |
parent | 0e68cb048bb8aadb14675f5d4286d8ab2fc35449 (diff) | |
parent | 437fd90c0250dee670290f9b714253671a990160 (diff) |
Merge "These changes are the raw update to qemu-2.6."
Diffstat (limited to 'qemu/roms/ipxe/src/drivers/usb/uhci.c')
-rw-r--r-- | qemu/roms/ipxe/src/drivers/usb/uhci.c | 1577 |
1 files changed, 1577 insertions, 0 deletions
diff --git a/qemu/roms/ipxe/src/drivers/usb/uhci.c b/qemu/roms/ipxe/src/drivers/usb/uhci.c new file mode 100644 index 000000000..b6bb92560 --- /dev/null +++ b/qemu/roms/ipxe/src/drivers/usb/uhci.c @@ -0,0 +1,1577 @@ +/* + * Copyright (C) 2015 Michael Brown <mbrown@fensystems.co.uk>. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * You can also choose to distribute this program under the terms of + * the Unmodified Binary Distribution Licence (as given in the file + * COPYING.UBDL), provided that you have satisfied its requirements. + */ + +FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); + +#include <strings.h> +#include <unistd.h> +#include <errno.h> +#include <byteswap.h> +#include <ipxe/malloc.h> +#include <ipxe/pci.h> +#include <ipxe/usb.h> +#include "ehci.h" +#include "uhci.h" + +/** @file + * + * USB Universal Host Controller Interface (UHCI) driver + * + */ + +/****************************************************************************** + * + * Register access + * + ****************************************************************************** + */ + +/** + * Check that address is reachable + * + * @v addr Address + * @v len Length + * @ret rc Return status code + */ +static inline __attribute__ (( always_inline)) int +uhci_reachable ( void *addr, size_t len ) { + physaddr_t phys = virt_to_phys ( addr ); + + /* Always reachable in a 32-bit build */ + if ( sizeof ( physaddr_t ) <= sizeof ( uint32_t ) ) + return 0; + + /* Reachable if below 4GB */ + if ( ( ( phys + len - 1 ) & ~0xffffffffULL ) == 0 ) + return 0; + + return -ENOTSUP; +} + +/****************************************************************************** + * + * Run / stop / reset + * + ****************************************************************************** + */ + +/** + * Start UHCI device + * + * @v uhci UHCI device + */ +static void uhci_run ( struct uhci_device *uhci ) { + uint16_t usbcmd; + + /* Set run/stop bit */ + usbcmd = inw ( uhci->regs + UHCI_USBCMD ); + usbcmd |= ( UHCI_USBCMD_RUN | UHCI_USBCMD_MAX64 ); + outw ( usbcmd, uhci->regs + UHCI_USBCMD ); +} + +/** + * Stop UHCI device + * + * @v uhci UHCI device + * @ret rc Return status code + */ +static int uhci_stop ( struct uhci_device *uhci ) { + uint16_t usbcmd; + uint16_t usbsts; + unsigned int i; + + /* Clear run/stop bit */ + usbcmd = inw ( uhci->regs + UHCI_USBCMD ); + usbcmd &= ~UHCI_USBCMD_RUN; + outw ( usbcmd, uhci->regs + UHCI_USBCMD ); + + /* Wait for device to stop */ + for ( i = 0 ; i < UHCI_STOP_MAX_WAIT_MS ; i++ ) { + + /* Check if device is stopped */ + usbsts = inw ( uhci->regs + UHCI_USBSTS ); + if ( usbsts & UHCI_USBSTS_HCHALTED ) + return 0; + + /* Delay */ + mdelay ( 1 ); + } + + DBGC ( uhci, "UHCI %s timed out waiting for stop\n", uhci->name ); + return -ETIMEDOUT; +} + +/** + * Reset UHCI device + * + * @v uhci UHCI device + * @ret rc Return status code + */ +static int uhci_reset ( struct uhci_device *uhci ) { + uint16_t usbcmd; + unsigned int i; + int rc; + + /* The UHCI specification states that resetting a running + * device may result in undefined behaviour, so try stopping + * it first. + */ + if ( ( rc = uhci_stop ( uhci ) ) != 0 ) { + /* Ignore errors and attempt to reset the device anyway */ + } + + /* Reset device */ + outw ( UHCI_USBCMD_HCRESET, uhci->regs + UHCI_USBCMD ); + + /* Wait for reset to complete */ + for ( i = 0 ; i < UHCI_RESET_MAX_WAIT_MS ; i++ ) { + + /* Check if reset is complete */ + usbcmd = inw ( uhci->regs + UHCI_USBCMD ); + if ( ! ( usbcmd & UHCI_USBCMD_HCRESET ) ) + return 0; + + /* Delay */ + mdelay ( 1 ); + } + + DBGC ( uhci, "UHCI %s timed out waiting for reset\n", uhci->name ); + return -ETIMEDOUT; +} + +/****************************************************************************** + * + * Transfer descriptor rings + * + ****************************************************************************** + */ + +/** + * Allocate transfer ring + * + * @v ring Transfer ring + * @ret rc Return status code + */ +static int uhci_ring_alloc ( struct uhci_ring *ring ) { + int rc; + + /* Initialise structure */ + memset ( ring, 0, sizeof ( *ring ) ); + + /* Allocate queue head */ + ring->head = malloc_dma ( sizeof ( *ring->head ), UHCI_ALIGN ); + if ( ! ring->head ) { + rc = -ENOMEM; + goto err_alloc; + } + if ( ( rc = uhci_reachable ( ring->head, + sizeof ( *ring->head ) ) ) != 0 ) + goto err_unreachable; + + /* Initialise queue head */ + ring->head->current = cpu_to_le32 ( UHCI_LINK_TERMINATE ); + + return 0; + + err_unreachable: + free_dma ( ring->head, sizeof ( *ring->head ) ); + err_alloc: + return rc; +} + +/** + * Free transfer ring + * + * @v ring Transfer ring + */ +static void uhci_ring_free ( struct uhci_ring *ring ) { + unsigned int i; + + /* Sanity checks */ + assert ( uhci_ring_fill ( ring ) == 0 ); + for ( i = 0 ; i < UHCI_RING_COUNT ; i++ ) + assert ( ring->xfer[i] == NULL ); + + /* Free queue head */ + free_dma ( ring->head, sizeof ( *ring->head ) ); +} + +/** + * Enqueue new transfer + * + * @v ring Transfer ring + * @v iobuf I/O buffer + * @v count Number of descriptors + * @ret rc Return status code + */ +static int uhci_enqueue ( struct uhci_ring *ring, struct io_buffer *iobuf, + unsigned int count ) { + struct uhci_transfer *xfer; + struct uhci_transfer *end; + struct uhci_transfer_descriptor *desc; + unsigned int index = ( ring->prod % UHCI_RING_COUNT ); + uint32_t link; + size_t len; + int rc; + + /* Sanity check */ + assert ( count > 0 ); + assert ( iobuf != NULL ); + + /* Check for space in ring */ + if ( ! uhci_ring_remaining ( ring ) ) { + rc = -ENOBUFS; + goto err_ring_full; + } + + /* Check for reachability of I/O buffer */ + if ( ( rc = uhci_reachable ( iobuf->data, iob_len ( iobuf ) ) ) != 0 ) + goto err_unreachable_iobuf; + + /* Allocate transfer */ + xfer = malloc ( sizeof ( *xfer ) ); + if ( ! xfer ) { + rc = -ENOMEM; + goto err_alloc_xfer; + } + + /* Initialise transfer */ + xfer->prod = 0; + xfer->cons = 0; + xfer->len = 0; + xfer->iobuf = iobuf; + + /* Allocate transfer descriptors */ + len = ( count * sizeof ( xfer->desc[0] ) ); + xfer->desc = malloc_dma ( len, UHCI_ALIGN ); + if ( ! xfer->desc ) { + rc = -ENOMEM; + goto err_alloc_desc; + } + if ( ( rc = uhci_reachable ( xfer->desc, len ) ) != 0 ) + goto err_unreachable_desc; + + /* Initialise transfer descriptors */ + memset ( xfer->desc, 0, len ); + desc = xfer->desc; + for ( ; --count ; desc++ ) { + link = ( virt_to_phys ( desc + 1 ) | UHCI_LINK_DEPTH_FIRST ); + desc->link = cpu_to_le32 ( link ); + desc->flags = ring->flags; + } + desc->link = cpu_to_le32 ( UHCI_LINK_TERMINATE ); + desc->flags = ( ring->flags | UHCI_FL_IOC ); + + /* Add to ring */ + wmb(); + link = virt_to_phys ( xfer->desc ); + if ( uhci_ring_fill ( ring ) > 0 ) { + end = ring->end; + end->desc[ end->prod - 1 ].link = cpu_to_le32 ( link ); + } else { + ring->head->current = cpu_to_le32 ( link ); + } + assert ( ring->xfer[index] == NULL ); + ring->xfer[index] = xfer; + ring->end = xfer; + ring->prod++; + + return 0; + + err_unreachable_desc: + free_dma ( xfer->desc, len ); + err_alloc_desc: + free ( xfer ); + err_alloc_xfer: + err_unreachable_iobuf: + err_ring_full: + return rc; +} + +/** + * Describe transfer + * + * @v ring Transfer ring + * @v data Data + * @v len Length of data + * @v pid Packet ID + */ +static void uhci_describe ( struct uhci_ring *ring, void *data, + size_t len, uint8_t pid ) { + struct uhci_transfer *xfer = ring->end; + struct uhci_transfer_descriptor *desc; + size_t frag_len; + uint32_t control; + + do { + /* Calculate fragment length */ + frag_len = len; + if ( frag_len > ring->mtu ) + frag_len = ring->mtu; + + /* Populate descriptor */ + desc = &xfer->desc[xfer->prod++]; + if ( pid == USB_PID_IN ) + desc->flags |= UHCI_FL_SPD; + control = ( ring->control | UHCI_CONTROL_PID ( pid ) | + UHCI_CONTROL_LEN ( frag_len ) ); + desc->control = cpu_to_le32 ( control ); + if ( data ) + desc->data = virt_to_phys ( data ); + wmb(); + desc->status = UHCI_STATUS_ACTIVE; + + /* Update data toggle */ + ring->control ^= UHCI_CONTROL_TOGGLE; + + /* Move to next descriptor */ + data += frag_len; + len -= frag_len; + + } while ( len ); +} + +/** + * Dequeue transfer + * + * @v ring Transfer ring + * @ret iobuf I/O buffer + */ +static struct io_buffer * uhci_dequeue ( struct uhci_ring *ring ) { + unsigned int index = ( ring->cons % UHCI_RING_COUNT ); + struct io_buffer *iobuf; + struct uhci_transfer *xfer; + size_t len; + + /* Sanity checks */ + assert ( uhci_ring_fill ( ring ) > 0 ); + + /* Consume transfer */ + xfer = ring->xfer[index]; + assert ( xfer != NULL ); + assert ( xfer->desc != NULL ); + iobuf = xfer->iobuf; + assert ( iobuf != NULL ); + ring->xfer[index] = NULL; + ring->cons++; + + /* Free transfer descriptors */ + len = ( xfer->prod * sizeof ( xfer->desc[0] ) ); + free_dma ( xfer->desc, len ); + + /* Free transfer */ + free ( xfer ); + + return iobuf; +} + +/** + * Restart ring + * + * @v ring Transfer ring + * @v toggle Expected data toggle for next descriptor + */ +static void uhci_restart ( struct uhci_ring *ring, uint32_t toggle ) { + struct uhci_transfer *xfer; + struct uhci_transfer_descriptor *desc; + struct uhci_transfer_descriptor *first; + uint32_t link; + unsigned int i; + unsigned int j; + + /* Sanity check */ + assert ( ring->head->current == cpu_to_le32 ( UHCI_LINK_TERMINATE ) ); + + /* If ring is empty, then just update the data toggle for the + * next descriptor. + */ + if ( uhci_ring_fill ( ring ) == 0 ) { + ring->control &= ~UHCI_CONTROL_TOGGLE; + ring->control |= toggle; + return; + } + + /* If expected toggle does not match the toggle in the first + * unconsumed descriptor, then invert all toggles. + */ + xfer = ring->xfer[ ring->cons % UHCI_RING_COUNT ]; + assert ( xfer != NULL ); + assert ( xfer->cons == 0 ); + first = &xfer->desc[0]; + if ( ( le32_to_cpu ( first->control ) ^ toggle ) & UHCI_CONTROL_TOGGLE){ + + /* Invert toggle on all unconsumed transfer descriptors */ + for ( i = ring->cons ; i != ring->prod ; i++ ) { + xfer = ring->xfer[ i % UHCI_RING_COUNT ]; + assert ( xfer != NULL ); + assert ( xfer->cons == 0 ); + for ( j = 0 ; j < xfer->prod ; j++ ) { + desc = &xfer->desc[j]; + desc->control ^= + cpu_to_le32 ( UHCI_CONTROL_TOGGLE ); + } + } + + /* Invert toggle for next descriptor to be enqueued */ + ring->control ^= UHCI_CONTROL_TOGGLE; + } + + /* Restart ring at first unconsumed transfer */ + link = virt_to_phys ( first ); + wmb(); + ring->head->current = cpu_to_le32 ( link ); +} + +/****************************************************************************** + * + * Schedule management + * + ****************************************************************************** + */ + +/** + * Get link value for a queue head + * + * @v queue Queue head + * @ret link Link value + */ +static inline uint32_t uhci_link_qh ( struct uhci_queue_head *queue ) { + + return ( virt_to_phys ( queue ) | UHCI_LINK_TYPE_QH ); +} + +/** + * (Re)build asynchronous schedule + * + * @v uhci UHCI device + */ +static void uhci_async_schedule ( struct uhci_device *uhci ) { + struct uhci_endpoint *endpoint; + struct uhci_queue_head *queue; + uint32_t end; + uint32_t link; + + /* Build schedule in reverse order of execution. Provided + * that we only ever add or remove single endpoints, this can + * safely run concurrently with hardware execution of the + * schedule. + */ + link = end = uhci_link_qh ( uhci->head ); + list_for_each_entry_reverse ( endpoint, &uhci->async, schedule ) { + queue = endpoint->ring.head; + queue->link = cpu_to_le32 ( link ); + wmb(); + link = uhci_link_qh ( queue ); + } + if ( link == end ) + link = UHCI_LINK_TERMINATE; + uhci->head->link = cpu_to_le32 ( link ); + wmb(); +} + +/** + * Add endpoint to asynchronous schedule + * + * @v endpoint Endpoint + */ +static void uhci_async_add ( struct uhci_endpoint *endpoint ) { + struct uhci_device *uhci = endpoint->uhci; + + /* Add to end of schedule */ + list_add_tail ( &endpoint->schedule, &uhci->async ); + + /* Rebuild schedule */ + uhci_async_schedule ( uhci ); +} + +/** + * Remove endpoint from asynchronous schedule + * + * @v endpoint Endpoint + */ +static void uhci_async_del ( struct uhci_endpoint *endpoint ) { + struct uhci_device *uhci = endpoint->uhci; + + /* Remove from schedule */ + list_check_contains_entry ( endpoint, &uhci->async, schedule ); + list_del ( &endpoint->schedule ); + + /* Rebuild schedule */ + uhci_async_schedule ( uhci ); + + /* Delay for a whole USB frame (with a 100% safety margin) */ + mdelay ( 2 ); +} + +/** + * (Re)build periodic schedule + * + * @v uhci UHCI device + */ +static void uhci_periodic_schedule ( struct uhci_device *uhci ) { + struct uhci_endpoint *endpoint; + struct uhci_queue_head *queue; + uint32_t link; + uint32_t end; + unsigned int max_interval; + unsigned int i; + + /* Build schedule in reverse order of execution. Provided + * that we only ever add or remove single endpoints, this can + * safely run concurrently with hardware execution of the + * schedule. + */ + DBGCP ( uhci, "UHCI %s periodic schedule: ", uhci->name ); + link = end = uhci_link_qh ( uhci->head ); + list_for_each_entry_reverse ( endpoint, &uhci->periodic, schedule ) { + queue = endpoint->ring.head; + queue->link = cpu_to_le32 ( link ); + wmb(); + DBGCP ( uhci, "%s%d", ( ( link == end ) ? "" : "<-" ), + endpoint->ep->interval ); + link = uhci_link_qh ( queue ); + } + DBGCP ( uhci, "\n" ); + + /* Populate periodic frame list */ + DBGCP ( uhci, "UHCI %s periodic frame list:", uhci->name ); + for ( i = 0 ; i < UHCI_FRAMES ; i++ ) { + + /* Calculate maximum interval (in microframes) which + * may appear as part of this frame list. + */ + if ( i == 0 ) { + /* Start of list: include all endpoints */ + max_interval = -1U; + } else { + /* Calculate highest power-of-two frame interval */ + max_interval = ( 1 << ( ffs ( i ) - 1 ) ); + /* Convert to microframes */ + max_interval <<= 3; + /* Round up to nearest 2^n-1 */ + max_interval = ( ( max_interval << 1 ) - 1 ); + } + + /* Find first endpoint in schedule satisfying this + * maximum interval constraint. + */ + link = uhci_link_qh ( uhci->head ); + list_for_each_entry ( endpoint, &uhci->periodic, schedule ) { + if ( endpoint->ep->interval <= max_interval ) { + queue = endpoint->ring.head; + link = uhci_link_qh ( queue ); + DBGCP ( uhci, " %d:%d", + i, endpoint->ep->interval ); + break; + } + } + uhci->frame->link[i] = cpu_to_le32 ( link ); + } + wmb(); + DBGCP ( uhci, "\n" ); +} + +/** + * Add endpoint to periodic schedule + * + * @v endpoint Endpoint + */ +static void uhci_periodic_add ( struct uhci_endpoint *endpoint ) { + struct uhci_device *uhci = endpoint->uhci; + struct uhci_endpoint *before; + unsigned int interval = endpoint->ep->interval; + + /* Find first endpoint with a smaller interval */ + list_for_each_entry ( before, &uhci->periodic, schedule ) { + if ( before->ep->interval < interval ) + break; + } + list_add_tail ( &endpoint->schedule, &before->schedule ); + + /* Rebuild schedule */ + uhci_periodic_schedule ( uhci ); +} + +/** + * Remove endpoint from periodic schedule + * + * @v endpoint Endpoint + */ +static void uhci_periodic_del ( struct uhci_endpoint *endpoint ) { + struct uhci_device *uhci = endpoint->uhci; + + /* Remove from schedule */ + list_check_contains_entry ( endpoint, &uhci->periodic, schedule ); + list_del ( &endpoint->schedule ); + + /* Rebuild schedule */ + uhci_periodic_schedule ( uhci ); + + /* Delay for a whole USB frame (with a 100% safety margin) */ + mdelay ( 2 ); +} + +/** + * Add endpoint to appropriate schedule + * + * @v endpoint Endpoint + */ +static void uhci_schedule_add ( struct uhci_endpoint *endpoint ) { + struct usb_endpoint *ep = endpoint->ep; + unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); + + if ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) { + uhci_periodic_add ( endpoint ); + } else { + uhci_async_add ( endpoint ); + } +} + +/** + * Remove endpoint from appropriate schedule + * + * @v endpoint Endpoint + */ +static void uhci_schedule_del ( struct uhci_endpoint *endpoint ) { + struct usb_endpoint *ep = endpoint->ep; + unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); + + if ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) { + uhci_periodic_del ( endpoint ); + } else { + uhci_async_del ( endpoint ); + } +} + +/****************************************************************************** + * + * Endpoint operations + * + ****************************************************************************** + */ + +/** + * Open endpoint + * + * @v ep USB endpoint + * @ret rc Return status code + */ +static int uhci_endpoint_open ( struct usb_endpoint *ep ) { + struct usb_device *usb = ep->usb; + struct uhci_device *uhci = usb_get_hostdata ( usb ); + struct uhci_endpoint *endpoint; + int rc; + + /* Allocate and initialise structure */ + endpoint = zalloc ( sizeof ( *endpoint ) ); + if ( ! endpoint ) { + rc = -ENOMEM; + goto err_alloc; + } + endpoint->uhci = uhci; + endpoint->ep = ep; + usb_endpoint_set_hostdata ( ep, endpoint ); + + /* Initialise descriptor ring */ + if ( ( rc = uhci_ring_alloc ( &endpoint->ring ) ) != 0 ) + goto err_ring_alloc; + endpoint->ring.mtu = ep->mtu; + endpoint->ring.flags = UHCI_FL_CERR_MAX; + if ( usb->port->speed < USB_SPEED_FULL ) + endpoint->ring.flags |= UHCI_FL_LS; + endpoint->ring.control = ( UHCI_CONTROL_DEVICE ( usb->address ) | + UHCI_CONTROL_ENDPOINT ( ep->address ) ); + + /* Add to list of endpoints */ + list_add_tail ( &endpoint->list, &uhci->endpoints ); + + /* Add to schedule */ + uhci_schedule_add ( endpoint ); + + return 0; + + uhci_ring_free ( &endpoint->ring ); + err_ring_alloc: + free ( endpoint ); + err_alloc: + return rc; +} + +/** + * Close endpoint + * + * @v ep USB endpoint + */ +static void uhci_endpoint_close ( struct usb_endpoint *ep ) { + struct uhci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); + struct io_buffer *iobuf; + + /* Remove from schedule */ + uhci_schedule_del ( endpoint ); + + /* Cancel any incomplete transfers */ + while ( uhci_ring_fill ( &endpoint->ring ) ) { + iobuf = uhci_dequeue ( &endpoint->ring ); + if ( iobuf ) + usb_complete_err ( ep, iobuf, -ECANCELED ); + } + + /* Remove from list of endpoints */ + list_del ( &endpoint->list ); + + /* Free descriptor ring */ + uhci_ring_free ( &endpoint->ring ); + + /* Free endpoint */ + free ( endpoint ); +} + +/** + * Reset endpoint + * + * @v ep USB endpoint + * @ret rc Return status code + */ +static int uhci_endpoint_reset ( struct usb_endpoint *ep ) { + struct uhci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); + struct uhci_ring *ring = &endpoint->ring; + + /* Restart ring */ + uhci_restart ( ring, 0 ); + + return 0; +} + +/** + * Update MTU + * + * @v ep USB endpoint + * @ret rc Return status code + */ +static int uhci_endpoint_mtu ( struct usb_endpoint *ep ) { + struct uhci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); + + /* Update endpoint MTU */ + endpoint->ring.mtu = ep->mtu; + + return 0; +} + +/** + * Enqueue message transfer + * + * @v ep USB endpoint + * @v iobuf I/O buffer + * @ret rc Return status code + */ +static int uhci_endpoint_message ( struct usb_endpoint *ep, + struct io_buffer *iobuf ) { + struct uhci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); + struct uhci_ring *ring = &endpoint->ring; + struct usb_setup_packet *packet; + unsigned int count; + size_t len; + int input; + int rc; + + /* Calculate number of descriptors */ + assert ( iob_len ( iobuf ) >= sizeof ( *packet ) ); + len = ( iob_len ( iobuf ) - sizeof ( *packet ) ); + count = ( 1 /* setup stage */ + + ( ( len + ring->mtu - 1 ) / ring->mtu ) /* data stage */ + + 1 /* status stage */ ); + + /* Enqueue transfer */ + if ( ( rc = uhci_enqueue ( ring, iobuf, count ) ) != 0 ) + return rc; + + /* Describe setup stage */ + packet = iobuf->data; + ring->control &= ~UHCI_CONTROL_TOGGLE; + uhci_describe ( ring, packet, sizeof ( *packet ), USB_PID_SETUP ); + iob_pull ( iobuf, sizeof ( *packet ) ); + + /* Describe data stage, if applicable */ + assert ( ring->control & UHCI_CONTROL_TOGGLE ); + input = ( packet->request & cpu_to_le16 ( USB_DIR_IN ) ); + if ( len ) { + uhci_describe ( ring, iobuf->data, len, + ( input ? USB_PID_IN : USB_PID_OUT ) ); + } + + /* Describe status stage */ + ring->control |= UHCI_CONTROL_TOGGLE; + uhci_describe ( ring, NULL, 0, + ( ( len && input ) ? USB_PID_OUT : USB_PID_IN ) ); + + /* Sanity check */ + assert ( ring->end->prod == count ); + + return 0; +} + +/** + * Enqueue stream transfer + * + * @v ep USB endpoint + * @v iobuf I/O buffer + * @v terminate Terminate using a short packet + * @ret rc Return status code + */ +static int uhci_endpoint_stream ( struct usb_endpoint *ep, + struct io_buffer *iobuf, int terminate ) { + struct uhci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); + struct uhci_ring *ring = &endpoint->ring; + unsigned int count; + size_t len; + int input; + int zlp; + int rc; + + /* Calculate number of descriptors */ + len = iob_len ( iobuf ); + zlp = ( terminate && ( ( len & ( ring->mtu - 1 ) ) == 0 ) ); + count = ( ( ( len + ring->mtu - 1 ) / ring->mtu ) + ( zlp ? 1 : 0 ) ); + + /* Enqueue transfer */ + if ( ( rc = uhci_enqueue ( ring, iobuf, count ) ) != 0 ) + return rc; + + /* Describe data packet */ + input = ( ep->address & USB_DIR_IN ); + uhci_describe ( ring, iobuf->data, len, + ( input ? USB_PID_IN : USB_PID_OUT ) ); + + /* Describe zero-length packet, if applicable */ + if ( zlp ) + uhci_describe ( ring, NULL, 0, USB_PID_OUT ); + + /* Sanity check */ + assert ( ring->end->prod == count ); + + return 0; +} + +/** + * Check if transfer is a message transfer + * + * @v xfer UHCI transfer + * @ret is_message Transfer is a message transfer + */ +static inline int uhci_is_message ( struct uhci_transfer *xfer ) { + struct uhci_transfer_descriptor *desc = &xfer->desc[0]; + + return ( ( desc->control & cpu_to_le32 ( UHCI_CONTROL_PID_MASK ) ) == + cpu_to_le32 ( UHCI_CONTROL_PID ( USB_PID_SETUP ) ) ); +} + +/** + * Poll for completions + * + * @v endpoint Endpoint + */ +static void uhci_endpoint_poll ( struct uhci_endpoint *endpoint ) { + struct uhci_ring *ring = &endpoint->ring; + struct uhci_device *uhci = endpoint->uhci; + struct usb_endpoint *ep = endpoint->ep; + struct usb_device *usb = ep->usb; + struct uhci_transfer *xfer; + struct uhci_transfer_descriptor *desc; + struct io_buffer *iobuf; + unsigned int index; + uint32_t link; + uint32_t toggle; + uint32_t control; + uint16_t actual; + size_t len; + + /* Consume all completed descriptors */ + while ( uhci_ring_fill ( ring ) ) { + + /* Stop if we reach an uncompleted descriptor */ + index = ( ring->cons % UHCI_RING_COUNT ); + xfer = ring->xfer[index]; + assert ( xfer != NULL ); + assert ( xfer->cons < xfer->prod ); + desc = &xfer->desc[xfer->cons]; + rmb(); + if ( desc->status & UHCI_STATUS_ACTIVE ) + break; + control = le32_to_cpu ( desc->control ); + actual = le16_to_cpu ( desc->actual ); + + /* Update data length, if applicable */ + if ( UHCI_DATA_PACKET ( control ) ) + xfer->len += UHCI_ACTUAL_LEN ( actual ); + + /* If we have encountered an error, then deactivate + * the queue head (to prevent further hardware + * accesses to this transfer), consume the transfer, + * and report the error to the USB core. + */ + if ( desc->status & UHCI_STATUS_STALLED ) { + DBGC ( uhci, "UHCI %s %s completion %d.%d failed " + "(status %02x)\n", usb->name, + usb_endpoint_name ( ep ), index, + xfer->cons, desc->status ); + link = UHCI_LINK_TERMINATE; + ring->head->current = cpu_to_le32 ( link ); + wmb(); + iobuf = uhci_dequeue ( ring ); + usb_complete_err ( ep, iobuf, -EIO ); + break; + } + + /* Consume this descriptor */ + xfer->cons++; + + /* Check for short packets */ + if ( UHCI_SHORT_PACKET ( control, actual ) ) { + + /* Sanity checks */ + assert ( desc->flags & UHCI_FL_SPD ); + link = virt_to_phys ( desc ); + assert ( ( le32_to_cpu ( ring->head->current ) & + ~( UHCI_ALIGN - 1 ) ) == link ); + + /* If this is a message transfer, then restart + * at the status stage. + */ + if ( uhci_is_message ( xfer ) ) { + xfer->cons = ( xfer->prod - 1 ); + link = virt_to_phys ( &xfer->desc[xfer->cons] ); + ring->head->current = cpu_to_le32 ( link ); + break; + } + + /* Otherwise, this is a stream transfer. + * First, prevent further hardware access to + * this transfer. + */ + link = UHCI_LINK_TERMINATE; + ring->head->current = cpu_to_le32 ( link ); + wmb(); + + /* Determine expected data toggle for next descriptor */ + toggle = ( ( control ^ UHCI_CONTROL_TOGGLE ) & + UHCI_CONTROL_TOGGLE ); + + /* Consume this transfer */ + len = xfer->len; + iobuf = uhci_dequeue ( ring ); + + /* Update packet length */ + assert ( len <= iob_len ( iobuf ) ); + iob_unput ( iobuf, ( iob_len ( iobuf ) - len ) ); + + /* Restart ring */ + uhci_restart ( ring, toggle ); + + } else if ( xfer->cons == xfer->prod ) { + + /* Completed a transfer: consume it */ + len = xfer->len; + iobuf = uhci_dequeue ( ring ); + assert ( len == iob_len ( iobuf ) ); + + } else { + + /* Not a short packet and not yet complete: + * continue processing. + */ + continue; + } + + /* Report completion to USB core */ + usb_complete ( ep, iobuf ); + } +} + +/****************************************************************************** + * + * Device operations + * + ****************************************************************************** + */ + +/** + * Open device + * + * @v usb USB device + * @ret rc Return status code + */ +static int uhci_device_open ( struct usb_device *usb ) { + struct uhci_device *uhci = usb_bus_get_hostdata ( usb->port->hub->bus ); + + usb_set_hostdata ( usb, uhci ); + return 0; +} + +/** + * Close device + * + * @v usb USB device + */ +static void uhci_device_close ( struct usb_device *usb ) { + struct uhci_device *uhci = usb_get_hostdata ( usb ); + struct usb_bus *bus = uhci->bus; + + /* Free device address, if assigned */ + if ( usb->address ) + usb_free_address ( bus, usb->address ); +} + +/** + * Assign device address + * + * @v usb USB device + * @ret rc Return status code + */ +static int uhci_device_address ( struct usb_device *usb ) { + struct uhci_device *uhci = usb_get_hostdata ( usb ); + struct usb_bus *bus = uhci->bus; + struct usb_endpoint *ep0 = usb_endpoint ( usb, USB_EP0_ADDRESS ); + struct uhci_endpoint *endpoint0 = usb_endpoint_get_hostdata ( ep0 ); + int address; + int rc; + + /* Sanity checks */ + assert ( usb->address == 0 ); + assert ( ep0 != NULL ); + + /* Allocate device address */ + address = usb_alloc_address ( bus ); + if ( address < 0 ) { + rc = address; + DBGC ( uhci, "UHCI %s could not allocate address: %s\n", + usb->name, strerror ( rc ) ); + goto err_alloc_address; + } + + /* Set address */ + if ( ( rc = usb_set_address ( usb, address ) ) != 0 ) + goto err_set_address; + + /* Update device address */ + usb->address = address; + endpoint0->ring.control |= UHCI_CONTROL_DEVICE ( address ); + + return 0; + + err_set_address: + usb_free_address ( bus, address ); + err_alloc_address: + return rc; +} + +/****************************************************************************** + * + * Hub operations + * + ****************************************************************************** + */ + +/** + * Open hub + * + * @v hub USB hub + * @ret rc Return status code + */ +static int uhci_hub_open ( struct usb_hub *hub __unused ) { + + /* Nothing to do */ + return 0; +} + +/** + * Close hub + * + * @v hub USB hub + */ +static void uhci_hub_close ( struct usb_hub *hub __unused ) { + + /* Nothing to do */ +} + +/****************************************************************************** + * + * Root hub operations + * + ****************************************************************************** + */ + +/** + * Open root hub + * + * @v hub USB hub + * @ret rc Return status code + */ +static int uhci_root_open ( struct usb_hub *hub ) { + struct usb_bus *bus = hub->bus; + struct uhci_device *uhci = usb_bus_get_hostdata ( bus ); + + /* Record hub driver private data */ + usb_hub_set_drvdata ( hub, uhci ); + + return 0; +} + +/** + * Close root hub + * + * @v hub USB hub + */ +static void uhci_root_close ( struct usb_hub *hub ) { + + /* Clear hub driver private data */ + usb_hub_set_drvdata ( hub, NULL ); +} + +/** + * Enable port + * + * @v hub USB hub + * @v port USB port + * @ret rc Return status code + */ +static int uhci_root_enable ( struct usb_hub *hub, struct usb_port *port ) { + struct uhci_device *uhci = usb_hub_get_drvdata ( hub ); + uint16_t portsc; + unsigned int i; + + /* Reset port */ + portsc = inw ( uhci->regs + UHCI_PORTSC ( port->address ) ); + portsc |= UHCI_PORTSC_PR; + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + mdelay ( USB_RESET_DELAY_MS ); + portsc &= ~UHCI_PORTSC_PR; + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + mdelay ( USB_RESET_RECOVER_DELAY_MS ); + + /* Enable port */ + portsc |= UHCI_PORTSC_PED; + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + mdelay ( USB_RESET_RECOVER_DELAY_MS ); + + /* Wait for port to become enabled */ + for ( i = 0 ; i < UHCI_PORT_ENABLE_MAX_WAIT_MS ; i++ ) { + + /* Check port status */ + portsc = inw ( uhci->regs + UHCI_PORTSC ( port->address ) ); + if ( portsc & UHCI_PORTSC_PED ) + return 0; + + /* Delay */ + mdelay ( 1 ); + } + + DBGC ( uhci, "UHCI %s-%d timed out waiting for port to enable " + "(status %04x)\n", uhci->name, port->address, portsc ); + return -ETIMEDOUT; +} + +/** + * Disable port + * + * @v hub USB hub + * @v port USB port + * @ret rc Return status code + */ +static int uhci_root_disable ( struct usb_hub *hub, struct usb_port *port ) { + struct uhci_device *uhci = usb_hub_get_drvdata ( hub ); + uint16_t portsc; + + /* Disable port */ + portsc = inw ( uhci->regs + UHCI_PORTSC ( port->address ) ); + portsc &= ~UHCI_PORTSC_PED; + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + + return 0; +} + +/** + * Update root hub port speed + * + * @v hub USB hub + * @v port USB port + * @ret rc Return status code + */ +static int uhci_root_speed ( struct usb_hub *hub, struct usb_port *port ) { + struct uhci_device *uhci = usb_hub_get_drvdata ( hub ); + struct pci_device pci; + uint16_t portsc; + unsigned int speed; + + /* Read port status */ + portsc = inw ( uhci->regs + UHCI_PORTSC ( port->address ) ); + if ( ! ( portsc & UHCI_PORTSC_CCS ) ) { + /* Port not connected */ + speed = USB_SPEED_NONE; + } else if ( uhci->companion && + ! find_usb_bus_by_location ( BUS_TYPE_PCI, + uhci->companion ) ) { + /* Defer connection detection until companion + * controller has been enumerated. + */ + pci_init ( &pci, uhci->companion ); + DBGC ( uhci, "UHCI %s-%d deferring for companion " PCI_FMT "\n", + uhci->name, port->address, PCI_ARGS ( &pci ) ); + speed = USB_SPEED_NONE; + } else if ( portsc & UHCI_PORTSC_LS ) { + /* Low-speed device */ + speed = USB_SPEED_LOW; + } else { + /* Full-speed device */ + speed = USB_SPEED_FULL; + } + port->speed = speed; + + /* Record disconnections and clear changes */ + port->disconnected |= ( portsc & UHCI_PORTSC_CSC ); + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + + return 0; +} + +/** + * Clear transaction translator buffer + * + * @v hub USB hub + * @v port USB port + * @v ep USB endpoint + * @ret rc Return status code + */ +static int uhci_root_clear_tt ( struct usb_hub *hub, struct usb_port *port, + struct usb_endpoint *ep ) { + struct uhci_device *uhci = usb_hub_get_drvdata ( hub ); + + /* Should never be called; this is a root hub */ + DBGC ( uhci, "UHCI %s-%d nonsensical CLEAR_TT for %s %s\n", uhci->name, + port->address, ep->usb->name, usb_endpoint_name ( ep ) ); + + return -ENOTSUP; +} + +/** + * Poll for port status changes + * + * @v hub USB hub + * @v port USB port + */ +static void uhci_root_poll ( struct usb_hub *hub, struct usb_port *port ) { + struct uhci_device *uhci = usb_hub_get_drvdata ( hub ); + uint16_t portsc; + uint16_t change; + + /* Do nothing unless something has changed */ + portsc = inw ( uhci->regs + UHCI_PORTSC ( port->address ) ); + change = ( portsc & UHCI_PORTSC_CHANGE ); + if ( ! change ) + return; + + /* Record disconnections and clear changes */ + port->disconnected |= ( portsc & UHCI_PORTSC_CSC ); + outw ( portsc, uhci->regs + UHCI_PORTSC ( port->address ) ); + + /* Report port status change */ + usb_port_changed ( port ); +} + +/****************************************************************************** + * + * Bus operations + * + ****************************************************************************** + */ + +/** + * Open USB bus + * + * @v bus USB bus + * @ret rc Return status code + */ +static int uhci_bus_open ( struct usb_bus *bus ) { + struct uhci_device *uhci = usb_bus_get_hostdata ( bus ); + int rc; + + /* Sanity checks */ + assert ( list_empty ( &uhci->async ) ); + assert ( list_empty ( &uhci->periodic ) ); + + /* Allocate and initialise asynchronous queue head */ + uhci->head = malloc_dma ( sizeof ( *uhci->head ), UHCI_ALIGN ); + if ( ! uhci->head ) { + rc = -ENOMEM; + goto err_alloc_head; + } + if ( ( rc = uhci_reachable ( uhci->head, sizeof ( *uhci->head ) ) ) !=0) + goto err_unreachable_head; + memset ( uhci->head, 0, sizeof ( *uhci->head ) ); + uhci->head->current = cpu_to_le32 ( UHCI_LINK_TERMINATE ); + uhci_async_schedule ( uhci ); + + /* Allocate periodic frame list */ + uhci->frame = malloc_dma ( sizeof ( *uhci->frame ), + sizeof ( *uhci->frame ) ); + if ( ! uhci->frame ) { + rc = -ENOMEM; + goto err_alloc_frame; + } + if ( ( rc = uhci_reachable ( uhci->frame, + sizeof ( *uhci->frame ) ) ) != 0 ) + goto err_unreachable_frame; + uhci_periodic_schedule ( uhci ); + outl ( virt_to_phys ( uhci->frame ), uhci->regs + UHCI_FLBASEADD ); + + /* Start controller */ + uhci_run ( uhci ); + + return 0; + + uhci_stop ( uhci ); + err_unreachable_frame: + free_dma ( uhci->frame, sizeof ( *uhci->frame ) ); + err_alloc_frame: + err_unreachable_head: + free_dma ( uhci->head, sizeof ( *uhci->head ) ); + err_alloc_head: + return rc; +} + +/** + * Close USB bus + * + * @v bus USB bus + */ +static void uhci_bus_close ( struct usb_bus *bus ) { + struct uhci_device *uhci = usb_bus_get_hostdata ( bus ); + + /* Sanity checks */ + assert ( list_empty ( &uhci->async ) ); + assert ( list_empty ( &uhci->periodic ) ); + + /* Stop controller */ + uhci_stop ( uhci ); + + /* Free periodic frame list */ + free_dma ( uhci->frame, sizeof ( *uhci->frame ) ); + + /* Free asynchronous schedule */ + free_dma ( uhci->head, sizeof ( *uhci->head ) ); +} + +/** + * Poll USB bus + * + * @v bus USB bus + */ +static void uhci_bus_poll ( struct usb_bus *bus ) { + struct uhci_device *uhci = usb_bus_get_hostdata ( bus ); + struct usb_hub *hub = bus->hub; + struct uhci_endpoint *endpoint; + unsigned int i; + + /* UHCI defers interrupts (including short packet detection) + * until the end of the frame. This can result in bulk IN + * endpoints remaining halted for much of the time, waiting + * for software action to reset the data toggles. We + * therefore ignore USBSTS and unconditionally poll all + * endpoints for completed transfer descriptors. + * + * As with EHCI, we trust that completion handlers are minimal + * and will not do anything that could plausibly affect the + * endpoint list itself. + */ + list_for_each_entry ( endpoint, &uhci->endpoints, list ) + uhci_endpoint_poll ( endpoint ); + + /* UHCI provides no single bit to indicate that a port status + * change has occurred. We therefore unconditionally iterate + * over all ports looking for status changes. + */ + for ( i = 1 ; i <= UHCI_PORTS ; i++ ) + uhci_root_poll ( hub, usb_port ( hub, i ) ); +} + +/****************************************************************************** + * + * PCI interface + * + ****************************************************************************** + */ + +/** USB host controller operations */ +static struct usb_host_operations uhci_operations = { + .endpoint = { + .open = uhci_endpoint_open, + .close = uhci_endpoint_close, + .reset = uhci_endpoint_reset, + .mtu = uhci_endpoint_mtu, + .message = uhci_endpoint_message, + .stream = uhci_endpoint_stream, + }, + .device = { + .open = uhci_device_open, + .close = uhci_device_close, + .address = uhci_device_address, + }, + .bus = { + .open = uhci_bus_open, + .close = uhci_bus_close, + .poll = uhci_bus_poll, + }, + .hub = { + .open = uhci_hub_open, + .close = uhci_hub_close, + }, + .root = { + .open = uhci_root_open, + .close = uhci_root_close, + .enable = uhci_root_enable, + .disable = uhci_root_disable, + .speed = uhci_root_speed, + .clear_tt = uhci_root_clear_tt, + }, +}; + +/** + * Locate EHCI companion controller (when no EHCI support is present) + * + * @v pci PCI device + * @ret busdevfn EHCI companion controller bus:dev.fn (if any) + */ +__weak unsigned int ehci_companion ( struct pci_device *pci __unused ) { + return 0; +} + +/** + * Probe PCI device + * + * @v pci PCI device + * @ret rc Return status code + */ +static int uhci_probe ( struct pci_device *pci ) { + struct uhci_device *uhci; + struct usb_port *port; + unsigned int i; + int rc; + + /* Allocate and initialise structure */ + uhci = zalloc ( sizeof ( *uhci ) ); + if ( ! uhci ) { + rc = -ENOMEM; + goto err_alloc; + } + uhci->name = pci->dev.name; + INIT_LIST_HEAD ( &uhci->endpoints ); + INIT_LIST_HEAD ( &uhci->async ); + INIT_LIST_HEAD ( &uhci->periodic ); + + /* Fix up PCI device */ + adjust_pci_device ( pci ); + + /* Identify EHCI companion controller, if any */ + uhci->companion = ehci_companion ( pci ); + + /* Claim ownership from BIOS. (There is no release mechanism + * for UHCI.) + */ + pci_write_config_word ( pci, UHCI_USBLEGSUP, UHCI_USBLEGSUP_DEFAULT ); + + /* Map registers */ + uhci->regs = pci->ioaddr; + if ( ! uhci->regs ) { + rc = -ENODEV; + goto err_ioremap; + } + + /* Reset device */ + if ( ( rc = uhci_reset ( uhci ) ) != 0 ) + goto err_reset; + + /* Allocate USB bus */ + uhci->bus = alloc_usb_bus ( &pci->dev, UHCI_PORTS, UHCI_MTU, + &uhci_operations ); + if ( ! uhci->bus ) { + rc = -ENOMEM; + goto err_alloc_bus; + } + usb_bus_set_hostdata ( uhci->bus, uhci ); + usb_hub_set_drvdata ( uhci->bus->hub, uhci ); + + /* Set port protocols */ + for ( i = 1 ; i <= UHCI_PORTS ; i++ ) { + port = usb_port ( uhci->bus->hub, i ); + port->protocol = USB_PROTO_2_0; + } + + /* Register USB bus */ + if ( ( rc = register_usb_bus ( uhci->bus ) ) != 0 ) + goto err_register; + + pci_set_drvdata ( pci, uhci ); + return 0; + + unregister_usb_bus ( uhci->bus ); + err_register: + free_usb_bus ( uhci->bus ); + err_alloc_bus: + uhci_reset ( uhci ); + err_reset: + err_ioremap: + free ( uhci ); + err_alloc: + return rc; +} + +/** + * Remove PCI device + * + * @v pci PCI device + */ +static void uhci_remove ( struct pci_device *pci ) { + struct uhci_device *uhci = pci_get_drvdata ( pci ); + struct usb_bus *bus = uhci->bus; + + unregister_usb_bus ( bus ); + assert ( list_empty ( &uhci->async ) ); + assert ( list_empty ( &uhci->periodic ) ); + free_usb_bus ( bus ); + uhci_reset ( uhci ); + free ( uhci ); +} + +/** UHCI PCI device IDs */ +static struct pci_device_id uhci_ids[] = { + PCI_ROM ( 0xffff, 0xffff, "uhci", "UHCI", 0 ), +}; + +/** UHCI PCI driver */ +struct pci_driver uhci_driver __pci_driver = { + .ids = uhci_ids, + .id_count = ( sizeof ( uhci_ids ) / sizeof ( uhci_ids[0] ) ), + .class = PCI_CLASS_ID ( PCI_CLASS_SERIAL, PCI_CLASS_SERIAL_USB, + PCI_CLASS_SERIAL_USB_UHCI ), + .probe = uhci_probe, + .remove = uhci_remove, +}; |