427 lines
12 KiB
C
427 lines
12 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
/*
|
||
|
* Wireless USB Host Controller
|
||
|
* Root Hub operations
|
||
|
*
|
||
|
*
|
||
|
* Copyright (C) 2005-2006 Intel Corporation
|
||
|
* Inaky Perez-Gonzalez <inaky.perez-gonzalez@intel.com>
|
||
|
*
|
||
|
* We fake a root hub that has fake ports (as many as simultaneous
|
||
|
* devices the Wireless USB Host Controller can deal with). For each
|
||
|
* port we keep an state in @wusbhc->port[index] identical to the one
|
||
|
* specified in the USB2.0[ch11] spec and some extra device
|
||
|
* information that complements the one in 'struct usb_device' (as
|
||
|
* this lacs a hcpriv pointer).
|
||
|
*
|
||
|
* Note this is common to WHCI and HWA host controllers.
|
||
|
*
|
||
|
* Through here we enable most of the state changes that the USB stack
|
||
|
* will use to connect or disconnect devices. We need to do some
|
||
|
* forced adaptation of Wireless USB device states vs. wired:
|
||
|
*
|
||
|
* USB: WUSB:
|
||
|
*
|
||
|
* Port Powered-off port slot n/a
|
||
|
* Powered-on port slot available
|
||
|
* Disconnected port slot available
|
||
|
* Connected port slot assigned device
|
||
|
* device sent DN_Connect
|
||
|
* device was authenticated
|
||
|
* Enabled device is authenticated, transitioned
|
||
|
* from unauth -> auth -> default address
|
||
|
* -> enabled
|
||
|
* Reset disconnect
|
||
|
* Disable disconnect
|
||
|
*
|
||
|
* This maps the standard USB port states with the WUSB device states
|
||
|
* so we can fake ports without having to modify the USB stack.
|
||
|
*
|
||
|
* FIXME: this process will change in the future
|
||
|
*
|
||
|
*
|
||
|
* ENTRY POINTS
|
||
|
*
|
||
|
* Our entry points into here are, as in hcd.c, the USB stack root hub
|
||
|
* ops defined in the usb_hcd struct:
|
||
|
*
|
||
|
* wusbhc_rh_status_data() Provide hub and port status data bitmap
|
||
|
*
|
||
|
* wusbhc_rh_control() Execution of all the major requests
|
||
|
* you can do to a hub (Set|Clear
|
||
|
* features, get descriptors, status, etc).
|
||
|
*
|
||
|
* wusbhc_rh_[suspend|resume]() That
|
||
|
*
|
||
|
* wusbhc_rh_start_port_reset() ??? unimplemented
|
||
|
*/
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/export.h>
|
||
|
#include "wusbhc.h"
|
||
|
|
||
|
/*
|
||
|
* Reset a fake port
|
||
|
*
|
||
|
* Using a Reset Device IE is too heavyweight as it causes the device
|
||
|
* to enter the UnConnected state and leave the cluster, this can mean
|
||
|
* that when the device reconnects it is connected to a different fake
|
||
|
* port.
|
||
|
*
|
||
|
* Instead, reset authenticated devices with a SetAddress(0), followed
|
||
|
* by a SetAddresss(AuthAddr).
|
||
|
*
|
||
|
* For unauthenticated devices just pretend to reset but do nothing.
|
||
|
* If the device initialization continues to fail it will eventually
|
||
|
* time out after TrustTimeout and enter the UnConnected state.
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*
|
||
|
* Supposedly we are the only thread accesing @wusbhc->port; in any
|
||
|
* case, maybe we should move the mutex locking from
|
||
|
* wusbhc_devconnect_auth() to here.
|
||
|
*
|
||
|
* @port_idx refers to the wusbhc's port index, not the USB port number
|
||
|
*/
|
||
|
static int wusbhc_rh_port_reset(struct wusbhc *wusbhc, u8 port_idx)
|
||
|
{
|
||
|
int result = 0;
|
||
|
struct wusb_port *port = wusb_port_by_idx(wusbhc, port_idx);
|
||
|
struct wusb_dev *wusb_dev = port->wusb_dev;
|
||
|
|
||
|
if (wusb_dev == NULL)
|
||
|
return -ENOTCONN;
|
||
|
|
||
|
port->status |= USB_PORT_STAT_RESET;
|
||
|
port->change |= USB_PORT_STAT_C_RESET;
|
||
|
|
||
|
if (wusb_dev->addr & WUSB_DEV_ADDR_UNAUTH)
|
||
|
result = 0;
|
||
|
else
|
||
|
result = wusb_dev_update_address(wusbhc, wusb_dev);
|
||
|
|
||
|
port->status &= ~USB_PORT_STAT_RESET;
|
||
|
port->status |= USB_PORT_STAT_ENABLE;
|
||
|
port->change |= USB_PORT_STAT_C_RESET | USB_PORT_STAT_C_ENABLE;
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Return the hub change status bitmap
|
||
|
*
|
||
|
* The bits in the change status bitmap are cleared when a
|
||
|
* ClearPortFeature request is issued (USB2.0[11.12.3,11.12.4].
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*
|
||
|
* WARNING!! This gets called from atomic context; we cannot get the
|
||
|
* mutex--the only race condition we can find is some bit
|
||
|
* changing just after we copy it, which shouldn't be too
|
||
|
* big of a problem [and we can't make it an spinlock
|
||
|
* because other parts need to take it and sleep] .
|
||
|
*
|
||
|
* @usb_hcd is refcounted, so it won't disappear under us
|
||
|
* and before killing a host, the polling of the root hub
|
||
|
* would be stopped anyway.
|
||
|
*/
|
||
|
int wusbhc_rh_status_data(struct usb_hcd *usb_hcd, char *_buf)
|
||
|
{
|
||
|
struct wusbhc *wusbhc = usb_hcd_to_wusbhc(usb_hcd);
|
||
|
size_t cnt, size, bits_set = 0;
|
||
|
|
||
|
/* WE DON'T LOCK, see comment */
|
||
|
/* round up to bytes. Hub bit is bit 0 so add 1. */
|
||
|
size = DIV_ROUND_UP(wusbhc->ports_max + 1, 8);
|
||
|
|
||
|
/* clear the output buffer. */
|
||
|
memset(_buf, 0, size);
|
||
|
/* set the bit for each changed port. */
|
||
|
for (cnt = 0; cnt < wusbhc->ports_max; cnt++) {
|
||
|
|
||
|
if (wusb_port_by_idx(wusbhc, cnt)->change) {
|
||
|
const int bitpos = cnt+1;
|
||
|
|
||
|
_buf[bitpos/8] |= (1 << (bitpos % 8));
|
||
|
bits_set++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return bits_set ? size : 0;
|
||
|
}
|
||
|
EXPORT_SYMBOL_GPL(wusbhc_rh_status_data);
|
||
|
|
||
|
/*
|
||
|
* Return the hub's descriptor
|
||
|
*
|
||
|
* NOTE: almost cut and paste from ehci-hub.c
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked
|
||
|
*/
|
||
|
static int wusbhc_rh_get_hub_descr(struct wusbhc *wusbhc, u16 wValue,
|
||
|
u16 wIndex,
|
||
|
struct usb_hub_descriptor *descr,
|
||
|
u16 wLength)
|
||
|
{
|
||
|
u16 temp = 1 + (wusbhc->ports_max / 8);
|
||
|
u8 length = 7 + 2 * temp;
|
||
|
|
||
|
if (wLength < length)
|
||
|
return -ENOSPC;
|
||
|
descr->bDescLength = 7 + 2 * temp;
|
||
|
descr->bDescriptorType = USB_DT_HUB; /* HUB type */
|
||
|
descr->bNbrPorts = wusbhc->ports_max;
|
||
|
descr->wHubCharacteristics = cpu_to_le16(
|
||
|
HUB_CHAR_COMMON_LPSM /* All ports power at once */
|
||
|
| 0x00 /* not part of compound device */
|
||
|
| HUB_CHAR_NO_OCPM /* No overcurrent protection */
|
||
|
| 0x00 /* 8 FS think time FIXME ?? */
|
||
|
| 0x00); /* No port indicators */
|
||
|
descr->bPwrOn2PwrGood = 0;
|
||
|
descr->bHubContrCurrent = 0;
|
||
|
/* two bitmaps: ports removable, and usb 1.0 legacy PortPwrCtrlMask */
|
||
|
memset(&descr->u.hs.DeviceRemovable[0], 0, temp);
|
||
|
memset(&descr->u.hs.DeviceRemovable[temp], 0xff, temp);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Clear a hub feature
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*
|
||
|
* Nothing to do, so no locking needed ;)
|
||
|
*/
|
||
|
static int wusbhc_rh_clear_hub_feat(struct wusbhc *wusbhc, u16 feature)
|
||
|
{
|
||
|
int result;
|
||
|
|
||
|
switch (feature) {
|
||
|
case C_HUB_LOCAL_POWER:
|
||
|
/* FIXME: maybe plug bit 0 to the power input status,
|
||
|
* if any?
|
||
|
* see wusbhc_rh_get_hub_status() */
|
||
|
case C_HUB_OVER_CURRENT:
|
||
|
result = 0;
|
||
|
break;
|
||
|
default:
|
||
|
result = -EPIPE;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Return hub status (it is always zero...)
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*
|
||
|
* Nothing to do, so no locking needed ;)
|
||
|
*/
|
||
|
static int wusbhc_rh_get_hub_status(struct wusbhc *wusbhc, u32 *buf,
|
||
|
u16 wLength)
|
||
|
{
|
||
|
/* FIXME: maybe plug bit 0 to the power input status (if any)? */
|
||
|
*buf = 0;
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Set a port feature
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*/
|
||
|
static int wusbhc_rh_set_port_feat(struct wusbhc *wusbhc, u16 feature,
|
||
|
u8 selector, u8 port_idx)
|
||
|
{
|
||
|
struct device *dev = wusbhc->dev;
|
||
|
|
||
|
if (port_idx > wusbhc->ports_max)
|
||
|
return -EINVAL;
|
||
|
|
||
|
switch (feature) {
|
||
|
/* According to USB2.0[11.24.2.13]p2, these features
|
||
|
* are not required to be implemented. */
|
||
|
case USB_PORT_FEAT_C_OVER_CURRENT:
|
||
|
case USB_PORT_FEAT_C_ENABLE:
|
||
|
case USB_PORT_FEAT_C_SUSPEND:
|
||
|
case USB_PORT_FEAT_C_CONNECTION:
|
||
|
case USB_PORT_FEAT_C_RESET:
|
||
|
return 0;
|
||
|
case USB_PORT_FEAT_POWER:
|
||
|
/* No such thing, but we fake it works */
|
||
|
mutex_lock(&wusbhc->mutex);
|
||
|
wusb_port_by_idx(wusbhc, port_idx)->status |= USB_PORT_STAT_POWER;
|
||
|
mutex_unlock(&wusbhc->mutex);
|
||
|
return 0;
|
||
|
case USB_PORT_FEAT_RESET:
|
||
|
return wusbhc_rh_port_reset(wusbhc, port_idx);
|
||
|
case USB_PORT_FEAT_ENABLE:
|
||
|
case USB_PORT_FEAT_SUSPEND:
|
||
|
dev_err(dev, "(port_idx %d) set feat %d/%d UNIMPLEMENTED\n",
|
||
|
port_idx, feature, selector);
|
||
|
return -ENOSYS;
|
||
|
default:
|
||
|
dev_err(dev, "(port_idx %d) set feat %d/%d UNKNOWN\n",
|
||
|
port_idx, feature, selector);
|
||
|
return -EPIPE;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Clear a port feature...
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*/
|
||
|
static int wusbhc_rh_clear_port_feat(struct wusbhc *wusbhc, u16 feature,
|
||
|
u8 selector, u8 port_idx)
|
||
|
{
|
||
|
int result = 0;
|
||
|
struct device *dev = wusbhc->dev;
|
||
|
|
||
|
if (port_idx > wusbhc->ports_max)
|
||
|
return -EINVAL;
|
||
|
|
||
|
mutex_lock(&wusbhc->mutex);
|
||
|
switch (feature) {
|
||
|
case USB_PORT_FEAT_POWER: /* fake port always on */
|
||
|
/* According to USB2.0[11.24.2.7.1.4], no need to implement? */
|
||
|
case USB_PORT_FEAT_C_OVER_CURRENT:
|
||
|
break;
|
||
|
case USB_PORT_FEAT_C_RESET:
|
||
|
wusb_port_by_idx(wusbhc, port_idx)->change &= ~USB_PORT_STAT_C_RESET;
|
||
|
break;
|
||
|
case USB_PORT_FEAT_C_CONNECTION:
|
||
|
wusb_port_by_idx(wusbhc, port_idx)->change &= ~USB_PORT_STAT_C_CONNECTION;
|
||
|
break;
|
||
|
case USB_PORT_FEAT_ENABLE:
|
||
|
__wusbhc_dev_disable(wusbhc, port_idx);
|
||
|
break;
|
||
|
case USB_PORT_FEAT_C_ENABLE:
|
||
|
wusb_port_by_idx(wusbhc, port_idx)->change &= ~USB_PORT_STAT_C_ENABLE;
|
||
|
break;
|
||
|
case USB_PORT_FEAT_SUSPEND:
|
||
|
case USB_PORT_FEAT_C_SUSPEND:
|
||
|
dev_err(dev, "(port_idx %d) Clear feat %d/%d UNIMPLEMENTED\n",
|
||
|
port_idx, feature, selector);
|
||
|
result = -ENOSYS;
|
||
|
break;
|
||
|
default:
|
||
|
dev_err(dev, "(port_idx %d) Clear feat %d/%d UNKNOWN\n",
|
||
|
port_idx, feature, selector);
|
||
|
result = -EPIPE;
|
||
|
break;
|
||
|
}
|
||
|
mutex_unlock(&wusbhc->mutex);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Return the port's status
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*/
|
||
|
static int wusbhc_rh_get_port_status(struct wusbhc *wusbhc, u16 port_idx,
|
||
|
u32 *_buf, u16 wLength)
|
||
|
{
|
||
|
__le16 *buf = (__le16 *)_buf;
|
||
|
|
||
|
if (port_idx > wusbhc->ports_max)
|
||
|
return -EINVAL;
|
||
|
|
||
|
mutex_lock(&wusbhc->mutex);
|
||
|
buf[0] = cpu_to_le16(wusb_port_by_idx(wusbhc, port_idx)->status);
|
||
|
buf[1] = cpu_to_le16(wusb_port_by_idx(wusbhc, port_idx)->change);
|
||
|
mutex_unlock(&wusbhc->mutex);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Entry point for Root Hub operations
|
||
|
*
|
||
|
* @wusbhc is assumed referenced and @wusbhc->mutex unlocked.
|
||
|
*/
|
||
|
int wusbhc_rh_control(struct usb_hcd *usb_hcd, u16 reqntype, u16 wValue,
|
||
|
u16 wIndex, char *buf, u16 wLength)
|
||
|
{
|
||
|
int result = -ENOSYS;
|
||
|
struct wusbhc *wusbhc = usb_hcd_to_wusbhc(usb_hcd);
|
||
|
|
||
|
switch (reqntype) {
|
||
|
case GetHubDescriptor:
|
||
|
result = wusbhc_rh_get_hub_descr(
|
||
|
wusbhc, wValue, wIndex,
|
||
|
(struct usb_hub_descriptor *) buf, wLength);
|
||
|
break;
|
||
|
case ClearHubFeature:
|
||
|
result = wusbhc_rh_clear_hub_feat(wusbhc, wValue);
|
||
|
break;
|
||
|
case GetHubStatus:
|
||
|
result = wusbhc_rh_get_hub_status(wusbhc, (u32 *)buf, wLength);
|
||
|
break;
|
||
|
|
||
|
case SetPortFeature:
|
||
|
result = wusbhc_rh_set_port_feat(wusbhc, wValue, wIndex >> 8,
|
||
|
(wIndex & 0xff) - 1);
|
||
|
break;
|
||
|
case ClearPortFeature:
|
||
|
result = wusbhc_rh_clear_port_feat(wusbhc, wValue, wIndex >> 8,
|
||
|
(wIndex & 0xff) - 1);
|
||
|
break;
|
||
|
case GetPortStatus:
|
||
|
result = wusbhc_rh_get_port_status(wusbhc, wIndex - 1,
|
||
|
(u32 *)buf, wLength);
|
||
|
break;
|
||
|
|
||
|
case SetHubFeature:
|
||
|
default:
|
||
|
dev_err(wusbhc->dev, "%s (%p [%p], %x, %x, %x, %p, %x) "
|
||
|
"UNIMPLEMENTED\n", __func__, usb_hcd, wusbhc, reqntype,
|
||
|
wValue, wIndex, buf, wLength);
|
||
|
/* dump_stack(); */
|
||
|
result = -ENOSYS;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
EXPORT_SYMBOL_GPL(wusbhc_rh_control);
|
||
|
|
||
|
int wusbhc_rh_start_port_reset(struct usb_hcd *usb_hcd, unsigned port_idx)
|
||
|
{
|
||
|
struct wusbhc *wusbhc = usb_hcd_to_wusbhc(usb_hcd);
|
||
|
dev_err(wusbhc->dev, "%s (%p [%p], port_idx %u) UNIMPLEMENTED\n",
|
||
|
__func__, usb_hcd, wusbhc, port_idx);
|
||
|
WARN_ON(1);
|
||
|
return -ENOSYS;
|
||
|
}
|
||
|
EXPORT_SYMBOL_GPL(wusbhc_rh_start_port_reset);
|
||
|
|
||
|
static void wusb_port_init(struct wusb_port *port)
|
||
|
{
|
||
|
port->status |= USB_PORT_STAT_HIGH_SPEED;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Alloc fake port specific fields and status.
|
||
|
*/
|
||
|
int wusbhc_rh_create(struct wusbhc *wusbhc)
|
||
|
{
|
||
|
int result = -ENOMEM;
|
||
|
size_t port_size, itr;
|
||
|
port_size = wusbhc->ports_max * sizeof(wusbhc->port[0]);
|
||
|
wusbhc->port = kzalloc(port_size, GFP_KERNEL);
|
||
|
if (wusbhc->port == NULL)
|
||
|
goto error_port_alloc;
|
||
|
for (itr = 0; itr < wusbhc->ports_max; itr++)
|
||
|
wusb_port_init(&wusbhc->port[itr]);
|
||
|
result = 0;
|
||
|
error_port_alloc:
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
void wusbhc_rh_destroy(struct wusbhc *wusbhc)
|
||
|
{
|
||
|
kfree(wusbhc->port);
|
||
|
}
|