282 lines
6.8 KiB
C
282 lines
6.8 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
/* Copyright (c) 2020, Broadcom */
|
||
|
|
||
|
#include <linux/clk.h>
|
||
|
#include <linux/dma-mapping.h>
|
||
|
#include <linux/err.h>
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/io.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/platform_device.h>
|
||
|
#include <linux/usb.h>
|
||
|
#include <linux/usb/hcd.h>
|
||
|
#include <linux/iopoll.h>
|
||
|
|
||
|
#include "ehci.h"
|
||
|
|
||
|
#define hcd_to_ehci_priv(h) ((struct brcm_priv *)hcd_to_ehci(h)->priv)
|
||
|
|
||
|
struct brcm_priv {
|
||
|
struct clk *clk;
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* ehci_brcm_wait_for_sof
|
||
|
* Wait for start of next microframe, then wait extra delay microseconds
|
||
|
*/
|
||
|
static inline void ehci_brcm_wait_for_sof(struct ehci_hcd *ehci, u32 delay)
|
||
|
{
|
||
|
u32 frame_idx = ehci_readl(ehci, &ehci->regs->frame_index);
|
||
|
u32 val;
|
||
|
int res;
|
||
|
|
||
|
/* Wait for next microframe (every 125 usecs) */
|
||
|
res = readl_relaxed_poll_timeout(&ehci->regs->frame_index, val,
|
||
|
val != frame_idx, 1, 130);
|
||
|
if (res)
|
||
|
ehci_err(ehci, "Error waiting for SOF\n");
|
||
|
udelay(delay);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* ehci_brcm_hub_control
|
||
|
* The EHCI controller has a bug where it can violate the SOF
|
||
|
* interval between the first two SOF's transmitted after resume
|
||
|
* if the resume occurs near the end of the microframe. This causees
|
||
|
* the controller to detect babble on the suspended port and
|
||
|
* will eventually cause the controller to reset the port.
|
||
|
* The fix is to Intercept the echi-hcd request to complete RESUME and
|
||
|
* align it to the start of the next microframe.
|
||
|
* See SWLINUX-1909 for more details
|
||
|
*/
|
||
|
static int ehci_brcm_hub_control(
|
||
|
struct usb_hcd *hcd,
|
||
|
u16 typeReq,
|
||
|
u16 wValue,
|
||
|
u16 wIndex,
|
||
|
char *buf,
|
||
|
u16 wLength)
|
||
|
{
|
||
|
struct ehci_hcd *ehci = hcd_to_ehci(hcd);
|
||
|
int ports = HCS_N_PORTS(ehci->hcs_params);
|
||
|
u32 __iomem *status_reg;
|
||
|
unsigned long flags;
|
||
|
int retval, irq_disabled = 0;
|
||
|
u32 temp;
|
||
|
|
||
|
temp = (wIndex & 0xff) - 1;
|
||
|
if (temp >= HCS_N_PORTS_MAX) /* Avoid index-out-of-bounds warning */
|
||
|
temp = 0;
|
||
|
status_reg = &ehci->regs->port_status[temp];
|
||
|
|
||
|
/*
|
||
|
* RESUME is cleared when GetPortStatus() is called 20ms after start
|
||
|
* of RESUME
|
||
|
*/
|
||
|
if ((typeReq == GetPortStatus) &&
|
||
|
(wIndex && wIndex <= ports) &&
|
||
|
ehci->reset_done[wIndex-1] &&
|
||
|
time_after_eq(jiffies, ehci->reset_done[wIndex-1]) &&
|
||
|
(ehci_readl(ehci, status_reg) & PORT_RESUME)) {
|
||
|
|
||
|
/*
|
||
|
* to make sure we are not interrupted until RESUME bit
|
||
|
* is cleared, disable interrupts on current CPU
|
||
|
*/
|
||
|
ehci_dbg(ehci, "SOF alignment workaround\n");
|
||
|
irq_disabled = 1;
|
||
|
local_irq_save(flags);
|
||
|
ehci_brcm_wait_for_sof(ehci, 5);
|
||
|
}
|
||
|
retval = ehci_hub_control(hcd, typeReq, wValue, wIndex, buf, wLength);
|
||
|
if (irq_disabled)
|
||
|
local_irq_restore(flags);
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
static int ehci_brcm_reset(struct usb_hcd *hcd)
|
||
|
{
|
||
|
struct ehci_hcd *ehci = hcd_to_ehci(hcd);
|
||
|
int len;
|
||
|
|
||
|
ehci->big_endian_mmio = 1;
|
||
|
|
||
|
ehci->caps = (void __iomem *)hcd->regs;
|
||
|
len = HC_LENGTH(ehci, ehci_readl(ehci, &ehci->caps->hc_capbase));
|
||
|
ehci->regs = (void __iomem *)(hcd->regs + len);
|
||
|
|
||
|
/* This fixes the lockup during reboot due to prior interrupts */
|
||
|
ehci_writel(ehci, CMD_RESET, &ehci->regs->command);
|
||
|
mdelay(10);
|
||
|
|
||
|
/*
|
||
|
* SWLINUX-1705: Avoid OUT packet underflows during high memory
|
||
|
* bus usage
|
||
|
*/
|
||
|
ehci_writel(ehci, 0x00800040, &ehci->regs->brcm_insnreg[1]);
|
||
|
ehci_writel(ehci, 0x00000001, &ehci->regs->brcm_insnreg[3]);
|
||
|
|
||
|
return ehci_setup(hcd);
|
||
|
}
|
||
|
|
||
|
static struct hc_driver __read_mostly ehci_brcm_hc_driver;
|
||
|
|
||
|
static const struct ehci_driver_overrides brcm_overrides __initconst = {
|
||
|
.reset = ehci_brcm_reset,
|
||
|
.extra_priv_size = sizeof(struct brcm_priv),
|
||
|
};
|
||
|
|
||
|
static int ehci_brcm_probe(struct platform_device *pdev)
|
||
|
{
|
||
|
struct device *dev = &pdev->dev;
|
||
|
struct resource *res_mem;
|
||
|
struct brcm_priv *priv;
|
||
|
struct usb_hcd *hcd;
|
||
|
int irq;
|
||
|
int err;
|
||
|
|
||
|
err = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
|
||
|
if (err)
|
||
|
return err;
|
||
|
|
||
|
irq = platform_get_irq(pdev, 0);
|
||
|
if (irq <= 0)
|
||
|
return irq ? irq : -EINVAL;
|
||
|
|
||
|
/* Hook the hub control routine to work around a bug */
|
||
|
ehci_brcm_hc_driver.hub_control = ehci_brcm_hub_control;
|
||
|
|
||
|
/* initialize hcd */
|
||
|
hcd = usb_create_hcd(&ehci_brcm_hc_driver, dev, dev_name(dev));
|
||
|
if (!hcd)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
platform_set_drvdata(pdev, hcd);
|
||
|
priv = hcd_to_ehci_priv(hcd);
|
||
|
|
||
|
priv->clk = devm_clk_get_optional(dev, NULL);
|
||
|
if (IS_ERR(priv->clk)) {
|
||
|
err = PTR_ERR(priv->clk);
|
||
|
goto err_hcd;
|
||
|
}
|
||
|
|
||
|
err = clk_prepare_enable(priv->clk);
|
||
|
if (err)
|
||
|
goto err_hcd;
|
||
|
|
||
|
hcd->regs = devm_platform_get_and_ioremap_resource(pdev, 0, &res_mem);
|
||
|
if (IS_ERR(hcd->regs)) {
|
||
|
err = PTR_ERR(hcd->regs);
|
||
|
goto err_clk;
|
||
|
}
|
||
|
hcd->rsrc_start = res_mem->start;
|
||
|
hcd->rsrc_len = resource_size(res_mem);
|
||
|
err = usb_add_hcd(hcd, irq, IRQF_SHARED);
|
||
|
if (err)
|
||
|
goto err_clk;
|
||
|
|
||
|
device_wakeup_enable(hcd->self.controller);
|
||
|
device_enable_async_suspend(hcd->self.controller);
|
||
|
|
||
|
return 0;
|
||
|
|
||
|
err_clk:
|
||
|
clk_disable_unprepare(priv->clk);
|
||
|
err_hcd:
|
||
|
usb_put_hcd(hcd);
|
||
|
|
||
|
return err;
|
||
|
}
|
||
|
|
||
|
static int ehci_brcm_remove(struct platform_device *dev)
|
||
|
{
|
||
|
struct usb_hcd *hcd = platform_get_drvdata(dev);
|
||
|
struct brcm_priv *priv = hcd_to_ehci_priv(hcd);
|
||
|
|
||
|
usb_remove_hcd(hcd);
|
||
|
clk_disable_unprepare(priv->clk);
|
||
|
usb_put_hcd(hcd);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int __maybe_unused ehci_brcm_suspend(struct device *dev)
|
||
|
{
|
||
|
int ret;
|
||
|
struct usb_hcd *hcd = dev_get_drvdata(dev);
|
||
|
struct brcm_priv *priv = hcd_to_ehci_priv(hcd);
|
||
|
bool do_wakeup = device_may_wakeup(dev);
|
||
|
|
||
|
ret = ehci_suspend(hcd, do_wakeup);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
clk_disable_unprepare(priv->clk);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int __maybe_unused ehci_brcm_resume(struct device *dev)
|
||
|
{
|
||
|
struct usb_hcd *hcd = dev_get_drvdata(dev);
|
||
|
struct ehci_hcd *ehci = hcd_to_ehci(hcd);
|
||
|
struct brcm_priv *priv = hcd_to_ehci_priv(hcd);
|
||
|
int err;
|
||
|
|
||
|
err = clk_prepare_enable(priv->clk);
|
||
|
if (err)
|
||
|
return err;
|
||
|
/*
|
||
|
* SWLINUX-1705: Avoid OUT packet underflows during high memory
|
||
|
* bus usage
|
||
|
*/
|
||
|
ehci_writel(ehci, 0x00800040, &ehci->regs->brcm_insnreg[1]);
|
||
|
ehci_writel(ehci, 0x00000001, &ehci->regs->brcm_insnreg[3]);
|
||
|
|
||
|
ehci_resume(hcd, false);
|
||
|
|
||
|
pm_runtime_disable(dev);
|
||
|
pm_runtime_set_active(dev);
|
||
|
pm_runtime_enable(dev);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static SIMPLE_DEV_PM_OPS(ehci_brcm_pm_ops, ehci_brcm_suspend,
|
||
|
ehci_brcm_resume);
|
||
|
|
||
|
static const struct of_device_id brcm_ehci_of_match[] = {
|
||
|
{ .compatible = "brcm,ehci-brcm-v2", },
|
||
|
{ .compatible = "brcm,bcm7445-ehci", },
|
||
|
{}
|
||
|
};
|
||
|
|
||
|
static struct platform_driver ehci_brcm_driver = {
|
||
|
.probe = ehci_brcm_probe,
|
||
|
.remove = ehci_brcm_remove,
|
||
|
.shutdown = usb_hcd_platform_shutdown,
|
||
|
.driver = {
|
||
|
.name = "ehci-brcm",
|
||
|
.pm = &ehci_brcm_pm_ops,
|
||
|
.of_match_table = brcm_ehci_of_match,
|
||
|
}
|
||
|
};
|
||
|
|
||
|
static int __init ehci_brcm_init(void)
|
||
|
{
|
||
|
if (usb_disabled())
|
||
|
return -ENODEV;
|
||
|
|
||
|
ehci_init_driver(&ehci_brcm_hc_driver, &brcm_overrides);
|
||
|
return platform_driver_register(&ehci_brcm_driver);
|
||
|
}
|
||
|
module_init(ehci_brcm_init);
|
||
|
|
||
|
static void __exit ehci_brcm_exit(void)
|
||
|
{
|
||
|
platform_driver_unregister(&ehci_brcm_driver);
|
||
|
}
|
||
|
module_exit(ehci_brcm_exit);
|
||
|
|
||
|
MODULE_ALIAS("platform:ehci-brcm");
|
||
|
MODULE_DESCRIPTION("EHCI Broadcom STB driver");
|
||
|
MODULE_AUTHOR("Al Cooper");
|
||
|
MODULE_LICENSE("GPL");
|