Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Benjamin Herrenschmidt 2399 100.00% 3 100.00%
Total 2399 3


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