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