| /* | |
| * Copyright (C) 2014 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 ); | |
| FILE_SECBOOT ( PERMITTED ); | |
| #include <stdlib.h> | |
| #include <unistd.h> | |
| #include <string.h> | |
| #include <strings.h> | |
| #include <errno.h> | |
| #include <byteswap.h> | |
| #include <ipxe/malloc.h> | |
| #include <ipxe/pci.h> | |
| #include <ipxe/usb.h> | |
| #include <ipxe/init.h> | |
| #include "ehci.h" | |
| /** @file | |
| * | |
| * USB Enhanced Host Controller Interface (EHCI) driver | |
| * | |
| */ | |
| /** | |
| * Construct error code from transfer descriptor status | |
| * | |
| * @v status Transfer descriptor status | |
| * @ret rc Error code | |
| * | |
| * Bits 2-5 of the status code provide some indication as to the root | |
| * cause of the error. We incorporate these into the error code as | |
| * reported to usb_complete_err(). | |
| */ | |
| #define EIO_STATUS( status ) EUNIQ ( EINFO_EIO, ( ( (status) >> 2 ) & 0xf ) ) | |
| /****************************************************************************** | |
| * | |
| * Register access | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Initialise device | |
| * | |
| * @v ehci EHCI device | |
| * @v regs MMIO registers | |
| */ | |
| static void ehci_init ( struct ehci_device *ehci, void *regs ) { | |
| uint32_t hcsparams; | |
| uint32_t hccparams; | |
| size_t caplength; | |
| /* Locate capability and operational registers */ | |
| ehci->cap = regs; | |
| caplength = readb ( ehci->cap + EHCI_CAP_CAPLENGTH ); | |
| ehci->op = ( ehci->cap + caplength ); | |
| DBGC2 ( ehci, "EHCI %s cap %08lx op %08lx\n", ehci->name, | |
| virt_to_phys ( ehci->cap ), virt_to_phys ( ehci->op ) ); | |
| /* Read structural parameters */ | |
| hcsparams = readl ( ehci->cap + EHCI_CAP_HCSPARAMS ); | |
| ehci->ports = EHCI_HCSPARAMS_PORTS ( hcsparams ); | |
| DBGC ( ehci, "EHCI %s has %d ports\n", ehci->name, ehci->ports ); | |
| /* Read capability parameters 1 */ | |
| hccparams = readl ( ehci->cap + EHCI_CAP_HCCPARAMS ); | |
| ehci->addr64 = EHCI_HCCPARAMS_ADDR64 ( hccparams ); | |
| ehci->flsize = ( EHCI_HCCPARAMS_FLSIZE ( hccparams ) ? | |
| EHCI_FLSIZE_SMALL : EHCI_FLSIZE_DEFAULT ); | |
| ehci->eecp = EHCI_HCCPARAMS_EECP ( hccparams ); | |
| DBGC2 ( ehci, "EHCI %s %d-bit flsize %d\n", ehci->name, | |
| ( ehci->addr64 ? 64 : 32 ), ehci->flsize ); | |
| } | |
| /** | |
| * Find extended capability | |
| * | |
| * @v ehci EHCI device | |
| * @v pci PCI device | |
| * @v id Capability ID | |
| * @v offset Offset to previous extended capability instance, or zero | |
| * @ret offset Offset to extended capability, or zero if not found | |
| */ | |
| static unsigned int ehci_extended_capability ( struct ehci_device *ehci, | |
| struct pci_device *pci, | |
| unsigned int id, | |
| unsigned int offset ) { | |
| uint32_t eecp; | |
| /* Locate the extended capability */ | |
| while ( 1 ) { | |
| /* Locate first or next capability as applicable */ | |
| if ( offset ) { | |
| pci_read_config_dword ( pci, offset, &eecp ); | |
| offset = EHCI_EECP_NEXT ( eecp ); | |
| } else { | |
| offset = ehci->eecp; | |
| } | |
| if ( ! offset ) | |
| return 0; | |
| /* Check if this is the requested capability */ | |
| pci_read_config_dword ( pci, offset, &eecp ); | |
| if ( EHCI_EECP_ID ( eecp ) == id ) | |
| return offset; | |
| } | |
| } | |
| /** | |
| * Calculate buffer alignment | |
| * | |
| * @v len Length | |
| * @ret align Buffer alignment | |
| * | |
| * Determine alignment required for a buffer which must be aligned to | |
| * at least EHCI_MIN_ALIGN and which must not cross a page boundary. | |
| */ | |
| static inline size_t ehci_align ( size_t len ) { | |
| size_t align; | |
| /* Align to own length (rounded up to a power of two) */ | |
| align = ( 1 << fls ( len - 1 ) ); | |
| /* Round up to EHCI_MIN_ALIGN if needed */ | |
| if ( align < EHCI_MIN_ALIGN ) | |
| align = EHCI_MIN_ALIGN; | |
| return align; | |
| } | |
| /** | |
| * Check control data structure reachability | |
| * | |
| * @v ehci EHCI device | |
| * @v ptr Data structure pointer | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_ctrl_reachable ( struct ehci_device *ehci, void *ptr ) { | |
| physaddr_t phys = virt_to_phys ( ptr ); | |
| uint32_t segment; | |
| /* Always reachable in a 32-bit build */ | |
| if ( sizeof ( physaddr_t ) <= sizeof ( uint32_t ) ) | |
| return 0; | |
| /* Reachable only if control segment matches in a 64-bit build */ | |
| segment = ( ( ( uint64_t ) phys ) >> 32 ); | |
| if ( segment == ehci->ctrldssegment ) | |
| return 0; | |
| return -ENOTSUP; | |
| } | |
| /****************************************************************************** | |
| * | |
| * Diagnostics | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Dump host controller registers | |
| * | |
| * @v ehci EHCI device | |
| */ | |
| static __unused void ehci_dump ( struct ehci_device *ehci ) { | |
| uint8_t caplength; | |
| uint16_t hciversion; | |
| uint32_t hcsparams; | |
| uint32_t hccparams; | |
| uint32_t usbcmd; | |
| uint32_t usbsts; | |
| uint32_t usbintr; | |
| uint32_t frindex; | |
| uint32_t ctrldssegment; | |
| uint32_t periodiclistbase; | |
| uint32_t asynclistaddr; | |
| uint32_t configflag; | |
| /* Do nothing unless debugging is enabled */ | |
| if ( ! DBG_LOG ) | |
| return; | |
| /* Dump capability registers */ | |
| caplength = readb ( ehci->cap + EHCI_CAP_CAPLENGTH ); | |
| hciversion = readw ( ehci->cap + EHCI_CAP_HCIVERSION ); | |
| hcsparams = readl ( ehci->cap + EHCI_CAP_HCSPARAMS ); | |
| hccparams = readl ( ehci->cap + EHCI_CAP_HCCPARAMS ); | |
| DBGC ( ehci, "EHCI %s caplen %02x hciversion %04x hcsparams %08x " | |
| "hccparams %08x\n", ehci->name, caplength, hciversion, | |
| hcsparams, hccparams ); | |
| /* Dump operational registers */ | |
| usbcmd = readl ( ehci->op + EHCI_OP_USBCMD ); | |
| usbsts = readl ( ehci->op + EHCI_OP_USBSTS ); | |
| usbintr = readl ( ehci->op + EHCI_OP_USBINTR ); | |
| frindex = readl ( ehci->op + EHCI_OP_FRINDEX ); | |
| ctrldssegment = readl ( ehci->op + EHCI_OP_CTRLDSSEGMENT ); | |
| periodiclistbase = readl ( ehci->op + EHCI_OP_PERIODICLISTBASE ); | |
| asynclistaddr = readl ( ehci->op + EHCI_OP_ASYNCLISTADDR ); | |
| configflag = readl ( ehci->op + EHCI_OP_CONFIGFLAG ); | |
| DBGC ( ehci, "EHCI %s usbcmd %08x usbsts %08x usbint %08x frindx " | |
| "%08x\n", ehci->name, usbcmd, usbsts, usbintr, frindex ); | |
| DBGC ( ehci, "EHCI %s ctrlds %08x period %08x asyncl %08x cfgflg " | |
| "%08x\n", ehci->name, ctrldssegment, periodiclistbase, | |
| asynclistaddr, configflag ); | |
| } | |
| /****************************************************************************** | |
| * | |
| * USB legacy support | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** Prevent the release of ownership back to BIOS */ | |
| static int ehci_legacy_prevent_release; | |
| /** | |
| * Initialise USB legacy support | |
| * | |
| * @v ehci EHCI device | |
| * @v pci PCI device | |
| */ | |
| static void ehci_legacy_init ( struct ehci_device *ehci, | |
| struct pci_device *pci ) { | |
| unsigned int legacy; | |
| uint8_t bios; | |
| /* Locate USB legacy support capability (if present) */ | |
| legacy = ehci_extended_capability ( ehci, pci, EHCI_EECP_ID_LEGACY, 0 ); | |
| if ( ! legacy ) { | |
| /* Not an error; capability may not be present */ | |
| DBGC ( ehci, "EHCI %s has no USB legacy support capability\n", | |
| ehci->name ); | |
| return; | |
| } | |
| /* Check if legacy USB support is enabled */ | |
| pci_read_config_byte ( pci, ( legacy + EHCI_USBLEGSUP_BIOS ), &bios ); | |
| if ( ! ( bios & EHCI_USBLEGSUP_BIOS_OWNED ) ) { | |
| /* Not an error; already owned by OS */ | |
| DBGC ( ehci, "EHCI %s USB legacy support already disabled\n", | |
| ehci->name ); | |
| return; | |
| } | |
| /* Record presence of USB legacy support capability */ | |
| ehci->legacy = legacy; | |
| } | |
| /** | |
| * Claim ownership from BIOS | |
| * | |
| * @v ehci EHCI device | |
| * @v pci PCI device | |
| */ | |
| static void ehci_legacy_claim ( struct ehci_device *ehci, | |
| struct pci_device *pci ) { | |
| unsigned int legacy = ehci->legacy; | |
| uint32_t ctlsts; | |
| uint8_t bios; | |
| unsigned int i; | |
| /* Do nothing unless legacy support capability is present */ | |
| if ( ! legacy ) | |
| return; | |
| /* Dump original SMI usage */ | |
| pci_read_config_dword ( pci, ( legacy + EHCI_USBLEGSUP_CTLSTS ), | |
| &ctlsts ); | |
| if ( ctlsts ) { | |
| DBGC ( ehci, "EHCI %s BIOS using SMIs: %08x\n", | |
| ehci->name, ctlsts ); | |
| } | |
| /* Claim ownership */ | |
| pci_write_config_byte ( pci, ( legacy + EHCI_USBLEGSUP_OS ), | |
| EHCI_USBLEGSUP_OS_OWNED ); | |
| /* Wait for BIOS to release ownership */ | |
| for ( i = 0 ; i < EHCI_USBLEGSUP_MAX_WAIT_MS ; i++ ) { | |
| /* Check if BIOS has released ownership */ | |
| pci_read_config_byte ( pci, ( legacy + EHCI_USBLEGSUP_BIOS ), | |
| &bios ); | |
| if ( ! ( bios & EHCI_USBLEGSUP_BIOS_OWNED ) ) { | |
| DBGC ( ehci, "EHCI %s claimed ownership from BIOS\n", | |
| ehci->name ); | |
| pci_read_config_dword ( pci, ( legacy + | |
| EHCI_USBLEGSUP_CTLSTS ), | |
| &ctlsts ); | |
| if ( ctlsts ) { | |
| DBGC ( ehci, "EHCI %s warning: BIOS retained " | |
| "SMIs: %08x\n", ehci->name, ctlsts ); | |
| } | |
| return; | |
| } | |
| /* Delay */ | |
| mdelay ( 1 ); | |
| } | |
| /* BIOS did not release ownership. Claim it forcibly by | |
| * disabling all SMIs. | |
| */ | |
| DBGC ( ehci, "EHCI %s could not claim ownership from BIOS: forcibly " | |
| "disabling SMIs\n", ehci->name ); | |
| pci_write_config_dword ( pci, ( legacy + EHCI_USBLEGSUP_CTLSTS ), 0 ); | |
| } | |
| /** | |
| * Release ownership back to BIOS | |
| * | |
| * @v ehci EHCI device | |
| * @v pci PCI device | |
| */ | |
| static void ehci_legacy_release ( struct ehci_device *ehci, | |
| struct pci_device *pci ) { | |
| unsigned int legacy = ehci->legacy; | |
| uint32_t ctlsts; | |
| /* Do nothing unless legacy support capability is present */ | |
| if ( ! legacy ) | |
| return; | |
| /* Do nothing if releasing ownership is prevented */ | |
| if ( ehci_legacy_prevent_release ) { | |
| DBGC ( ehci, "EHCI %s not releasing ownership to BIOS\n", | |
| ehci->name ); | |
| return; | |
| } | |
| /* Release ownership */ | |
| pci_write_config_byte ( pci, ( legacy + EHCI_USBLEGSUP_OS ), 0 ); | |
| DBGC ( ehci, "EHCI %s released ownership to BIOS\n", ehci->name ); | |
| /* Dump restored SMI usage */ | |
| pci_read_config_dword ( pci, ( legacy + EHCI_USBLEGSUP_CTLSTS ), | |
| &ctlsts ); | |
| DBGC ( ehci, "EHCI %s BIOS reclaimed SMIs: %08x\n", | |
| ehci->name, ctlsts ); | |
| } | |
| /****************************************************************************** | |
| * | |
| * Companion controllers | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Poll child companion controllers | |
| * | |
| * @v ehci EHCI device | |
| */ | |
| static void ehci_poll_companions ( struct ehci_device *ehci ) { | |
| struct usb_bus *bus; | |
| struct device_description *desc; | |
| /* Poll any USB buses belonging to child companion controllers */ | |
| for_each_usb_bus ( bus ) { | |
| /* Get underlying devices description */ | |
| desc = &bus->dev->desc; | |
| /* Skip buses that are not PCI devices */ | |
| if ( desc->bus_type != BUS_TYPE_PCI ) | |
| continue; | |
| /* Skip buses that are not part of the same PCI device */ | |
| if ( PCI_FIRST_FUNC ( desc->location ) != | |
| PCI_FIRST_FUNC ( ehci->bus->dev->desc.location ) ) | |
| continue; | |
| /* Skip buses that are not UHCI or OHCI PCI devices */ | |
| if ( ( desc->class != PCI_CLASS ( PCI_CLASS_SERIAL, | |
| PCI_CLASS_SERIAL_USB, | |
| PCI_CLASS_SERIAL_USB_UHCI ))&& | |
| ( desc->class != PCI_CLASS ( PCI_CLASS_SERIAL, | |
| PCI_CLASS_SERIAL_USB, | |
| PCI_CLASS_SERIAL_USB_OHCI ) )) | |
| continue; | |
| /* Poll child companion controller bus */ | |
| DBGC2 ( ehci, "EHCI %s polling companion %s\n", | |
| ehci->name, bus->name ); | |
| usb_poll ( bus ); | |
| } | |
| } | |
| /** | |
| * Locate EHCI companion controller | |
| * | |
| * @v pci PCI device | |
| * @ret busdevfn EHCI companion controller bus:dev.fn (if any) | |
| */ | |
| unsigned int ehci_companion ( struct pci_device *pci ) { | |
| struct pci_device tmp; | |
| unsigned int busdevfn; | |
| int rc; | |
| /* Look for an EHCI function on the same PCI device */ | |
| busdevfn = pci->busdevfn; | |
| while ( ++busdevfn <= PCI_LAST_FUNC ( pci->busdevfn ) ) { | |
| pci_init ( &tmp, busdevfn ); | |
| if ( ( rc = pci_read_config ( &tmp ) ) != 0 ) | |
| continue; | |
| if ( tmp.class == PCI_CLASS ( PCI_CLASS_SERIAL, | |
| PCI_CLASS_SERIAL_USB, | |
| PCI_CLASS_SERIAL_USB_EHCI ) ) | |
| return busdevfn; | |
| } | |
| return 0; | |
| } | |
| /****************************************************************************** | |
| * | |
| * Run / stop / reset | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Start EHCI device | |
| * | |
| * @v ehci EHCI device | |
| */ | |
| static void ehci_run ( struct ehci_device *ehci ) { | |
| uint32_t usbcmd; | |
| /* Set run/stop bit */ | |
| usbcmd = readl ( ehci->op + EHCI_OP_USBCMD ); | |
| usbcmd &= ~EHCI_USBCMD_FLSIZE_MASK; | |
| usbcmd |= ( EHCI_USBCMD_RUN | EHCI_USBCMD_FLSIZE ( ehci->flsize ) | | |
| EHCI_USBCMD_PERIODIC | EHCI_USBCMD_ASYNC ); | |
| writel ( usbcmd, ehci->op + EHCI_OP_USBCMD ); | |
| } | |
| /** | |
| * Stop EHCI device | |
| * | |
| * @v ehci EHCI device | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_stop ( struct ehci_device *ehci ) { | |
| uint32_t usbcmd; | |
| uint32_t usbsts; | |
| unsigned int i; | |
| /* Clear run/stop bit */ | |
| usbcmd = readl ( ehci->op + EHCI_OP_USBCMD ); | |
| usbcmd &= ~( EHCI_USBCMD_RUN | EHCI_USBCMD_PERIODIC | | |
| EHCI_USBCMD_ASYNC ); | |
| writel ( usbcmd, ehci->op + EHCI_OP_USBCMD ); | |
| /* Wait for device to stop */ | |
| for ( i = 0 ; i < EHCI_STOP_MAX_WAIT_MS ; i++ ) { | |
| /* Check if device is stopped */ | |
| usbsts = readl ( ehci->op + EHCI_OP_USBSTS ); | |
| if ( usbsts & EHCI_USBSTS_HCH ) | |
| return 0; | |
| /* Delay */ | |
| mdelay ( 1 ); | |
| } | |
| DBGC ( ehci, "EHCI %s timed out waiting for stop\n", ehci->name ); | |
| return -ETIMEDOUT; | |
| } | |
| /** | |
| * Reset EHCI device | |
| * | |
| * @v ehci EHCI device | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_reset ( struct ehci_device *ehci ) { | |
| uint32_t usbcmd; | |
| unsigned int i; | |
| int rc; | |
| /* The EHCI specification states that resetting a running | |
| * device may result in undefined behaviour, so try stopping | |
| * it first. | |
| */ | |
| if ( ( rc = ehci_stop ( ehci ) ) != 0 ) { | |
| /* Ignore errors and attempt to reset the device anyway */ | |
| } | |
| /* Reset device */ | |
| writel ( EHCI_USBCMD_HCRST, ehci->op + EHCI_OP_USBCMD ); | |
| /* Wait for reset to complete */ | |
| for ( i = 0 ; i < EHCI_RESET_MAX_WAIT_MS ; i++ ) { | |
| /* Check if reset is complete */ | |
| usbcmd = readl ( ehci->op + EHCI_OP_USBCMD ); | |
| if ( ! ( usbcmd & EHCI_USBCMD_HCRST ) ) | |
| return 0; | |
| /* Delay */ | |
| mdelay ( 1 ); | |
| } | |
| DBGC ( ehci, "EHCI %s timed out waiting for reset\n", ehci->name ); | |
| return -ETIMEDOUT; | |
| } | |
| /****************************************************************************** | |
| * | |
| * Transfer descriptor rings | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Allocate transfer descriptor ring | |
| * | |
| * @v ehci EHCI device | |
| * @v ring Transfer descriptor ring | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_ring_alloc ( struct ehci_device *ehci, | |
| struct ehci_ring *ring ) { | |
| struct ehci_transfer_descriptor *desc; | |
| struct ehci_transfer_descriptor *next; | |
| unsigned int i; | |
| size_t len; | |
| uint32_t link; | |
| int rc; | |
| /* Initialise structure */ | |
| memset ( ring, 0, sizeof ( *ring ) ); | |
| /* Allocate I/O buffers */ | |
| ring->iobuf = zalloc ( EHCI_RING_COUNT * sizeof ( ring->iobuf[0] ) ); | |
| if ( ! ring->iobuf ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_iobuf; | |
| } | |
| /* Allocate queue head */ | |
| ring->head = malloc_phys ( sizeof ( *ring->head ), | |
| ehci_align ( sizeof ( *ring->head ) ) ); | |
| if ( ! ring->head ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_queue; | |
| } | |
| if ( ( rc = ehci_ctrl_reachable ( ehci, ring->head ) ) != 0 ) { | |
| DBGC ( ehci, "EHCI %s queue head unreachable\n", ehci->name ); | |
| goto err_unreachable_queue; | |
| } | |
| memset ( ring->head, 0, sizeof ( *ring->head ) ); | |
| /* Allocate transfer descriptors */ | |
| len = ( EHCI_RING_COUNT * sizeof ( ring->desc[0] ) ); | |
| ring->desc = malloc_phys ( len, sizeof ( ring->desc[0] ) ); | |
| if ( ! ring->desc ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_desc; | |
| } | |
| memset ( ring->desc, 0, len ); | |
| /* Initialise transfer descriptors */ | |
| for ( i = 0 ; i < EHCI_RING_COUNT ; i++ ) { | |
| desc = &ring->desc[i]; | |
| if ( ( rc = ehci_ctrl_reachable ( ehci, desc ) ) != 0 ) { | |
| DBGC ( ehci, "EHCI %s descriptor unreachable\n", | |
| ehci->name ); | |
| goto err_unreachable_desc; | |
| } | |
| next = &ring->desc[ ( i + 1 ) % EHCI_RING_COUNT ]; | |
| link = virt_to_phys ( next ); | |
| desc->next = cpu_to_le32 ( link ); | |
| desc->alt = cpu_to_le32 ( link ); | |
| } | |
| /* Initialise queue head */ | |
| link = virt_to_phys ( &ring->desc[0] ); | |
| ring->head->cache.next = cpu_to_le32 ( link ); | |
| return 0; | |
| err_unreachable_desc: | |
| free_phys ( ring->desc, len ); | |
| err_alloc_desc: | |
| err_unreachable_queue: | |
| free_phys ( ring->head, sizeof ( *ring->head ) ); | |
| err_alloc_queue: | |
| free ( ring->iobuf ); | |
| err_alloc_iobuf: | |
| return rc; | |
| } | |
| /** | |
| * Free transfer descriptor ring | |
| * | |
| * @v ring Transfer descriptor ring | |
| */ | |
| static void ehci_ring_free ( struct ehci_ring *ring ) { | |
| unsigned int i; | |
| /* Sanity checks */ | |
| assert ( ehci_ring_fill ( ring ) == 0 ); | |
| for ( i = 0 ; i < EHCI_RING_COUNT ; i++ ) | |
| assert ( ring->iobuf[i] == NULL ); | |
| /* Free transfer descriptors */ | |
| free_phys ( ring->desc, ( EHCI_RING_COUNT * | |
| sizeof ( ring->desc[0] ) ) ); | |
| /* Free queue head */ | |
| free_phys ( ring->head, sizeof ( *ring->head ) ); | |
| /* Free I/O buffers */ | |
| free ( ring->iobuf ); | |
| } | |
| /** | |
| * Enqueue transfer descriptors | |
| * | |
| * @v ehci EHCI device | |
| * @v ring Transfer descriptor ring | |
| * @v iobuf I/O buffer | |
| * @v xfers Transfers | |
| * @v count Number of transfers | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_enqueue ( struct ehci_device *ehci, struct ehci_ring *ring, | |
| struct io_buffer *iobuf, | |
| const struct ehci_transfer *xfer, | |
| unsigned int count ) { | |
| struct ehci_transfer_descriptor *desc; | |
| physaddr_t phys; | |
| void *data; | |
| size_t len; | |
| size_t offset; | |
| size_t frag_len; | |
| unsigned int toggle; | |
| unsigned int index; | |
| unsigned int i; | |
| /* Sanity check */ | |
| assert ( iobuf != NULL ); | |
| assert ( count > 0 ); | |
| /* Fail if ring does not have sufficient space */ | |
| if ( ehci_ring_remaining ( ring ) < count ) | |
| return -ENOBUFS; | |
| /* Fail if any portion is unreachable */ | |
| for ( i = 0 ; i < count ; i++ ) { | |
| if ( ! xfer[i].len ) | |
| continue; | |
| phys = ( virt_to_phys ( xfer[i].data ) + xfer[i].len - 1 ); | |
| if ( ( phys > 0xffffffffUL ) && ( ! ehci->addr64 ) ) | |
| return -ENOTSUP; | |
| } | |
| /* Enqueue each transfer, recording the I/O buffer with the last */ | |
| for ( ; count ; ring->prod++, xfer++ ) { | |
| /* Populate descriptor header */ | |
| index = ( ring->prod % EHCI_RING_COUNT ); | |
| desc = &ring->desc[index]; | |
| toggle = ( xfer->flags & EHCI_FL_TOGGLE ); | |
| assert ( xfer->len <= EHCI_LEN_MASK ); | |
| assert ( EHCI_FL_TOGGLE == EHCI_LEN_TOGGLE ); | |
| desc->len = cpu_to_le16 ( xfer->len | toggle ); | |
| desc->flags = ( xfer->flags | EHCI_FL_CERR_MAX ); | |
| /* Populate buffer pointers */ | |
| data = xfer->data; | |
| len = xfer->len; | |
| for ( i = 0 ; len ; i++ ) { | |
| /* Calculate length of this fragment */ | |
| phys = virt_to_phys ( data ); | |
| offset = ( phys & ( EHCI_PAGE_ALIGN - 1 ) ); | |
| frag_len = ( EHCI_PAGE_ALIGN - offset ); | |
| if ( frag_len > len ) | |
| frag_len = len; | |
| /* Sanity checks */ | |
| assert ( ( i == 0 ) || ( offset == 0 ) ); | |
| assert ( i < ( sizeof ( desc->low ) / | |
| sizeof ( desc->low[0] ) ) ); | |
| /* Populate buffer pointer */ | |
| desc->low[i] = cpu_to_le32 ( phys ); | |
| if ( sizeof ( physaddr_t ) > sizeof ( uint32_t ) ) { | |
| desc->high[i] = | |
| cpu_to_le32 ( ((uint64_t) phys) >> 32 ); | |
| } | |
| /* Move to next fragment */ | |
| data += frag_len; | |
| len -= frag_len; | |
| } | |
| /* Ensure everything is valid before activating descriptor */ | |
| wmb(); | |
| desc->status = EHCI_STATUS_ACTIVE; | |
| /* Record I/O buffer against last ring index */ | |
| if ( --count == 0 ) | |
| ring->iobuf[index] = iobuf; | |
| } | |
| return 0; | |
| } | |
| /** | |
| * Dequeue a transfer descriptor | |
| * | |
| * @v ring Transfer descriptor ring | |
| * @ret iobuf I/O buffer (or NULL) | |
| */ | |
| static struct io_buffer * ehci_dequeue ( struct ehci_ring *ring ) { | |
| struct ehci_transfer_descriptor *desc; | |
| struct io_buffer *iobuf; | |
| unsigned int index = ( ring->cons % EHCI_RING_COUNT ); | |
| /* Sanity check */ | |
| assert ( ehci_ring_fill ( ring ) > 0 ); | |
| /* Mark descriptor as inactive (and not halted) */ | |
| desc = &ring->desc[index]; | |
| desc->status = 0; | |
| /* Retrieve I/O buffer */ | |
| iobuf = ring->iobuf[index]; | |
| ring->iobuf[index] = NULL; | |
| /* Update consumer counter */ | |
| ring->cons++; | |
| return iobuf; | |
| } | |
| /****************************************************************************** | |
| * | |
| * Schedule management | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Get link value for a queue head | |
| * | |
| * @v queue Queue head | |
| * @ret link Link value | |
| */ | |
| static inline uint32_t ehci_link_qh ( struct ehci_queue_head *queue ) { | |
| return ( virt_to_phys ( queue ) | EHCI_LINK_TYPE_QH ); | |
| } | |
| /** | |
| * (Re)build asynchronous schedule | |
| * | |
| * @v ehci EHCI device | |
| */ | |
| static void ehci_async_schedule ( struct ehci_device *ehci ) { | |
| struct ehci_endpoint *endpoint; | |
| struct ehci_queue_head *queue; | |
| 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 = ehci_link_qh ( ehci->head ); | |
| list_for_each_entry_reverse ( endpoint, &ehci->async, schedule ) { | |
| queue = endpoint->ring.head; | |
| queue->link = cpu_to_le32 ( link ); | |
| wmb(); | |
| link = ehci_link_qh ( queue ); | |
| } | |
| ehci->head->link = cpu_to_le32 ( link ); | |
| wmb(); | |
| } | |
| /** | |
| * Add endpoint to asynchronous schedule | |
| * | |
| * @v endpoint Endpoint | |
| */ | |
| static void ehci_async_add ( struct ehci_endpoint *endpoint ) { | |
| struct ehci_device *ehci = endpoint->ehci; | |
| /* Add to end of schedule */ | |
| list_add_tail ( &endpoint->schedule, &ehci->async ); | |
| /* Rebuild schedule */ | |
| ehci_async_schedule ( ehci ); | |
| } | |
| /** | |
| * Remove endpoint from asynchronous schedule | |
| * | |
| * @v endpoint Endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_async_del ( struct ehci_endpoint *endpoint ) { | |
| struct ehci_device *ehci = endpoint->ehci; | |
| uint32_t usbcmd; | |
| uint32_t usbsts; | |
| unsigned int i; | |
| /* Remove from schedule */ | |
| list_check_contains_entry ( endpoint, &ehci->async, schedule ); | |
| list_del ( &endpoint->schedule ); | |
| /* Rebuild schedule */ | |
| ehci_async_schedule ( ehci ); | |
| /* Request notification when asynchronous schedule advances */ | |
| usbcmd = readl ( ehci->op + EHCI_OP_USBCMD ); | |
| usbcmd |= EHCI_USBCMD_ASYNC_ADVANCE; | |
| writel ( usbcmd, ehci->op + EHCI_OP_USBCMD ); | |
| /* Wait for asynchronous schedule to advance */ | |
| for ( i = 0 ; i < EHCI_ASYNC_ADVANCE_MAX_WAIT_MS ; i++ ) { | |
| /* Check for asynchronous schedule advancing */ | |
| usbsts = readl ( ehci->op + EHCI_OP_USBSTS ); | |
| if ( usbsts & EHCI_USBSTS_ASYNC_ADVANCE ) { | |
| usbsts &= ~EHCI_USBSTS_CHANGE; | |
| usbsts |= EHCI_USBSTS_ASYNC_ADVANCE; | |
| writel ( usbsts, ehci->op + EHCI_OP_USBSTS ); | |
| return 0; | |
| } | |
| /* Delay */ | |
| mdelay ( 1 ); | |
| } | |
| /* Bad things will probably happen now */ | |
| DBGC ( ehci, "EHCI %s timed out waiting for asynchronous schedule " | |
| "to advance\n", ehci->name ); | |
| return -ETIMEDOUT; | |
| } | |
| /** | |
| * (Re)build periodic schedule | |
| * | |
| * @v ehci EHCI device | |
| */ | |
| static void ehci_periodic_schedule ( struct ehci_device *ehci ) { | |
| struct ehci_endpoint *endpoint; | |
| struct ehci_queue_head *queue; | |
| uint32_t link; | |
| unsigned int frames; | |
| 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 ( ehci, "EHCI %s periodic schedule: ", ehci->name ); | |
| link = EHCI_LINK_TERMINATE; | |
| list_for_each_entry_reverse ( endpoint, &ehci->periodic, schedule ) { | |
| queue = endpoint->ring.head; | |
| queue->link = cpu_to_le32 ( link ); | |
| wmb(); | |
| DBGCP ( ehci, "%s%d", | |
| ( ( link == EHCI_LINK_TERMINATE ) ? "" : "<-" ), | |
| endpoint->ep->interval ); | |
| link = ehci_link_qh ( queue ); | |
| } | |
| DBGCP ( ehci, "\n" ); | |
| /* Populate periodic frame list */ | |
| DBGCP ( ehci, "EHCI %s periodic frame list:", ehci->name ); | |
| frames = EHCI_PERIODIC_FRAMES ( ehci->flsize ); | |
| for ( i = 0 ; i < 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 = EHCI_LINK_TERMINATE; | |
| list_for_each_entry ( endpoint, &ehci->periodic, schedule ) { | |
| if ( endpoint->ep->interval <= max_interval ) { | |
| queue = endpoint->ring.head; | |
| link = ehci_link_qh ( queue ); | |
| DBGCP ( ehci, " %d:%d", | |
| i, endpoint->ep->interval ); | |
| break; | |
| } | |
| } | |
| ehci->frame[i].link = cpu_to_le32 ( link ); | |
| } | |
| wmb(); | |
| DBGCP ( ehci, "\n" ); | |
| } | |
| /** | |
| * Add endpoint to periodic schedule | |
| * | |
| * @v endpoint Endpoint | |
| */ | |
| static void ehci_periodic_add ( struct ehci_endpoint *endpoint ) { | |
| struct ehci_device *ehci = endpoint->ehci; | |
| struct ehci_endpoint *before; | |
| unsigned int interval = endpoint->ep->interval; | |
| /* Find first endpoint with a smaller interval */ | |
| list_for_each_entry ( before, &ehci->periodic, schedule ) { | |
| if ( before->ep->interval < interval ) | |
| break; | |
| } | |
| list_add_tail ( &endpoint->schedule, &before->schedule ); | |
| /* Rebuild schedule */ | |
| ehci_periodic_schedule ( ehci ); | |
| } | |
| /** | |
| * Remove endpoint from periodic schedule | |
| * | |
| * @v endpoint Endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_periodic_del ( struct ehci_endpoint *endpoint ) { | |
| struct ehci_device *ehci = endpoint->ehci; | |
| /* Remove from schedule */ | |
| list_check_contains_entry ( endpoint, &ehci->periodic, schedule ); | |
| list_del ( &endpoint->schedule ); | |
| /* Rebuild schedule */ | |
| ehci_periodic_schedule ( ehci ); | |
| /* Delay for a whole USB frame (with a 100% safety margin) */ | |
| mdelay ( 2 ); | |
| return 0; | |
| } | |
| /** | |
| * Add endpoint to appropriate schedule | |
| * | |
| * @v endpoint Endpoint | |
| */ | |
| static void ehci_schedule_add ( struct ehci_endpoint *endpoint ) { | |
| struct usb_endpoint *ep = endpoint->ep; | |
| unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); | |
| if ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) { | |
| ehci_periodic_add ( endpoint ); | |
| } else { | |
| ehci_async_add ( endpoint ); | |
| } | |
| } | |
| /** | |
| * Remove endpoint from appropriate schedule | |
| * | |
| * @v endpoint Endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_schedule_del ( struct ehci_endpoint *endpoint ) { | |
| struct usb_endpoint *ep = endpoint->ep; | |
| unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); | |
| if ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) { | |
| return ehci_periodic_del ( endpoint ); | |
| } else { | |
| return ehci_async_del ( endpoint ); | |
| } | |
| } | |
| /****************************************************************************** | |
| * | |
| * Endpoint operations | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Determine endpoint characteristics | |
| * | |
| * @v ep USB endpoint | |
| * @ret chr Endpoint characteristics | |
| */ | |
| static uint32_t ehci_endpoint_characteristics ( struct usb_endpoint *ep ) { | |
| struct usb_device *usb = ep->usb; | |
| unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); | |
| uint32_t chr; | |
| /* Determine basic characteristics */ | |
| chr = ( EHCI_CHR_ADDRESS ( usb->address ) | | |
| EHCI_CHR_ENDPOINT ( ep->address ) | | |
| EHCI_CHR_MAX_LEN ( ep->mtu ) ); | |
| /* Control endpoints require manual control of the data toggle */ | |
| if ( attr == USB_ENDPOINT_ATTR_CONTROL ) | |
| chr |= EHCI_CHR_TOGGLE; | |
| /* Determine endpoint speed */ | |
| if ( usb->speed == USB_SPEED_HIGH ) { | |
| chr |= EHCI_CHR_EPS_HIGH; | |
| } else { | |
| if ( usb->speed == USB_SPEED_FULL ) { | |
| chr |= EHCI_CHR_EPS_FULL; | |
| } else { | |
| chr |= EHCI_CHR_EPS_LOW; | |
| } | |
| if ( attr == USB_ENDPOINT_ATTR_CONTROL ) | |
| chr |= EHCI_CHR_CONTROL; | |
| } | |
| return chr; | |
| } | |
| /** | |
| * Determine endpoint capabilities | |
| * | |
| * @v ep USB endpoint | |
| * @ret cap Endpoint capabilities | |
| */ | |
| static uint32_t ehci_endpoint_capabilities ( struct usb_endpoint *ep ) { | |
| struct usb_device *usb = ep->usb; | |
| struct usb_port *tt = usb_transaction_translator ( usb ); | |
| unsigned int attr = ( ep->attributes & USB_ENDPOINT_ATTR_TYPE_MASK ); | |
| uint32_t cap; | |
| unsigned int i; | |
| /* Determine basic capabilities */ | |
| cap = EHCI_CAP_MULT ( ep->burst + 1 ); | |
| /* Determine interrupt schedule mask, if applicable */ | |
| if ( ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) && | |
| ( ( ep->interval != 0 ) /* avoid infinite loop */ ) ) { | |
| for ( i = 0 ; i < 8 /* microframes per frame */ ; | |
| i += ep->interval ) { | |
| cap |= EHCI_CAP_INTR_SCHED ( i ); | |
| } | |
| } | |
| /* Set transaction translator hub address and port, if applicable */ | |
| if ( tt ) { | |
| assert ( tt->hub->usb ); | |
| cap |= ( EHCI_CAP_TT_HUB ( tt->hub->usb->address ) | | |
| EHCI_CAP_TT_PORT ( tt->address ) ); | |
| if ( attr == USB_ENDPOINT_ATTR_INTERRUPT ) | |
| cap |= EHCI_CAP_SPLIT_SCHED_DEFAULT; | |
| } | |
| return cap; | |
| } | |
| /** | |
| * Update endpoint characteristics and capabilities | |
| * | |
| * @v ep USB endpoint | |
| */ | |
| static void ehci_endpoint_update ( struct usb_endpoint *ep ) { | |
| struct ehci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); | |
| struct ehci_queue_head *head; | |
| /* Update queue characteristics and capabilities */ | |
| head = endpoint->ring.head; | |
| head->chr = cpu_to_le32 ( ehci_endpoint_characteristics ( ep ) ); | |
| head->cap = cpu_to_le32 ( ehci_endpoint_capabilities ( ep ) ); | |
| } | |
| /** | |
| * Open endpoint | |
| * | |
| * @v ep USB endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_endpoint_open ( struct usb_endpoint *ep ) { | |
| struct usb_device *usb = ep->usb; | |
| struct ehci_device *ehci = usb_get_hostdata ( usb ); | |
| struct ehci_endpoint *endpoint; | |
| int rc; | |
| /* Allocate and initialise structure */ | |
| endpoint = zalloc ( sizeof ( *endpoint ) ); | |
| if ( ! endpoint ) { | |
| rc = -ENOMEM; | |
| goto err_alloc; | |
| } | |
| endpoint->ehci = ehci; | |
| endpoint->ep = ep; | |
| usb_endpoint_set_hostdata ( ep, endpoint ); | |
| /* Initialise descriptor ring */ | |
| if ( ( rc = ehci_ring_alloc ( ehci, &endpoint->ring ) ) != 0 ) | |
| goto err_ring_alloc; | |
| /* Update queue characteristics and capabilities */ | |
| ehci_endpoint_update ( ep ); | |
| /* Add to list of endpoints */ | |
| list_add_tail ( &endpoint->list, &ehci->endpoints ); | |
| /* Add to schedule */ | |
| ehci_schedule_add ( endpoint ); | |
| return 0; | |
| ehci_ring_free ( &endpoint->ring ); | |
| err_ring_alloc: | |
| free ( endpoint ); | |
| err_alloc: | |
| return rc; | |
| } | |
| /** | |
| * Close endpoint | |
| * | |
| * @v ep USB endpoint | |
| */ | |
| static void ehci_endpoint_close ( struct usb_endpoint *ep ) { | |
| struct ehci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); | |
| struct ehci_device *ehci = endpoint->ehci; | |
| struct usb_device *usb = ep->usb; | |
| struct io_buffer *iobuf; | |
| int rc; | |
| /* Remove from schedule */ | |
| if ( ( rc = ehci_schedule_del ( endpoint ) ) != 0 ) { | |
| /* No way to prevent hardware from continuing to | |
| * access the memory, so leak it. | |
| */ | |
| DBGC ( ehci, "EHCI %s %s could not unschedule: %s\n", | |
| usb->name, usb_endpoint_name ( ep ), strerror ( rc ) ); | |
| return; | |
| } | |
| /* Cancel any incomplete transfers */ | |
| while ( ehci_ring_fill ( &endpoint->ring ) ) { | |
| iobuf = ehci_dequeue ( &endpoint->ring ); | |
| if ( iobuf ) | |
| usb_complete_err ( ep, iobuf, -ECANCELED ); | |
| } | |
| /* Remove from list of endpoints */ | |
| list_del ( &endpoint->list ); | |
| /* Free descriptor ring */ | |
| ehci_ring_free ( &endpoint->ring ); | |
| /* Free endpoint */ | |
| free ( endpoint ); | |
| } | |
| /** | |
| * Reset endpoint | |
| * | |
| * @v ep USB endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_endpoint_reset ( struct usb_endpoint *ep ) { | |
| struct ehci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); | |
| struct ehci_ring *ring = &endpoint->ring; | |
| struct ehci_transfer_descriptor *cache = &ring->head->cache; | |
| uint32_t link; | |
| /* Sanity checks */ | |
| assert ( ! ( cache->status & EHCI_STATUS_ACTIVE ) ); | |
| assert ( cache->status & EHCI_STATUS_HALTED ); | |
| /* Reset residual count */ | |
| ring->residual = 0; | |
| /* Reset data toggle */ | |
| cache->len = 0; | |
| /* Prepare to restart at next unconsumed descriptor */ | |
| link = virt_to_phys ( &ring->desc[ ring->cons % EHCI_RING_COUNT ] ); | |
| cache->next = cpu_to_le32 ( link ); | |
| /* Restart ring */ | |
| wmb(); | |
| cache->status = 0; | |
| return 0; | |
| } | |
| /** | |
| * Update MTU | |
| * | |
| * @v ep USB endpoint | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_endpoint_mtu ( struct usb_endpoint *ep ) { | |
| /* Update endpoint characteristics and capabilities */ | |
| ehci_endpoint_update ( ep ); | |
| return 0; | |
| } | |
| /** | |
| * Enqueue message transfer | |
| * | |
| * @v ep USB endpoint | |
| * @v iobuf I/O buffer | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_endpoint_message ( struct usb_endpoint *ep, | |
| struct io_buffer *iobuf ) { | |
| struct ehci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); | |
| struct ehci_device *ehci = endpoint->ehci; | |
| struct usb_setup_packet *packet; | |
| unsigned int input; | |
| struct ehci_transfer xfers[3]; | |
| struct ehci_transfer *xfer = xfers; | |
| size_t len; | |
| int rc; | |
| /* Construct setup stage */ | |
| assert ( iob_len ( iobuf ) >= sizeof ( *packet ) ); | |
| packet = iobuf->data; | |
| iob_pull ( iobuf, sizeof ( *packet ) ); | |
| xfer->data = packet; | |
| xfer->len = sizeof ( *packet ); | |
| xfer->flags = EHCI_FL_PID_SETUP; | |
| xfer++; | |
| /* Construct data stage, if applicable */ | |
| len = iob_len ( iobuf ); | |
| input = ( packet->request & cpu_to_le16 ( USB_DIR_IN ) ); | |
| if ( len ) { | |
| xfer->data = iobuf->data; | |
| xfer->len = len; | |
| xfer->flags = ( EHCI_FL_TOGGLE | | |
| ( input ? EHCI_FL_PID_IN : EHCI_FL_PID_OUT ) ); | |
| xfer++; | |
| } | |
| /* Construct status stage */ | |
| xfer->data = NULL; | |
| xfer->len = 0; | |
| xfer->flags = ( EHCI_FL_TOGGLE | EHCI_FL_IOC | | |
| ( ( len && input ) ? EHCI_FL_PID_OUT : EHCI_FL_PID_IN)); | |
| xfer++; | |
| /* Enqueue transfer */ | |
| if ( ( rc = ehci_enqueue ( ehci, &endpoint->ring, iobuf, xfers, | |
| ( xfer - xfers ) ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** | |
| * Calculate number of transfer descriptors | |
| * | |
| * @v len Length of data | |
| * @v zlp Append a zero-length packet | |
| * @ret count Number of transfer descriptors | |
| */ | |
| static unsigned int ehci_endpoint_count ( size_t len, int zlp ) { | |
| unsigned int count; | |
| /* Split into 16kB transfers. A single transfer can handle up | |
| * to 20kB if it happens to be page-aligned, or up to 16kB | |
| * with arbitrary alignment. We simplify the code by assuming | |
| * that we can fit only 16kB into each transfer. | |
| */ | |
| count = ( ( len + EHCI_MTU - 1 ) / EHCI_MTU ); | |
| /* Append a zero-length transfer if applicable */ | |
| if ( zlp || ( count == 0 ) ) | |
| count++; | |
| return count; | |
| } | |
| /** | |
| * Enqueue stream transfer | |
| * | |
| * @v ep USB endpoint | |
| * @v iobuf I/O buffer | |
| * @v zlp Append a zero-length packet | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_endpoint_stream ( struct usb_endpoint *ep, | |
| struct io_buffer *iobuf, int zlp ) { | |
| struct ehci_endpoint *endpoint = usb_endpoint_get_hostdata ( ep ); | |
| struct ehci_device *ehci = endpoint->ehci; | |
| void *data = iobuf->data; | |
| size_t len = iob_len ( iobuf ); | |
| unsigned int count = ehci_endpoint_count ( len, zlp ); | |
| unsigned int input = ( ep->address & USB_DIR_IN ); | |
| unsigned int flags = ( input ? EHCI_FL_PID_IN : EHCI_FL_PID_OUT ); | |
| struct ehci_transfer xfers[count]; | |
| struct ehci_transfer *xfer = xfers; | |
| size_t xfer_len; | |
| unsigned int i; | |
| int rc; | |
| /* Create transfers */ | |
| for ( i = 0 ; i < count ; i++ ) { | |
| /* Calculate transfer length */ | |
| xfer_len = EHCI_MTU; | |
| if ( xfer_len > len ) | |
| xfer_len = len; | |
| /* Create transfer */ | |
| xfer->data = data; | |
| xfer->len = xfer_len; | |
| xfer->flags = flags; | |
| /* Move to next transfer */ | |
| data += xfer_len; | |
| len -= xfer_len; | |
| xfer++; | |
| } | |
| xfer[-1].flags |= EHCI_FL_IOC; | |
| /* Enqueue transfer */ | |
| if ( ( rc = ehci_enqueue ( ehci, &endpoint->ring, iobuf, xfers, | |
| count ) ) != 0 ) | |
| return rc; | |
| return 0; | |
| } | |
| /** | |
| * Poll for completions | |
| * | |
| * @v endpoint Endpoint | |
| */ | |
| static void ehci_endpoint_poll ( struct ehci_endpoint *endpoint ) { | |
| struct ehci_device *ehci = endpoint->ehci; | |
| struct ehci_ring *ring = &endpoint->ring; | |
| struct ehci_transfer_descriptor *desc; | |
| struct usb_endpoint *ep = endpoint->ep; | |
| struct usb_device *usb = ep->usb; | |
| struct io_buffer *iobuf; | |
| unsigned int index; | |
| unsigned int status; | |
| int rc; | |
| /* Consume all completed descriptors */ | |
| while ( ehci_ring_fill ( &endpoint->ring ) ) { | |
| /* Stop if we reach an uncompleted descriptor */ | |
| rmb(); | |
| index = ( ring->cons % EHCI_RING_COUNT ); | |
| desc = &ring->desc[index]; | |
| status = desc->status; | |
| if ( status & EHCI_STATUS_ACTIVE ) | |
| break; | |
| /* Consume this descriptor */ | |
| iobuf = ehci_dequeue ( ring ); | |
| /* If we have encountered an error, then consume all | |
| * remaining descriptors in this transaction, report | |
| * the error to the USB core, and stop further | |
| * processing. | |
| */ | |
| if ( status & EHCI_STATUS_HALTED ) { | |
| rc = -EIO_STATUS ( status ); | |
| DBGC ( ehci, "EHCI %s %s completion %d failed (status " | |
| "%02x): %s\n", usb->name, | |
| usb_endpoint_name ( ep ), index, status, | |
| strerror ( rc ) ); | |
| while ( ! iobuf ) | |
| iobuf = ehci_dequeue ( ring ); | |
| usb_complete_err ( endpoint->ep, iobuf, rc ); | |
| return; | |
| } | |
| /* Accumulate residual data count */ | |
| ring->residual += ( le16_to_cpu ( desc->len ) & EHCI_LEN_MASK ); | |
| /* If this is not the end of a transaction (i.e. has | |
| * no I/O buffer), then continue to next descriptor. | |
| */ | |
| if ( ! iobuf ) | |
| continue; | |
| /* Update I/O buffer length */ | |
| iob_unput ( iobuf, ring->residual ); | |
| ring->residual = 0; | |
| /* Report completion to USB core */ | |
| usb_complete ( endpoint->ep, iobuf ); | |
| } | |
| } | |
| /****************************************************************************** | |
| * | |
| * Device operations | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** | |
| * Open device | |
| * | |
| * @v usb USB device | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_device_open ( struct usb_device *usb ) { | |
| struct ehci_device *ehci = usb_bus_get_hostdata ( usb->port->hub->bus ); | |
| usb_set_hostdata ( usb, ehci ); | |
| return 0; | |
| } | |
| /** | |
| * Close device | |
| * | |
| * @v usb USB device | |
| */ | |
| static void ehci_device_close ( struct usb_device *usb ) { | |
| struct ehci_device *ehci = usb_get_hostdata ( usb ); | |
| struct usb_bus *bus = ehci->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 ehci_device_address ( struct usb_device *usb ) { | |
| struct ehci_device *ehci = usb_get_hostdata ( usb ); | |
| struct usb_bus *bus = ehci->bus; | |
| struct usb_endpoint *ep0 = usb_endpoint ( usb, USB_EP0_ADDRESS ); | |
| 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 ( ehci, "EHCI %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; | |
| /* Update control endpoint characteristics and capabilities */ | |
| ehci_endpoint_update ( ep0 ); | |
| 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 ehci_hub_open ( struct usb_hub *hub __unused ) { | |
| /* Nothing to do */ | |
| return 0; | |
| } | |
| /** | |
| * Close hub | |
| * | |
| * @v hub USB hub | |
| */ | |
| static void ehci_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 ehci_root_open ( struct usb_hub *hub ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| uint32_t portsc; | |
| unsigned int i; | |
| /* Route all ports to EHCI controller */ | |
| writel ( EHCI_CONFIGFLAG_CF, ehci->op + EHCI_OP_CONFIGFLAG ); | |
| /* Enable power to all ports */ | |
| for ( i = 1 ; i <= ehci->ports ; i++ ) { | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( i ) ); | |
| portsc &= ~EHCI_PORTSC_CHANGE; | |
| portsc |= EHCI_PORTSC_PP; | |
| writel ( portsc, ehci->op + EHCI_OP_PORTSC ( i ) ); | |
| } | |
| /* Wait 20ms after potentially enabling power to a port */ | |
| mdelay ( EHCI_PORT_POWER_DELAY_MS ); | |
| return 0; | |
| } | |
| /** | |
| * Close root hub | |
| * | |
| * @v hub USB hub | |
| */ | |
| static void ehci_root_close ( struct usb_hub *hub ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| /* Route all ports back to companion controllers */ | |
| writel ( 0, ehci->op + EHCI_OP_CONFIGFLAG ); | |
| } | |
| /** | |
| * Enable port | |
| * | |
| * @v hub USB hub | |
| * @v port USB port | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_root_enable ( struct usb_hub *hub, struct usb_port *port ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| uint32_t portsc; | |
| unsigned int line; | |
| unsigned int i; | |
| /* Check for a low-speed device */ | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| line = EHCI_PORTSC_LINE_STATUS ( portsc ); | |
| if ( line == EHCI_PORTSC_LINE_STATUS_LOW ) { | |
| DBGC ( ehci, "EHCI %s-%d detected low-speed device: " | |
| "disowning\n", ehci->name, port->address ); | |
| goto disown; | |
| } | |
| /* Reset port */ | |
| portsc &= ~( EHCI_PORTSC_PED | EHCI_PORTSC_CHANGE ); | |
| portsc |= EHCI_PORTSC_PR; | |
| writel ( portsc, ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| mdelay ( USB_RESET_DELAY_MS ); | |
| portsc &= ~EHCI_PORTSC_PR; | |
| writel ( portsc, ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| /* Wait for reset to complete */ | |
| for ( i = 0 ; i < EHCI_PORT_RESET_MAX_WAIT_MS ; i++ ) { | |
| /* Check port status */ | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| if ( ! ( portsc & EHCI_PORTSC_PR ) ) { | |
| if ( portsc & EHCI_PORTSC_PED ) | |
| return 0; | |
| DBGC ( ehci, "EHCI %s-%d not enabled after reset: " | |
| "disowning\n", ehci->name, port->address ); | |
| goto disown; | |
| } | |
| /* Delay */ | |
| mdelay ( 1 ); | |
| } | |
| DBGC ( ehci, "EHCI %s-%d timed out waiting for port to reset\n", | |
| ehci->name, port->address ); | |
| return -ETIMEDOUT; | |
| disown: | |
| /* Disown port */ | |
| portsc &= ~EHCI_PORTSC_CHANGE; | |
| portsc |= EHCI_PORTSC_OWNER; | |
| writel ( portsc, ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| /* Delay to allow child companion controllers to settle */ | |
| mdelay ( EHCI_DISOWN_DELAY_MS ); | |
| /* Poll child companion controllers */ | |
| ehci_poll_companions ( ehci ); | |
| return -ENODEV; | |
| } | |
| /** | |
| * Disable port | |
| * | |
| * @v hub USB hub | |
| * @v port USB port | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_root_disable ( struct usb_hub *hub, struct usb_port *port ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| uint32_t portsc; | |
| /* Disable port */ | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| portsc &= ~( EHCI_PORTSC_PED | EHCI_PORTSC_CHANGE ); | |
| writel ( portsc, ehci->op + EHCI_OP_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 ehci_root_speed ( struct usb_hub *hub, struct usb_port *port ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| uint32_t portsc; | |
| unsigned int speed; | |
| unsigned int line; | |
| int ccs; | |
| int csc; | |
| int ped; | |
| /* Read port status */ | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| DBGC2 ( ehci, "EHCI %s-%d status is %08x\n", | |
| ehci->name, port->address, portsc ); | |
| ccs = ( portsc & EHCI_PORTSC_CCS ); | |
| csc = ( portsc & EHCI_PORTSC_CSC ); | |
| ped = ( portsc & EHCI_PORTSC_PED ); | |
| line = EHCI_PORTSC_LINE_STATUS ( portsc ); | |
| /* Record disconnections and clear changes */ | |
| port->disconnected |= csc; | |
| writel ( portsc, ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| /* Determine port speed */ | |
| if ( ! ccs ) { | |
| /* Port not connected */ | |
| speed = USB_SPEED_NONE; | |
| } else if ( line == EHCI_PORTSC_LINE_STATUS_LOW ) { | |
| /* Detected as low-speed */ | |
| speed = USB_SPEED_LOW; | |
| } else if ( ped ) { | |
| /* Port already enabled: must be high-speed */ | |
| speed = USB_SPEED_HIGH; | |
| } else { | |
| /* Not low-speed and not yet enabled. Could be either | |
| * full-speed or high-speed; we can't yet tell. | |
| */ | |
| speed = USB_SPEED_FULL; | |
| } | |
| port->speed = speed; | |
| 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 ehci_root_clear_tt ( struct usb_hub *hub, struct usb_port *port, | |
| struct usb_endpoint *ep ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| /* Should never be called; this is a root hub */ | |
| DBGC ( ehci, "EHCI %s-%d nonsensical CLEAR_TT for %s %s\n", ehci->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 ehci_root_poll ( struct usb_hub *hub, struct usb_port *port ) { | |
| struct ehci_device *ehci = usb_hub_get_drvdata ( hub ); | |
| uint32_t portsc; | |
| uint32_t change; | |
| /* Do nothing unless something has changed */ | |
| portsc = readl ( ehci->op + EHCI_OP_PORTSC ( port->address ) ); | |
| change = ( portsc & EHCI_PORTSC_CHANGE ); | |
| if ( ! change ) | |
| return; | |
| /* Record disconnections and clear changes */ | |
| port->disconnected |= ( portsc & EHCI_PORTSC_CSC ); | |
| writel ( portsc, ehci->op + EHCI_OP_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 ehci_bus_open ( struct usb_bus *bus ) { | |
| struct ehci_device *ehci = usb_bus_get_hostdata ( bus ); | |
| unsigned int frames; | |
| size_t len; | |
| int rc; | |
| /* Sanity checks */ | |
| assert ( list_empty ( &ehci->async ) ); | |
| assert ( list_empty ( &ehci->periodic ) ); | |
| /* Allocate and initialise asynchronous queue head */ | |
| ehci->head = malloc_phys ( sizeof ( *ehci->head ), | |
| ehci_align ( sizeof ( *ehci->head ) ) ); | |
| if ( ! ehci->head ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_head; | |
| } | |
| memset ( ehci->head, 0, sizeof ( *ehci->head ) ); | |
| ehci->head->chr = cpu_to_le32 ( EHCI_CHR_HEAD ); | |
| ehci->head->cache.next = cpu_to_le32 ( EHCI_LINK_TERMINATE ); | |
| ehci->head->cache.status = EHCI_STATUS_HALTED; | |
| ehci_async_schedule ( ehci ); | |
| writel ( virt_to_phys ( ehci->head ), | |
| ehci->op + EHCI_OP_ASYNCLISTADDR ); | |
| /* Use async queue head to determine control data structure segment */ | |
| ehci->ctrldssegment = | |
| ( ( ( uint64_t ) virt_to_phys ( ehci->head ) ) >> 32 ); | |
| if ( ehci->addr64 ) { | |
| writel ( ehci->ctrldssegment, ehci->op + EHCI_OP_CTRLDSSEGMENT); | |
| } else if ( ehci->ctrldssegment ) { | |
| DBGC ( ehci, "EHCI %s CTRLDSSEGMENT not supported\n", | |
| ehci->name ); | |
| rc = -ENOTSUP; | |
| goto err_ctrldssegment; | |
| } | |
| /* Allocate periodic frame list */ | |
| frames = EHCI_PERIODIC_FRAMES ( ehci->flsize ); | |
| len = ( frames * sizeof ( ehci->frame[0] ) ); | |
| ehci->frame = malloc_phys ( len, EHCI_PAGE_ALIGN ); | |
| if ( ! ehci->frame ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_frame; | |
| } | |
| if ( ( rc = ehci_ctrl_reachable ( ehci, ehci->frame ) ) != 0 ) { | |
| DBGC ( ehci, "EHCI %s frame list unreachable\n", ehci->name ); | |
| goto err_unreachable_frame; | |
| } | |
| ehci_periodic_schedule ( ehci ); | |
| writel ( virt_to_phys ( ehci->frame ), | |
| ehci->op + EHCI_OP_PERIODICLISTBASE ); | |
| /* Start controller */ | |
| ehci_run ( ehci ); | |
| return 0; | |
| ehci_stop ( ehci ); | |
| err_unreachable_frame: | |
| free_phys ( ehci->frame, len ); | |
| err_alloc_frame: | |
| err_ctrldssegment: | |
| free_phys ( ehci->head, sizeof ( *ehci->head ) ); | |
| err_alloc_head: | |
| return rc; | |
| } | |
| /** | |
| * Close USB bus | |
| * | |
| * @v bus USB bus | |
| */ | |
| static void ehci_bus_close ( struct usb_bus *bus ) { | |
| struct ehci_device *ehci = usb_bus_get_hostdata ( bus ); | |
| unsigned int frames = EHCI_PERIODIC_FRAMES ( ehci->flsize ); | |
| /* Sanity checks */ | |
| assert ( list_empty ( &ehci->async ) ); | |
| assert ( list_empty ( &ehci->periodic ) ); | |
| /* Stop controller */ | |
| ehci_stop ( ehci ); | |
| /* Free periodic frame list */ | |
| free_phys ( ehci->frame, ( frames * sizeof ( ehci->frame[0] ) ) ); | |
| /* Free asynchronous schedule */ | |
| free_phys ( ehci->head, sizeof ( *ehci->head ) ); | |
| } | |
| /** | |
| * Poll USB bus | |
| * | |
| * @v bus USB bus | |
| */ | |
| static void ehci_bus_poll ( struct usb_bus *bus ) { | |
| struct ehci_device *ehci = usb_bus_get_hostdata ( bus ); | |
| struct usb_hub *hub = bus->hub; | |
| struct ehci_endpoint *endpoint; | |
| unsigned int i; | |
| uint32_t usbsts; | |
| uint32_t change; | |
| /* Do nothing unless something has changed */ | |
| usbsts = readl ( ehci->op + EHCI_OP_USBSTS ); | |
| assert ( usbsts & EHCI_USBSTS_ASYNC ); | |
| assert ( usbsts & EHCI_USBSTS_PERIODIC ); | |
| assert ( ! ( usbsts & EHCI_USBSTS_HCH ) ); | |
| change = ( usbsts & EHCI_USBSTS_CHANGE ); | |
| if ( ! change ) | |
| return; | |
| /* Acknowledge changes */ | |
| writel ( usbsts, ehci->op + EHCI_OP_USBSTS ); | |
| /* Process completions, if applicable */ | |
| if ( change & ( EHCI_USBSTS_USBINT | EHCI_USBSTS_USBERRINT ) ) { | |
| /* Iterate over all endpoints looking for completed | |
| * descriptors. 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, &ehci->endpoints, list ) | |
| ehci_endpoint_poll ( endpoint ); | |
| } | |
| /* Process port status changes, if applicable */ | |
| if ( change & EHCI_USBSTS_PORT ) { | |
| /* Iterate over all ports looking for status changes */ | |
| for ( i = 1 ; i <= ehci->ports ; i++ ) | |
| ehci_root_poll ( hub, usb_port ( hub, i ) ); | |
| } | |
| /* Report fatal errors */ | |
| if ( change & EHCI_USBSTS_SYSERR ) | |
| DBGC ( ehci, "EHCI %s host system error\n", ehci->name ); | |
| } | |
| /****************************************************************************** | |
| * | |
| * PCI interface | |
| * | |
| ****************************************************************************** | |
| */ | |
| /** USB host controller operations */ | |
| static struct usb_host_operations ehci_operations = { | |
| .endpoint = { | |
| .open = ehci_endpoint_open, | |
| .close = ehci_endpoint_close, | |
| .reset = ehci_endpoint_reset, | |
| .mtu = ehci_endpoint_mtu, | |
| .message = ehci_endpoint_message, | |
| .stream = ehci_endpoint_stream, | |
| }, | |
| .device = { | |
| .open = ehci_device_open, | |
| .close = ehci_device_close, | |
| .address = ehci_device_address, | |
| }, | |
| .bus = { | |
| .open = ehci_bus_open, | |
| .close = ehci_bus_close, | |
| .poll = ehci_bus_poll, | |
| }, | |
| .hub = { | |
| .open = ehci_hub_open, | |
| .close = ehci_hub_close, | |
| }, | |
| .root = { | |
| .open = ehci_root_open, | |
| .close = ehci_root_close, | |
| .enable = ehci_root_enable, | |
| .disable = ehci_root_disable, | |
| .speed = ehci_root_speed, | |
| .clear_tt = ehci_root_clear_tt, | |
| }, | |
| }; | |
| /** | |
| * Probe PCI device | |
| * | |
| * @v pci PCI device | |
| * @ret rc Return status code | |
| */ | |
| static int ehci_probe ( struct pci_device *pci ) { | |
| struct ehci_device *ehci; | |
| struct usb_port *port; | |
| unsigned long bar_start; | |
| size_t bar_size; | |
| unsigned int i; | |
| int rc; | |
| /* Allocate and initialise structure */ | |
| ehci = zalloc ( sizeof ( *ehci ) ); | |
| if ( ! ehci ) { | |
| rc = -ENOMEM; | |
| goto err_alloc; | |
| } | |
| ehci->name = pci->dev.name; | |
| INIT_LIST_HEAD ( &ehci->endpoints ); | |
| INIT_LIST_HEAD ( &ehci->async ); | |
| INIT_LIST_HEAD ( &ehci->periodic ); | |
| /* Fix up PCI device */ | |
| adjust_pci_device ( pci ); | |
| /* Map registers */ | |
| bar_start = pci_bar_start ( pci, EHCI_BAR ); | |
| bar_size = pci_bar_size ( pci, EHCI_BAR ); | |
| ehci->regs = pci_ioremap ( pci, bar_start, bar_size ); | |
| if ( ! ehci->regs ) { | |
| rc = -ENODEV; | |
| goto err_ioremap; | |
| } | |
| /* Initialise EHCI device */ | |
| ehci_init ( ehci, ehci->regs ); | |
| /* Initialise USB legacy support and claim ownership */ | |
| ehci_legacy_init ( ehci, pci ); | |
| ehci_legacy_claim ( ehci, pci ); | |
| /* Reset device */ | |
| if ( ( rc = ehci_reset ( ehci ) ) != 0 ) | |
| goto err_reset; | |
| /* Allocate USB bus */ | |
| ehci->bus = alloc_usb_bus ( &pci->dev, ehci->ports, EHCI_MTU, | |
| &ehci_operations ); | |
| if ( ! ehci->bus ) { | |
| rc = -ENOMEM; | |
| goto err_alloc_bus; | |
| } | |
| usb_bus_set_hostdata ( ehci->bus, ehci ); | |
| usb_hub_set_drvdata ( ehci->bus->hub, ehci ); | |
| /* Set port protocols */ | |
| for ( i = 1 ; i <= ehci->ports ; i++ ) { | |
| port = usb_port ( ehci->bus->hub, i ); | |
| port->protocol = USB_PROTO_2_0; | |
| } | |
| /* Register USB bus */ | |
| if ( ( rc = register_usb_bus ( ehci->bus ) ) != 0 ) | |
| goto err_register; | |
| pci_set_drvdata ( pci, ehci ); | |
| return 0; | |
| unregister_usb_bus ( ehci->bus ); | |
| err_register: | |
| free_usb_bus ( ehci->bus ); | |
| err_alloc_bus: | |
| ehci_reset ( ehci ); | |
| err_reset: | |
| ehci_legacy_release ( ehci, pci ); | |
| iounmap ( ehci->regs ); | |
| err_ioremap: | |
| free ( ehci ); | |
| err_alloc: | |
| return rc; | |
| } | |
| /** | |
| * Remove PCI device | |
| * | |
| * @v pci PCI device | |
| */ | |
| static void ehci_remove ( struct pci_device *pci ) { | |
| struct ehci_device *ehci = pci_get_drvdata ( pci ); | |
| struct usb_bus *bus = ehci->bus; | |
| unregister_usb_bus ( bus ); | |
| assert ( list_empty ( &ehci->async ) ); | |
| assert ( list_empty ( &ehci->periodic ) ); | |
| free_usb_bus ( bus ); | |
| ehci_reset ( ehci ); | |
| ehci_legacy_release ( ehci, pci ); | |
| iounmap ( ehci->regs ); | |
| free ( ehci ); | |
| } | |
| /** EHCI PCI device IDs */ | |
| static struct pci_device_id ehci_ids[] = { | |
| PCI_ROM ( 0xffff, 0xffff, "ehci", "EHCI", 0 ), | |
| }; | |
| /** EHCI PCI driver */ | |
| struct pci_driver ehci_driver __pci_driver = { | |
| .ids = ehci_ids, | |
| .id_count = ( sizeof ( ehci_ids ) / sizeof ( ehci_ids[0] ) ), | |
| .class = PCI_CLASS_ID ( PCI_CLASS_SERIAL, PCI_CLASS_SERIAL_USB, | |
| PCI_CLASS_SERIAL_USB_EHCI ), | |
| .probe = ehci_probe, | |
| .remove = ehci_remove, | |
| }; | |
| /** | |
| * Prepare for exit | |
| * | |
| * @v booting System is shutting down for OS boot | |
| */ | |
| static void ehci_shutdown ( int booting ) { | |
| /* If we are shutting down to boot an OS, then prevent the | |
| * release of ownership back to BIOS. | |
| */ | |
| ehci_legacy_prevent_release = booting; | |
| } | |
| /** Startup/shutdown function */ | |
| struct startup_fn ehci_startup __startup_fn ( STARTUP_LATE ) = { | |
| .name = "ehci", | |
| .shutdown = ehci_shutdown, | |
| }; |