aboutsummaryrefslogtreecommitdiff
path: root/payloads/libpayload/drivers/udc/chipidea.c
diff options
context:
space:
mode:
authorPatrick Georgi <pgeorgi@google.com>2015-01-26 20:17:49 +0100
committerPatrick Georgi <pgeorgi@google.com>2015-04-18 08:40:19 +0200
commit1bd3050c27508036576c1f34ca500a6b4bccbda0 (patch)
tree85966013d5bf40a82ba2b6aea8eb1d7c375b6f9d /payloads/libpayload/drivers/udc/chipidea.c
parent2df124db36ab7a456fa301e7334cd4fe894cb86a (diff)
libpayload: Add USB device mode driver
Add a framework for USB device mode controllers and a driver for the ChipIdea controller which is part of the tegra platform. TODO: - fix USB detach/attach - implement zero length packet handling properly BUG=chrome-os-partner:35861 TEST=none BRANCH=none Change-Id: I8defeea78b5a3bdbf9c1b1222c2702eaf3256b81 Signed-off-by: Patrick Georgi <pgeorgi@chromium.org> Original-Commit-Id: 542332291880c4026a05a960ceb91d37891ee018 Original-Change-Id: Ib4068d201dd63ebeda80157bd3130f3059919cdd Original-Signed-off-by: Patrick Georgi <pgeorgi@chromium.org> Original-Reviewed-on: https://chromium-review.googlesource.com/243272 Original-Reviewed-by: Aaron Durbin <adurbin@chromium.org> Reviewed-on: http://review.coreboot.org/8756 Tested-by: build bot (Jenkins) Reviewed-by: Stefan Reinauer <stefan.reinauer@coreboot.org>
Diffstat (limited to 'payloads/libpayload/drivers/udc/chipidea.c')
-rw-r--r--payloads/libpayload/drivers/udc/chipidea.c472
1 files changed, 472 insertions, 0 deletions
diff --git a/payloads/libpayload/drivers/udc/chipidea.c b/payloads/libpayload/drivers/udc/chipidea.c
new file mode 100644
index 0000000000..e06f0d2824
--- /dev/null
+++ b/payloads/libpayload/drivers/udc/chipidea.c
@@ -0,0 +1,472 @@
+/*
+ * This file is part of the libpayload project.
+ *
+ * Copyright (C) 2015 Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <libpayload.h>
+#include <arch/cache.h>
+#include <assert.h>
+#include <endian.h>
+#include <queue.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <usb/usb.h>
+
+#include <udc/udc.h>
+#include <udc/chipidea.h>
+#include "chipidea_priv.h"
+
+#ifdef DEBUG
+#define debug(x...) printf(x)
+#else
+#define debug(x...) do {} while (0)
+#endif
+
+#define min(a, b) (((a) < (b)) ? (a) : (b))
+
+static struct qh *get_qh(struct chipidea_pdata *p, int endpoint, int in_dir)
+{
+ assert(in_dir <= 1);
+ return &p->qhlist[2 * endpoint + in_dir];
+}
+
+static unsigned int ep_to_bits(int ep, int in_dir)
+{
+ return ep + (in_dir ? 16 : 0);
+}
+
+static void clear_setup_ep(struct chipidea_pdata *p, int endpoint)
+{
+ writel(1 << endpoint, &p->opreg->epsetupstat);
+}
+
+static void clear_ep(struct chipidea_pdata *p, int endpoint, int in_dir)
+{
+ writel(1 << ep_to_bits(endpoint, in_dir), &p->opreg->epcomplete);
+}
+
+static int chipidea_hw_init(struct usbdev_ctrl *this, void *_opreg,
+ const device_descriptor_t *dd)
+{
+ struct chipidea_opreg *opreg = _opreg;
+ struct chipidea_pdata *p = CI_PDATA(this);
+
+ p->opreg = phys_to_virt(opreg);
+ p->qhlist = dma_memalign(4096, sizeof(struct qh) * CI_QHELEMENTS);
+ memcpy(&this->device_descriptor, dd, sizeof(*dd));
+
+ if (p->qhlist == NULL)
+ die("failed to allocate memory for usb device mode");
+
+ memset(p->qhlist, 0, sizeof(struct qh) * CI_QHELEMENTS);
+
+ SLIST_INIT(&this->configs);
+
+ int i;
+ for (i = 0; i < 16; i++) {
+ SIMPLEQ_INIT(&p->job_queue[i][0]);
+ SIMPLEQ_INIT(&p->job_queue[i][1]);
+ }
+
+ for (i = 0; i < CI_QHELEMENTS; i++) {
+ p->qhlist[i].config = QH_MPS(512) | QH_NO_AUTO_ZLT | QH_IOS;
+ p->qhlist[i].td.next = TD_TERMINATE;
+ }
+ /* EP0 in/out are hardwired for SETUP */
+ p->qhlist[0].config = QH_MPS(64) | QH_NO_AUTO_ZLT | QH_IOS;
+ p->qhlist[1].config = QH_MPS(64) | QH_NO_AUTO_ZLT | QH_IOS;
+
+ do {
+ debug("waiting for usb phy clk valid: %x\n",
+ readl(&p->opreg->susp_ctrl));
+ mdelay(1);
+ } while ((readl(&p->opreg->susp_ctrl) & (1 << 7)) == 0);
+
+ writel(USBCMD_8MICRO | USBCMD_RST, &p->opreg->usbcmd);
+ mdelay(1);
+
+ /* enable device mode */
+ writel(2, &p->opreg->usbmode);
+
+ dcache_clean_by_mva(p->qhlist, sizeof(struct qh) * CI_QHELEMENTS);
+
+ writel(virt_to_phys(p->qhlist), &p->opreg->epbase);
+ writel(0xffffffff, &p->opreg->epflush);
+
+ /* enable EP0 */
+ writel((1 << 23) | (1 << 22) | (1 << 7) | (1 << 6),
+ &p->opreg->epctrl[0]);
+
+ /* clear status register */
+ writel(readl(&p->opreg->usbsts), &p->opreg->usbsts);
+
+ debug("taking controller out of reset\n");
+ writel(USBCMD_8MICRO | USBCMD_RUN, &p->opreg->usbcmd);
+
+ return 1;
+}
+
+static void chipidea_halt_ep(struct usbdev_ctrl *this, int ep, int in_dir)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ writel(1 << ep_to_bits(ep, in_dir), &p->opreg->epflush);
+ while (readl(&p->opreg->epflush))
+ ;
+ clrbits_le32(&p->opreg->epctrl[ep], 1 << (7 + (in_dir ? 16 : 0)));
+}
+
+static void chipidea_start_ep(struct usbdev_ctrl *this,
+ int ep, int in_dir, int ep_type, int mps)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ struct qh *qh = get_qh(p, ep, in_dir);
+ qh->config = (mps << 16) | QH_NO_AUTO_ZLT | QH_IOS;
+ dcache_clean_by_mva(qh, sizeof(*qh));
+ in_dir = in_dir ? 1 : 0;
+ debug("enabling %d-%d (type %d)\n", ep, in_dir, ep_type);
+ /* enable endpoint, reset data toggle */
+ setbits_le32(&p->opreg->epctrl[ep],
+ ((1 << 7) | (1 << 6) | (ep_type << 2)) << (in_dir*16));
+ p->ep_busy[ep][in_dir] = 0;
+}
+
+static void advance_endpoint(struct chipidea_pdata *p, int endpoint, int in_dir)
+{
+ if (p->ep_busy[endpoint][in_dir])
+ return;
+ if (SIMPLEQ_EMPTY(&p->job_queue[endpoint][in_dir]))
+ return;
+
+ struct job *job = SIMPLEQ_FIRST(&p->job_queue[endpoint][in_dir]);
+ struct qh *qh = get_qh(p, endpoint, in_dir);
+
+ uint32_t start = (uint32_t)(uintptr_t)job->data;
+ uint32_t offset = (start & 0xfff);
+ /* unlike with typical EHCI controllers,
+ * a full TD transfers either 0x5000 bytes if
+ * page aligned or 0x4000 bytes if not.
+ */
+ int maxsize = 0x5000;
+ if (offset > 0)
+ maxsize = 0x4000;
+ uint32_t td_count = (job->length + maxsize - 1) / maxsize;
+
+ /* special case for zero length packets */
+ if (td_count == 0)
+ td_count = 1;
+
+ if (job->zlp)
+ td_count++;
+
+ struct td *tds = dma_memalign(32, sizeof(struct td) * td_count);
+ memset(tds, 0, sizeof(struct td) * td_count);
+
+ int i;
+ int remaining = job->length;
+ for (i = 0; i < td_count; i++) {
+ int datacount = min(maxsize, remaining);
+
+ debug("td %d, %d bytes\n", i, datacount);
+ tds[i].next = (uint32_t)virt_to_phys(&tds[i+1]);
+ tds[i].info = TD_INFO_LEN(datacount) | TD_INFO_ACTIVE;
+ tds[i].page0 = start;
+ tds[i].page1 = (start & 0xfffff000) + 0x1000;
+ tds[i].page2 = (start & 0xfffff000) + 0x2000;
+ tds[i].page3 = (start & 0xfffff000) + 0x3000;
+ tds[i].page4 = (start & 0xfffff000) + 0x4000;
+ remaining -= datacount;
+ start = start + datacount;
+ }
+ tds[td_count - 1].next = TD_TERMINATE;
+ tds[td_count - 1].info |= TD_INFO_IOC;
+
+ qh->td.next = (uint32_t)virt_to_phys(tds);
+ qh->td.info = 0;
+
+ job->tds = tds;
+ job->td_count = td_count;
+
+ dcache_clean_by_mva(tds, sizeof(struct td) * td_count);
+ dcache_clean_by_mva(job->data, job->length);
+ dcache_clean_by_mva(qh, sizeof(*qh));
+
+ debug("priming EP %d-%d with %zx bytes starting at %x (%p)\n", endpoint,
+ in_dir, job->length, tds[0].page0, job->data);
+ writel(1 << ep_to_bits(endpoint, in_dir), &p->opreg->epprime);
+ while (readl(&p->opreg->epprime))
+ ;
+ p->ep_busy[endpoint][in_dir] = 1;
+}
+
+static void handle_endpoint(struct usbdev_ctrl *this, int endpoint, int in_dir)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ struct job *job = SIMPLEQ_FIRST(&p->job_queue[endpoint][in_dir]);
+ SIMPLEQ_REMOVE_HEAD(&p->job_queue[endpoint][in_dir], queue);
+
+ if (in_dir)
+ dcache_invalidate_by_mva(job->data, job->length);
+
+ int length = job->length;
+
+ int i = 0;
+ do {
+ int active;
+ do {
+ dcache_invalidate_by_mva(&job->tds[i],
+ sizeof(struct td));
+ active = job->tds[i].info & TD_INFO_ACTIVE;
+ debug("%d-%d: info %08x, page0 %x, next %x\n",
+ endpoint, in_dir, job->tds[i].info,
+ job->tds[i].page0, job->tds[i].next);
+ } while (active);
+ /*
+ * The controller writes back the length field in info
+ * with the number of bytes it did _not_ process.
+ * Hence, take the originally scheduled length and
+ * subtract whatever lengths we still find - that gives
+ * us the data that the controller did transfer.
+ */
+ int remaining = job->tds[i].info >> 16;
+ length -= remaining;
+ } while (job->tds[i++].next != TD_TERMINATE);
+ debug("%d-%d: scheduled %zd, now %d bytes\n", endpoint, in_dir,
+ job->length, length);
+
+ if (this->current_config &&
+ this->current_config->interfaces[0].handle_packet)
+ this->current_config->interfaces[0].handle_packet(this,
+ endpoint, in_dir, job->data, length);
+
+ free(job->tds);
+ if (job->autofree)
+ free(job->data);
+ free(job);
+ p->ep_busy[endpoint][in_dir] = 0;
+
+ advance_endpoint(p, endpoint, in_dir);
+}
+
+static void start_setup(struct usbdev_ctrl *this, int ep)
+{
+ dev_req_t dr;
+ struct chipidea_pdata *p = CI_PDATA(this);
+ struct qh *qh = get_qh(p, ep, 0);
+
+ dcache_invalidate_by_mva(qh, sizeof(*qh));
+ memcpy(&dr, qh->setup_data, sizeof(qh->setup_data));
+ clear_setup_ep(p, ep);
+
+#ifdef DEBUG
+ hexdump((unsigned long)&dr, sizeof(dr));
+#endif
+
+ udc_handle_setup(this, ep, &dr);
+}
+
+
+static void chipidea_enqueue_packet(struct usbdev_ctrl *this, int endpoint,
+ int in_dir, void *data, int len, int zlp, int autofree)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ struct job *job = malloc(sizeof(*job));
+
+ job->data = data;
+ job->length = len;
+ job->zlp = zlp;
+ job->autofree = autofree;
+
+ debug("adding job of %d bytes to EP %d-%d\n", len, endpoint, in_dir);
+ SIMPLEQ_INSERT_TAIL(&p->job_queue[endpoint][in_dir], job, queue);
+
+ if ((endpoint == 0) || (this->initialized))
+ advance_endpoint(p, endpoint, in_dir);
+}
+
+static int chipidea_poll(struct usbdev_ctrl *this)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ uint32_t sts = readl(&p->opreg->usbsts);
+ writel(sts, &p->opreg->usbsts); /* clear */
+
+ /* new information if the bus is high speed or not */
+ if (sts & USBSTS_PCI) {
+ debug("USB speed negotiation: ");
+ if ((readl(&p->opreg->devlc) & DEVLC_HOSTSPEED_MASK)
+ == DEVLC_HOSTSPEED(2)) {
+ debug("high speed\n");
+ // TODO: implement
+ } else {
+ debug("full speed\n");
+ // TODO: implement
+ }
+ }
+
+ /* reset requested. stop all activities */
+ if (sts & USBSTS_URI) {
+ int i;
+ debug("USB reset requested\n");
+ if (this->initialized) {
+ writel(readl(&p->opreg->epstat), &p->opreg->epstat);
+ writel(readl(&p->opreg->epsetupstat),
+ &p->opreg->epsetupstat);
+ writel(0xffffffff, &p->opreg->epflush);
+ for (i = 1; i < 16; i++)
+ writel(0, &p->opreg->epctrl[i]);
+ this->initialized = 0;
+ }
+ writel((1 << 22) | (1 << 6), &p->opreg->epctrl[0]);
+ p->qhlist[0].config = QH_MPS(64) | QH_NO_AUTO_ZLT | QH_IOS;
+ p->qhlist[1].config = QH_MPS(64) | QH_NO_AUTO_ZLT | QH_IOS;
+ dcache_clean_by_mva(p->qhlist, 2 * sizeof(struct qh));
+ }
+
+ if (sts & (USBSTS_UEI | USBSTS_UI)) {
+ uint32_t bitmap = readl(&p->opreg->epsetupstat);
+ int ep = 0;
+ while (bitmap) {
+ if (bitmap & 1) {
+ debug("incoming packet on EP %d (setup)\n", ep);
+ start_setup(this, ep);
+ }
+ bitmap >>= 1;
+ ep++;
+ }
+ bitmap = readl(&p->opreg->epcomplete);
+ ep = 0;
+ int dir_in = 0;
+ while (bitmap) {
+ if (bitmap & 1) {
+ debug("incoming packet on EP %d (%s)\n",
+ ep, dir_in ? "intr/in" : "out");
+ handle_endpoint(this, ep & 0xf, dir_in);
+ clear_ep(p, ep & 0xf, dir_in);
+ }
+ bitmap >>= 1;
+ ep++;
+ if (ep == 16)
+ dir_in = 1;
+ }
+ }
+
+ return 1;
+}
+
+static void chipidea_shutdown(struct usbdev_ctrl *this)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ int i, j;
+ int is_empty = 0;
+ while (!is_empty) {
+ is_empty = 1;
+ this->poll(this);
+ for (i = 0; i < 16; i++)
+ for (j = 0; j < 2; j++)
+ if (!SIMPLEQ_EMPTY(&p->job_queue[i][j]))
+ is_empty = 0;
+ }
+ writel(0xffffffff, &p->opreg->epflush);
+ writel(USBCMD_8MICRO | USBCMD_RST, &p->opreg->usbcmd);
+ writel(0, &p->opreg->usbmode);
+ writel(USBCMD_8MICRO, &p->opreg->usbcmd);
+ free(p->qhlist);
+ free(p);
+ free(this);
+}
+
+static void chipidea_set_address(struct usbdev_ctrl *this, int address)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ writel((address << 25) | (1 << 24), &p->opreg->usbadr);
+}
+
+static void chipidea_stall(struct usbdev_ctrl *this,
+ uint8_t ep, int in_dir, int set)
+{
+ struct chipidea_pdata *p = CI_PDATA(this);
+ assert(ep < 16);
+ uint32_t *ctrl = &p->opreg->epctrl[ep];
+ in_dir = in_dir ? 1 : 0;
+ if (set) {
+ if (in_dir)
+ setbits_le32(ctrl, 1 << 16);
+ else
+ setbits_le32(ctrl, 1 << 0);
+ } else {
+ /* reset STALL bit, reset data toggle */
+ if (in_dir) {
+ setbits_le32(ctrl, 1 << 22);
+ clrbits_le32(ctrl, 1 << 16);
+ } else {
+ setbits_le32(ctrl, 1 << 6);
+ setbits_le32(ctrl, 1 << 0);
+ }
+ }
+ this->ep_halted[ep][in_dir] = set;
+}
+
+static void *chipidea_malloc(size_t size)
+{
+ return dma_malloc(size);
+}
+
+static void chipidea_free(void *ptr)
+{
+ free(ptr);
+}
+
+struct usbdev_ctrl *chipidea_init(device_descriptor_t *dd)
+{
+ struct usbdev_ctrl *ctrl = calloc(1, sizeof(*ctrl));
+ if (ctrl == NULL)
+ return NULL;
+ ctrl->pdata = calloc(1, sizeof(struct chipidea_pdata));
+ if (ctrl->pdata == NULL) {
+ free(ctrl);
+ return NULL;
+ }
+
+ ctrl->poll = chipidea_poll;
+ ctrl->add_gadget = udc_add_gadget;
+ ctrl->enqueue_packet = chipidea_enqueue_packet;
+ ctrl->shutdown = chipidea_shutdown;
+ ctrl->set_address = chipidea_set_address;
+ ctrl->stall = chipidea_stall;
+ ctrl->halt_ep = chipidea_halt_ep;
+ ctrl->start_ep = chipidea_start_ep;
+ ctrl->alloc_data = chipidea_malloc;
+ ctrl->free_data = chipidea_free;
+ ctrl->initialized = 0;
+
+ if (!chipidea_hw_init(ctrl, (void *)0x7d000000, dd)) {
+ free(ctrl->pdata);
+ free(ctrl);
+ return NULL;
+ }
+ return ctrl;
+}