diff options
Diffstat (limited to 'drivers/usb/chipidea/core.c')
-rw-r--r-- | drivers/usb/chipidea/core.c | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/drivers/usb/chipidea/core.c b/drivers/usb/chipidea/core.c new file mode 100644 index 000000000000..15e03b308f8a --- /dev/null +++ b/drivers/usb/chipidea/core.c @@ -0,0 +1,474 @@ +/* + * core.c - ChipIdea USB IP core family device controller + * + * Copyright (C) 2008 Chipidea - MIPS Technologies, Inc. All rights reserved. + * + * Author: David Lopo + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +/* + * Description: ChipIdea USB IP core family device controller + * + * This driver is composed of several blocks: + * - HW: hardware interface + * - DBG: debug facilities (optional) + * - UTIL: utilities + * - ISR: interrupts handling + * - ENDPT: endpoint operations (Gadget API) + * - GADGET: gadget operations (Gadget API) + * - BUS: bus glue code, bus abstraction layer + * + * Compile Options + * - CONFIG_USB_GADGET_DEBUG_FILES: enable debug facilities + * - STALL_IN: non-empty bulk-in pipes cannot be halted + * if defined mass storage compliance succeeds but with warnings + * => case 4: Hi > Dn + * => case 5: Hi > Di + * => case 8: Hi <> Do + * if undefined usbtest 13 fails + * - TRACE: enable function tracing (depends on DEBUG) + * + * Main Features + * - Chapter 9 & Mass Storage Compliance with Gadget File Storage + * - Chapter 9 Compliance with Gadget Zero (STALL_IN undefined) + * - Normal & LPM support + * + * USBTEST Report + * - OK: 0-12, 13 (STALL_IN defined) & 14 + * - Not Supported: 15 & 16 (ISO) + * + * TODO List + * - OTG + * - Isochronous & Interrupt Traffic + * - Handle requests which spawns into several TDs + * - GET_STATUS(device) - always reports 0 + * - Gadget API (majority of optional features) + * - Suspend & Remote Wakeup + */ +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/dmapool.h> +#include <linux/dma-mapping.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/io.h> +#include <linux/irq.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/pm_runtime.h> +#include <linux/usb/ch9.h> +#include <linux/usb/gadget.h> +#include <linux/usb/otg.h> +#include <linux/usb/chipidea.h> + +#include "ci.h" +#include "udc.h" +#include "bits.h" +#include "host.h" +#include "debug.h" + +/* Controller register map */ +static uintptr_t ci_regs_nolpm[] = { + [CAP_CAPLENGTH] = 0x000UL, + [CAP_HCCPARAMS] = 0x008UL, + [CAP_DCCPARAMS] = 0x024UL, + [CAP_TESTMODE] = 0x038UL, + [OP_USBCMD] = 0x000UL, + [OP_USBSTS] = 0x004UL, + [OP_USBINTR] = 0x008UL, + [OP_DEVICEADDR] = 0x014UL, + [OP_ENDPTLISTADDR] = 0x018UL, + [OP_PORTSC] = 0x044UL, + [OP_DEVLC] = 0x084UL, + [OP_OTGSC] = 0x064UL, + [OP_USBMODE] = 0x068UL, + [OP_ENDPTSETUPSTAT] = 0x06CUL, + [OP_ENDPTPRIME] = 0x070UL, + [OP_ENDPTFLUSH] = 0x074UL, + [OP_ENDPTSTAT] = 0x078UL, + [OP_ENDPTCOMPLETE] = 0x07CUL, + [OP_ENDPTCTRL] = 0x080UL, +}; + +static uintptr_t ci_regs_lpm[] = { + [CAP_CAPLENGTH] = 0x000UL, + [CAP_HCCPARAMS] = 0x008UL, + [CAP_DCCPARAMS] = 0x024UL, + [CAP_TESTMODE] = 0x0FCUL, + [OP_USBCMD] = 0x000UL, + [OP_USBSTS] = 0x004UL, + [OP_USBINTR] = 0x008UL, + [OP_DEVICEADDR] = 0x014UL, + [OP_ENDPTLISTADDR] = 0x018UL, + [OP_PORTSC] = 0x044UL, + [OP_DEVLC] = 0x084UL, + [OP_OTGSC] = 0x0C4UL, + [OP_USBMODE] = 0x0C8UL, + [OP_ENDPTSETUPSTAT] = 0x0D8UL, + [OP_ENDPTPRIME] = 0x0DCUL, + [OP_ENDPTFLUSH] = 0x0E0UL, + [OP_ENDPTSTAT] = 0x0E4UL, + [OP_ENDPTCOMPLETE] = 0x0E8UL, + [OP_ENDPTCTRL] = 0x0ECUL, +}; + +static int hw_alloc_regmap(struct ci13xxx *ci, bool is_lpm) +{ + int i; + + kfree(ci->hw_bank.regmap); + + ci->hw_bank.regmap = kzalloc((OP_LAST + 1) * sizeof(void *), + GFP_KERNEL); + if (!ci->hw_bank.regmap) + return -ENOMEM; + + for (i = 0; i < OP_ENDPTCTRL; i++) + ci->hw_bank.regmap[i] = + (i <= CAP_LAST ? ci->hw_bank.cap : ci->hw_bank.op) + + (is_lpm ? ci_regs_lpm[i] : ci_regs_nolpm[i]); + + for (; i <= OP_LAST; i++) + ci->hw_bank.regmap[i] = ci->hw_bank.op + + 4 * (i - OP_ENDPTCTRL) + + (is_lpm + ? ci_regs_lpm[OP_ENDPTCTRL] + : ci_regs_nolpm[OP_ENDPTCTRL]); + + return 0; +} + +/** + * hw_port_test_set: writes port test mode (execute without interruption) + * @mode: new value + * + * This function returns an error code + */ +int hw_port_test_set(struct ci13xxx *ci, u8 mode) +{ + const u8 TEST_MODE_MAX = 7; + + if (mode > TEST_MODE_MAX) + return -EINVAL; + + hw_write(ci, OP_PORTSC, PORTSC_PTC, mode << ffs_nr(PORTSC_PTC)); + return 0; +} + +/** + * hw_port_test_get: reads port test mode value + * + * This function returns port test mode value + */ +u8 hw_port_test_get(struct ci13xxx *ci) +{ + return hw_read(ci, OP_PORTSC, PORTSC_PTC) >> ffs_nr(PORTSC_PTC); +} + +static int hw_device_init(struct ci13xxx *ci, void __iomem *base) +{ + u32 reg; + + /* bank is a module variable */ + ci->hw_bank.abs = base; + + ci->hw_bank.cap = ci->hw_bank.abs; + ci->hw_bank.cap += ci->udc_driver->capoffset; + ci->hw_bank.op = ci->hw_bank.cap + ioread8(ci->hw_bank.cap); + + hw_alloc_regmap(ci, false); + reg = hw_read(ci, CAP_HCCPARAMS, HCCPARAMS_LEN) >> + ffs_nr(HCCPARAMS_LEN); + ci->hw_bank.lpm = reg; + hw_alloc_regmap(ci, !!reg); + ci->hw_bank.size = ci->hw_bank.op - ci->hw_bank.abs; + ci->hw_bank.size += OP_LAST; + ci->hw_bank.size /= sizeof(u32); + + reg = hw_read(ci, CAP_DCCPARAMS, DCCPARAMS_DEN) >> + ffs_nr(DCCPARAMS_DEN); + ci->hw_ep_max = reg * 2; /* cache hw ENDPT_MAX */ + + if (ci->hw_ep_max > ENDPT_MAX) + return -ENODEV; + + dev_dbg(ci->dev, "ChipIdea HDRC found, lpm: %d; cap: %p op: %p\n", + ci->hw_bank.lpm, ci->hw_bank.cap, ci->hw_bank.op); + + /* setup lock mode ? */ + + /* ENDPTSETUPSTAT is '0' by default */ + + /* HCSPARAMS.bf.ppc SHOULD BE zero for device */ + + return 0; +} + +/** + * hw_device_reset: resets chip (execute without interruption) + * @ci: the controller + * + * This function returns an error code + */ +int hw_device_reset(struct ci13xxx *ci, u32 mode) +{ + /* should flush & stop before reset */ + hw_write(ci, OP_ENDPTFLUSH, ~0, ~0); + hw_write(ci, OP_USBCMD, USBCMD_RS, 0); + + hw_write(ci, OP_USBCMD, USBCMD_RST, USBCMD_RST); + while (hw_read(ci, OP_USBCMD, USBCMD_RST)) + udelay(10); /* not RTOS friendly */ + + + if (ci->udc_driver->notify_event) + ci->udc_driver->notify_event(ci, + CI13XXX_CONTROLLER_RESET_EVENT); + + if (ci->udc_driver->flags & CI13XXX_DISABLE_STREAMING) + hw_write(ci, OP_USBMODE, USBMODE_CI_SDIS, USBMODE_CI_SDIS); + + /* USBMODE should be configured step by step */ + hw_write(ci, OP_USBMODE, USBMODE_CM, USBMODE_CM_IDLE); + hw_write(ci, OP_USBMODE, USBMODE_CM, mode); + /* HW >= 2.3 */ + hw_write(ci, OP_USBMODE, USBMODE_SLOM, USBMODE_SLOM); + + if (hw_read(ci, OP_USBMODE, USBMODE_CM) != mode) { + pr_err("cannot enter in %s mode", ci_role(ci)->name); + pr_err("lpm = %i", ci->hw_bank.lpm); + return -ENODEV; + } + + return 0; +} + +/** + * ci_otg_role - pick role based on ID pin state + * @ci: the controller + */ +static enum ci_role ci_otg_role(struct ci13xxx *ci) +{ + u32 sts = hw_read(ci, OP_OTGSC, ~0); + enum ci_role role = sts & OTGSC_ID + ? CI_ROLE_GADGET + : CI_ROLE_HOST; + + return role; +} + +/** + * ci_role_work - perform role changing based on ID pin + * @work: work struct + */ +static void ci_role_work(struct work_struct *work) +{ + struct ci13xxx *ci = container_of(work, struct ci13xxx, work); + enum ci_role role = ci_otg_role(ci); + + hw_write(ci, OP_OTGSC, OTGSC_IDIS, OTGSC_IDIS); + + if (role != ci->role) { + dev_dbg(ci->dev, "switching from %s to %s\n", + ci_role(ci)->name, ci->roles[role]->name); + + ci_role_stop(ci); + ci_role_start(ci, role); + } +} + +static ssize_t show_role(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ci13xxx *ci = dev_get_drvdata(dev); + + return sprintf(buf, "%s\n", ci_role(ci)->name); +} + +static ssize_t store_role(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct ci13xxx *ci = dev_get_drvdata(dev); + enum ci_role role; + int ret; + + for (role = CI_ROLE_HOST; role < CI_ROLE_END; role++) + if (ci->roles[role] && !strcmp(buf, ci->roles[role]->name)) + break; + + if (role == CI_ROLE_END || role == ci->role) + return -EINVAL; + + ci_role_stop(ci); + ret = ci_role_start(ci, role); + if (ret) + return ret; + + return count; +} + +static DEVICE_ATTR(role, S_IRUSR | S_IWUSR, show_role, store_role); + +static irqreturn_t ci_irq(int irq, void *data) +{ + struct ci13xxx *ci = data; + irqreturn_t ret = IRQ_NONE; + + if (ci->is_otg) { + u32 sts = hw_read(ci, OP_OTGSC, ~0); + + if (sts & OTGSC_IDIS) { + queue_work(ci->wq, &ci->work); + ret = IRQ_HANDLED; + } + } + + return ci->role == CI_ROLE_END ? ret : ci_role(ci)->irq(ci); +} + +static int __devinit ci_hdrc_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ci13xxx *ci; + struct resource *res; + void __iomem *base; + int ret; + + if (!dev->platform_data) { + dev_err(dev, "platform data missing\n"); + return -ENODEV; + } + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(dev, "missing resource\n"); + return -ENODEV; + } + + base = devm_request_and_ioremap(dev, res); + if (!res) { + dev_err(dev, "can't request and ioremap resource\n"); + return -ENOMEM; + } + + ci = devm_kzalloc(dev, sizeof(*ci), GFP_KERNEL); + if (!ci) { + dev_err(dev, "can't allocate device\n"); + return -ENOMEM; + } + + ci->dev = dev; + ci->udc_driver = dev->platform_data; + + ret = hw_device_init(ci, base); + if (ret < 0) { + dev_err(dev, "can't initialize hardware\n"); + return -ENODEV; + } + + ci->hw_bank.phys = res->start; + + ci->irq = platform_get_irq(pdev, 0); + if (ci->irq < 0) { + dev_err(dev, "missing IRQ\n"); + return -ENODEV; + } + + INIT_WORK(&ci->work, ci_role_work); + ci->wq = create_singlethread_workqueue("ci_otg"); + if (!ci->wq) { + dev_err(dev, "can't create workqueue\n"); + return -ENODEV; + } + + /* initialize role(s) before the interrupt is requested */ + ret = ci_hdrc_host_init(ci); + if (ret) + dev_info(dev, "doesn't support host\n"); + + ret = ci_hdrc_gadget_init(ci); + if (ret) + dev_info(dev, "doesn't support gadget\n"); + + if (!ci->roles[CI_ROLE_HOST] && !ci->roles[CI_ROLE_GADGET]) { + dev_err(dev, "no supported roles\n"); + ret = -ENODEV; + goto rm_wq; + } + + if (ci->roles[CI_ROLE_HOST] && ci->roles[CI_ROLE_GADGET]) { + ci->is_otg = true; + ci->role = ci_otg_role(ci); + } else { + ci->role = ci->roles[CI_ROLE_HOST] + ? CI_ROLE_HOST + : CI_ROLE_GADGET; + } + + ret = ci_role_start(ci, ci->role); + if (ret) { + dev_err(dev, "can't start %s role\n", ci_role(ci)->name); + ret = -ENODEV; + goto rm_wq; + } + + platform_set_drvdata(pdev, ci); + ret = request_irq(ci->irq, ci_irq, IRQF_SHARED, ci->udc_driver->name, + ci); + if (ret) + goto stop; + + ret = device_create_file(dev, &dev_attr_role); + if (ret) + goto rm_attr; + + if (ci->is_otg) + hw_write(ci, OP_OTGSC, OTGSC_IDIE, OTGSC_IDIE); + + return ret; + +rm_attr: + device_remove_file(dev, &dev_attr_role); +stop: + ci_role_stop(ci); +rm_wq: + flush_workqueue(ci->wq); + destroy_workqueue(ci->wq); + + return ret; +} + +static int __devexit ci_hdrc_remove(struct platform_device *pdev) +{ + struct ci13xxx *ci = platform_get_drvdata(pdev); + + flush_workqueue(ci->wq); + destroy_workqueue(ci->wq); + device_remove_file(ci->dev, &dev_attr_role); + free_irq(ci->irq, ci); + ci_role_stop(ci); + + return 0; +} + +static struct platform_driver ci_hdrc_driver = { + .probe = ci_hdrc_probe, + .remove = __devexit_p(ci_hdrc_remove), + .driver = { + .name = "ci_hdrc", + }, +}; + +module_platform_driver(ci_hdrc_driver); + +MODULE_ALIAS("platform:ci_hdrc"); +MODULE_ALIAS("platform:ci13xxx"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("David Lopo <dlopo@chipidea.mips.com>"); +MODULE_DESCRIPTION("ChipIdea HDRC Driver"); |