1612 lines
38 KiB
C
1612 lines
38 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* xen-hcd.c
|
|
*
|
|
* Xen USB Virtual Host Controller driver
|
|
*
|
|
* Copyright (C) 2009, FUJITSU LABORATORIES LTD.
|
|
* Author: Noboru Iwamatsu <n_iwamatsu@jp.fujitsu.com>
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/usb.h>
|
|
#include <linux/list.h>
|
|
#include <linux/usb/hcd.h>
|
|
#include <linux/io.h>
|
|
|
|
#include <xen/xen.h>
|
|
#include <xen/xenbus.h>
|
|
#include <xen/grant_table.h>
|
|
#include <xen/events.h>
|
|
#include <xen/page.h>
|
|
|
|
#include <xen/interface/io/usbif.h>
|
|
|
|
/* Private per-URB data */
|
|
struct urb_priv {
|
|
struct list_head list;
|
|
struct urb *urb;
|
|
int req_id; /* RING_REQUEST id for submitting */
|
|
int unlink_req_id; /* RING_REQUEST id for unlinking */
|
|
int status;
|
|
bool unlinked; /* dequeued marker */
|
|
};
|
|
|
|
/* virtual roothub port status */
|
|
struct rhport_status {
|
|
__u32 status;
|
|
bool resuming; /* in resuming */
|
|
bool c_connection; /* connection changed */
|
|
unsigned long timeout;
|
|
};
|
|
|
|
/* status of attached device */
|
|
struct vdevice_status {
|
|
int devnum;
|
|
enum usb_device_state status;
|
|
enum usb_device_speed speed;
|
|
};
|
|
|
|
/* RING request shadow */
|
|
struct usb_shadow {
|
|
struct xenusb_urb_request req;
|
|
struct urb *urb;
|
|
bool in_flight;
|
|
};
|
|
|
|
struct xenhcd_info {
|
|
/* Virtual Host Controller has 4 urb queues */
|
|
struct list_head pending_submit_list;
|
|
struct list_head pending_unlink_list;
|
|
struct list_head in_progress_list;
|
|
struct list_head giveback_waiting_list;
|
|
|
|
spinlock_t lock;
|
|
|
|
/* timer that kick pending and giveback waiting urbs */
|
|
struct timer_list watchdog;
|
|
unsigned long actions;
|
|
|
|
/* virtual root hub */
|
|
int rh_numports;
|
|
struct rhport_status ports[XENUSB_MAX_PORTNR];
|
|
struct vdevice_status devices[XENUSB_MAX_PORTNR];
|
|
|
|
/* Xen related staff */
|
|
struct xenbus_device *xbdev;
|
|
int urb_ring_ref;
|
|
int conn_ring_ref;
|
|
struct xenusb_urb_front_ring urb_ring;
|
|
struct xenusb_conn_front_ring conn_ring;
|
|
|
|
unsigned int evtchn;
|
|
unsigned int irq;
|
|
struct usb_shadow shadow[XENUSB_URB_RING_SIZE];
|
|
unsigned int shadow_free;
|
|
|
|
bool error;
|
|
};
|
|
|
|
#define XENHCD_RING_JIFFIES (HZ/200)
|
|
#define XENHCD_SCAN_JIFFIES 1
|
|
|
|
enum xenhcd_timer_action {
|
|
TIMER_RING_WATCHDOG,
|
|
TIMER_SCAN_PENDING_URBS,
|
|
};
|
|
|
|
static struct kmem_cache *xenhcd_urbp_cachep;
|
|
|
|
static inline struct xenhcd_info *xenhcd_hcd_to_info(struct usb_hcd *hcd)
|
|
{
|
|
return (struct xenhcd_info *)hcd->hcd_priv;
|
|
}
|
|
|
|
static inline struct usb_hcd *xenhcd_info_to_hcd(struct xenhcd_info *info)
|
|
{
|
|
return container_of((void *)info, struct usb_hcd, hcd_priv);
|
|
}
|
|
|
|
static void xenhcd_set_error(struct xenhcd_info *info, const char *msg)
|
|
{
|
|
info->error = true;
|
|
|
|
pr_alert("xen-hcd: protocol error: %s!\n", msg);
|
|
}
|
|
|
|
static inline void xenhcd_timer_action_done(struct xenhcd_info *info,
|
|
enum xenhcd_timer_action action)
|
|
{
|
|
clear_bit(action, &info->actions);
|
|
}
|
|
|
|
static void xenhcd_timer_action(struct xenhcd_info *info,
|
|
enum xenhcd_timer_action action)
|
|
{
|
|
if (timer_pending(&info->watchdog) &&
|
|
test_bit(TIMER_SCAN_PENDING_URBS, &info->actions))
|
|
return;
|
|
|
|
if (!test_and_set_bit(action, &info->actions)) {
|
|
unsigned long t;
|
|
|
|
switch (action) {
|
|
case TIMER_RING_WATCHDOG:
|
|
t = XENHCD_RING_JIFFIES;
|
|
break;
|
|
default:
|
|
t = XENHCD_SCAN_JIFFIES;
|
|
break;
|
|
}
|
|
mod_timer(&info->watchdog, t + jiffies);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* set virtual port connection status
|
|
*/
|
|
static void xenhcd_set_connect_state(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
if (info->ports[port].status & USB_PORT_STAT_POWER) {
|
|
switch (info->devices[port].speed) {
|
|
case XENUSB_SPEED_NONE:
|
|
info->ports[port].status &=
|
|
~(USB_PORT_STAT_CONNECTION |
|
|
USB_PORT_STAT_ENABLE |
|
|
USB_PORT_STAT_LOW_SPEED |
|
|
USB_PORT_STAT_HIGH_SPEED |
|
|
USB_PORT_STAT_SUSPEND);
|
|
break;
|
|
case XENUSB_SPEED_LOW:
|
|
info->ports[port].status |= USB_PORT_STAT_CONNECTION;
|
|
info->ports[port].status |= USB_PORT_STAT_LOW_SPEED;
|
|
break;
|
|
case XENUSB_SPEED_FULL:
|
|
info->ports[port].status |= USB_PORT_STAT_CONNECTION;
|
|
break;
|
|
case XENUSB_SPEED_HIGH:
|
|
info->ports[port].status |= USB_PORT_STAT_CONNECTION;
|
|
info->ports[port].status |= USB_PORT_STAT_HIGH_SPEED;
|
|
break;
|
|
default: /* error */
|
|
return;
|
|
}
|
|
info->ports[port].status |= (USB_PORT_STAT_C_CONNECTION << 16);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* set virtual device connection status
|
|
*/
|
|
static int xenhcd_rhport_connect(struct xenhcd_info *info, __u8 portnum,
|
|
__u8 speed)
|
|
{
|
|
int port;
|
|
|
|
if (portnum < 1 || portnum > info->rh_numports)
|
|
return -EINVAL; /* invalid port number */
|
|
|
|
port = portnum - 1;
|
|
if (info->devices[port].speed != speed) {
|
|
switch (speed) {
|
|
case XENUSB_SPEED_NONE: /* disconnect */
|
|
info->devices[port].status = USB_STATE_NOTATTACHED;
|
|
break;
|
|
case XENUSB_SPEED_LOW:
|
|
case XENUSB_SPEED_FULL:
|
|
case XENUSB_SPEED_HIGH:
|
|
info->devices[port].status = USB_STATE_ATTACHED;
|
|
break;
|
|
default: /* error */
|
|
return -EINVAL;
|
|
}
|
|
info->devices[port].speed = speed;
|
|
info->ports[port].c_connection = true;
|
|
|
|
xenhcd_set_connect_state(info, portnum);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* SetPortFeature(PORT_SUSPENDED)
|
|
*/
|
|
static void xenhcd_rhport_suspend(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
info->ports[port].status |= USB_PORT_STAT_SUSPEND;
|
|
info->devices[port].status = USB_STATE_SUSPENDED;
|
|
}
|
|
|
|
/*
|
|
* ClearPortFeature(PORT_SUSPENDED)
|
|
*/
|
|
static void xenhcd_rhport_resume(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
if (info->ports[port].status & USB_PORT_STAT_SUSPEND) {
|
|
info->ports[port].resuming = true;
|
|
info->ports[port].timeout = jiffies + msecs_to_jiffies(20);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* SetPortFeature(PORT_POWER)
|
|
*/
|
|
static void xenhcd_rhport_power_on(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
if ((info->ports[port].status & USB_PORT_STAT_POWER) == 0) {
|
|
info->ports[port].status |= USB_PORT_STAT_POWER;
|
|
if (info->devices[port].status != USB_STATE_NOTATTACHED)
|
|
info->devices[port].status = USB_STATE_POWERED;
|
|
if (info->ports[port].c_connection)
|
|
xenhcd_set_connect_state(info, portnum);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ClearPortFeature(PORT_POWER)
|
|
* SetConfiguration(non-zero)
|
|
* Power_Source_Off
|
|
* Over-current
|
|
*/
|
|
static void xenhcd_rhport_power_off(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
if (info->ports[port].status & USB_PORT_STAT_POWER) {
|
|
info->ports[port].status = 0;
|
|
if (info->devices[port].status != USB_STATE_NOTATTACHED)
|
|
info->devices[port].status = USB_STATE_ATTACHED;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ClearPortFeature(PORT_ENABLE)
|
|
*/
|
|
static void xenhcd_rhport_disable(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
info->ports[port].status &= ~USB_PORT_STAT_ENABLE;
|
|
info->ports[port].status &= ~USB_PORT_STAT_SUSPEND;
|
|
info->ports[port].resuming = false;
|
|
if (info->devices[port].status != USB_STATE_NOTATTACHED)
|
|
info->devices[port].status = USB_STATE_POWERED;
|
|
}
|
|
|
|
/*
|
|
* SetPortFeature(PORT_RESET)
|
|
*/
|
|
static void xenhcd_rhport_reset(struct xenhcd_info *info, int portnum)
|
|
{
|
|
int port;
|
|
|
|
port = portnum - 1;
|
|
info->ports[port].status &= ~(USB_PORT_STAT_ENABLE |
|
|
USB_PORT_STAT_LOW_SPEED |
|
|
USB_PORT_STAT_HIGH_SPEED);
|
|
info->ports[port].status |= USB_PORT_STAT_RESET;
|
|
|
|
if (info->devices[port].status != USB_STATE_NOTATTACHED)
|
|
info->devices[port].status = USB_STATE_ATTACHED;
|
|
|
|
/* 10msec reset signaling */
|
|
info->ports[port].timeout = jiffies + msecs_to_jiffies(10);
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int xenhcd_bus_suspend(struct usb_hcd *hcd)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
int ret = 0;
|
|
int i, ports;
|
|
|
|
ports = info->rh_numports;
|
|
|
|
spin_lock_irq(&info->lock);
|
|
if (!test_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags)) {
|
|
ret = -ESHUTDOWN;
|
|
} else {
|
|
/* suspend any active ports*/
|
|
for (i = 1; i <= ports; i++)
|
|
xenhcd_rhport_suspend(info, i);
|
|
}
|
|
spin_unlock_irq(&info->lock);
|
|
|
|
del_timer_sync(&info->watchdog);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int xenhcd_bus_resume(struct usb_hcd *hcd)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
int ret = 0;
|
|
int i, ports;
|
|
|
|
ports = info->rh_numports;
|
|
|
|
spin_lock_irq(&info->lock);
|
|
if (!test_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags)) {
|
|
ret = -ESHUTDOWN;
|
|
} else {
|
|
/* resume any suspended ports*/
|
|
for (i = 1; i <= ports; i++)
|
|
xenhcd_rhport_resume(info, i);
|
|
}
|
|
spin_unlock_irq(&info->lock);
|
|
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
static void xenhcd_hub_descriptor(struct xenhcd_info *info,
|
|
struct usb_hub_descriptor *desc)
|
|
{
|
|
__u16 temp;
|
|
int ports = info->rh_numports;
|
|
|
|
desc->bDescriptorType = 0x29;
|
|
desc->bPwrOn2PwrGood = 10; /* EHCI says 20ms max */
|
|
desc->bHubContrCurrent = 0;
|
|
desc->bNbrPorts = ports;
|
|
|
|
/* size of DeviceRemovable and PortPwrCtrlMask fields */
|
|
temp = 1 + (ports / 8);
|
|
desc->bDescLength = 7 + 2 * temp;
|
|
|
|
/* bitmaps for DeviceRemovable and PortPwrCtrlMask */
|
|
memset(&desc->u.hs.DeviceRemovable[0], 0, temp);
|
|
memset(&desc->u.hs.DeviceRemovable[temp], 0xff, temp);
|
|
|
|
/* per-port over current reporting and no power switching */
|
|
temp = 0x000a;
|
|
desc->wHubCharacteristics = cpu_to_le16(temp);
|
|
}
|
|
|
|
/* port status change mask for hub_status_data */
|
|
#define PORT_C_MASK ((USB_PORT_STAT_C_CONNECTION | \
|
|
USB_PORT_STAT_C_ENABLE | \
|
|
USB_PORT_STAT_C_SUSPEND | \
|
|
USB_PORT_STAT_C_OVERCURRENT | \
|
|
USB_PORT_STAT_C_RESET) << 16)
|
|
|
|
/*
|
|
* See USB 2.0 Spec, 11.12.4 Hub and Port Status Change Bitmap.
|
|
* If port status changed, writes the bitmap to buf and return
|
|
* that length(number of bytes).
|
|
* If Nothing changed, return 0.
|
|
*/
|
|
static int xenhcd_hub_status_data(struct usb_hcd *hcd, char *buf)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
int ports;
|
|
int i;
|
|
unsigned long flags;
|
|
int ret;
|
|
int changed = 0;
|
|
|
|
/* initialize the status to no-changes */
|
|
ports = info->rh_numports;
|
|
ret = 1 + (ports / 8);
|
|
memset(buf, 0, ret);
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
|
|
for (i = 0; i < ports; i++) {
|
|
/* check status for each port */
|
|
if (info->ports[i].status & PORT_C_MASK) {
|
|
buf[(i + 1) / 8] |= 1 << (i + 1) % 8;
|
|
changed = 1;
|
|
}
|
|
}
|
|
|
|
if ((hcd->state == HC_STATE_SUSPENDED) && (changed == 1))
|
|
usb_hcd_resume_root_hub(hcd);
|
|
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
return changed ? ret : 0;
|
|
}
|
|
|
|
static int xenhcd_hub_control(struct usb_hcd *hcd, __u16 typeReq, __u16 wValue,
|
|
__u16 wIndex, char *buf, __u16 wLength)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
int ports = info->rh_numports;
|
|
unsigned long flags;
|
|
int ret = 0;
|
|
int i;
|
|
int changed = 0;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
switch (typeReq) {
|
|
case ClearHubFeature:
|
|
/* ignore this request */
|
|
break;
|
|
case ClearPortFeature:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
|
|
switch (wValue) {
|
|
case USB_PORT_FEAT_SUSPEND:
|
|
xenhcd_rhport_resume(info, wIndex);
|
|
break;
|
|
case USB_PORT_FEAT_POWER:
|
|
xenhcd_rhport_power_off(info, wIndex);
|
|
break;
|
|
case USB_PORT_FEAT_ENABLE:
|
|
xenhcd_rhport_disable(info, wIndex);
|
|
break;
|
|
case USB_PORT_FEAT_C_CONNECTION:
|
|
info->ports[wIndex - 1].c_connection = false;
|
|
fallthrough;
|
|
default:
|
|
info->ports[wIndex - 1].status &= ~(1 << wValue);
|
|
break;
|
|
}
|
|
break;
|
|
case GetHubDescriptor:
|
|
xenhcd_hub_descriptor(info, (struct usb_hub_descriptor *)buf);
|
|
break;
|
|
case GetHubStatus:
|
|
/* always local power supply good and no over-current exists. */
|
|
*(__le32 *)buf = cpu_to_le32(0);
|
|
break;
|
|
case GetPortStatus:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
|
|
wIndex--;
|
|
|
|
/* resume completion */
|
|
if (info->ports[wIndex].resuming &&
|
|
time_after_eq(jiffies, info->ports[wIndex].timeout)) {
|
|
info->ports[wIndex].status |=
|
|
USB_PORT_STAT_C_SUSPEND << 16;
|
|
info->ports[wIndex].status &= ~USB_PORT_STAT_SUSPEND;
|
|
}
|
|
|
|
/* reset completion */
|
|
if ((info->ports[wIndex].status & USB_PORT_STAT_RESET) != 0 &&
|
|
time_after_eq(jiffies, info->ports[wIndex].timeout)) {
|
|
info->ports[wIndex].status |=
|
|
USB_PORT_STAT_C_RESET << 16;
|
|
info->ports[wIndex].status &= ~USB_PORT_STAT_RESET;
|
|
|
|
if (info->devices[wIndex].status !=
|
|
USB_STATE_NOTATTACHED) {
|
|
info->ports[wIndex].status |=
|
|
USB_PORT_STAT_ENABLE;
|
|
info->devices[wIndex].status =
|
|
USB_STATE_DEFAULT;
|
|
}
|
|
|
|
switch (info->devices[wIndex].speed) {
|
|
case XENUSB_SPEED_LOW:
|
|
info->ports[wIndex].status |=
|
|
USB_PORT_STAT_LOW_SPEED;
|
|
break;
|
|
case XENUSB_SPEED_HIGH:
|
|
info->ports[wIndex].status |=
|
|
USB_PORT_STAT_HIGH_SPEED;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
*(__le32 *)buf = cpu_to_le32(info->ports[wIndex].status);
|
|
break;
|
|
case SetPortFeature:
|
|
if (!wIndex || wIndex > ports)
|
|
goto error;
|
|
|
|
switch (wValue) {
|
|
case USB_PORT_FEAT_POWER:
|
|
xenhcd_rhport_power_on(info, wIndex);
|
|
break;
|
|
case USB_PORT_FEAT_RESET:
|
|
xenhcd_rhport_reset(info, wIndex);
|
|
break;
|
|
case USB_PORT_FEAT_SUSPEND:
|
|
xenhcd_rhport_suspend(info, wIndex);
|
|
break;
|
|
default:
|
|
if (info->ports[wIndex-1].status & USB_PORT_STAT_POWER)
|
|
info->ports[wIndex-1].status |= (1 << wValue);
|
|
}
|
|
break;
|
|
|
|
case SetHubFeature:
|
|
/* not supported */
|
|
default:
|
|
error:
|
|
ret = -EPIPE;
|
|
}
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
/* check status for each port */
|
|
for (i = 0; i < ports; i++) {
|
|
if (info->ports[i].status & PORT_C_MASK)
|
|
changed = 1;
|
|
}
|
|
if (changed)
|
|
usb_hcd_poll_rh_status(hcd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void xenhcd_free_urb_priv(struct urb_priv *urbp)
|
|
{
|
|
urbp->urb->hcpriv = NULL;
|
|
kmem_cache_free(xenhcd_urbp_cachep, urbp);
|
|
}
|
|
|
|
static inline unsigned int xenhcd_get_id_from_freelist(struct xenhcd_info *info)
|
|
{
|
|
unsigned int free;
|
|
|
|
free = info->shadow_free;
|
|
info->shadow_free = info->shadow[free].req.id;
|
|
info->shadow[free].req.id = 0x0fff; /* debug */
|
|
return free;
|
|
}
|
|
|
|
static inline void xenhcd_add_id_to_freelist(struct xenhcd_info *info,
|
|
unsigned int id)
|
|
{
|
|
info->shadow[id].req.id = info->shadow_free;
|
|
info->shadow[id].urb = NULL;
|
|
info->shadow_free = id;
|
|
}
|
|
|
|
static inline int xenhcd_count_pages(void *addr, int length)
|
|
{
|
|
unsigned long vaddr = (unsigned long)addr;
|
|
|
|
return PFN_UP(vaddr + length) - PFN_DOWN(vaddr);
|
|
}
|
|
|
|
static void xenhcd_gnttab_map(struct xenhcd_info *info, void *addr, int length,
|
|
grant_ref_t *gref_head,
|
|
struct xenusb_request_segment *seg,
|
|
int nr_pages, int flags)
|
|
{
|
|
grant_ref_t ref;
|
|
unsigned int offset;
|
|
unsigned int len = length;
|
|
unsigned int bytes;
|
|
int i;
|
|
|
|
for (i = 0; i < nr_pages; i++) {
|
|
offset = offset_in_page(addr);
|
|
|
|
bytes = PAGE_SIZE - offset;
|
|
if (bytes > len)
|
|
bytes = len;
|
|
|
|
ref = gnttab_claim_grant_reference(gref_head);
|
|
gnttab_grant_foreign_access_ref(ref, info->xbdev->otherend_id,
|
|
virt_to_gfn(addr), flags);
|
|
seg[i].gref = ref;
|
|
seg[i].offset = (__u16)offset;
|
|
seg[i].length = (__u16)bytes;
|
|
|
|
addr += bytes;
|
|
len -= bytes;
|
|
}
|
|
}
|
|
|
|
static __u32 xenhcd_pipe_urb_to_xenusb(__u32 urb_pipe, __u8 port)
|
|
{
|
|
static __u32 pipe;
|
|
|
|
pipe = usb_pipedevice(urb_pipe) << XENUSB_PIPE_DEV_SHIFT;
|
|
pipe |= usb_pipeendpoint(urb_pipe) << XENUSB_PIPE_EP_SHIFT;
|
|
if (usb_pipein(urb_pipe))
|
|
pipe |= XENUSB_PIPE_DIR;
|
|
switch (usb_pipetype(urb_pipe)) {
|
|
case PIPE_ISOCHRONOUS:
|
|
pipe |= XENUSB_PIPE_TYPE_ISOC << XENUSB_PIPE_TYPE_SHIFT;
|
|
break;
|
|
case PIPE_INTERRUPT:
|
|
pipe |= XENUSB_PIPE_TYPE_INT << XENUSB_PIPE_TYPE_SHIFT;
|
|
break;
|
|
case PIPE_CONTROL:
|
|
pipe |= XENUSB_PIPE_TYPE_CTRL << XENUSB_PIPE_TYPE_SHIFT;
|
|
break;
|
|
case PIPE_BULK:
|
|
pipe |= XENUSB_PIPE_TYPE_BULK << XENUSB_PIPE_TYPE_SHIFT;
|
|
break;
|
|
}
|
|
pipe = xenusb_setportnum_pipe(pipe, port);
|
|
|
|
return pipe;
|
|
}
|
|
|
|
static int xenhcd_map_urb_for_request(struct xenhcd_info *info, struct urb *urb,
|
|
struct xenusb_urb_request *req)
|
|
{
|
|
grant_ref_t gref_head;
|
|
int nr_buff_pages = 0;
|
|
int nr_isodesc_pages = 0;
|
|
int nr_grants = 0;
|
|
|
|
if (urb->transfer_buffer_length) {
|
|
nr_buff_pages = xenhcd_count_pages(urb->transfer_buffer,
|
|
urb->transfer_buffer_length);
|
|
|
|
if (usb_pipeisoc(urb->pipe))
|
|
nr_isodesc_pages = xenhcd_count_pages(
|
|
&urb->iso_frame_desc[0],
|
|
sizeof(struct usb_iso_packet_descriptor) *
|
|
urb->number_of_packets);
|
|
|
|
nr_grants = nr_buff_pages + nr_isodesc_pages;
|
|
if (nr_grants > XENUSB_MAX_SEGMENTS_PER_REQUEST) {
|
|
pr_err("xenhcd: error: %d grants\n", nr_grants);
|
|
return -E2BIG;
|
|
}
|
|
|
|
if (gnttab_alloc_grant_references(nr_grants, &gref_head)) {
|
|
pr_err("xenhcd: gnttab_alloc_grant_references() error\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
xenhcd_gnttab_map(info, urb->transfer_buffer,
|
|
urb->transfer_buffer_length, &gref_head,
|
|
&req->seg[0], nr_buff_pages,
|
|
usb_pipein(urb->pipe) ? 0 : GTF_readonly);
|
|
}
|
|
|
|
req->pipe = xenhcd_pipe_urb_to_xenusb(urb->pipe, urb->dev->portnum);
|
|
req->transfer_flags = 0;
|
|
if (urb->transfer_flags & URB_SHORT_NOT_OK)
|
|
req->transfer_flags |= XENUSB_SHORT_NOT_OK;
|
|
req->buffer_length = urb->transfer_buffer_length;
|
|
req->nr_buffer_segs = nr_buff_pages;
|
|
|
|
switch (usb_pipetype(urb->pipe)) {
|
|
case PIPE_ISOCHRONOUS:
|
|
req->u.isoc.interval = urb->interval;
|
|
req->u.isoc.start_frame = urb->start_frame;
|
|
req->u.isoc.number_of_packets = urb->number_of_packets;
|
|
req->u.isoc.nr_frame_desc_segs = nr_isodesc_pages;
|
|
|
|
xenhcd_gnttab_map(info, &urb->iso_frame_desc[0],
|
|
sizeof(struct usb_iso_packet_descriptor) *
|
|
urb->number_of_packets,
|
|
&gref_head, &req->seg[nr_buff_pages],
|
|
nr_isodesc_pages, 0);
|
|
break;
|
|
case PIPE_INTERRUPT:
|
|
req->u.intr.interval = urb->interval;
|
|
break;
|
|
case PIPE_CONTROL:
|
|
if (urb->setup_packet)
|
|
memcpy(req->u.ctrl, urb->setup_packet, 8);
|
|
break;
|
|
case PIPE_BULK:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (nr_grants)
|
|
gnttab_free_grant_references(gref_head);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void xenhcd_gnttab_done(struct xenhcd_info *info, unsigned int id)
|
|
{
|
|
struct usb_shadow *shadow = info->shadow + id;
|
|
int nr_segs = 0;
|
|
int i;
|
|
|
|
if (!shadow->in_flight) {
|
|
xenhcd_set_error(info, "Illegal request id");
|
|
return;
|
|
}
|
|
shadow->in_flight = false;
|
|
|
|
nr_segs = shadow->req.nr_buffer_segs;
|
|
|
|
if (xenusb_pipeisoc(shadow->req.pipe))
|
|
nr_segs += shadow->req.u.isoc.nr_frame_desc_segs;
|
|
|
|
for (i = 0; i < nr_segs; i++) {
|
|
if (!gnttab_try_end_foreign_access(shadow->req.seg[i].gref))
|
|
xenhcd_set_error(info, "backend didn't release grant");
|
|
}
|
|
|
|
shadow->req.nr_buffer_segs = 0;
|
|
shadow->req.u.isoc.nr_frame_desc_segs = 0;
|
|
}
|
|
|
|
static int xenhcd_translate_status(int status)
|
|
{
|
|
switch (status) {
|
|
case XENUSB_STATUS_OK:
|
|
return 0;
|
|
case XENUSB_STATUS_NODEV:
|
|
return -ENODEV;
|
|
case XENUSB_STATUS_INVAL:
|
|
return -EINVAL;
|
|
case XENUSB_STATUS_STALL:
|
|
return -EPIPE;
|
|
case XENUSB_STATUS_IOERROR:
|
|
return -EPROTO;
|
|
case XENUSB_STATUS_BABBLE:
|
|
return -EOVERFLOW;
|
|
default:
|
|
return -ESHUTDOWN;
|
|
}
|
|
}
|
|
|
|
static void xenhcd_giveback_urb(struct xenhcd_info *info, struct urb *urb,
|
|
int status)
|
|
{
|
|
struct urb_priv *urbp = (struct urb_priv *)urb->hcpriv;
|
|
int priv_status = urbp->status;
|
|
|
|
list_del_init(&urbp->list);
|
|
xenhcd_free_urb_priv(urbp);
|
|
|
|
if (urb->status == -EINPROGRESS)
|
|
urb->status = xenhcd_translate_status(status);
|
|
|
|
spin_unlock(&info->lock);
|
|
usb_hcd_giveback_urb(xenhcd_info_to_hcd(info), urb,
|
|
priv_status <= 0 ? priv_status : urb->status);
|
|
spin_lock(&info->lock);
|
|
}
|
|
|
|
static int xenhcd_do_request(struct xenhcd_info *info, struct urb_priv *urbp)
|
|
{
|
|
struct xenusb_urb_request *req;
|
|
struct urb *urb = urbp->urb;
|
|
unsigned int id;
|
|
int notify;
|
|
int ret;
|
|
|
|
id = xenhcd_get_id_from_freelist(info);
|
|
req = &info->shadow[id].req;
|
|
req->id = id;
|
|
|
|
if (unlikely(urbp->unlinked)) {
|
|
req->u.unlink.unlink_id = urbp->req_id;
|
|
req->pipe = xenusb_setunlink_pipe(xenhcd_pipe_urb_to_xenusb(
|
|
urb->pipe, urb->dev->portnum));
|
|
urbp->unlink_req_id = id;
|
|
} else {
|
|
ret = xenhcd_map_urb_for_request(info, urb, req);
|
|
if (ret) {
|
|
xenhcd_add_id_to_freelist(info, id);
|
|
return ret;
|
|
}
|
|
urbp->req_id = id;
|
|
}
|
|
|
|
req = RING_GET_REQUEST(&info->urb_ring, info->urb_ring.req_prod_pvt);
|
|
*req = info->shadow[id].req;
|
|
|
|
info->urb_ring.req_prod_pvt++;
|
|
info->shadow[id].urb = urb;
|
|
info->shadow[id].in_flight = true;
|
|
|
|
RING_PUSH_REQUESTS_AND_CHECK_NOTIFY(&info->urb_ring, notify);
|
|
if (notify)
|
|
notify_remote_via_irq(info->irq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void xenhcd_kick_pending_urbs(struct xenhcd_info *info)
|
|
{
|
|
struct urb_priv *urbp;
|
|
|
|
while (!list_empty(&info->pending_submit_list)) {
|
|
if (RING_FULL(&info->urb_ring)) {
|
|
xenhcd_timer_action(info, TIMER_RING_WATCHDOG);
|
|
return;
|
|
}
|
|
|
|
urbp = list_entry(info->pending_submit_list.next,
|
|
struct urb_priv, list);
|
|
if (!xenhcd_do_request(info, urbp))
|
|
list_move_tail(&urbp->list, &info->in_progress_list);
|
|
else
|
|
xenhcd_giveback_urb(info, urbp->urb, -ESHUTDOWN);
|
|
}
|
|
xenhcd_timer_action_done(info, TIMER_SCAN_PENDING_URBS);
|
|
}
|
|
|
|
/*
|
|
* caller must lock info->lock
|
|
*/
|
|
static void xenhcd_cancel_all_enqueued_urbs(struct xenhcd_info *info)
|
|
{
|
|
struct urb_priv *urbp, *tmp;
|
|
int req_id;
|
|
|
|
list_for_each_entry_safe(urbp, tmp, &info->in_progress_list, list) {
|
|
req_id = urbp->req_id;
|
|
if (!urbp->unlinked) {
|
|
xenhcd_gnttab_done(info, req_id);
|
|
if (info->error)
|
|
return;
|
|
if (urbp->urb->status == -EINPROGRESS)
|
|
/* not dequeued */
|
|
xenhcd_giveback_urb(info, urbp->urb,
|
|
-ESHUTDOWN);
|
|
else /* dequeued */
|
|
xenhcd_giveback_urb(info, urbp->urb,
|
|
urbp->urb->status);
|
|
}
|
|
info->shadow[req_id].urb = NULL;
|
|
}
|
|
|
|
list_for_each_entry_safe(urbp, tmp, &info->pending_submit_list, list)
|
|
xenhcd_giveback_urb(info, urbp->urb, -ESHUTDOWN);
|
|
}
|
|
|
|
/*
|
|
* caller must lock info->lock
|
|
*/
|
|
static void xenhcd_giveback_unlinked_urbs(struct xenhcd_info *info)
|
|
{
|
|
struct urb_priv *urbp, *tmp;
|
|
|
|
list_for_each_entry_safe(urbp, tmp, &info->giveback_waiting_list, list)
|
|
xenhcd_giveback_urb(info, urbp->urb, urbp->urb->status);
|
|
}
|
|
|
|
static int xenhcd_submit_urb(struct xenhcd_info *info, struct urb_priv *urbp)
|
|
{
|
|
int ret;
|
|
|
|
if (RING_FULL(&info->urb_ring)) {
|
|
list_add_tail(&urbp->list, &info->pending_submit_list);
|
|
xenhcd_timer_action(info, TIMER_RING_WATCHDOG);
|
|
return 0;
|
|
}
|
|
|
|
if (!list_empty(&info->pending_submit_list)) {
|
|
list_add_tail(&urbp->list, &info->pending_submit_list);
|
|
xenhcd_timer_action(info, TIMER_SCAN_PENDING_URBS);
|
|
return 0;
|
|
}
|
|
|
|
ret = xenhcd_do_request(info, urbp);
|
|
if (ret == 0)
|
|
list_add_tail(&urbp->list, &info->in_progress_list);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int xenhcd_unlink_urb(struct xenhcd_info *info, struct urb_priv *urbp)
|
|
{
|
|
int ret;
|
|
|
|
/* already unlinked? */
|
|
if (urbp->unlinked)
|
|
return -EBUSY;
|
|
|
|
urbp->unlinked = true;
|
|
|
|
/* the urb is still in pending_submit queue */
|
|
if (urbp->req_id == ~0) {
|
|
list_move_tail(&urbp->list, &info->giveback_waiting_list);
|
|
xenhcd_timer_action(info, TIMER_SCAN_PENDING_URBS);
|
|
return 0;
|
|
}
|
|
|
|
/* send unlink request to backend */
|
|
if (RING_FULL(&info->urb_ring)) {
|
|
list_move_tail(&urbp->list, &info->pending_unlink_list);
|
|
xenhcd_timer_action(info, TIMER_RING_WATCHDOG);
|
|
return 0;
|
|
}
|
|
|
|
if (!list_empty(&info->pending_unlink_list)) {
|
|
list_move_tail(&urbp->list, &info->pending_unlink_list);
|
|
xenhcd_timer_action(info, TIMER_SCAN_PENDING_URBS);
|
|
return 0;
|
|
}
|
|
|
|
ret = xenhcd_do_request(info, urbp);
|
|
if (ret == 0)
|
|
list_move_tail(&urbp->list, &info->in_progress_list);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void xenhcd_res_to_urb(struct xenhcd_info *info,
|
|
struct xenusb_urb_response *res, struct urb *urb)
|
|
{
|
|
if (unlikely(!urb))
|
|
return;
|
|
|
|
if (res->actual_length > urb->transfer_buffer_length)
|
|
urb->actual_length = urb->transfer_buffer_length;
|
|
else if (res->actual_length < 0)
|
|
urb->actual_length = 0;
|
|
else
|
|
urb->actual_length = res->actual_length;
|
|
urb->error_count = res->error_count;
|
|
urb->start_frame = res->start_frame;
|
|
xenhcd_giveback_urb(info, urb, res->status);
|
|
}
|
|
|
|
static int xenhcd_urb_request_done(struct xenhcd_info *info,
|
|
unsigned int *eoiflag)
|
|
{
|
|
struct xenusb_urb_response res;
|
|
RING_IDX i, rp;
|
|
__u16 id;
|
|
int more_to_do = 0;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
|
|
rp = info->urb_ring.sring->rsp_prod;
|
|
if (RING_RESPONSE_PROD_OVERFLOW(&info->urb_ring, rp)) {
|
|
xenhcd_set_error(info, "Illegal index on urb-ring");
|
|
goto err;
|
|
}
|
|
rmb(); /* ensure we see queued responses up to "rp" */
|
|
|
|
for (i = info->urb_ring.rsp_cons; i != rp; i++) {
|
|
RING_COPY_RESPONSE(&info->urb_ring, i, &res);
|
|
id = res.id;
|
|
if (id >= XENUSB_URB_RING_SIZE) {
|
|
xenhcd_set_error(info, "Illegal data on urb-ring");
|
|
goto err;
|
|
}
|
|
|
|
if (likely(xenusb_pipesubmit(info->shadow[id].req.pipe))) {
|
|
xenhcd_gnttab_done(info, id);
|
|
if (info->error)
|
|
goto err;
|
|
xenhcd_res_to_urb(info, &res, info->shadow[id].urb);
|
|
}
|
|
|
|
xenhcd_add_id_to_freelist(info, id);
|
|
|
|
*eoiflag = 0;
|
|
}
|
|
info->urb_ring.rsp_cons = i;
|
|
|
|
if (i != info->urb_ring.req_prod_pvt)
|
|
RING_FINAL_CHECK_FOR_RESPONSES(&info->urb_ring, more_to_do);
|
|
else
|
|
info->urb_ring.sring->rsp_event = i + 1;
|
|
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
return more_to_do;
|
|
|
|
err:
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
return 0;
|
|
}
|
|
|
|
static int xenhcd_conn_notify(struct xenhcd_info *info, unsigned int *eoiflag)
|
|
{
|
|
struct xenusb_conn_response res;
|
|
struct xenusb_conn_request *req;
|
|
RING_IDX rc, rp;
|
|
__u16 id;
|
|
__u8 portnum, speed;
|
|
int more_to_do = 0;
|
|
int notify;
|
|
int port_changed = 0;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
|
|
rc = info->conn_ring.rsp_cons;
|
|
rp = info->conn_ring.sring->rsp_prod;
|
|
if (RING_RESPONSE_PROD_OVERFLOW(&info->conn_ring, rp)) {
|
|
xenhcd_set_error(info, "Illegal index on conn-ring");
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
return 0;
|
|
}
|
|
rmb(); /* ensure we see queued responses up to "rp" */
|
|
|
|
while (rc != rp) {
|
|
RING_COPY_RESPONSE(&info->conn_ring, rc, &res);
|
|
id = res.id;
|
|
portnum = res.portnum;
|
|
speed = res.speed;
|
|
info->conn_ring.rsp_cons = ++rc;
|
|
|
|
if (xenhcd_rhport_connect(info, portnum, speed)) {
|
|
xenhcd_set_error(info, "Illegal data on conn-ring");
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
return 0;
|
|
}
|
|
|
|
if (info->ports[portnum - 1].c_connection)
|
|
port_changed = 1;
|
|
|
|
barrier();
|
|
|
|
req = RING_GET_REQUEST(&info->conn_ring,
|
|
info->conn_ring.req_prod_pvt);
|
|
req->id = id;
|
|
info->conn_ring.req_prod_pvt++;
|
|
|
|
*eoiflag = 0;
|
|
}
|
|
|
|
if (rc != info->conn_ring.req_prod_pvt)
|
|
RING_FINAL_CHECK_FOR_RESPONSES(&info->conn_ring, more_to_do);
|
|
else
|
|
info->conn_ring.sring->rsp_event = rc + 1;
|
|
|
|
RING_PUSH_REQUESTS_AND_CHECK_NOTIFY(&info->conn_ring, notify);
|
|
if (notify)
|
|
notify_remote_via_irq(info->irq);
|
|
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
if (port_changed)
|
|
usb_hcd_poll_rh_status(xenhcd_info_to_hcd(info));
|
|
|
|
return more_to_do;
|
|
}
|
|
|
|
static irqreturn_t xenhcd_int(int irq, void *dev_id)
|
|
{
|
|
struct xenhcd_info *info = (struct xenhcd_info *)dev_id;
|
|
unsigned int eoiflag = XEN_EOI_FLAG_SPURIOUS;
|
|
|
|
if (unlikely(info->error)) {
|
|
xen_irq_lateeoi(irq, XEN_EOI_FLAG_SPURIOUS);
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
while (xenhcd_urb_request_done(info, &eoiflag) |
|
|
xenhcd_conn_notify(info, &eoiflag))
|
|
/* Yield point for this unbounded loop. */
|
|
cond_resched();
|
|
|
|
xen_irq_lateeoi(irq, eoiflag);
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void xenhcd_destroy_rings(struct xenhcd_info *info)
|
|
{
|
|
if (info->irq)
|
|
unbind_from_irqhandler(info->irq, info);
|
|
info->irq = 0;
|
|
|
|
xenbus_teardown_ring((void **)&info->urb_ring.sring, 1,
|
|
&info->urb_ring_ref);
|
|
xenbus_teardown_ring((void **)&info->conn_ring.sring, 1,
|
|
&info->conn_ring_ref);
|
|
}
|
|
|
|
static int xenhcd_setup_rings(struct xenbus_device *dev,
|
|
struct xenhcd_info *info)
|
|
{
|
|
struct xenusb_urb_sring *urb_sring;
|
|
struct xenusb_conn_sring *conn_sring;
|
|
int err;
|
|
|
|
info->conn_ring_ref = INVALID_GRANT_REF;
|
|
err = xenbus_setup_ring(dev, GFP_NOIO | __GFP_HIGH,
|
|
(void **)&urb_sring, 1, &info->urb_ring_ref);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "allocating urb ring");
|
|
return err;
|
|
}
|
|
XEN_FRONT_RING_INIT(&info->urb_ring, urb_sring, PAGE_SIZE);
|
|
|
|
err = xenbus_setup_ring(dev, GFP_NOIO | __GFP_HIGH,
|
|
(void **)&conn_sring, 1, &info->conn_ring_ref);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "allocating conn ring");
|
|
goto fail;
|
|
}
|
|
XEN_FRONT_RING_INIT(&info->conn_ring, conn_sring, PAGE_SIZE);
|
|
|
|
err = xenbus_alloc_evtchn(dev, &info->evtchn);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "xenbus_alloc_evtchn");
|
|
goto fail;
|
|
}
|
|
|
|
err = bind_evtchn_to_irq_lateeoi(info->evtchn);
|
|
if (err <= 0) {
|
|
xenbus_dev_fatal(dev, err, "bind_evtchn_to_irq_lateeoi");
|
|
goto fail;
|
|
}
|
|
|
|
info->irq = err;
|
|
|
|
err = request_threaded_irq(info->irq, NULL, xenhcd_int,
|
|
IRQF_ONESHOT, "xenhcd", info);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "request_threaded_irq");
|
|
goto free_irq;
|
|
}
|
|
|
|
return 0;
|
|
|
|
free_irq:
|
|
unbind_from_irqhandler(info->irq, info);
|
|
fail:
|
|
xenhcd_destroy_rings(info);
|
|
return err;
|
|
}
|
|
|
|
static int xenhcd_talk_to_backend(struct xenbus_device *dev,
|
|
struct xenhcd_info *info)
|
|
{
|
|
const char *message;
|
|
struct xenbus_transaction xbt;
|
|
int err;
|
|
|
|
err = xenhcd_setup_rings(dev, info);
|
|
if (err)
|
|
return err;
|
|
|
|
again:
|
|
err = xenbus_transaction_start(&xbt);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "starting transaction");
|
|
goto destroy_ring;
|
|
}
|
|
|
|
err = xenbus_printf(xbt, dev->nodename, "urb-ring-ref", "%u",
|
|
info->urb_ring_ref);
|
|
if (err) {
|
|
message = "writing urb-ring-ref";
|
|
goto abort_transaction;
|
|
}
|
|
|
|
err = xenbus_printf(xbt, dev->nodename, "conn-ring-ref", "%u",
|
|
info->conn_ring_ref);
|
|
if (err) {
|
|
message = "writing conn-ring-ref";
|
|
goto abort_transaction;
|
|
}
|
|
|
|
err = xenbus_printf(xbt, dev->nodename, "event-channel", "%u",
|
|
info->evtchn);
|
|
if (err) {
|
|
message = "writing event-channel";
|
|
goto abort_transaction;
|
|
}
|
|
|
|
err = xenbus_transaction_end(xbt, 0);
|
|
if (err) {
|
|
if (err == -EAGAIN)
|
|
goto again;
|
|
xenbus_dev_fatal(dev, err, "completing transaction");
|
|
goto destroy_ring;
|
|
}
|
|
|
|
return 0;
|
|
|
|
abort_transaction:
|
|
xenbus_transaction_end(xbt, 1);
|
|
xenbus_dev_fatal(dev, err, "%s", message);
|
|
|
|
destroy_ring:
|
|
xenhcd_destroy_rings(info);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int xenhcd_connect(struct xenbus_device *dev)
|
|
{
|
|
struct xenhcd_info *info = dev_get_drvdata(&dev->dev);
|
|
struct xenusb_conn_request *req;
|
|
int idx, err;
|
|
int notify;
|
|
char name[TASK_COMM_LEN];
|
|
struct usb_hcd *hcd;
|
|
|
|
hcd = xenhcd_info_to_hcd(info);
|
|
snprintf(name, TASK_COMM_LEN, "xenhcd.%d", hcd->self.busnum);
|
|
|
|
err = xenhcd_talk_to_backend(dev, info);
|
|
if (err)
|
|
return err;
|
|
|
|
/* prepare ring for hotplug notification */
|
|
for (idx = 0; idx < XENUSB_CONN_RING_SIZE; idx++) {
|
|
req = RING_GET_REQUEST(&info->conn_ring, idx);
|
|
req->id = idx;
|
|
}
|
|
info->conn_ring.req_prod_pvt = idx;
|
|
|
|
RING_PUSH_REQUESTS_AND_CHECK_NOTIFY(&info->conn_ring, notify);
|
|
if (notify)
|
|
notify_remote_via_irq(info->irq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void xenhcd_disconnect(struct xenbus_device *dev)
|
|
{
|
|
struct xenhcd_info *info = dev_get_drvdata(&dev->dev);
|
|
struct usb_hcd *hcd = xenhcd_info_to_hcd(info);
|
|
|
|
usb_remove_hcd(hcd);
|
|
xenbus_frontend_closed(dev);
|
|
}
|
|
|
|
static void xenhcd_watchdog(struct timer_list *timer)
|
|
{
|
|
struct xenhcd_info *info = from_timer(info, timer, watchdog);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
if (likely(HC_IS_RUNNING(xenhcd_info_to_hcd(info)->state))) {
|
|
xenhcd_timer_action_done(info, TIMER_RING_WATCHDOG);
|
|
xenhcd_giveback_unlinked_urbs(info);
|
|
xenhcd_kick_pending_urbs(info);
|
|
}
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
}
|
|
|
|
/*
|
|
* one-time HC init
|
|
*/
|
|
static int xenhcd_setup(struct usb_hcd *hcd)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
|
|
spin_lock_init(&info->lock);
|
|
INIT_LIST_HEAD(&info->pending_submit_list);
|
|
INIT_LIST_HEAD(&info->pending_unlink_list);
|
|
INIT_LIST_HEAD(&info->in_progress_list);
|
|
INIT_LIST_HEAD(&info->giveback_waiting_list);
|
|
timer_setup(&info->watchdog, xenhcd_watchdog, 0);
|
|
|
|
hcd->has_tt = (hcd->driver->flags & HCD_MASK) != HCD_USB11;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* start HC running
|
|
*/
|
|
static int xenhcd_run(struct usb_hcd *hcd)
|
|
{
|
|
hcd->uses_new_polling = 1;
|
|
clear_bit(HCD_FLAG_POLL_RH, &hcd->flags);
|
|
hcd->state = HC_STATE_RUNNING;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* stop running HC
|
|
*/
|
|
static void xenhcd_stop(struct usb_hcd *hcd)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
|
|
del_timer_sync(&info->watchdog);
|
|
spin_lock_irq(&info->lock);
|
|
/* cancel all urbs */
|
|
hcd->state = HC_STATE_HALT;
|
|
xenhcd_cancel_all_enqueued_urbs(info);
|
|
xenhcd_giveback_unlinked_urbs(info);
|
|
spin_unlock_irq(&info->lock);
|
|
}
|
|
|
|
/*
|
|
* called as .urb_enqueue()
|
|
* non-error returns are promise to giveback the urb later
|
|
*/
|
|
static int xenhcd_urb_enqueue(struct usb_hcd *hcd, struct urb *urb,
|
|
gfp_t mem_flags)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
struct urb_priv *urbp;
|
|
unsigned long flags;
|
|
int ret;
|
|
|
|
if (unlikely(info->error))
|
|
return -ESHUTDOWN;
|
|
|
|
urbp = kmem_cache_zalloc(xenhcd_urbp_cachep, mem_flags);
|
|
if (!urbp)
|
|
return -ENOMEM;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
|
|
urbp->urb = urb;
|
|
urb->hcpriv = urbp;
|
|
urbp->req_id = ~0;
|
|
urbp->unlink_req_id = ~0;
|
|
INIT_LIST_HEAD(&urbp->list);
|
|
urbp->status = 1;
|
|
urb->unlinked = false;
|
|
|
|
ret = xenhcd_submit_urb(info, urbp);
|
|
|
|
if (ret)
|
|
xenhcd_free_urb_priv(urbp);
|
|
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* called as .urb_dequeue()
|
|
*/
|
|
static int xenhcd_urb_dequeue(struct usb_hcd *hcd, struct urb *urb, int status)
|
|
{
|
|
struct xenhcd_info *info = xenhcd_hcd_to_info(hcd);
|
|
struct urb_priv *urbp;
|
|
unsigned long flags;
|
|
int ret = 0;
|
|
|
|
spin_lock_irqsave(&info->lock, flags);
|
|
|
|
urbp = urb->hcpriv;
|
|
if (urbp) {
|
|
urbp->status = status;
|
|
ret = xenhcd_unlink_urb(info, urbp);
|
|
}
|
|
|
|
spin_unlock_irqrestore(&info->lock, flags);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* called from usb_get_current_frame_number(),
|
|
* but, almost all drivers not use such function.
|
|
*/
|
|
static int xenhcd_get_frame(struct usb_hcd *hcd)
|
|
{
|
|
/* it means error, but probably no problem :-) */
|
|
return 0;
|
|
}
|
|
|
|
static struct hc_driver xenhcd_usb20_hc_driver = {
|
|
.description = "xen-hcd",
|
|
.product_desc = "Xen USB2.0 Virtual Host Controller",
|
|
.hcd_priv_size = sizeof(struct xenhcd_info),
|
|
.flags = HCD_USB2,
|
|
|
|
/* basic HC lifecycle operations */
|
|
.reset = xenhcd_setup,
|
|
.start = xenhcd_run,
|
|
.stop = xenhcd_stop,
|
|
|
|
/* managing urb I/O */
|
|
.urb_enqueue = xenhcd_urb_enqueue,
|
|
.urb_dequeue = xenhcd_urb_dequeue,
|
|
.get_frame_number = xenhcd_get_frame,
|
|
|
|
/* root hub operations */
|
|
.hub_status_data = xenhcd_hub_status_data,
|
|
.hub_control = xenhcd_hub_control,
|
|
#ifdef CONFIG_PM
|
|
.bus_suspend = xenhcd_bus_suspend,
|
|
.bus_resume = xenhcd_bus_resume,
|
|
#endif
|
|
};
|
|
|
|
static struct hc_driver xenhcd_usb11_hc_driver = {
|
|
.description = "xen-hcd",
|
|
.product_desc = "Xen USB1.1 Virtual Host Controller",
|
|
.hcd_priv_size = sizeof(struct xenhcd_info),
|
|
.flags = HCD_USB11,
|
|
|
|
/* basic HC lifecycle operations */
|
|
.reset = xenhcd_setup,
|
|
.start = xenhcd_run,
|
|
.stop = xenhcd_stop,
|
|
|
|
/* managing urb I/O */
|
|
.urb_enqueue = xenhcd_urb_enqueue,
|
|
.urb_dequeue = xenhcd_urb_dequeue,
|
|
.get_frame_number = xenhcd_get_frame,
|
|
|
|
/* root hub operations */
|
|
.hub_status_data = xenhcd_hub_status_data,
|
|
.hub_control = xenhcd_hub_control,
|
|
#ifdef CONFIG_PM
|
|
.bus_suspend = xenhcd_bus_suspend,
|
|
.bus_resume = xenhcd_bus_resume,
|
|
#endif
|
|
};
|
|
|
|
static struct usb_hcd *xenhcd_create_hcd(struct xenbus_device *dev)
|
|
{
|
|
int i;
|
|
int err = 0;
|
|
int num_ports;
|
|
int usb_ver;
|
|
struct usb_hcd *hcd = NULL;
|
|
struct xenhcd_info *info;
|
|
|
|
err = xenbus_scanf(XBT_NIL, dev->otherend, "num-ports", "%d",
|
|
&num_ports);
|
|
if (err != 1) {
|
|
xenbus_dev_fatal(dev, err, "reading num-ports");
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
if (num_ports < 1 || num_ports > XENUSB_MAX_PORTNR) {
|
|
xenbus_dev_fatal(dev, err, "invalid num-ports");
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
|
|
err = xenbus_scanf(XBT_NIL, dev->otherend, "usb-ver", "%d", &usb_ver);
|
|
if (err != 1) {
|
|
xenbus_dev_fatal(dev, err, "reading usb-ver");
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
switch (usb_ver) {
|
|
case XENUSB_VER_USB11:
|
|
hcd = usb_create_hcd(&xenhcd_usb11_hc_driver, &dev->dev,
|
|
dev_name(&dev->dev));
|
|
break;
|
|
case XENUSB_VER_USB20:
|
|
hcd = usb_create_hcd(&xenhcd_usb20_hc_driver, &dev->dev,
|
|
dev_name(&dev->dev));
|
|
break;
|
|
default:
|
|
xenbus_dev_fatal(dev, err, "invalid usb-ver");
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
if (!hcd) {
|
|
xenbus_dev_fatal(dev, err,
|
|
"fail to allocate USB host controller");
|
|
return ERR_PTR(-ENOMEM);
|
|
}
|
|
|
|
info = xenhcd_hcd_to_info(hcd);
|
|
info->xbdev = dev;
|
|
info->rh_numports = num_ports;
|
|
|
|
for (i = 0; i < XENUSB_URB_RING_SIZE; i++) {
|
|
info->shadow[i].req.id = i + 1;
|
|
info->shadow[i].urb = NULL;
|
|
info->shadow[i].in_flight = false;
|
|
}
|
|
info->shadow[XENUSB_URB_RING_SIZE - 1].req.id = 0x0fff;
|
|
|
|
return hcd;
|
|
}
|
|
|
|
static void xenhcd_backend_changed(struct xenbus_device *dev,
|
|
enum xenbus_state backend_state)
|
|
{
|
|
switch (backend_state) {
|
|
case XenbusStateInitialising:
|
|
case XenbusStateReconfiguring:
|
|
case XenbusStateReconfigured:
|
|
case XenbusStateUnknown:
|
|
break;
|
|
|
|
case XenbusStateInitWait:
|
|
case XenbusStateInitialised:
|
|
case XenbusStateConnected:
|
|
if (dev->state != XenbusStateInitialising)
|
|
break;
|
|
if (!xenhcd_connect(dev))
|
|
xenbus_switch_state(dev, XenbusStateConnected);
|
|
break;
|
|
|
|
case XenbusStateClosed:
|
|
if (dev->state == XenbusStateClosed)
|
|
break;
|
|
fallthrough; /* Missed the backend's Closing state. */
|
|
case XenbusStateClosing:
|
|
xenhcd_disconnect(dev);
|
|
break;
|
|
|
|
default:
|
|
xenbus_dev_fatal(dev, -EINVAL, "saw state %d at frontend",
|
|
backend_state);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void xenhcd_remove(struct xenbus_device *dev)
|
|
{
|
|
struct xenhcd_info *info = dev_get_drvdata(&dev->dev);
|
|
struct usb_hcd *hcd = xenhcd_info_to_hcd(info);
|
|
|
|
xenhcd_destroy_rings(info);
|
|
usb_put_hcd(hcd);
|
|
}
|
|
|
|
static int xenhcd_probe(struct xenbus_device *dev,
|
|
const struct xenbus_device_id *id)
|
|
{
|
|
int err;
|
|
struct usb_hcd *hcd;
|
|
struct xenhcd_info *info;
|
|
|
|
if (usb_disabled())
|
|
return -ENODEV;
|
|
|
|
hcd = xenhcd_create_hcd(dev);
|
|
if (IS_ERR(hcd)) {
|
|
err = PTR_ERR(hcd);
|
|
xenbus_dev_fatal(dev, err,
|
|
"fail to create usb host controller");
|
|
return err;
|
|
}
|
|
|
|
info = xenhcd_hcd_to_info(hcd);
|
|
dev_set_drvdata(&dev->dev, info);
|
|
|
|
err = usb_add_hcd(hcd, 0, 0);
|
|
if (err) {
|
|
xenbus_dev_fatal(dev, err, "fail to add USB host controller");
|
|
usb_put_hcd(hcd);
|
|
dev_set_drvdata(&dev->dev, NULL);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static const struct xenbus_device_id xenhcd_ids[] = {
|
|
{ "vusb" },
|
|
{ "" },
|
|
};
|
|
|
|
static struct xenbus_driver xenhcd_driver = {
|
|
.ids = xenhcd_ids,
|
|
.probe = xenhcd_probe,
|
|
.otherend_changed = xenhcd_backend_changed,
|
|
.remove = xenhcd_remove,
|
|
};
|
|
|
|
static int __init xenhcd_init(void)
|
|
{
|
|
if (!xen_domain())
|
|
return -ENODEV;
|
|
|
|
xenhcd_urbp_cachep = kmem_cache_create("xenhcd_urb_priv",
|
|
sizeof(struct urb_priv), 0, 0, NULL);
|
|
if (!xenhcd_urbp_cachep) {
|
|
pr_err("xenhcd failed to create kmem cache\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return xenbus_register_frontend(&xenhcd_driver);
|
|
}
|
|
module_init(xenhcd_init);
|
|
|
|
static void __exit xenhcd_exit(void)
|
|
{
|
|
kmem_cache_destroy(xenhcd_urbp_cachep);
|
|
xenbus_unregister_driver(&xenhcd_driver);
|
|
}
|
|
module_exit(xenhcd_exit);
|
|
|
|
MODULE_ALIAS("xen:vusb");
|
|
MODULE_AUTHOR("Juergen Gross <jgross@suse.com>");
|
|
MODULE_DESCRIPTION("Xen USB Virtual Host Controller driver (xen-hcd)");
|
|
MODULE_LICENSE("Dual BSD/GPL");
|