diff mbox series

[v1,04/12] usb: phy: tegra: Support OTG mode programming

Message ID 20210701022405.10817-5-digetx@gmail.com (mailing list archive)
State Superseded
Headers show
Series Add OTG mode support to Tegra USB PHY, SMB347 and Nexus 7 | expand

Commit Message

Dmitry Osipenko July 1, 2021, 2:23 a.m. UTC
Support programming USB PHY into OTG mode.

Signed-off-by: Dmitry Osipenko <digetx@gmail.com>
---
 drivers/usb/phy/phy-tegra-usb.c   | 166 +++++++++++++++++++++++++++++-
 include/linux/usb/tegra_usb_phy.h |   5 +
 2 files changed, 167 insertions(+), 4 deletions(-)

Comments

Dmitry Osipenko July 1, 2021, 1:55 p.m. UTC | #1
01.07.2021 05:23, Dmitry Osipenko пишет:
>  static int tegra_usb_phy_init(struct usb_phy *u_phy)
> @@ -967,12 +1057,26 @@ static int tegra_usb_phy_init(struct usb_phy *u_phy)
>  			goto disable_vbus;
>  	}
>  
> +	err = tegra_usb_phy_configure_pmc(phy);
> +	if (err)
> +		goto close_phy;
> +
>  	err = tegra_usb_phy_power_on(phy);
>  	if (err)
>  		goto close_phy;
>  
> +	if (phy->irq > 0) {
> +		err = request_irq(phy->irq, tegra_usb_phy_isr, IRQF_SHARED,
> +				  dev_name(phy->u_phy.dev), phy);
> +		if (err)
> +			goto pwr_off_phy;
> +	}

There were reports that this patch was casing an unhandled USB interrupt
event on some devices. I thought this problem was fixed already, but
looking again at the offending kernel log again, it still should be a
problem.

The interrupt fires from the usb_add_hcd() of the CI driver before CI
driver have requested interrupt in ci_hdrc_probe(). So either CI driver
should request interrupt earlier or Tegra PHY driver should keep shared
interrupt disabled after requesting it, the latter variant should be
more robust. I'll improve it in v2.
Michał Mirosław July 8, 2021, 10:32 p.m. UTC | #2
On Thu, Jul 01, 2021 at 04:55:03PM +0300, Dmitry Osipenko wrote:
> 01.07.2021 05:23, Dmitry Osipenko пишет:
> >  static int tegra_usb_phy_init(struct usb_phy *u_phy)
> > @@ -967,12 +1057,26 @@ static int tegra_usb_phy_init(struct usb_phy *u_phy)
> >  			goto disable_vbus;
> >  	}
> >  
> > +	err = tegra_usb_phy_configure_pmc(phy);
> > +	if (err)
> > +		goto close_phy;
> > +
> >  	err = tegra_usb_phy_power_on(phy);
> >  	if (err)
> >  		goto close_phy;
> >  
> > +	if (phy->irq > 0) {
> > +		err = request_irq(phy->irq, tegra_usb_phy_isr, IRQF_SHARED,
> > +				  dev_name(phy->u_phy.dev), phy);
> > +		if (err)
> > +			goto pwr_off_phy;
> > +	}
> 
> There were reports that this patch was casing an unhandled USB interrupt
> event on some devices. I thought this problem was fixed already, but
> looking again at the offending kernel log again, it still should be a
> problem.
> 
> The interrupt fires from the usb_add_hcd() of the CI driver before CI
> driver have requested interrupt in ci_hdrc_probe(). So either CI driver
> should request interrupt earlier or Tegra PHY driver should keep shared
> interrupt disabled after requesting it, the latter variant should be
> more robust. I'll improve it in v2.

I'd suggest the first solution, as the latter is a workaround for what
is a normal shared interrupt behaviour. Maybe a controller reset is
needed in CI driver before going on with PHY init?

Best Regards
Michał Mirosław
Dmitry Osipenko July 9, 2021, 9:29 p.m. UTC | #3
09.07.2021 01:32, Michał Mirosław пишет:
> On Thu, Jul 01, 2021 at 04:55:03PM +0300, Dmitry Osipenko wrote:
>> 01.07.2021 05:23, Dmitry Osipenko пишет:
>>>  static int tegra_usb_phy_init(struct usb_phy *u_phy)
>>> @@ -967,12 +1057,26 @@ static int tegra_usb_phy_init(struct usb_phy *u_phy)
>>>  			goto disable_vbus;
>>>  	}
>>>  
>>> +	err = tegra_usb_phy_configure_pmc(phy);
>>> +	if (err)
>>> +		goto close_phy;
>>> +
>>>  	err = tegra_usb_phy_power_on(phy);
>>>  	if (err)
>>>  		goto close_phy;
>>>  
>>> +	if (phy->irq > 0) {
>>> +		err = request_irq(phy->irq, tegra_usb_phy_isr, IRQF_SHARED,
>>> +				  dev_name(phy->u_phy.dev), phy);
>>> +		if (err)
>>> +			goto pwr_off_phy;
>>> +	}
>>
>> There were reports that this patch was casing an unhandled USB interrupt
>> event on some devices. I thought this problem was fixed already, but
>> looking again at the offending kernel log again, it still should be a
>> problem.
>>
>> The interrupt fires from the usb_add_hcd() of the CI driver before CI
>> driver have requested interrupt in ci_hdrc_probe(). So either CI driver
>> should request interrupt earlier or Tegra PHY driver should keep shared
>> interrupt disabled after requesting it, the latter variant should be
>> more robust. I'll improve it in v2.
> 
> I'd suggest the first solution, as the latter is a workaround for what
> is a normal shared interrupt behaviour. Maybe a controller reset is
> needed in CI driver before going on with PHY init?

