Skip to content

Commit

Permalink
usb: host: ehci-platform: add a quirk to avoid stuck
Browse files Browse the repository at this point in the history
Since EHCI/OHCI controllers on R-Car Gen3 SoCs are possible to
be getting stuck very rarely after a full/low usb device was
disconnected. To detect/recover from such a situation, the controllers
require a special way which poll the EHCI PORTSC register and changes
the OHCI functional state.

So, this patch adds a polling timer into the ehci-platform driver,
and if the ehci driver detects the issue by the EHCI PORTSC register,
the ehci driver removes a companion device (= the OHCI controller)
to change the OHCI functional state to USB Reset once. And then,
the ehci driver adds the companion device again.

Delay time 10usec is decided from detection time of connection
(2.5usec) plus margin time. If it is difficult to read every 10usec
by S/W, the shortest time (but more than 10usec) which is feasible
by S/W is applicable.

Using sleepable function like usleep_range() is possible
to do long time delay, it may cause increasing misdetection rate.
So, udelay() should be used in this case.

This patch is cherry-picked from cc7eac1("usb: host: ehci-platform:
add a quirk to avoid stuck")

Signed-off-by: Yoshihiro Shimoda <[email protected]>
[thovu: update commit log]
Signed-off-by: Tho Vu <[email protected]>
  • Loading branch information
shimoday authored and r-kataok committed Apr 3, 2020
1 parent 31d570f commit b2a2567
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 1 deletion.
127 changes: 127 additions & 0 deletions drivers/usb/host/ehci-platform.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include <linux/phy/phy.h>
#include <linux/platform_device.h>
#include <linux/reset.h>
#include <linux/sys_soc.h>
#include <linux/timer.h>
#include <linux/usb.h>
#include <linux/usb/hcd.h>
#include <linux/usb/ehci_pdriver.h>
Expand All @@ -50,6 +52,9 @@ struct ehci_platform_priv {
struct phy **phys;
int num_phys;
bool reset_on_resume;
bool quirk_poll;
struct timer_list poll_timer;
struct delayed_work poll_work;
};

static const char hcd_name[] = "ehci-platform";
Expand Down Expand Up @@ -145,6 +150,111 @@ static struct usb_ehci_pdata ehci_platform_defaults = {
.power_off = ehci_platform_power_off,
};

/**
* quirk_poll_check_port_status - Poll port_status if the device sticks
* @ehci: the ehci hcd pointer
*
* Since EHCI/OHCI controllers on R-Car Gen3 SoCs are possible to be getting
* stuck very rarely after a full/low usb device was disconnected. To
* detect such a situation, the controllers require a special way which poll
* the EHCI PORTSC register.
*
* Return: true if the controller's port_status indicated getting stuck
*/
static bool quirk_poll_check_port_status(struct ehci_hcd *ehci)
{
u32 port_status = ehci_readl(ehci, &ehci->regs->port_status[0]);

if (!(port_status & PORT_OWNER) &&
(port_status & PORT_POWER) &&
!(port_status & PORT_CONNECT) &&
(port_status & PORT_LS_MASK))
return true;

return false;
}

/**
* quirk_poll_rebind_companion - rebind comanion device to recover
* @ehci: the ehci hcd pointer
*
* Since EHCI/OHCI controllers on R-Car Gen3 SoCs are possible to be getting
* stuck very rarely after a full/low usb device was disconnected. To
* recover from such a situation, the controllers require changing the OHCI
* functional state.
*/
static void quirk_poll_rebind_companion(struct ehci_hcd *ehci)
{
struct device *companion_dev;
struct usb_hcd *hcd = ehci_to_hcd(ehci);

companion_dev = usb_of_get_companion_dev(hcd->self.controller);
if (!companion_dev)
return;

device_release_driver(companion_dev);
if (device_attach(companion_dev) < 0)
ehci_err(ehci, "%s: failed\n", __func__);

put_device(companion_dev);
}

static void quirk_poll_work(struct work_struct *work)
{
struct ehci_platform_priv *priv =
container_of(to_delayed_work(work), struct ehci_platform_priv,
poll_work);
struct ehci_hcd *ehci = container_of((void *)priv, struct ehci_hcd,
priv);

/* check the status twice to reduce misdetection rate */
if (!quirk_poll_check_port_status(ehci))
return;
udelay(10);
if (!quirk_poll_check_port_status(ehci))
return;

ehci_dbg(ehci, "%s: detected getting stuck. rebind now!\n", __func__);
quirk_poll_rebind_companion(ehci);
}

static void quirk_poll_timer(struct timer_list *t)
{
struct ehci_platform_priv *priv = from_timer(priv, t, poll_timer);
struct ehci_hcd *ehci = container_of((void *)priv, struct ehci_hcd,
priv);

if (quirk_poll_check_port_status(ehci)) {
/*
* Now scheduling the work for testing the port more. Note that
* updating the status is possible to be delayed when
* reconnection. So, this uses delayed work with 5 ms delay
* to avoid misdetection.
*/
schedule_delayed_work(&priv->poll_work, msecs_to_jiffies(5));
}

mod_timer(&priv->poll_timer, jiffies + HZ);
}

static void quirk_poll_init(struct ehci_platform_priv *priv)
{
INIT_DELAYED_WORK(&priv->poll_work, quirk_poll_work);
timer_setup(&priv->poll_timer, quirk_poll_timer, 0);
mod_timer(&priv->poll_timer, jiffies + HZ);
}

static void quirk_poll_end(struct ehci_platform_priv *priv)
{
del_timer_sync(&priv->poll_timer);
cancel_delayed_work(&priv->poll_work);
}

static const struct soc_device_attribute quirk_poll_match[] = {
{ .family = "R-Car Gen3" },
{ /* sentinel*/ }
};

static int ehci_platform_probe(struct platform_device *dev)
{
struct usb_hcd *hcd;
Expand Down Expand Up @@ -205,6 +315,9 @@ static int ehci_platform_probe(struct platform_device *dev)
"has-transaction-translator"))
hcd->has_tt = 1;

