From 2e39427ef65df3de4d7bd08419d7e67517f751eb Mon Sep 17 00:00:00 2001 From: Benoit Goby Date: Thu, 8 Jul 2010 17:00:48 -0700 Subject: [PATCH] usb: host: Add EHCI driver for NVIDIA Tegra SoCs Change-Id: I53c560f2c31e043f139b840f58786429ded6ec62 Signed-off-by: Benoit Goby --- drivers/usb/Kconfig | 1 + drivers/usb/host/Kconfig | 8 + drivers/usb/host/ehci-hcd.c | 5 + drivers/usb/host/ehci-tegra.c | 594 ++++++++++++++++++++++++++++++++++ 4 files changed, 608 insertions(+) create mode 100644 drivers/usb/host/ehci-tegra.c diff --git a/drivers/usb/Kconfig b/drivers/usb/Kconfig index 4aa00e6e57ad..69e8a096c35a 100644 --- a/drivers/usb/Kconfig +++ b/drivers/usb/Kconfig @@ -61,6 +61,7 @@ config USB_ARCH_HAS_EHCI default y if PPC_83xx default y if SOC_AU1200 default y if ARCH_IXP4XX + default y if ARCH_TEGRA default y if ARCH_W90X900 default y if ARCH_AT91SAM9G45 default y if ARCH_MXC diff --git a/drivers/usb/host/Kconfig b/drivers/usb/host/Kconfig index 2d926cec0725..10f6ab5f9150 100644 --- a/drivers/usb/host/Kconfig +++ b/drivers/usb/host/Kconfig @@ -418,6 +418,14 @@ config USB_HWA_HCD To compile this driver a module, choose M here: the module will be called "hwa-hc". +config USB_TEGRA_HCD + boolean "NVIDIA Tegra HCD support" + depends on USB && ARCH_TEGRA && USB_EHCI_HCD + select USB_EHCI_ROOT_HUB_TT + help + This driver enables support for the internal USB Host Controller + found in NVIDIA Tegra SoCs. The Tegra controller is EHCI compliant. + config USB_IMX21_HCD tristate "iMX21 HCD support" depends on USB && ARM && MACH_MX21 diff --git a/drivers/usb/host/ehci-hcd.c b/drivers/usb/host/ehci-hcd.c index 34a928d3b7d2..1b89b2dbd9ea 100644 --- a/drivers/usb/host/ehci-hcd.c +++ b/drivers/usb/host/ehci-hcd.c @@ -1197,6 +1197,11 @@ MODULE_LICENSE ("GPL"); #define PLATFORM_DRIVER ehci_atmel_driver #endif +#ifdef CONFIG_ARCH_TEGRA +#include "ehci-tegra.c" +#define PLATFORM_DRIVER tegra_ehci_driver +#endif + #if !defined(PCI_DRIVER) && !defined(PLATFORM_DRIVER) && \ !defined(PS3_SYSTEM_BUS_DRIVER) && !defined(OF_PLATFORM_DRIVER) && \ !defined(XILINX_OF_PLATFORM_DRIVER) diff --git a/drivers/usb/host/ehci-tegra.c b/drivers/usb/host/ehci-tegra.c new file mode 100644 index 000000000000..40eff7f0d323 --- /dev/null +++ b/drivers/usb/host/ehci-tegra.c @@ -0,0 +1,594 @@ +/* + * EHCI-compliant USB host controller driver for NVIDIA Tegra SoCs + * + * Copyright (C) 2010 Google, Inc. + * Copyright (C) 2009 NVIDIA 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. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include +#include +#include +#include +#include + +#define TEGRA_USB_USBCMD_REG_OFFSET 0x140 +#define TEGRA_USB_USBCMD_RESET (1 << 1) +#define TEGRA_USB_USBMODE_REG_OFFSET 0x1a8 +#define TEGRA_USB_USBMODE_HOST (3 << 0) +#define TEGRA_USB_PHY_WAKEUP_REG_OFFSET 0x408 +#define TEGRA_USB_ID_INT_ENABLE (1 << 0) +#define TEGRA_USB_ID_INT_STATUS (1 << 1) +#define TEGRA_USB_ID_PIN_STATUS (1 << 2) +#define TEGRA_USB_ID_PIN_WAKEUP_ENABLE (1 << 6) + +struct tegra_ehci_hcd { + struct ehci_hcd *ehci; + struct work_struct work; + struct tegra_usb_phy *phy; + struct clk *clk; + struct otg_transceiver *transceiver; + int host_reinited; + int host_resumed; +}; + +static void tegra_ehci_power_up(struct usb_hcd *hcd) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + + clk_enable(tegra->clk); + tegra_usb_phy_power_on(tegra->phy); + tegra->host_resumed = 1; +} + +static void tegra_ehci_power_down(struct usb_hcd *hcd) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + + tegra_usb_phy_power_off(tegra->phy); + clk_disable(tegra->clk); + tegra->host_resumed = 0; +} + +static int tegra_ehci_hub_control( + struct usb_hcd *hcd, + u16 typeReq, + u16 wValue, + u16 wIndex, + char *buf, + u16 wLength +) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + u32 __iomem *status_reg; + u32 temp; + unsigned long flags; + int retval = 0; + + status_reg = &ehci->regs->port_status[(wIndex & 0xff) - 1]; + + /* if hardware is not accessable then don't read the registers */ + if (!test_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags)) { + if (buf) + memset(buf, 0, wLength); + return retval; + } + + /* + * In ehci_hub_control() for USB_PORT_FEAT_ENABLE clears the other bits + * that are write on clear, by writing back the register read value, so + * USB_PORT_FEAT_ENABLE is handled by masking the set on clear bits + */ + if ((typeReq == ClearPortFeature) && (wValue == USB_PORT_FEAT_ENABLE)) { + spin_lock_irqsave(&ehci->lock, flags); + temp = ehci_readl(ehci, status_reg); + ehci_writel(ehci, (temp & ~PORT_RWC_BITS) & ~PORT_PE, status_reg); + spin_unlock_irqrestore(&ehci->lock, flags); + return retval; + } + + /* Handle the hub control events here */ + retval = ehci_hub_control(hcd, typeReq, wValue, wIndex, buf, wLength); + + /* + * Power down the USB phy when there is no port connection and all + * HUB events are cleared by checking the lower four bits + * (PORT_CONNECT | PORT_CSC | PORT_PE | PORT_PEC) + */ + if (tegra->transceiver && tegra->transceiver->state == OTG_STATE_A_SUSPEND) { + temp = ehci_readl(ehci, status_reg); + if (!(temp & (PORT_CONNECT | PORT_CSC | PORT_PE | PORT_PEC)) + && tegra->host_reinited) { + /* indicate hcd flags, that hardware is not accessable now */ + clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags); + tegra_ehci_power_down(hcd); + tegra->host_reinited = 0; + } + } + return retval; +} + +static void tegra_ehci_restart(struct usb_hcd *hcd) +{ + unsigned int temp; + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + + /* Set to Host mode by setting bit 0-1 of USB device mode register */ + temp = readl(hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET); + writel((temp | TEGRA_USB_USBMODE_HOST), + (hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET)); + + /* reset the ehci controller */ + ehci->controller_resets_phy = 0; + ehci_reset(ehci); + ehci->controller_resets_phy = 1; + /* setup the frame list and Async q heads */ + ehci_writel(ehci, ehci->periodic_dma, &ehci->regs->frame_list); + ehci_writel(ehci, (u32)ehci->async->qh_dma, &ehci->regs->async_next); + /* setup the command register and set the controller in RUN mode */ + ehci->command &= ~(CMD_LRESET|CMD_IAAD|CMD_PSE|CMD_ASE|CMD_RESET); + ehci->command |= CMD_RUN; + ehci_writel(ehci, ehci->command, &ehci->regs->command); + + down_write(&ehci_cf_port_reset_rwsem); + hcd->state = HC_STATE_RUNNING; + ehci_writel(ehci, FLAG_CF, &ehci->regs->configured_flag); + /* flush posted writes */ + ehci_readl(ehci, &ehci->regs->command); + up_write(&ehci_cf_port_reset_rwsem); + + /* Turn On Interrupts */ + ehci_writel(ehci, INTR_MASK, &ehci->regs->intr_enable); +} + +static int tegra_ehci_reset(struct usb_hcd *hcd) +{ + unsigned long temp; + int usec = 250*1000; /* see ehci_reset */ + + temp = readl(hcd->regs + TEGRA_USB_USBCMD_REG_OFFSET); + temp |= TEGRA_USB_USBCMD_RESET; + writel(temp, hcd->regs + TEGRA_USB_USBCMD_REG_OFFSET); + + do { + temp = readl(hcd->regs + TEGRA_USB_USBCMD_REG_OFFSET); + if (!(temp & TEGRA_USB_USBCMD_RESET)) + break; + udelay(1); + usec--; + } while (usec); + + if (!usec) + return -ETIMEDOUT; + + return 0; +} + +static void tegra_ehci_shutdown(struct usb_hcd *hcd) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + /* ehci_shutdown touches the USB controller registers, make sure + * controller has clocks to it */ + if (!tegra->host_resumed) + tegra_ehci_power_up(hcd); + + /* call ehci shut down */ + ehci_shutdown(hcd); + + /* we are ready to shut down, powerdown the phy */ + tegra_ehci_power_down(hcd); +} + +/* + * Work thread function for handling the USB power sequence. + * + * This work thread is created to avoid the pre-emption from the ISR context. + * USB Power Rail and Vbus are controlled based on the USB cable connection. + * USB Power rail function and VBUS control function cannot be called from ISR + * as the PMU uses I2C driver, that waits on semaphore during the I2C transaction + * this will cause the pre-emption if called in ISR. + */ +static void tegra_ehci_irq_work(struct work_struct *irq_work) +{ + struct tegra_ehci_hcd *tegra = container_of(irq_work, struct tegra_ehci_hcd, work); + struct usb_hcd *hcd = ehci_to_hcd(tegra->ehci); + + if (tegra->transceiver) { + if (tegra->transceiver->state == OTG_STATE_A_HOST && + !tegra->host_reinited) { + tegra->host_reinited = 1; + tegra_ehci_power_up(hcd); + tegra_ehci_restart(hcd); + } else if (tegra->transceiver->state == OTG_STATE_A_SUSPEND && + tegra->host_reinited) { + tegra_ehci_power_down(hcd); + tegra->host_reinited = 0; + } + } +} + +static irqreturn_t tegra_ehci_irq(struct usb_hcd *hcd) +{ + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + unsigned long status; + + spin_lock(&ehci->lock); + + if (tegra->transceiver) { + if (tegra->transceiver->state == OTG_STATE_A_HOST) { + if (!tegra->host_reinited) + schedule_work(&tegra->work); + } else if (tegra->transceiver->state == OTG_STATE_A_SUSPEND) { + if (!tegra->host_reinited) { + spin_unlock(&ehci->lock); + return IRQ_HANDLED; + } else { + schedule_work(&tegra->work); + } + } else { + spin_unlock(&ehci->lock); + return IRQ_HANDLED; + } + } else { + /* read otgsc register for ID pin status change */ + status = readl(hcd->regs + TEGRA_USB_PHY_WAKEUP_REG_OFFSET); + writel(status, (hcd->regs + TEGRA_USB_PHY_WAKEUP_REG_OFFSET)); + + /* Check if there is any ID pin interrupt */ + if (status & TEGRA_USB_ID_INT_STATUS) + schedule_work(&tegra->work); + } + + spin_unlock(&ehci->lock); + return ehci_irq(hcd); +} + +static int tegra_ehci_setup(struct usb_hcd *hcd) +{ + struct ehci_hcd *ehci = hcd_to_ehci(hcd); + int retval; + + /* EHCI registers start at offset 0x100 */ + ehci->caps = hcd->regs + 0x100; + ehci->regs = hcd->regs + 0x100 + + HC_LENGTH(readl(&ehci->caps->hc_capbase)); + + dbg_hcs_params(ehci, "reset"); + dbg_hcc_params(ehci, "reset"); + + /* cache this readonly data; minimize chip reads */ + ehci->hcs_params = readl(&ehci->caps->hcs_params); + + retval = ehci_halt(ehci); + if (retval) + return retval; + + /* data structure init */ + retval = ehci_init(hcd); + if (retval) + return retval; + + hcd->has_tt = 1; + ehci->sbrn = 0x20; + + ehci_reset(ehci); + + /* + * Resetting the controller has the side effect of resetting the PHY. + * So, never reset the controller after the calling + * tegra_ehci_reinit API. + */ + ehci->controller_resets_phy = 1; + + ehci_port_power(ehci, 0); + return retval; +} + + +static int tegra_ehci_bus_suspend(struct usb_hcd *hcd) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + int error_status = 0; + + /* we are not in host mode, return */ + if (tegra->transceiver && tegra->transceiver->state != OTG_STATE_A_HOST) + return 0; + + if (tegra->host_resumed) { + error_status = ehci_bus_suspend(hcd); + if (!error_status) + tegra_ehci_power_down(hcd); + } + + return error_status; +} + +static int tegra_ehci_bus_resume(struct usb_hcd *hcd) +{ + struct tegra_ehci_hcd *tegra = dev_get_drvdata(hcd->self.controller); + + if (tegra->transceiver && tegra->transceiver->state != OTG_STATE_A_HOST) + return 0; + + if (!tegra->host_resumed) + tegra_ehci_power_up(hcd); + + return ehci_bus_resume(hcd); +} + +static const struct hc_driver tegra_ehci_hc_driver = { + .description = hcd_name, + .product_desc = "Tegra EHCI Host Controller", + .hcd_priv_size = sizeof(struct ehci_hcd), + + .flags = HCD_USB2, + + .reset = tegra_ehci_setup, + .irq = tegra_ehci_irq, + + .start = ehci_run, + .stop = ehci_stop, + .shutdown = tegra_ehci_shutdown, + .urb_enqueue = ehci_urb_enqueue, + .urb_dequeue = ehci_urb_dequeue, + .endpoint_disable = ehci_endpoint_disable, + .endpoint_reset = ehci_endpoint_reset, + .get_frame_number = ehci_get_frame, + .hub_status_data = ehci_hub_status_data, + .hub_control = tegra_ehci_hub_control, + .clear_tt_buffer_complete = ehci_clear_tt_buffer_complete, +#ifdef CONFIG_PM + .bus_suspend = tegra_ehci_bus_suspend, + .bus_resume = tegra_ehci_bus_resume, +#endif + .relinquish_port = ehci_relinquish_port, + .port_handed_over = ehci_port_handed_over, +}; + +static int tegra_ehci_probe(struct platform_device *pdev) +{ + struct resource *res; + struct usb_hcd *hcd; + struct ehci_hcd *ehci; + struct tegra_ehci_hcd *tegra; + int err = 0; + int irq; + unsigned int temp; + int instance = pdev->id; + + tegra = kzalloc(sizeof(struct tegra_ehci_hcd), GFP_KERNEL); + if (!tegra) + return -ENOMEM; + + hcd = usb_create_hcd(&tegra_ehci_hc_driver, &pdev->dev, + dev_name(&pdev->dev)); + if (!hcd) { + dev_err(&pdev->dev, "Unable to create HCD\n"); + err = -ENOMEM; + goto fail_hcd; + } + + platform_set_drvdata(pdev, tegra); + INIT_WORK(&tegra->work, tegra_ehci_irq_work); + + tegra->clk = clk_get(&pdev->dev, NULL); + if (IS_ERR(tegra->clk)) { + dev_err(&pdev->dev, "Can't get ehci clock\n"); + err = PTR_ERR(tegra->clk); + goto fail_clk; + } + + err = clk_enable(tegra->clk); + if (err) + goto fail_clken; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + dev_err(&pdev->dev, "Failed to get I/O memory\n"); + err = -ENXIO; + goto fail_io; + } + hcd->rsrc_start = res->start; + hcd->rsrc_len = resource_size(res); + hcd->regs = ioremap(res->start, resource_size(res)); + if (!hcd->regs) { + dev_err(&pdev->dev, "Failed to remap I/O memory\n"); + err = -ENOMEM; + goto fail_io; + } + + tegra->phy = tegra_usb_phy_open(instance, hcd->regs); + if (IS_ERR(tegra->phy)) { + dev_err(&pdev->dev, "Failed to open USB phy\n"); + err = -ENXIO; + goto fail_phy; + } + + tegra_usb_phy_power_on(tegra->phy); + + err = tegra_ehci_reset(hcd); + if (err) { + dev_err(&pdev->dev, "Failed to reset controller\n"); + goto fail; + } + + tegra->host_resumed = 1; + + /* Set to Host mode by setting bit 0-1 of USB device mode register */ + temp = readl(hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET); + writel((temp | TEGRA_USB_USBMODE_HOST), + (hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET)); + temp = readl(hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET); + + irq = platform_get_irq(pdev, 0); + if (!irq) { + dev_err(&pdev->dev, "Failed to get IRQ\n"); + err = -ENODEV; + goto fail; + } + + set_irq_flags(irq, IRQF_VALID); + + ehci = hcd_to_ehci(hcd); + tegra->ehci = ehci; + +#ifdef CONFIG_USB_OTG_UTILS + if (instance == 0) + tegra->transceiver = otg_get_transceiver(); +#endif + + err = usb_add_hcd(hcd, irq, IRQF_DISABLED | IRQF_SHARED); + if (err != 0) { + dev_err(&pdev->dev, "Failed to add USB HCD\n"); + goto fail; + } + + if (tegra->transceiver) { + otg_set_host(tegra->transceiver, (struct usb_bus *)hcd); + + /* + * Stop the controller and power down the phy, OTG will + * start the host driver based on the ID pin + * detection + */ + ehci_halt(ehci); + + /* reset the host and put the controller in idle mode */ + temp = ehci_readl(ehci, &ehci->regs->command); + temp |= CMD_RESET; + ehci_writel(ehci, temp, &ehci->regs->command); + + temp = readl(hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET); + writel((temp & ~TEGRA_USB_USBMODE_HOST), + (hcd->regs + TEGRA_USB_USBMODE_REG_OFFSET)); + + /* indicate hcd flags, that hardware is not accessable now in host mode*/ + clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags); + + tegra_ehci_power_down(hcd); + tegra->host_reinited = 0; + } else if (instance == 0) { + /* enable the cable ID interrupt */ + temp = readl(hcd->regs + TEGRA_USB_PHY_WAKEUP_REG_OFFSET); + temp |= TEGRA_USB_ID_INT_ENABLE; + temp |= TEGRA_USB_ID_PIN_WAKEUP_ENABLE; + writel(temp, (hcd->regs + TEGRA_USB_PHY_WAKEUP_REG_OFFSET)); + + /* Check if we detect any device connected */ + if (temp & TEGRA_USB_ID_PIN_STATUS) + tegra_ehci_power_down(hcd); + else + tegra_ehci_power_up(hcd); + } + + return err; + +fail: + tegra_usb_phy_close(tegra->phy); +fail_phy: + iounmap(hcd->regs); +fail_io: + clk_disable(tegra->clk); +fail_clken: + clk_put(tegra->clk); +fail_clk: + usb_put_hcd(hcd); +fail_hcd: + kfree(tegra); + return err; +} + +#ifdef CONFIG_PM +static int tegra_ehci_resume(struct platform_device *pdev) +{ + struct tegra_ehci_hcd *tegra = platform_get_drvdata(pdev); + struct usb_hcd *hcd = ehci_to_hcd(tegra->ehci); + + if (!tegra->host_resumed) { + tegra_ehci_power_up(hcd); + tegra_ehci_restart(hcd); + } + + return 0; +} + +static int tegra_ehci_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct tegra_ehci_hcd *tegra = platform_get_drvdata(pdev); + struct usb_hcd *hcd = ehci_to_hcd(tegra->ehci); + + if (tegra->transceiver) { + if (tegra->transceiver->state != OTG_STATE_A_HOST) { + /* we are not in host mode, return */ + return 0; + } else { + tegra->host_reinited = 0; + ehci_halt(tegra->ehci); + /* indicate hcd flags, that hardware is not accessable now */ + clear_bit(HCD_FLAG_HW_ACCESSIBLE, &hcd->flags); + } + } + + if (tegra->host_resumed) + tegra_ehci_power_down(hcd); + + return 0; +} +#endif + +static int tegra_ehci_remove(struct platform_device *pdev) +{ + struct tegra_ehci_hcd *tegra = platform_get_drvdata(pdev); + struct usb_hcd *hcd = ehci_to_hcd(tegra->ehci); + + if (tegra == NULL || hcd == NULL) + return -EINVAL; + + usb_remove_hcd(hcd); + tegra_usb_phy_close(tegra->phy); + iounmap(hcd->regs); + + clk_disable(tegra->clk); + clk_put(tegra->clk); + usb_put_hcd(hcd); + + kfree(tegra); + return 0; +} + +static void tegra_ehci_hcd_shutdown(struct platform_device *pdev) +{ + struct tegra_ehci_hcd *tegra = platform_get_drvdata(pdev); + struct usb_hcd *hcd = ehci_to_hcd(tegra->ehci); + + if (hcd->driver->shutdown) + hcd->driver->shutdown(hcd); +} + +static struct platform_driver tegra_ehci_driver = { + .probe = tegra_ehci_probe, + .remove = tegra_ehci_remove, +#ifdef CONFIG_PM + .suspend = tegra_ehci_suspend, + .resume = tegra_ehci_resume, +#endif + .shutdown = tegra_ehci_hcd_shutdown, + .driver = { + .name = "tegra-ehci", + } +}; -- 2.34.1