I already implemented the second solution. The controller reset should
be okay. We could improve it all later on if will ever be needed, so far
it's unnecessary. I can't really work on improving the CI interrupt
because it requires to have a special testing setup to reproduce the
problem and I don't have that setup.
diff mbox series

Patch

diff --git a/drivers/usb/phy/phy-tegra-usb.c b/drivers/usb/phy/phy-tegra-usb.c
index c0f432d509aa..621f527baa8a 100644
--- a/drivers/usb/phy/phy-tegra-usb.c
+++ b/drivers/usb/phy/phy-tegra-usb.c
@@ -63,6 +63,10 @@ 
 #define   A_VBUS_VLD_WAKEUP_EN			BIT(30)
 
 #define USB_PHY_VBUS_WAKEUP_ID			0x408
+#define   ID_INT_EN				BIT(0)
+#define   ID_CHG_DET				BIT(1)
+#define   VBUS_WAKEUP_INT_EN			BIT(8)
+#define   VBUS_WAKEUP_CHG_DET			BIT(9)
 #define   VBUS_WAKEUP_STS			BIT(10)
 #define   VBUS_WAKEUP_WAKEUP_EN			BIT(30)
 
@@ -158,6 +162,10 @@ 
 #define   USB_USBMODE_HOST			(3 << 0)
 #define   USB_USBMODE_DEVICE			(2 << 0)
 
+#define PMC_USB_AO				0xf0
+#define   VBUS_WAKEUP_PD_P0			BIT(2)
+#define   ID_PD_P0				BIT(3)
+
 static DEFINE_SPINLOCK(utmip_pad_lock);
 static unsigned int utmip_pad_count;
 
@@ -533,13 +541,14 @@  static int utmi_phy_power_on(struct tegra_usb_phy *phy)
 	val &= ~USB_WAKE_ON_RESUME_EN;
 	writel_relaxed(val, base + USB_SUSP_CTRL);
 
