blob: 28d6cb33d8147ceb3544f750dbb37622ca6e55d1 [file] [log] [blame]
/*
* 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 <string.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <byteswap.h>
#include <ipxe/usb.h>
#include "usbhub.h"
/** @file
*
* USB hub driver
*
*/
/**
* Refill interrupt ring
*
* @v hubdev Hub device
*/
static void hub_refill ( struct usb_hub_device *hubdev ) {
int rc;
/* Refill interrupt endpoint */
if ( ( rc = usb_refill ( &hubdev->intr ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not refill interrupt: %s\n",
hubdev->name, strerror ( rc ) );
/* Continue attempting to refill */
return;
}
/* Stop refill process */
process_del ( &hubdev->refill );
}
/** Refill process descriptor */
static struct process_descriptor hub_refill_desc =
PROC_DESC ( struct usb_hub_device, refill, hub_refill );
/**
* Complete interrupt transfer
*
* @v ep USB endpoint
* @v iobuf I/O buffer
* @v rc Completion status code
*/
static void hub_complete ( struct usb_endpoint *ep,
struct io_buffer *iobuf, int rc ) {
struct usb_hub_device *hubdev =
container_of ( ep, struct usb_hub_device, intr );
struct usb_hub *hub = hubdev->hub;
uint8_t *data = iobuf->data;
unsigned int bits = ( 8 * iob_len ( iobuf ) );
unsigned int i;
/* Ignore packets cancelled when the endpoint closes */
if ( ! ep->open )
goto done;
/* Ignore packets with errors */
if ( rc != 0 ) {
DBGC ( hubdev, "HUB %s interrupt failed: %s\n",
hubdev->name, strerror ( rc ) );
DBGC_HDA ( hubdev, 0, iobuf->data, iob_len ( iobuf ) );
goto done;
}
/* Report any port status changes */
for ( i = 1 ; i <= hub->ports ; i++ ) {
/* Sanity check */
if ( i > bits ) {
DBGC ( hubdev, "HUB %s underlength interrupt:\n",
hubdev->name );
DBGC_HDA ( hubdev, 0, iobuf->data, iob_len ( iobuf ) );
goto done;
}
/* Report port status change if applicable */
if ( data[ i / 8 ] & ( 1 << ( i % 8 ) ) ) {
DBGC2 ( hubdev, "HUB %s port %d status changed\n",
hubdev->name, i );
usb_port_changed ( usb_port ( hub, i ) );
}
}
done:
/* Recycle I/O buffer */
usb_recycle ( &hubdev->intr, iobuf );
/* Start refill process */
process_add ( &hubdev->refill );
}
/** Interrupt endpoint operations */
static struct usb_endpoint_driver_operations usb_hub_intr_operations = {
.complete = hub_complete,
};
/**
* Open hub
*
* @v hub USB hub
* @ret rc Return status code
*/
static int hub_open ( struct usb_hub *hub ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
struct usb_device *usb = hubdev->usb;
unsigned int i;
int rc;
/* Ensure ports are powered */
for ( i = 1 ; i <= hub->ports ; i++ ) {
if ( ( rc = usb_hub_set_port_feature ( usb, i,
USB_HUB_PORT_POWER,
0 ) ) != 0 ) {
DBGC ( hubdev, "HUB %s port %d could not apply power: "
"%s\n", hubdev->name, i, strerror ( rc ) );
goto err_power;
}
}
/* Open interrupt endpoint */
if ( ( rc = usb_endpoint_open ( &hubdev->intr ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not register interrupt: %s\n",
hubdev->name, strerror ( rc ) );
goto err_open;
}
/* Start refill process */
process_add ( &hubdev->refill );
/* Refill interrupt ring */
hub_refill ( hubdev );
/* Delay to allow ports to stabilise on out-of-spec hubs */
if ( hubdev->flags & USB_HUB_SLOW_START )
mdelay ( USB_HUB_SLOW_START_DELAY_MS );
return 0;
usb_endpoint_close ( &hubdev->intr );
err_open:
err_power:
return rc;
}
/**
* Close hub
*
* @v hub USB hub
*/
static void hub_close ( struct usb_hub *hub ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
/* Close interrupt endpoint */
usb_endpoint_close ( &hubdev->intr );
/* Stop refill process */
process_del ( &hubdev->refill );
}
/**
* Enable port
*
* @v hub USB hub
* @v port USB port
* @ret rc Return status code
*/
static int hub_enable ( struct usb_hub *hub, struct usb_port *port ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
struct usb_device *usb = hubdev->usb;
struct usb_hub_port_status status;
unsigned int current;
unsigned int i;
int rc;
/* Initiate reset if applicable */
if ( ( hub->protocol < USB_PROTO_3_0 ) &&
( ( rc = usb_hub_set_port_feature ( usb, port->address,
USB_HUB_PORT_RESET, 0 ) )!=0)){
DBGC ( hubdev, "HUB %s port %d could not initiate reset: %s\n",
hubdev->name, port->address, strerror ( rc ) );
return rc;
}
/* Wait for port to become enabled */
for ( i = 0 ; i < USB_HUB_ENABLE_MAX_WAIT_MS ; i++ ) {
/* Check for port being enabled */
if ( ( rc = usb_hub_get_port_status ( usb, port->address,
&status ) ) != 0 ) {
DBGC ( hubdev, "HUB %s port %d could not get status: "
"%s\n", hubdev->name, port->address,
strerror ( rc ) );
return rc;
}
current = le16_to_cpu ( status.current );
if ( current & ( 1 << USB_HUB_PORT_ENABLE ) )
return 0;
/* Delay */
mdelay ( 1 );
}
DBGC ( hubdev, "HUB %s port %d timed out waiting for enable\n",
hubdev->name, port->address );
return -ETIMEDOUT;
}
/**
* Disable port
*
* @v hub USB hub
* @v port USB port
* @ret rc Return status code
*/
static int hub_disable ( struct usb_hub *hub, struct usb_port *port ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
struct usb_device *usb = hubdev->usb;
int rc;
/* Disable port */
if ( ( hub->protocol < USB_PROTO_3_0 ) &&
( ( rc = usb_hub_clear_port_feature ( usb, port->address,
USB_HUB_PORT_ENABLE,
0 ) ) != 0 ) ) {
DBGC ( hubdev, "HUB %s port %d could not disable: %s\n",
hubdev->name, port->address, strerror ( rc ) );
return rc;
}
return 0;
}
/**
* Clear port status change bits
*
* @v hubdev USB hub device
* @v port Port number
* @v changed Port status change bits
* @ret rc Return status code
*/
static int hub_clear_changes ( struct usb_hub_device *hubdev,
unsigned int port, uint16_t changed ) {
struct usb_device *usb = hubdev->usb;
unsigned int bit;
unsigned int feature;
int rc;
/* Clear each set bit */
for ( bit = 0 ; bit < 16 ; bit++ ) {
/* Skip unset bits */
if ( ! ( changed & ( 1 << bit ) ) )
continue;
/* Skip unused features */
feature = USB_HUB_C_FEATURE ( bit );
if ( ! ( hubdev->features & ( 1 << feature ) ) )
continue;
/* Clear bit */
if ( ( rc = usb_hub_clear_port_feature ( usb, port,
feature, 0 ) ) != 0 ) {
DBGC ( hubdev, "HUB %s port %d could not clear feature "
"%d: %s\n", hubdev->name, port, feature,
strerror ( rc ) );
return rc;
}
}
return 0;
}
/**
* Update port speed
*
* @v hub USB hub
* @v port USB port
* @ret rc Return status code
*/
static int hub_speed ( struct usb_hub *hub, struct usb_port *port ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
struct usb_device *usb = hubdev->usb;
struct usb_hub_port_status status;
unsigned int current;
unsigned int changed;
int rc;
/* Get port status */
if ( ( rc = usb_hub_get_port_status ( usb, port->address,
&status ) ) != 0 ) {
DBGC ( hubdev, "HUB %s port %d could not get status: %s\n",
hubdev->name, port->address, strerror ( rc ) );
return rc;
}
current = le16_to_cpu ( status.current );
changed = le16_to_cpu ( status.changed );
DBGC2 ( hubdev, "HUB %s port %d status is %04x:%04x\n",
hubdev->name, port->address, changed, current );
/* Update port speed */
if ( current & ( 1 << USB_HUB_PORT_CONNECTION ) ) {
if ( hub->protocol >= USB_PROTO_3_0 ) {
port->speed = USB_SPEED_SUPER;
} else if ( current & ( 1 << USB_HUB_PORT_LOW_SPEED ) ) {
port->speed = USB_SPEED_LOW;
} else if ( current & ( 1 << USB_HUB_PORT_HIGH_SPEED ) ) {
port->speed = USB_SPEED_HIGH;
} else {
port->speed = USB_SPEED_FULL;
}
} else {
port->speed = USB_SPEED_NONE;
}
/* Record disconnections */
port->disconnected |= ( changed & ( 1 << USB_HUB_PORT_CONNECTION ) );
/* Clear port status change bits */
if ( ( rc = hub_clear_changes ( hubdev, port->address, changed ) ) != 0)
return rc;
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 hub_clear_tt ( struct usb_hub *hub, struct usb_port *port,
struct usb_endpoint *ep ) {
struct usb_hub_device *hubdev = usb_hub_get_drvdata ( hub );
struct usb_device *usb = hubdev->usb;
int rc;
/* Clear transaction translator buffer. All hubs must support
* single-TT operation; we simplify our code by supporting
* only this configuration.
*/
if ( ( rc = usb_hub_clear_tt_buffer ( usb, ep->usb->address,
ep->address, ep->attributes,
USB_HUB_TT_SINGLE ) ) != 0 ) {
DBGC ( hubdev, "HUB %s port %d could not clear TT buffer: %s\n",
hubdev->name, port->address, strerror ( rc ) );
return rc;
}
return 0;
}
/** USB hub operations */
static struct usb_hub_driver_operations hub_operations = {
.open = hub_open,
.close = hub_close,
.enable = hub_enable,
.disable = hub_disable,
.speed = hub_speed,
.clear_tt = hub_clear_tt,
};
/**
* Probe USB hub
*
* @v func USB function
* @v config Configuration descriptor
* @ret rc Return status code
*/
static int hub_probe ( struct usb_function *func,
struct usb_configuration_descriptor *config ) {
struct usb_device *usb = func->usb;
struct usb_bus *bus = usb->port->hub->bus;
struct usb_hub_device *hubdev;
struct usb_interface_descriptor *interface;
union usb_hub_descriptor desc;
unsigned int depth;
unsigned int ports;
int enhanced;
int rc;
/* Allocate and initialise structure */
hubdev = zalloc ( sizeof ( *hubdev ) );
if ( ! hubdev ) {
rc = -ENOMEM;
goto err_alloc;
}
enhanced = ( usb->port->protocol >= USB_PROTO_3_0 );
hubdev->name = func->name;
hubdev->usb = usb;
hubdev->features =
( enhanced ? USB_HUB_FEATURES_ENHANCED : USB_HUB_FEATURES );
hubdev->flags = func->id->driver_data;
usb_endpoint_init ( &hubdev->intr, usb, &usb_hub_intr_operations );
usb_refill_init ( &hubdev->intr, 0, 0, USB_HUB_INTR_FILL );
process_init_stopped ( &hubdev->refill, &hub_refill_desc, NULL );
/* Locate hub interface descriptor */
interface = usb_interface_descriptor ( config, func->interface[0], 0 );
if ( ! interface ) {
DBGC ( hubdev, "HUB %s has no interface descriptor\n",
hubdev->name );
rc = -EINVAL;
goto err_interface;
}
/* Locate interrupt endpoint descriptor */
if ( ( rc = usb_endpoint_described ( &hubdev->intr, config, interface,
USB_INTERRUPT_IN, 0 ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not describe interrupt endpoint: "
"%s\n", hubdev->name, strerror ( rc ) );
goto err_endpoint;
}
/* Set hub depth */
depth = usb_depth ( usb );
if ( enhanced ) {
if ( ( rc = usb_hub_set_hub_depth ( usb, depth ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not set hub depth to %d: "
"%s\n", hubdev->name, depth, strerror ( rc ) );
goto err_set_hub_depth;
}
}
/* Get hub descriptor */
if ( ( rc = usb_hub_get_descriptor ( usb, enhanced, &desc ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not get hub descriptor: %s\n",
hubdev->name, strerror ( rc ) );
goto err_hub_descriptor;
}
ports = desc.basic.ports;
DBGC ( hubdev, "HUB %s has %d ports at depth %d%s\n", hubdev->name,
ports, depth, ( enhanced ? " (enhanced)" : "" ) );
/* Allocate hub */
hubdev->hub = alloc_usb_hub ( bus, usb, ports, &hub_operations );
if ( ! hubdev->hub ) {
rc = -ENOMEM;
goto err_alloc_hub;
}
usb_hub_set_drvdata ( hubdev->hub, hubdev );
/* Register hub */
if ( ( rc = register_usb_hub ( hubdev->hub ) ) != 0 ) {
DBGC ( hubdev, "HUB %s could not register: %s\n",
hubdev->name, strerror ( rc ) );
goto err_register_hub;
}
usb_func_set_drvdata ( func, hubdev );
return 0;
unregister_usb_hub ( hubdev->hub );
err_register_hub:
free_usb_hub ( hubdev->hub );
err_alloc_hub:
err_hub_descriptor:
err_set_hub_depth:
err_endpoint:
err_interface:
free ( hubdev );
err_alloc:
return rc;
}
/**
* Remove USB hub
*
* @v func USB function
* @ret rc Return status code
*/
static void hub_remove ( struct usb_function *func ) {
struct usb_hub_device *hubdev = usb_func_get_drvdata ( func );
struct usb_hub *hub = hubdev->hub;
struct usb_device *usb = hubdev->usb;
struct usb_port *port;
unsigned int i;
/* If hub has been unplugged, mark all ports as unplugged */
if ( usb->port->disconnected ) {
for ( i = 1 ; i <= hub->ports ; i++ ) {
port = usb_port ( hub, i );
port->disconnected = 1;
port->speed = USB_SPEED_NONE;
}
}
/* Unregister hub */
unregister_usb_hub ( hubdev->hub );
assert ( ! process_running ( &hubdev->refill ) );
/* Free hub */
free_usb_hub ( hubdev->hub );
/* Free hub device */
free ( hubdev );
}
/** USB hub device IDs */
static struct usb_device_id hub_ids[] = {
{
.name = "avocent-hub",
.vendor = 0x0624,
.product = 0x0248,
.driver_data = USB_HUB_SLOW_START,
},
{
.name = "hub",
.vendor = USB_ANY_ID,
.product = USB_ANY_ID,
},
};
/** USB hub driver */
struct usb_driver usb_hub_driver __usb_driver = {
.ids = hub_ids,
.id_count = ( sizeof ( hub_ids ) / sizeof ( hub_ids[0] ) ),
.class = USB_CLASS_ID ( USB_CLASS_HUB, 0, USB_ANY_ID ),
.score = USB_SCORE_NORMAL,
.probe = hub_probe,
.remove = hub_remove,
};