summaryrefslogblamecommitdiffstats
path: root/src/drivers/usb/ehci.c
blob: cd39670707e7af286500a91d92a7cbd6067de673 (plain) (tree)












































































                                                                               
                                                                 




                                                                        
                                                                         






                                                                   
                                                               




















































































                                                                                






















































                                                                               






















                                                                                

                                                                              






                                                                              

                                                                             























                                                                    







                                                                        










                                                                             

                                                                             



                                                                               

                                                                              










                                                                

                                                                             










                                                                              

                                           

                                                                    
                       



                                                            

                                                                         



                               
                                                                         
                                                                          





                                                                        



                                                                               






































                                                                                

                                                               



                                 

























                                                                           

                                                                               

















































                                                                               
                                                                          




































                                                                          
                                                                           











































                                                                               
                                                                              
















                                                                         

                                                                        

















































































                                                                                

                                    














                                                                           
                                                                 
 
                                              

                                  





































































































































































                                                                                

                                                                           




















                                                                     
                                                                  












                                                                             
                                                                   












































































































































                                                                               
                                             
                                         
                
                                                     



                                                 

                                                        











                                                                        

                                                                 















                                                                             








                                                                            














































































                                                                          

                                                                              






































































                                                                            



                                                           


                                                                          

                                        

                                           
                   


                                   


                                                           

                                       
                                        


                                                 

                                                                 























                                                                                























                                                                      



                                    
                                                   


                                                          
                                                                      

                                                                          


                                                              
                                                          

                                                                          
                                           

                        

               















                                               

                       
                                      


                                                                       
                                                   









































                                                                    



                                                                               














































































                                                                                

                                                                        






















                                                                               




























                                                                               










                                                                               
                                                   





























                                                                     
                                                     















                                                               
                                                                            








                                                                       

                                                                     


















                                                                               

                                                                           






                                    

                                                                        


                          
                         


                                                                       






                                                                  









                                          
                                                                             

















                                                                       
                                                                           




                                                               
                



                                                                       

                                                    
                                           
                                           


                                                  



                                                                       




















                                                                      






                                          

                                                                           


                                                               

                                                                               




                        




                                
                                                                           









                                                                       

                                                           

















































                                                                                

                                                                     












                                                                        
                                                                              





















































































                                                                         
                                                                    



                                          
                                                                         































                                                                               







                                               






















                                                  
                                   



























































































                                                                     

                                                                       

















                                                                  
                       

                                  
/*
 * 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_dma ( 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_dma ( 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_dma ( ring->desc, len );
 err_alloc_desc:
 err_unreachable_queue:
	free_dma ( 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_dma ( ring->desc, ( EHCI_RING_COUNT * sizeof ( ring->desc[0] ) ) );

	/* Free queue head */
	free_dma ( 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 usb_bus *bus = hub->bus;
	struct ehci_device *ehci = usb_bus_get_hostdata ( bus );
	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 );

	/* Record hub driver private data */
	usb_hub_set_drvdata ( hub, ehci );

	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 );

	/* Clear hub driver private data */
	usb_hub_set_drvdata ( hub, NULL );
}

/**
 * Enable port
 *
 * @v hub		USB hub
 * @v port		USB port
 * @ret rc		Return status code
 */
static int 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_dma ( 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_dma ( 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_dma ( ehci->frame, len );
 err_alloc_frame:
 err_ctrldssegment:
	free_dma ( 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_dma ( ehci->frame, ( frames * sizeof ( ehci->frame[0] ) ) );

	/* Free asynchronous schedule */
	free_dma ( 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 = ioremap ( 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,
};