| /* | |
| * Copyright (C) 2014 Michael Brown <mbrown@fensystems.co.uk>. | |
| * | |
| * This program is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU General Public License as | |
| * published by the Free Software Foundation; either version 2 of the | |
| * License, or (at your option) any later version. | |
| * | |
| * This program is distributed in the hope that it will be useful, but | |
| * WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| * General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU General Public License | |
| * along with this program; if not, write to the Free Software | |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| * 02110-1301, USA. | |
| * | |
| * You can also choose to distribute this program under the terms of | |
| * the Unmodified Binary Distribution Licence (as given in the file | |
| * COPYING.UBDL), provided that you have satisfied its requirements. | |
| */ | |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); | |
| FILE_SECBOOT ( PERMITTED ); | |
| #include <stdlib.h> | |
| #include <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[] = { | |
| USB_ID ( 0x0624, 0x0248, "avocent-hub", "Avocent USB hub", | |
| USB_HUB_SLOW_START ), | |
| USB_ID ( 0xffff, 0xffff, "hub", "USB hub", 0 ), | |
| }; | |
| /** 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, | |
| }; |