diff options
Diffstat (limited to 'payloads/libpayload/drivers/udc/chipidea.c')
-rw-r--r-- | payloads/libpayload/drivers/udc/chipidea.c | 472 |
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; +} |