| /* |
| * 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 ); |
| |
| #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, |
| }; |