[v6,1/5] phy: add a driver for the Rockchip SoC internal USB2.0 PHY
diff mbox

Message ID 1418291722-25448-2-git-send-email-lyz@rock-chips.com
State New, archived
Headers show

Commit Message

LiYunzhi Dec. 11, 2014, 9:55 a.m. UTC
This patch to add a generic PHY driver for ROCKCHIP usb PHYs,
currently this driver can support RK3288. The RK3288 SoC have
three independent USB PHY IPs which are all configured through a
set of registers located in the GRF (general register files)
module.

Signed-off-by: Yunzhi Li <lyz@rock-chips.com>

---

Changes in v6:
- Rename SIDDQ_MSK to SIDDQ_WRITE_ENA.
- Use phandle args to find a phy struct directly.

Changes in v5: None
Changes in v4:
- Get number of PHYs from device tree.
- Model each PHY as subnode of the phy provider node.

Changes in v3:
- Use BIT macro instead of bit shift ops.
- Rename the config entry to PHY_ROCKCHIP_USB.

 drivers/phy/Kconfig            |   7 ++
 drivers/phy/Makefile           |   1 +
 drivers/phy/phy-rockchip-usb.c | 198 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 206 insertions(+)
 create mode 100644 drivers/phy/phy-rockchip-usb.c

Comments

Kishon Vijay Abraham I Dec. 11, 2014, 10:27 a.m. UTC | #1
Hi,