if (soc_device_match(quirk_poll_match))
priv->quirk_poll = true;

priv->num_phys = of_count_phandle_with_args(dev->dev.of_node,
"phys", "#phy-cells");

Expand Down Expand Up @@ -305,6 +418,9 @@ static int ehci_platform_probe(struct platform_device *dev)
device_enable_async_suspend(hcd->self.controller);
platform_set_drvdata(dev, hcd);

if (priv->quirk_poll)
quirk_poll_init(priv);

return err;

err_power:
Expand Down Expand Up @@ -332,6 +448,9 @@ static int ehci_platform_remove(struct platform_device *dev)
struct ehci_platform_priv *priv = hcd_to_ehci_priv(hcd);
int clk, rst;

if (priv->quirk_poll)
quirk_poll_end(priv);

usb_remove_hcd(hcd);

if (pdata->power_off)
Expand All @@ -357,9 +476,13 @@ static int ehci_platform_suspend(struct device *dev)
struct usb_hcd *hcd = dev_get_drvdata(dev);
struct usb_ehci_pdata *pdata = dev_get_platdata(dev);
struct platform_device *pdev = to_platform_device(dev);
struct ehci_platform_priv *priv = hcd_to_ehci_priv(hcd);
bool do_wakeup = device_may_wakeup(dev);
int ret;

if (priv->quirk_poll)
quirk_poll_end(priv);

ret = ehci_suspend(hcd, do_wakeup);
if (ret)
return ret;
Expand Down Expand Up @@ -391,6 +514,10 @@ static int ehci_platform_resume(struct device *dev)
}

ehci_resume(hcd, priv->reset_on_resume);

if (priv->quirk_poll)
quirk_poll_init(priv);

return 0;
}
#endif /* CONFIG_PM_SLEEP */
Expand Down
2 changes: 1 addition & 1 deletion include/linux/usb/ehci_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ struct ehci_regs {
#define PORT_OWNER (1<<13) /* true: companion hc owns this port */
#define PORT_POWER (1<<12) /* true: has power (see PPC) */
#define PORT_USB11(x) (((x)&(3<<10)) == (1<<10)) /* USB 1.1 device */
/* 11:10 for detecting lowspeed devices (reset vs release ownership) */
#define PORT_LS_MASK (3 << 10) /* Link status (SE0, K or J */
/* 9 reserved */
#define PORT_LPM (1<<9) /* LPM transaction */
#define PORT_RESET (1<<8) /* reset port */
Expand Down

0 comments on commit b2a2567

Please sign in to comment.