496 lines
13 KiB
C
496 lines
13 KiB
C
|
// SPDX-License-Identifier: GPL-2.0+
|
||
|
/*
|
||
|
* aspeed-vhub -- Driver for Aspeed SoC "vHub" USB gadget
|
||
|
*
|
||
|
* ep0.c - Endpoint 0 handling
|
||
|
*
|
||
|
* Copyright 2017 IBM Corporation
|
||
|
*
|
||
|
* 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.
|
||
|
*/
|
||
|
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/platform_device.h>
|
||
|
#include <linux/delay.h>
|
||
|
#include <linux/ioport.h>
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/errno.h>
|
||
|
#include <linux/list.h>
|
||
|
#include <linux/interrupt.h>
|
||
|
#include <linux/proc_fs.h>
|
||
|
#include <linux/prefetch.h>
|
||
|
#include <linux/clk.h>
|
||
|
#include <linux/usb/gadget.h>
|
||
|
#include <linux/of.h>
|
||
|
#include <linux/of_gpio.h>
|
||
|
#include <linux/regmap.h>
|
||
|
#include <linux/dma-mapping.h>
|
||
|
|
||
|
#include "vhub.h"
|
||
|
|
||
|
int ast_vhub_reply(struct ast_vhub_ep *ep, char *ptr, int len)
|
||
|
{
|
||
|
struct usb_request *req = &ep->ep0.req.req;
|
||
|
int rc;
|
||
|
|
||
|
if (WARN_ON(ep->d_idx != 0))
|
||
|
return std_req_stall;
|
||
|
if (WARN_ON(!ep->ep0.dir_in))
|
||
|
return std_req_stall;
|
||
|
if (WARN_ON(len > AST_VHUB_EP0_MAX_PACKET))
|
||
|
return std_req_stall;
|
||
|
if (WARN_ON(req->status == -EINPROGRESS))
|
||
|
return std_req_stall;
|
||
|
|
||
|
req->buf = ptr;
|
||
|
req->length = len;
|
||
|
req->complete = NULL;
|
||
|
req->zero = true;
|
||
|
|
||
|
/*
|
||
|
* Call internal queue directly after dropping the lock. This is
|
||
|
* safe to do as the reply is always the last thing done when
|
||
|
* processing a SETUP packet, usually as a tail call
|
||
|
*/
|
||
|
spin_unlock(&ep->vhub->lock);
|
||
|
if (ep->ep.ops->queue(&ep->ep, req, GFP_ATOMIC))
|
||
|
rc = std_req_stall;
|
||
|
else
|
||
|
rc = std_req_data;
|
||
|
spin_lock(&ep->vhub->lock);
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
int __ast_vhub_simple_reply(struct ast_vhub_ep *ep, int len, ...)
|
||
|
{
|
||
|
u8 *buffer = ep->buf;
|
||
|
unsigned int i;
|
||
|
va_list args;
|
||
|
|
||
|
va_start(args, len);
|
||
|
|
||
|
/* Copy data directly into EP buffer */
|
||
|
for (i = 0; i < len; i++)
|
||
|
buffer[i] = va_arg(args, int);
|
||
|
va_end(args);
|
||
|
|
||
|
/* req->buf NULL means data is already there */
|
||
|
return ast_vhub_reply(ep, NULL, len);
|
||
|
}
|
||
|
|
||
|
void ast_vhub_ep0_handle_setup(struct ast_vhub_ep *ep)
|
||
|
{
|
||
|
struct usb_ctrlrequest crq;
|
||
|
enum std_req_rc std_req_rc;
|
||
|
int rc = -ENODEV;
|
||
|
|
||
|
if (WARN_ON(ep->d_idx != 0))
|
||
|
return;
|
||
|
|
||
|
/*
|
||
|
* Grab the setup packet from the chip and byteswap
|
||
|
* interesting fields
|
||
|
*/
|
||
|
memcpy_fromio(&crq, ep->ep0.setup, sizeof(crq));
|
||
|
|
||
|
EPDBG(ep, "SETUP packet %02x/%02x/%04x/%04x/%04x [%s] st=%d\n",
|
||
|
crq.bRequestType, crq.bRequest,
|
||
|
le16_to_cpu(crq.wValue),
|
||
|
le16_to_cpu(crq.wIndex),
|
||
|
le16_to_cpu(crq.wLength),
|
||
|
(crq.bRequestType & USB_DIR_IN) ? "in" : "out",
|
||
|
ep->ep0.state);
|
||
|
|
||
|
/* Check our state, cancel pending requests if needed */
|
||
|
if (ep->ep0.state != ep0_state_token) {
|
||
|
EPDBG(ep, "wrong state\n");
|
||
|
ast_vhub_nuke(ep, -EIO);
|
||
|
|
||
|
/*
|
||
|
* Accept the packet regardless, this seems to happen
|
||
|
* when stalling a SETUP packet that has an OUT data
|
||
|
* phase.
|
||
|
*/
|
||
|
ast_vhub_nuke(ep, 0);
|
||
|
goto stall;
|
||
|
}
|
||
|
|
||
|
/* Calculate next state for EP0 */
|
||
|
ep->ep0.state = ep0_state_data;
|
||
|
ep->ep0.dir_in = !!(crq.bRequestType & USB_DIR_IN);
|
||
|
|
||
|
/* If this is the vHub, we handle requests differently */
|
||
|
std_req_rc = std_req_driver;
|
||
|
if (ep->dev == NULL) {
|
||
|
if ((crq.bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD)
|
||
|
std_req_rc = ast_vhub_std_hub_request(ep, &crq);
|
||
|
else if ((crq.bRequestType & USB_TYPE_MASK) == USB_TYPE_CLASS)
|
||
|
std_req_rc = ast_vhub_class_hub_request(ep, &crq);
|
||
|
else
|
||
|
std_req_rc = std_req_stall;
|
||
|
} else if ((crq.bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD)
|
||
|
std_req_rc = ast_vhub_std_dev_request(ep, &crq);
|
||
|
|
||
|
/* Act upon result */
|
||
|
switch(std_req_rc) {
|
||
|
case std_req_complete:
|
||
|
goto complete;
|
||
|
case std_req_stall:
|
||
|
goto stall;
|
||
|
case std_req_driver:
|
||
|
break;
|
||
|
case std_req_data:
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/* Pass request up to the gadget driver */
|
||
|
if (WARN_ON(!ep->dev))
|
||
|
goto stall;
|
||
|
if (ep->dev->driver) {
|
||
|
EPDBG(ep, "forwarding to gadget...\n");
|
||
|
spin_unlock(&ep->vhub->lock);
|
||
|
rc = ep->dev->driver->setup(&ep->dev->gadget, &crq);
|
||
|
spin_lock(&ep->vhub->lock);
|
||
|
EPDBG(ep, "driver returned %d\n", rc);
|
||
|
} else {
|
||
|
EPDBG(ep, "no gadget for request !\n");
|
||
|
}
|
||
|
if (rc >= 0)
|
||
|
return;
|
||
|
|
||
|
stall:
|
||
|
EPDBG(ep, "stalling\n");
|
||
|
writel(VHUB_EP0_CTRL_STALL, ep->ep0.ctlstat);
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
ep->ep0.dir_in = false;
|
||
|
return;
|
||
|
|
||
|
complete:
|
||
|
EPVDBG(ep, "sending [in] status with no data\n");
|
||
|
writel(VHUB_EP0_TX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
ep->ep0.dir_in = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
static void ast_vhub_ep0_do_send(struct ast_vhub_ep *ep,
|
||
|
struct ast_vhub_req *req)
|
||
|
{
|
||
|
unsigned int chunk;
|
||
|
u32 reg;
|
||
|
|
||
|
/* If this is a 0-length request, it's the gadget trying to
|
||
|
* send a status on our behalf. We take it from here.
|
||
|
*/
|
||
|
if (req->req.length == 0)
|
||
|
req->last_desc = 1;
|
||
|
|
||
|
/* Are we done ? Complete request, otherwise wait for next interrupt */
|
||
|
if (req->last_desc >= 0) {
|
||
|
EPVDBG(ep, "complete send %d/%d\n",
|
||
|
req->req.actual, req->req.length);
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
writel(VHUB_EP0_RX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
ast_vhub_done(ep, req, 0);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Next chunk cropped to max packet size. Also check if this
|
||
|
* is the last packet
|
||
|
*/
|
||
|
chunk = req->req.length - req->req.actual;
|
||
|
if (chunk > ep->ep.maxpacket)
|
||
|
chunk = ep->ep.maxpacket;
|
||
|
else if ((chunk < ep->ep.maxpacket) || !req->req.zero)
|
||
|
req->last_desc = 1;
|
||
|
|
||
|
EPVDBG(ep, "send chunk=%d last=%d, req->act=%d mp=%d\n",
|
||
|
chunk, req->last_desc, req->req.actual, ep->ep.maxpacket);
|
||
|
|
||
|
/*
|
||
|
* Copy data if any (internal requests already have data
|
||
|
* in the EP buffer)
|
||
|
*/
|
||
|
if (chunk && req->req.buf)
|
||
|
memcpy(ep->buf, req->req.buf + req->req.actual, chunk);
|
||
|
|
||
|
vhub_dma_workaround(ep->buf);
|
||
|
|
||
|
/* Remember chunk size and trigger send */
|
||
|
reg = VHUB_EP0_SET_TX_LEN(chunk);
|
||
|
writel(reg, ep->ep0.ctlstat);
|
||
|
writel(reg | VHUB_EP0_TX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
req->req.actual += chunk;
|
||
|
}
|
||
|
|
||
|
static void ast_vhub_ep0_rx_prime(struct ast_vhub_ep *ep)
|
||
|
{
|
||
|
EPVDBG(ep, "rx prime\n");
|
||
|
|
||
|
/* Prime endpoint for receiving data */
|
||
|
writel(VHUB_EP0_RX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
}
|
||
|
|
||
|
static void ast_vhub_ep0_do_receive(struct ast_vhub_ep *ep, struct ast_vhub_req *req,
|
||
|
unsigned int len)
|
||
|
{
|
||
|
unsigned int remain;
|
||
|
int rc = 0;
|
||
|
|
||
|
/* We are receiving... grab request */
|
||
|
remain = req->req.length - req->req.actual;
|
||
|
|
||
|
EPVDBG(ep, "receive got=%d remain=%d\n", len, remain);
|
||
|
|
||
|
/* Are we getting more than asked ? */
|
||
|
if (len > remain) {
|
||
|
EPDBG(ep, "receiving too much (ovf: %d) !\n",
|
||
|
len - remain);
|
||
|
len = remain;
|
||
|
rc = -EOVERFLOW;
|
||
|
}
|
||
|
if (len && req->req.buf)
|
||
|
memcpy(req->req.buf + req->req.actual, ep->buf, len);
|
||
|
req->req.actual += len;
|
||
|
|
||
|
/* Done ? */
|
||
|
if (len < ep->ep.maxpacket || len == remain) {
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
writel(VHUB_EP0_TX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
ast_vhub_done(ep, req, rc);
|
||
|
} else
|
||
|
ast_vhub_ep0_rx_prime(ep);
|
||
|
}
|
||
|
|
||
|
void ast_vhub_ep0_handle_ack(struct ast_vhub_ep *ep, bool in_ack)
|
||
|
{
|
||
|
struct ast_vhub_req *req;
|
||
|
struct ast_vhub *vhub = ep->vhub;
|
||
|
struct device *dev = &vhub->pdev->dev;
|
||
|
bool stall = false;
|
||
|
u32 stat;
|
||
|
|
||
|
/* Read EP0 status */
|
||
|
stat = readl(ep->ep0.ctlstat);
|
||
|
|
||
|
/* Grab current request if any */
|
||
|
req = list_first_entry_or_null(&ep->queue, struct ast_vhub_req, queue);
|
||
|
|
||
|
EPVDBG(ep, "ACK status=%08x,state=%d is_in=%d in_ack=%d req=%p\n",
|
||
|
stat, ep->ep0.state, ep->ep0.dir_in, in_ack, req);
|
||
|
|
||
|
switch(ep->ep0.state) {
|
||
|
case ep0_state_token:
|
||
|
/* There should be no request queued in that state... */
|
||
|
if (req) {
|
||
|
dev_warn(dev, "request present while in TOKEN state\n");
|
||
|
ast_vhub_nuke(ep, -EINVAL);
|
||
|
}
|
||
|
dev_warn(dev, "ack while in TOKEN state\n");
|
||
|
stall = true;
|
||
|
break;
|
||
|
case ep0_state_data:
|
||
|
/* Check the state bits corresponding to our direction */
|
||
|
if ((ep->ep0.dir_in && (stat & VHUB_EP0_TX_BUFF_RDY)) ||
|
||
|
(!ep->ep0.dir_in && (stat & VHUB_EP0_RX_BUFF_RDY)) ||
|
||
|
(ep->ep0.dir_in != in_ack)) {
|
||
|
dev_warn(dev, "irq state mismatch");
|
||
|
stall = true;
|
||
|
break;
|
||
|
}
|
||
|
/*
|
||
|
* We are in data phase and there's no request, something is
|
||
|
* wrong, stall
|
||
|
*/
|
||
|
if (!req) {
|
||
|
dev_warn(dev, "data phase, no request\n");
|
||
|
stall = true;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
/* We have a request, handle data transfers */
|
||
|
if (ep->ep0.dir_in)
|
||
|
ast_vhub_ep0_do_send(ep, req);
|
||
|
else
|
||
|
ast_vhub_ep0_do_receive(ep, req, VHUB_EP0_RX_LEN(stat));
|
||
|
return;
|
||
|
case ep0_state_status:
|
||
|
/* Nuke stale requests */
|
||
|
if (req) {
|
||
|
dev_warn(dev, "request present while in STATUS state\n");
|
||
|
ast_vhub_nuke(ep, -EINVAL);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* If the status phase completes with the wrong ack, stall
|
||
|
* the endpoint just in case, to abort whatever the host
|
||
|
* was doing.
|
||
|
*/
|
||
|
if (ep->ep0.dir_in == in_ack) {
|
||
|
dev_warn(dev, "status direction mismatch\n");
|
||
|
stall = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Reset to token state */
|
||
|
ep->ep0.state = ep0_state_token;
|
||
|
if (stall)
|
||
|
writel(VHUB_EP0_CTRL_STALL, ep->ep0.ctlstat);
|
||
|
}
|
||
|
|
||
|
static int ast_vhub_ep0_queue(struct usb_ep* u_ep, struct usb_request *u_req,
|
||
|
gfp_t gfp_flags)
|
||
|
{
|
||
|
struct ast_vhub_req *req = to_ast_req(u_req);
|
||
|
struct ast_vhub_ep *ep = to_ast_ep(u_ep);
|
||
|
struct ast_vhub *vhub = ep->vhub;
|
||
|
struct device *dev = &vhub->pdev->dev;
|
||
|
unsigned long flags;
|
||
|
|
||
|
/* Paranoid cheks */
|
||
|
if (!u_req || (!u_req->complete && !req->internal)) {
|
||
|
dev_warn(dev, "Bogus EP0 request ! u_req=%p\n", u_req);
|
||
|
if (u_req) {
|
||
|
dev_warn(dev, "complete=%p internal=%d\n",
|
||
|
u_req->complete, req->internal);
|
||
|
}
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
/* Not endpoint 0 ? */
|
||
|
if (WARN_ON(ep->d_idx != 0))
|
||
|
return -EINVAL;
|
||
|
|
||
|
/* Disabled device */
|
||
|
if (ep->dev && (!ep->dev->enabled || ep->dev->suspended))
|
||
|
return -ESHUTDOWN;
|
||
|
|
||
|
/* Data, no buffer and not internal ? */
|
||
|
if (u_req->length && !u_req->buf && !req->internal) {
|
||
|
dev_warn(dev, "Request with no buffer !\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
EPVDBG(ep, "enqueue req @%p\n", req);
|
||
|
EPVDBG(ep, " l=%d zero=%d noshort=%d is_in=%d\n",
|
||
|
u_req->length, u_req->zero,
|
||
|
u_req->short_not_ok, ep->ep0.dir_in);
|
||
|
|
||
|
/* Initialize request progress fields */
|
||
|
u_req->status = -EINPROGRESS;
|
||
|
u_req->actual = 0;
|
||
|
req->last_desc = -1;
|
||
|
req->active = false;
|
||
|
|
||
|
spin_lock_irqsave(&vhub->lock, flags);
|
||
|
|
||
|
/* EP0 can only support a single request at a time */
|
||
|
if (!list_empty(&ep->queue) || ep->ep0.state == ep0_state_token) {
|
||
|
dev_warn(dev, "EP0: Request in wrong state\n");
|
||
|
spin_unlock_irqrestore(&vhub->lock, flags);
|
||
|
return -EBUSY;
|
||
|
}
|
||
|
|
||
|
/* Add request to list and kick processing if empty */
|
||
|
list_add_tail(&req->queue, &ep->queue);
|
||
|
|
||
|
if (ep->ep0.dir_in) {
|
||
|
/* IN request, send data */
|
||
|
ast_vhub_ep0_do_send(ep, req);
|
||
|
} else if (u_req->length == 0) {
|
||
|
/* 0-len request, send completion as rx */
|
||
|
EPVDBG(ep, "0-length rx completion\n");
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
writel(VHUB_EP0_TX_BUFF_RDY, ep->ep0.ctlstat);
|
||
|
ast_vhub_done(ep, req, 0);
|
||
|
} else {
|
||
|
/* OUT request, start receiver */
|
||
|
ast_vhub_ep0_rx_prime(ep);
|
||
|
}
|
||
|
|
||
|
spin_unlock_irqrestore(&vhub->lock, flags);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int ast_vhub_ep0_dequeue(struct usb_ep* u_ep, struct usb_request *u_req)
|
||
|
{
|
||
|
struct ast_vhub_ep *ep = to_ast_ep(u_ep);
|
||
|
struct ast_vhub *vhub = ep->vhub;
|
||
|
struct ast_vhub_req *req;
|
||
|
unsigned long flags;
|
||
|
int rc = -EINVAL;
|
||
|
|
||
|
spin_lock_irqsave(&vhub->lock, flags);
|
||
|
|
||
|
/* Only one request can be in the queue */
|
||
|
req = list_first_entry_or_null(&ep->queue, struct ast_vhub_req, queue);
|
||
|
|
||
|
/* Is it ours ? */
|
||
|
if (req && u_req == &req->req) {
|
||
|
EPVDBG(ep, "dequeue req @%p\n", req);
|
||
|
|
||
|
/*
|
||
|
* We don't have to deal with "active" as all
|
||
|
* DMAs go to the EP buffers, not the request.
|
||
|
*/
|
||
|
ast_vhub_done(ep, req, -ECONNRESET);
|
||
|
|
||
|
/* We do stall the EP to clean things up in HW */
|
||
|
writel(VHUB_EP0_CTRL_STALL, ep->ep0.ctlstat);
|
||
|
ep->ep0.state = ep0_state_status;
|
||
|
ep->ep0.dir_in = false;
|
||
|
rc = 0;
|
||
|
}
|
||
|
spin_unlock_irqrestore(&vhub->lock, flags);
|
||
|
return rc;
|
||
|
}
|
||
|
|
||
|
|
||
|
static const struct usb_ep_ops ast_vhub_ep0_ops = {
|
||
|
.queue = ast_vhub_ep0_queue,
|
||
|
.dequeue = ast_vhub_ep0_dequeue,
|
||
|
.alloc_request = ast_vhub_alloc_request,
|
||
|
.free_request = ast_vhub_free_request,
|
||
|
};
|
||
|
|
||
|
void ast_vhub_init_ep0(struct ast_vhub *vhub, struct ast_vhub_ep *ep,
|
||
|
struct ast_vhub_dev *dev)
|
||
|
{
|
||
|
memset(ep, 0, sizeof(*ep));
|
||
|
|
||
|
INIT_LIST_HEAD(&ep->ep.ep_list);
|
||
|
INIT_LIST_HEAD(&ep->queue);
|
||
|
ep->ep.ops = &ast_vhub_ep0_ops;
|
||
|
ep->ep.name = "ep0";
|
||
|
ep->ep.caps.type_control = true;
|
||
|
usb_ep_set_maxpacket_limit(&ep->ep, AST_VHUB_EP0_MAX_PACKET);
|
||
|
ep->d_idx = 0;
|
||
|
ep->dev = dev;
|
||
|
ep->vhub = vhub;
|
||
|
ep->ep0.state = ep0_state_token;
|
||
|
INIT_LIST_HEAD(&ep->ep0.req.queue);
|
||
|
ep->ep0.req.internal = true;
|
||
|
|
||
|
/* Small difference between vHub and devices */
|
||
|
if (dev) {
|
||
|
ep->ep0.ctlstat = dev->regs + AST_VHUB_DEV_EP0_CTRL;
|
||
|
ep->ep0.setup = vhub->regs +
|
||
|
AST_VHUB_SETUP0 + 8 * (dev->index + 1);
|
||
|
ep->buf = vhub->ep0_bufs +
|
||
|
AST_VHUB_EP0_MAX_PACKET * (dev->index + 1);
|
||
|
ep->buf_dma = vhub->ep0_bufs_dma +
|
||
|
AST_VHUB_EP0_MAX_PACKET * (dev->index + 1);
|
||
|
} else {
|
||
|
ep->ep0.ctlstat = vhub->regs + AST_VHUB_EP0_CTRL;
|
||
|
ep->ep0.setup = vhub->regs + AST_VHUB_SETUP0;
|
||
|
ep->buf = vhub->ep0_bufs;
|
||
|
ep->buf_dma = vhub->ep0_bufs_dma;
|
||
|
}
|
||
|
}
|