On Thursday 11 December 2014 03:25 PM, Yunzhi Li wrote:
> This patch to add a generic PHY driver for ROCKCHIP usb PHYs,
> currently this driver can support RK3288. The RK3288 SoC have
> three independent USB PHY IPs which are all configured through a
> set of registers located in the GRF (general register files)
> module.
> 
> Signed-off-by: Yunzhi Li <lyz@rock-chips.com>
> 
> ---
> 
> Changes in v6:
> - Rename SIDDQ_MSK to SIDDQ_WRITE_ENA.
> - Use phandle args to find a phy struct directly.
> 
> Changes in v5: None
> Changes in v4:
> - Get number of PHYs from device tree.
> - Model each PHY as subnode of the phy provider node.
> 
> Changes in v3:
> - Use BIT macro instead of bit shift ops.
> - Rename the config entry to PHY_ROCKCHIP_USB.
> 
>  drivers/phy/Kconfig            |   7 ++
>  drivers/phy/Makefile           |   1 +
>  drivers/phy/phy-rockchip-usb.c | 198 +++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 206 insertions(+)
>  create mode 100644 drivers/phy/phy-rockchip-usb.c
> 
> diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
> index ccad880..b24500a 100644
> --- a/drivers/phy/Kconfig
> +++ b/drivers/phy/Kconfig
> @@ -239,6 +239,13 @@ config PHY_QCOM_IPQ806X_SATA
>  	depends on OF
>  	select GENERIC_PHY
>  
> +config PHY_ROCKCHIP_USB
> +	tristate "Rockchip USB2 PHY Driver"
> +	depends on ARCH_ROCKCHIP && OF
> +	select GENERIC_PHY
> +	help
> +	  Enable this to support the Rockchip USB 2.0 PHY.
> +
>  config PHY_ST_SPEAR1310_MIPHY
>  	tristate "ST SPEAR1310-MIPHY driver"
>  	select GENERIC_PHY
> diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
> index aa74f96..48bf5a1 100644
> --- a/drivers/phy/Makefile
> +++ b/drivers/phy/Makefile
> @@ -28,6 +28,7 @@ phy-exynos-usb2-$(CONFIG_PHY_EXYNOS5250_USB2)	+= phy-exynos5250-usb2.o
>  phy-exynos-usb2-$(CONFIG_PHY_S5PV210_USB2)	+= phy-s5pv210-usb2.o
>  obj-$(CONFIG_PHY_EXYNOS5_USBDRD)	+= phy-exynos5-usbdrd.o
>  obj-$(CONFIG_PHY_QCOM_APQ8064_SATA)	+= phy-qcom-apq8064-sata.o
> +obj-$(CONFIG_PHY_ROCKCHIP_USB) += phy-rockchip-usb.o
>  obj-$(CONFIG_PHY_QCOM_IPQ806X_SATA)	+= phy-qcom-ipq806x-sata.o
>  obj-$(CONFIG_PHY_ST_SPEAR1310_MIPHY)	+= phy-spear1310-miphy.o
>  obj-$(CONFIG_PHY_ST_SPEAR1340_MIPHY)	+= phy-spear1340-miphy.o
> diff --git a/drivers/phy/phy-rockchip-usb.c b/drivers/phy/phy-rockchip-usb.c
> new file mode 100644
> index 0000000..dad5194
> --- /dev/null
> +++ b/drivers/phy/phy-rockchip-usb.c
> @@ -0,0 +1,198 @@
> +/*
> + * Rockchip usb PHY driver
> + *
> + * Copyright (C) 2014 Yunzhi Li <lyz@rock-chips.com>
> + * Copyright (C) 2014 ROCKCHIP, Inc.
> + *
> + * 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.
> + *
> + * 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.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/io.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/of.h>
> +#include <linux/of_address.h>
> +#include <linux/phy/phy.h>
> +#include <linux/platform_device.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/reset.h>
> +#include <linux/regmap.h>
> +#include <linux/mfd/syscon.h>
> +
> +#define ROCKCHIP_RK3288_UOC(n)	(0x320 + n * 0x14)
> +
> +/*
> + * The higher 16-bit of this register is used for write protection
> + * only if BIT(13 + 16) set to 1 the BIT(13) can be written.
> + */
> +#define SIDDQ_WRITE_ENA	BIT(29)
> +#define SIDDQ_ON		BIT(13)
> +#define SIDDQ_OFF		(0 << 13)
> +
> +struct rockchip_usb_phy {
> +	struct regmap	*reg_base;
> +	unsigned int	reg_offset;
> +	struct clk	*clk;
> +	struct phy	*phy;
> +};
> +
> +struct rockchip_usb_phy_priv {
> +	struct rockchip_usb_phy	*phys;
> +	unsigned		nphys;
> +};
> +
> +static int rockchip_usb_phy_power(struct rockchip_usb_phy *phy,
> +					   bool siddq)
> +{
> +	return regmap_write(phy->reg_base, phy->reg_offset,
> +			    SIDDQ_WRITE_ENA | (siddq ? SIDDQ_ON : SIDDQ_OFF));
> +}
> +
> +static int rockchip_usb_phy_power_off(struct phy *_phy)
> +{
> +	struct rockchip_usb_phy *phy = phy_get_drvdata(_phy);
> +	int ret = 0;
> +
> +	/* Power down usb phy analog blocks by set siddq 1 */
> +	ret = rockchip_usb_phy_power(phy, 1);
> +	if (ret)
> +		return ret;
> +
> +	clk_disable_unprepare(phy->clk);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +static int rockchip_usb_phy_power_on(struct phy *_phy)
> +{
> +	struct rockchip_usb_phy *phy = phy_get_drvdata(_phy);
> +	int ret = 0;
> +
> +	ret = clk_prepare_enable(phy->clk);
> +	if (ret)
> +		return ret;
> +
> +	/* Power up usb phy analog blocks by set siddq 0 */
> +	ret = rockchip_usb_phy_power(phy, 0);
> +	if (ret)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +static struct phy *rockchip_usb_phy_xlate(struct device *dev,
> +					struct of_phandle_args *args)
> +{
> +	struct rockchip_usb_phy_priv *priv = dev_get_drvdata(dev);
> +	unsigned int phy_id = args->args[0];
> +
> +	if (WARN_ON(phy_id < 0 || phy_id >= priv->nphys))
> +		return ERR_PTR(-ENODEV);
> +
> +	return priv->phys[phy_id].phy;

I didn't mean that. You can get rid of this entire xlate stuff if you use
something like below

phy@xxx {
	compatible = "";
	phy1:usb_phy {
	}
	phy2:usb_phy {
	};
};


usb@xx {
	compatible = "";
	phys = <&phy1>; //doesn't need xlate
	/* this needs xlate
	   phys = <&phy 1>;
	*/
	phy-names = "phy";
};

Thanks
Kishon
LiYunzhi Dec. 11, 2014, 1:45 p.m. UTC | #2
Hi Kishon:

On 2014/12/11 18:27, Kishon Vijay Abraham I wrote:
> Hi,
>
> On Thursday 11 December 2014 03:25 PM, Yunzhi Li wrote:
>> +
>> +static struct phy *rockchip_usb_phy_xlate(struct device *dev,
>> +					struct of_phandle_args *args)
>> +{
>> +	struct rockchip_usb_phy_priv *priv = dev_get_drvdata(dev);
>> +	unsigned int phy_id = args->args[0];
>> +
>> +	if (WARN_ON(phy_id < 0 || phy_id >= priv->nphys))
>> +		return ERR_PTR(-ENODEV);
>> +
>> +	return priv->phys[phy_id].phy;
> I didn't mean that. You can get rid of this entire xlate stuff if you use
> something like below
>
> phy@xxx {
> 	compatible = "";
> 	phy1:usb_phy {
> 	}
> 	phy2:usb_phy {
> 	};
> };
>
>
> usb@xx {
> 	compatible = "";
> 	phys = <&phy1>; //doesn't need xlate
> 	/* this needs xlate
> 	   phys = <&phy 1>;
> 	*/
> 	phy-names = "phy";
> };

Thank you so much for your suggestion, but still have a question:
I have to add the #phy-cells property in each phy sub-node, otherwise 
devm_get_phy() will fail and I get log info like
"/usb@ff500000: could not get #phy-cells for /phy/usbp-phy1". So can the 
#phy-cells property defines in patent node
also valid for it's child nodes like #address-cells ?

---
Yunzhi Li @ rockchip
Doug Anderson Dec. 11, 2014, 6:09 p.m. UTC | #3
Yunzhi,

On Thu, Dec 11, 2014 at 1:55 AM, Yunzhi Li <lyz@rock-chips.com> wrote:
> +               rk_phy->clk = of_clk_get(child, 0);
> +               if (IS_ERR(rk_phy->clk)) {
> +                       dev_warn(dev, "failed to get clock\n");
> +                       rk_phy->clk = NULL;
> +               }

The device tree bindings don't specify a clock and the "dtsi" added to
rk3288 don't reference a clock.  Take that code out and avoid a
warning in the logs at bootup.

...or should there be a clock?


> +               rk_phy->phy = devm_phy_create(dev, NULL, &ops);

This has the wrong number of arguments.  Even before the change that
added the 4th argument, this is still wrong because "ops" is supposed
to be the 2nd argument, not the 3rd.

...so I'm confused how this compiled for you.  I think this ought to be:

rk_phy->phy = devm_phy_create(dev, child, &ops, NULL);

...but please correct me if I'm mistaken!
Doug Anderson Dec. 11, 2014, 6:41 p.m. UTC | #4
Kishon,

On Thu, Dec 11, 2014 at 2:27 AM, Kishon Vijay Abraham I <kishon@ti.com> wrote:
> I didn't mean that. You can get rid of this entire xlate stuff if you use
> something like below
>
> phy@xxx {
>         compatible = "";
>         phy1:usb_phy {
>         }
>         phy2:usb_phy {
>         };
> };
>
>
> usb@xx {
>         compatible = "";
>         phys = <&phy1>; //doesn't need xlate
>         /* this needs xlate
>            phys = <&phy 1>;
>         */
>         phy-names = "phy";
> };

Is the syntax you proposed really better?  Are you saying that you
advocate never using "#phy-cells" other than 0 for new bindings?  Is
that your own personal preference, or is there a discussion somewhere
where everyone agreed on this?

My vote is that since "phy-cells" exists and is part of the generic
phy bindings that it's meant to be used whenever you have a single PHY
driver that controls multiple PHYs.


-Doug
Doug Anderson Dec. 12, 2014, 3:05 a.m. UTC | #5
Yunzhi,

On Thu, Dec 11, 2014 at 10:09 AM, Doug Anderson <dianders@chromium.org> wrote:
>> +               rk_phy->phy = devm_phy_create(dev, NULL, &ops);
>
> This has the wrong number of arguments.  Even before the change that
> added the 4th argument, this is still wrong because "ops" is supposed
> to be the 2nd argument, not the 3rd.
>
> ...so I'm confused how this compiled for you.  I think this ought to be:
>
> rk_phy->phy = devm_phy_create(dev, child, &ops, NULL);
>
> ...but please correct me if I'm mistaken!

As you pointed out privately, I didn't have (dbc9863 phy: remove the
old lookup method).  Sorry about the noise..

Hopefully my other comments are not quite as stupid...

-Doug
LiYunzhi Dec. 12, 2014, 3:43 a.m. UTC | #6
Hi Doug:

On 2014/12/12 2:09, Doug Anderson wrote:
> Yunzhi,
>
> On Thu, Dec 11, 2014 at 1:55 AM, Yunzhi Li <lyz@rock-chips.com> wrote:
>> +               rk_phy->clk = of_clk_get(child, 0);
>> +               if (IS_ERR(rk_phy->clk)) {
>> +                       dev_warn(dev, "failed to get clock\n");
>> +                       rk_phy->clk = NULL;
>> +               }
> The device tree bindings don't specify a clock and the "dtsi" added to
> rk3288 don't reference a clock.  Take that code out and avoid a
> warning in the logs at bootup.
>
> ...or should there be a clock?
Actually, there is a clk gating control bit in CRU for each usb phy and I
think we should manage these clocks by the usb phy driver, so I will add
clock property to usb PHYs nodes in next version of patche set.

>
>> +               rk_phy->phy = devm_phy_create(dev, NULL, &ops);
> This has the wrong number of arguments.  Even before the change that
> added the 4th argument, this is still wrong because "ops" is supposed
> to be the 2nd argument, not the 3rd.
>
> ...so I'm confused how this compiled for you.  I think this ought to be:
>
> rk_phy->phy = devm_phy_create(dev, child, &ops, NULL);
>
> ...but please correct me if I'm mistaken!
>
>
>
Kishon Vijay Abraham I Dec. 12, 2014, 5:34 a.m. UTC | #7
Hi,

On Thursday 11 December 2014 07:15 PM, Yunzhi Li wrote:
> Hi Kishon:
> 
> On 2014/12/11 18:27, Kishon Vijay Abraham I wrote:
>> Hi,
>>
>> On Thursday 11 December 2014 03:25 PM, Yunzhi Li wrote:
>>> +
>>> +static struct phy *rockchip_usb_phy_xlate(struct device *dev,
>>> +                    struct of_phandle_args *args)
>>> +{
>>> +    struct rockchip_usb_phy_priv *priv = dev_get_drvdata(dev);
>>> +    unsigned int phy_id = args->args[0];
>>> +
>>> +    if (WARN_ON(phy_id < 0 || phy_id >= priv->nphys))
>>> +        return ERR_PTR(-ENODEV);
>>> +
>>> +    return priv->phys[phy_id].phy;
>> I didn't mean that. You can get rid of this entire xlate stuff if you use
>> something like below
>>
>> phy@xxx {
>>     compatible = "";
>>     phy1:usb_phy {
>>     }
>>     phy2:usb_phy {
>>     };
>> };
>>
>>
>> usb@xx {
>>     compatible = "";
>>     phys = <&phy1>; //doesn't need xlate
>>     /* this needs xlate
>>        phys = <&phy 1>;
>>     */
>>     phy-names = "phy";
>> };
> 
> Thank you so much for your suggestion, but still have a question:
> I have to add the #phy-cells property in each phy sub-node, otherwise
> devm_get_phy() will fail and I get log info like
> "/usb@ff500000: could not get #phy-cells for /phy/usbp-phy1". So can the
> #phy-cells property defines in patent node
> also valid for it's child nodes like #address-cells ?

No. You have to add #phy-cells property for every PHY node.

Thanks
Kishon
Kishon Vijay Abraham I Dec. 12, 2014, 5:47 a.m. UTC | #8
Hi,

On Friday 12 December 2014 12:11 AM, Doug Anderson wrote:
> Kishon,
> 
> On Thu, Dec 11, 2014 at 2:27 AM, Kishon Vijay Abraham I <kishon@ti.com> wrote:
>> I didn't mean that. You can get rid of this entire xlate stuff if you use
>> something like below
>>
>> phy@xxx {
>>         compatible = "";
>>         phy1:usb_phy {
>>         }
>>         phy2:usb_phy {
>>         };
>> };
>>
>>
>> usb@xx {
>>         compatible = "";
>>         phys = <&phy1>; //doesn't need xlate
>>         /* this needs xlate
>>            phys = <&phy 1>;
>>         */
>>         phy-names = "phy";
>> };
> 
> Is the syntax you proposed really better?  Are you saying that you
> advocate never using "#phy-cells" other than 0 for new bindings?  Is

No. It can still be used for configuring the PHY. For example, in the case of
PIPE3 PHY we configure it to USB PHY, SATA PHY or PCIE PHY depending on to
which controller the PHY is connected to.

I feel using phy-cells just for differentiating the PHY is pointless when you
have a separate node for each PHY.

Thanks
Kishon

Patch
diff mbox

diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
index ccad880..b24500a 100644
--- a/drivers/phy/Kconfig
+++ b/drivers/phy/Kconfig
@@ -239,6 +239,13 @@  config PHY_QCOM_IPQ806X_SATA
 	depends on OF
 	select GENERIC_PHY
 
+config PHY_ROCKCHIP_USB
+	tristate "Rockchip USB2 PHY Driver"
+	depends on ARCH_ROCKCHIP && OF
+	select GENERIC_PHY
+	help
+	  Enable this to support the Rockchip USB 2.0 PHY.
+
 config PHY_ST_SPEAR1310_MIPHY
 	tristate "ST SPEAR1310-MIPHY driver"
 	select GENERIC_PHY
diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
index aa74f96..48bf5a1 100644
--- a/drivers/phy/Makefile
+++ b/drivers/phy/Makefile
@@ -28,6 +28,7 @@  phy-exynos-usb2-$(CONFIG_PHY_EXYNOS5250_USB2)	+= phy-exynos5250-usb2.o
 phy-exynos-usb2-$(CONFIG_PHY_S5PV210_USB2)	+= phy-s5pv210-usb2.o
 obj-$(CONFIG_PHY_EXYNOS5_USBDRD)	+= phy-exynos5-usbdrd.o
 obj-$(CONFIG_PHY_QCOM_APQ8064_SATA)	+= phy-qcom-apq8064-sata.o
+obj-$(CONFIG_PHY_ROCKCHIP_USB) += phy-rockchip-usb.o
 obj-$(CONFIG_PHY_QCOM_IPQ806X_SATA)	+= phy-qcom-ipq806x-sata.o
 obj-$(CONFIG_PHY_ST_SPEAR1310_MIPHY)	+= phy-spear1310-miphy.o
 obj-$(CONFIG_PHY_ST_SPEAR1340_MIPHY)	+= phy-spear1340-miphy.o
diff --git a/drivers/phy/phy-rockchip-usb.c b/drivers/phy/phy-rockchip-usb.c
new file mode 100644
index 0000000..dad5194
--- /dev/null
+++ b/drivers/phy/phy-rockchip-usb.c
@@ -0,0 +1,198 @@ 
+/*
+ * Rockchip usb PHY driver
+ *
+ * Copyright (C) 2014 Yunzhi Li <lyz@rock-chips.com>
+ * Copyright (C) 2014 ROCKCHIP, Inc.
+ *
+ * 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.
+ *
+ * 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.
+ */
+
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/regmap.h>
+#include <linux/mfd/syscon.h>
+
+#define ROCKCHIP_RK3288_UOC(n)	(0x320 + n * 0x14)
+
+/*
+ * The higher 16-bit of this register is used for write protection
+ * only if BIT(13 + 16) set to 1 the BIT(13) can be written.
+ */
+#define SIDDQ_WRITE_ENA	BIT(29)
+#define SIDDQ_ON		BIT(13)
+#define SIDDQ_OFF		(0 << 13)
+
+struct rockchip_usb_phy {
+	struct regmap	*reg_base;
+	unsigned int	reg_offset;
+	struct clk	*clk;
+	struct phy	*phy;
+};
+
+struct rockchip_usb_phy_priv {
+	struct rockchip_usb_phy	*phys;
+	unsigned		nphys;
+};
+
+static int rockchip_usb_phy_power(struct rockchip_usb_phy *phy,
+					   bool siddq)
+{
+	return regmap_write(phy->reg_base, phy->reg_offset,
+			    SIDDQ_WRITE_ENA | (siddq ? SIDDQ_ON : SIDDQ_OFF));
+}
+
+static int rockchip_usb_phy_power_off(struct phy *_phy)
+{
+	struct rockchip_usb_phy *phy = phy_get_drvdata(_phy);
+	int ret = 0;
+
+	/* Power down usb phy analog blocks by set siddq 1 */
+	ret = rockchip_usb_phy_power(phy, 1);
+	if (ret)
+		return ret;
+
+	clk_disable_unprepare(phy->clk);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static int rockchip_usb_phy_power_on(struct phy *_phy)
+{
+	struct rockchip_usb_phy *phy = phy_get_drvdata(_phy);
+	int ret = 0;
+
+	ret = clk_prepare_enable(phy->clk);
+	if (ret)
+		return ret;
+
+	/* Power up usb phy analog blocks by set siddq 0 */
+	ret = rockchip_usb_phy_power(phy, 0);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static struct phy *rockchip_usb_phy_xlate(struct device *dev,
+					struct of_phandle_args *args)
+{
+	struct rockchip_usb_phy_priv *priv = dev_get_drvdata(dev);
+	unsigned int phy_id = args->args[0];
+
+	if (WARN_ON(phy_id < 0 || phy_id >= priv->nphys))
+		return ERR_PTR(-ENODEV);
+
+	return priv->phys[phy_id].phy;
+}
+
+static struct phy_ops ops = {
+	.power_on	= rockchip_usb_phy_power_on,
+	.power_off	= rockchip_usb_phy_power_off,
+	.owner		= THIS_MODULE,
+};
+
+static int rockchip_usb_phy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct rockchip_usb_phy *rk_phy;
+	struct rockchip_usb_phy_priv *priv;
+	struct phy_provider *phy_provider;
+	struct device_node *child;
+	struct regmap *grf;
+	unsigned int phy_id;
+
+	grf = syscon_regmap_lookup_by_phandle(dev->of_node, "rockchip,grf");
+	if (IS_ERR(grf)) {
+		dev_err(&pdev->dev, "Missing rockchip,grf property\n");
+		return PTR_ERR(grf);
+	}
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	/* Get number of phys from device tree */
+	priv->nphys = of_get_child_count(dev->of_node);
+	if (priv->nphys == 0)
+		return -ENODEV;
+
+	priv->phys = devm_kzalloc(dev, priv->nphys * sizeof(*priv->phys),
+				  GFP_KERNEL);
+	if (!priv->phys)
+		return -ENOMEM;
+
+	for_each_available_child_of_node(dev->of_node, child) {
+		if (of_property_read_u32(child, "reg", &phy_id)) {
+			dev_err(dev, "missing reg property in node %s\n",
+				child->name);
+			return -EINVAL;
+		}
+
+		if (phy_id < 0 || phy_id >= priv->nphys) {
+			dev_err(dev, "invalid phy id\n");
+			return -EINVAL;
+		}
+		rk_phy = &priv->phys[phy_id];
+		rk_phy->reg_offset = ROCKCHIP_RK3288_UOC(phy_id);
+		rk_phy->reg_base = grf;
+
+		rk_phy->clk = of_clk_get(child, 0);
+		if (IS_ERR(rk_phy->clk)) {
+			dev_warn(dev, "failed to get clock\n");
+			rk_phy->clk = NULL;
+		}
+
+		rk_phy->phy = devm_phy_create(dev, NULL, &ops);
+		if (IS_ERR(rk_phy->phy)) {
+			dev_err(dev, "failed to create PHY %d\n", phy_id);
+			return PTR_ERR(rk_phy->phy);
+		}
+		phy_set_drvdata(rk_phy->phy, rk_phy);
+	}
+
+	platform_set_drvdata(pdev, priv);
+
+	phy_provider = devm_of_phy_provider_register(dev,
+						     rockchip_usb_phy_xlate);
+	return PTR_ERR_OR_ZERO(phy_provider);
+}
+
+static const struct of_device_id rockchip_usb_phy_dt_ids[] = {
+	{ .compatible = "rockchip,rk3288-usb-phy" },
+	{}
+};
+
+MODULE_DEVICE_TABLE(of, rockchip_usb_phy_dt_ids);
+
+static struct platform_driver rockchip_usb_driver = {
+	.probe		= rockchip_usb_phy_probe,
+	.driver		= {
+		.name	= "rockchip-usb-phy",
+		.owner	= THIS_MODULE,
+		.of_match_table = rockchip_usb_phy_dt_ids,
+	},
+};
+
+module_platform_driver(rockchip_usb_driver);
+
+MODULE_AUTHOR("Yunzhi Li <lyz@rock-chips.com>");
+MODULE_DESCRIPTION("Rockchip USB 2.0 PHY driver");
+MODULE_LICENSE("GPL v2");