-	if (phy->mode == USB_DR_MODE_PERIPHERAL) {
+	if (phy->mode != USB_DR_MODE_HOST) {
 		val = readl_relaxed(base + USB_SUSP_CTRL);
 		val &= ~(USB_WAKE_ON_CNNT_EN_DEV | USB_WAKE_ON_DISCON_EN_DEV);
 		writel_relaxed(val, base + USB_SUSP_CTRL);
 
 		val = readl_relaxed(base + USB_PHY_VBUS_WAKEUP_ID);
 		val &= ~VBUS_WAKEUP_WAKEUP_EN;
+		val &= ~(ID_INT_EN | VBUS_WAKEUP_INT_EN);
 		writel_relaxed(val, base + USB_PHY_VBUS_WAKEUP_ID);
 
 		val = readl_relaxed(base + USB_PHY_VBUS_SENSORS);
@@ -687,9 +696,11 @@  static int utmi_phy_power_off(struct tegra_usb_phy *phy)
 		 * Ask VBUS sensor to generate wake event once cable is
 		 * connected.
 		 */
-		if (phy->mode == USB_DR_MODE_PERIPHERAL) {
+		if (phy->mode != USB_DR_MODE_HOST) {
 			val = readl_relaxed(base + USB_PHY_VBUS_WAKEUP_ID);
 			val |= VBUS_WAKEUP_WAKEUP_EN;
+			val |= ID_INT_EN | VBUS_WAKEUP_INT_EN;
+			val &= ~(ID_CHG_DET | VBUS_WAKEUP_CHG_DET);
 			writel_relaxed(val, base + USB_PHY_VBUS_WAKEUP_ID);
 
 			val = readl_relaxed(base + USB_PHY_VBUS_SENSORS);
@@ -893,6 +904,9 @@  static void tegra_usb_phy_shutdown(struct usb_phy *u_phy)
 	if (WARN_ON(!phy->freq))
 		return;
 
+	if (phy->irq > 0)
+		free_irq(phy->irq, phy);
+
 	tegra_usb_phy_power_off(phy);
 
 	if (!phy->is_ulpi_phy)
@@ -916,14 +930,90 @@  static int tegra_usb_phy_set_wakeup(struct usb_phy *u_phy, bool enable)
 static int tegra_usb_phy_set_suspend(struct usb_phy *u_phy, int suspend)
 {
 	struct tegra_usb_phy *phy = to_tegra_usb_phy(u_phy);
+	int ret;
 
 	if (WARN_ON(!phy->freq))
 		return -EINVAL;
 
+	if (phy->irq > 0)
+		disable_irq(phy->irq);
+
 	if (suspend)
-		return tegra_usb_phy_power_off(phy);
+		ret = tegra_usb_phy_power_off(phy);
 	else
-		return tegra_usb_phy_power_on(phy);
+		ret = tegra_usb_phy_power_on(phy);
+
+	if (phy->irq > 0)
+		enable_irq(phy->irq);
+
+	return ret;
+}
+
+static irqreturn_t tegra_usb_phy_isr(int irq, void *data)
+{
+	u32 val, int_mask = ID_CHG_DET | VBUS_WAKEUP_CHG_DET;
+	struct tegra_usb_phy *phy = data;
+	void __iomem *base = phy->regs;
+
+	/*
+	 * The PHY interrupt also wakes the USB controller driver since
+	 * interrupt is shared. We don't do anything in the PHY driver,
+	 * so just clear the interrupt.
+	 */
+	val = readl_relaxed(base + USB_PHY_VBUS_WAKEUP_ID);
+	writel_relaxed(val, base + USB_PHY_VBUS_WAKEUP_ID);
+
+	return val & int_mask ? IRQ_HANDLED : IRQ_NONE;
+}
+
+static int tegra_usb_phy_configure_pmc(struct tegra_usb_phy *phy)
+{
+	int err, val = 0;
+
+	/* older device-trees don't have PMC regmap */
+	if (!phy->pmc_regmap)
+		return 0;
+
+	/* should be initialized if regmap presents */
+	if (WARN_ON_ONCE(phy->instance < 0))
+		return 0;
+
+	/*
+	 * Tegra20 has a different layout of PMC USB register bits and AO is
+	 * enabled by default after system reset on Tegra20, so assume nothing
+	 * to do on Tegra20.
+	 */
+	if (!phy->soc_config->requires_pmc_ao_power_up)
+		return 0;
+
+	/* enable VBUS wake-up detector */
+	if (phy->mode != USB_DR_MODE_HOST)
+		val |= VBUS_WAKEUP_PD_P0 << phy->instance * 4;
+
+	/* enable ID-pin ACC detector for OTG mode switching */
+	if (phy->mode == USB_DR_MODE_OTG)
+		val |= ID_PD_P0 << phy->instance * 4;
+
+	/* disable detectors to reset them */
+	err = regmap_set_bits(phy->pmc_regmap, PMC_USB_AO, val);
+	if (err) {
+		dev_err(phy->u_phy.dev, "Failed to disable PMC AO: %d\n", err);
+		return err;
+	}
+
+	usleep_range(10, 100);
+
+	/* enable detectors */
+	err = regmap_clear_bits(phy->pmc_regmap, PMC_USB_AO, val);
+	if (err) {
+		dev_err(phy->u_phy.dev, "Failed to enable PMC AO: %d\n", err);
+		return err;
+	}
+
+	/* detectors starts to work after 10ms */
+	usleep_range(10000, 15000);
+
+	return 0;
 }
 
 static int tegra_usb_phy_init(struct usb_phy *u_phy)
@@ -967,12 +1057,26 @@  static int tegra_usb_phy_init(struct usb_phy *u_phy)
 			goto disable_vbus;
 	}
 
+	err = tegra_usb_phy_configure_pmc(phy);
+	if (err)
+		goto close_phy;
+
 	err = tegra_usb_phy_power_on(phy);
 	if (err)
 		goto close_phy;
 
+	if (phy->irq > 0) {
+		err = request_irq(phy->irq, tegra_usb_phy_isr, IRQF_SHARED,
+				  dev_name(phy->u_phy.dev), phy);
+		if (err)
+			goto pwr_off_phy;
+	}
+
 	return 0;
 
+pwr_off_phy:
+	tegra_usb_phy_power_off(phy);
+
 close_phy:
 	if (!phy->is_ulpi_phy)
 		utmip_pad_close(phy);
@@ -1135,11 +1239,50 @@  static int utmi_phy_probe(struct tegra_usb_phy *tegra_phy,
 	return 0;
 }
 
+static void tegra_usb_phy_put_pmc_device(void *dev)
+{
+	put_device(dev);
+}
+
+static struct regmap *tegra_usb_phy_get_pmc_regmap(struct device *dev)
+{
+	struct platform_device *pmc_pdev;
+	struct device_node *np;
+	struct regmap *regmap;
+	int err;
+
+	np = of_parse_phandle(dev->of_node, "nvidia,pmc", 0);
+	if (!np) {
+		dev_warn_once(dev, "nvidia,pmc is missing, please update your device-tree\n");
+		return NULL;
+	}
+
+	pmc_pdev = of_find_device_by_node(np);
+	of_node_put(np);
+	if (!pmc_pdev)
+		return ERR_PTR(-ENODEV);
+
+	err = devm_add_action_or_reset(dev, tegra_usb_phy_put_pmc_device,
+				       &pmc_pdev->dev);
+	if (err)
+		return ERR_PTR(err);
+
+	if (!platform_get_drvdata(pmc_pdev))
+		return ERR_PTR(-EPROBE_DEFER);
+
+	regmap = dev_get_regmap(&pmc_pdev->dev, "usb_sleepwalk");
+	if (!regmap)
+		return ERR_PTR(-EINVAL);
+
+	return regmap;
+}
+
 static const struct tegra_phy_soc_config tegra20_soc_config = {
 	.utmi_pll_config_in_car_module = false,
 	.has_hostpc = false,
 	.requires_usbmode_setup = false,
 	.requires_extra_tuning_parameters = false,
+	.requires_pmc_ao_power_up = false,
 };
 
 static const struct tegra_phy_soc_config tegra30_soc_config = {
@@ -1147,6 +1290,7 @@  static const struct tegra_phy_soc_config tegra30_soc_config = {
 	.has_hostpc = true,
 	.requires_usbmode_setup = true,
 	.requires_extra_tuning_parameters = true,
+	.requires_pmc_ao_power_up = true,
 };
 
 static const struct of_device_id tegra_usb_phy_id_table[] = {
@@ -1172,6 +1316,7 @@  static int tegra_usb_phy_probe(struct platform_device *pdev)
 		return -ENOMEM;
 
 	tegra_phy->soc_config = of_device_get_match_data(&pdev->dev);
+	tegra_phy->irq = platform_get_irq_optional(pdev, 0);
 
 	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 	if (!res) {
@@ -1215,6 +1360,19 @@  static int tegra_usb_phy_probe(struct platform_device *pdev)
 		return err;
 	}
 
+	tegra_phy->pmc_regmap = tegra_usb_phy_get_pmc_regmap(&pdev->dev);
+	err = PTR_ERR_OR_ZERO(tegra_phy->pmc_regmap);
+	if (err) {
+		dev_err_probe(&pdev->dev, err, "Failed to get PMC regmap\n");
+		return err;
+	}
+
+	/* older device-trees don't specify instance ID */
+	err = of_property_read_u32(np, "nvidia,phy-instance",
+				   &tegra_phy->instance);
+	if (err)
+		tegra_phy->instance = -1;
+
 	phy_type = of_usb_get_phy_mode(np);
 	switch (phy_type) {
 	case USBPHY_INTERFACE_MODE_UTMI:
diff --git a/include/linux/usb/tegra_usb_phy.h b/include/linux/usb/tegra_usb_phy.h
index fd1c9f6a4e37..d3e65eb9e16f 100644
--- a/include/linux/usb/tegra_usb_phy.h
+++ b/include/linux/usb/tegra_usb_phy.h
@@ -18,6 +18,7 @@ 
 
 #include <linux/clk.h>
 #include <linux/gpio.h>
+#include <linux/regmap.h>
 #include <linux/reset.h>
 #include <linux/usb/otg.h>
 
@@ -30,6 +31,7 @@ 
  *      enter host mode
  * requires_extra_tuning_parameters: true if xcvr_hsslew, hssquelch_level
  *      and hsdiscon_level should be set for adequate signal quality
+ * requires_pmc_ao_power_up: true if USB AO is powered down by default
  */
 
 struct tegra_phy_soc_config {
@@ -37,6 +39,7 @@  struct tegra_phy_soc_config {
 	bool has_hostpc;
 	bool requires_usbmode_setup;
 	bool requires_extra_tuning_parameters;
+	bool requires_pmc_ao_power_up;
 };
 
 struct tegra_utmip_config {
@@ -62,6 +65,7 @@  enum tegra_usb_phy_port_speed {
 struct tegra_xtal_freq;
 
 struct tegra_usb_phy {
+	int irq;
 	int instance;
 	const struct tegra_xtal_freq *freq;
 	void __iomem *regs;
@@ -70,6 +74,7 @@  struct tegra_usb_phy {
 	struct clk *pll_u;
 	struct clk *pad_clk;
 	struct regulator *vbus;
+	struct regmap *pmc_regmap;
 	enum usb_dr_mode mode;
 	void *config;
 	const struct tegra_phy_soc_config *soc_config;