[PATCH 1/9] phy: samsung: Add Exynos USB DRD PHY driver
Mattijs Korpershoek
mkorpershoek at kernel.org
Thu Jul 17 08:58:40 CEST 2025
Hi Sam,
Thank you for the patch.
On Wed, Jul 09, 2025 at 17:29, Sam Protsenko <semen.protsenko at linaro.org> wrote:
> Add DM driver for Exynos USB PHY controllers. For now it only supports
> Exynos850 SoC. Only UTMI+ (USB 2.0) PHY interface is implemented, as
> Exynos850 doesn't support USB 3.0. Only two clocks are used for this
> controller:
> - phy: bus clock, used for PHY registers access
> - ref: PHY reference clock (OSCCLK)
>
> Ported from Linux kernel: drivers/phy/samsung/phy-exynos5-usbdrd.c
>
> Signed-off-by: Sam Protsenko <semen.protsenko at linaro.org>
> ---
> MAINTAINERS | 1 +
> drivers/phy/Kconfig | 9 +
> drivers/phy/Makefile | 1 +
> drivers/phy/phy-exynos-usbdrd.c | 386 ++++++++++++++++++++++++++++++++
> 4 files changed, 397 insertions(+)
> create mode 100644 drivers/phy/phy-exynos-usbdrd.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index d5264c8f5df6..fda0811d1500 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -602,6 +602,7 @@ ARM SAMSUNG EXYNOS850 SOC
> M: Sam Protsenko <semen.protsenko at linaro.org>
> S: Maintained
> F: drivers/clk/exynos/clk-exynos850.c
> +F: drivers/phy/phy-exynos-usbdrd.c
> F: drivers/pinctrl/exynos/pinctrl-exynos850.c
>
> ARM SAMSUNG SOC DRIVERS
> diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
> index d3fe90d939e8..c297fa03ea7f 100644
> --- a/drivers/phy/Kconfig
> +++ b/drivers/phy/Kconfig
> @@ -259,6 +259,15 @@ config MT76X8_USB_PHY
>
> This PHY is found on MT76x8 devices supporting USB.
>
> +config PHY_EXYNOS_USBDRD
> + bool "Exynos SoC series USB DRD PHY driver"
> + depends on PHY && CLK
> + depends on ARCH_EXYNOS
> + select REGMAP
> + select SYSCON
> + help
> + Enable USB DRD PHY support for Exynos SoC series.
> +
> config PHY_MTK_TPHY
> bool "MediaTek T-PHY Driver"
> depends on PHY
> diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
> index b4d01fc700da..98c1ef8683b7 100644
> --- a/drivers/phy/Makefile
> +++ b/drivers/phy/Makefile
> @@ -35,6 +35,7 @@ obj-$(CONFIG_KEYSTONE_USB_PHY) += keystone-usb-phy.o
> obj-$(CONFIG_MT7620_USB_PHY) += mt7620-usb-phy.o
> obj-$(CONFIG_MT76X8_USB_PHY) += mt76x8-usb-phy.o
> obj-$(CONFIG_PHY_DA8XX_USB) += phy-da8xx-usb.o
> +obj-$(CONFIG_PHY_EXYNOS_USBDRD) += phy-exynos-usbdrd.o
> obj-$(CONFIG_PHY_MTK_TPHY) += phy-mtk-tphy.o
> obj-$(CONFIG_PHY_NPCM_USB) += phy-npcm-usb.o
> obj-$(CONFIG_PHY_IMX8MQ_USB) += phy-imx8mq-usb.o
> diff --git a/drivers/phy/phy-exynos-usbdrd.c b/drivers/phy/phy-exynos-usbdrd.c
> new file mode 100644
> index 000000000000..db5815ed1840
> --- /dev/null
> +++ b/drivers/phy/phy-exynos-usbdrd.c
> @@ -0,0 +1,386 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2025 Linaro Ltd.
> + * Sam Protsenko <semen.protsenko at linaro.org>
> + *
> + * Samsung Exynos SoC series USB DRD PHY driver.
> + * Based on Linux kernel PHY driver: drivers/phy/samsung/phy-exynos5-usbdrd.c
> + */
> +
> +#include <clk.h>
> +#include <dm.h>
> +#include <generic-phy.h>
> +#include <regmap.h>
> +#include <syscon.h>
> +#include <asm/io.h>
> +#include <dm/device_compat.h>
> +#include <linux/bitfield.h>
> +#include <linux/bitops.h>
> +#include <linux/delay.h>
> +
> +/* Offset of PMU register controlling USB PHY output isolation */
> +#define EXYNOS_USBDRD_PHY_CONTROL 0x0704
> +#define EXYNOS_PHY_ENABLE BIT(0)
> +
> +/* Exynos USB PHY registers */
> +#define EXYNOS5_FSEL_9MHZ6 0x0
> +#define EXYNOS5_FSEL_10MHZ 0x1
> +#define EXYNOS5_FSEL_12MHZ 0x2
> +#define EXYNOS5_FSEL_19MHZ2 0x3
> +#define EXYNOS5_FSEL_20MHZ 0x4
> +#define EXYNOS5_FSEL_24MHZ 0x5
> +#define EXYNOS5_FSEL_26MHZ 0x6
> +#define EXYNOS5_FSEL_50MHZ 0x7
> +
> +/* Exynos850: USB DRD PHY registers */
> +#define EXYNOS850_DRD_LINKCTRL 0x04
> +#define LINKCTRL_FORCE_QACT BIT(8)
> +#define LINKCTRL_BUS_FILTER_BYPASS GENMASK(7, 4)
> +
> +#define EXYNOS850_DRD_CLKRST 0x20
> +#define CLKRST_LINK_SW_RST BIT(0)
> +#define CLKRST_PORT_RST BIT(1)
> +#define CLKRST_PHY_SW_RST BIT(3)
> +
> +#define EXYNOS850_DRD_SSPPLLCTL 0x30
> +#define SSPPLLCTL_FSEL GENMASK(2, 0)
> +
> +#define EXYNOS850_DRD_UTMI 0x50
> +#define UTMI_FORCE_SLEEP BIT(0)
> +#define UTMI_FORCE_SUSPEND BIT(1)
> +#define UTMI_DM_PULLDOWN BIT(2)
> +#define UTMI_DP_PULLDOWN BIT(3)
> +#define UTMI_FORCE_BVALID BIT(4)
> +#define UTMI_FORCE_VBUSVALID BIT(5)
Comparing with the linux driver using
commit cc52a697f87e ("phy: exynos5-usbdrd: support Exynos USBDRD 3.2 4nm controller")
I notice that the defines are in reverse order (from 0 to 5 and linux
has from 5 to 0).
Is there any particular reason for this?
I don't mind it too much but it makes diffing between linux and U-Boot a
bit harder.
Anyway, I've compared this with the linux driver and it looks good to
me!
Reviewed-by: Mattijs Korpershoek <mkorpershoek at kernel.org>
> +
> +#define EXYNOS850_DRD_HSP 0x54
> +#define HSP_COMMONONN BIT(8)
> +#define HSP_EN_UTMISUSPEND BIT(9)
> +#define HSP_VBUSVLDEXT BIT(12)
> +#define HSP_VBUSVLDEXTSEL BIT(13)
> +#define HSP_FSV_OUT_EN BIT(24)
> +
> +#define EXYNOS850_DRD_HSP_TEST 0x5c
> +#define HSP_TEST_SIDDQ BIT(24)
> +
> +#define KHZ 1000
> +#define MHZ (KHZ * KHZ)
> +
> +/**
> + * struct exynos_usbdrd_phy - driver data for Exynos USB PHY
> + * @reg_phy: USB PHY controller register memory base
> + * @clk: clock for register access
> + * @core_clk: core clock for phy (ref clock)
> + * @reg_pmu: regmap for PMU block
> + * @extrefclk: frequency select settings when using 'separate reference clocks'
> + */
> +struct exynos_usbdrd_phy {
> + void __iomem *reg_phy;
> + struct clk *clk;
> + struct clk *core_clk;
> + struct regmap *reg_pmu;
> + u32 extrefclk;
> +};
> +
> +static void exynos_usbdrd_phy_isol(struct regmap *reg_pmu, bool isolate)
> +{
> + unsigned int val;
> +
> + if (!reg_pmu)
> + return;
> +
> + val = isolate ? 0 : EXYNOS_PHY_ENABLE;
> + regmap_update_bits(reg_pmu, EXYNOS_USBDRD_PHY_CONTROL,
> + EXYNOS_PHY_ENABLE, val);
> +}
> +
> +/*
> + * Convert the supplied clock rate to the value that can be written to the PHY
> + * register.
> + */
> +static unsigned int exynos_rate_to_clk(unsigned long rate, u32 *reg)
> +{
> + switch (rate) {
> + case 9600 * KHZ:
> + *reg = EXYNOS5_FSEL_9MHZ6;
> + break;
> + case 10 * MHZ:
> + *reg = EXYNOS5_FSEL_10MHZ;
> + break;
> + case 12 * MHZ:
> + *reg = EXYNOS5_FSEL_12MHZ;
> + break;
> + case 19200 * KHZ:
> + *reg = EXYNOS5_FSEL_19MHZ2;
> + break;
> + case 20 * MHZ:
> + *reg = EXYNOS5_FSEL_20MHZ;
> + break;
> + case 24 * MHZ:
> + *reg = EXYNOS5_FSEL_24MHZ;
> + break;
> + case 26 * MHZ:
> + *reg = EXYNOS5_FSEL_26MHZ;
> + break;
> + case 50 * MHZ:
> + *reg = EXYNOS5_FSEL_50MHZ;
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static void exynos850_usbdrd_utmi_init(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> + void __iomem *regs_base = phy_drd->reg_phy;
> + u32 reg;
> +
> + /*
> + * Disable HWACG (hardware auto clock gating control). This will force
> + * QACTIVE signal in Q-Channel interface to HIGH level, to make sure
> + * the PHY clock is not gated by the hardware.
> + */
> + reg = readl(regs_base + EXYNOS850_DRD_LINKCTRL);
> + reg |= LINKCTRL_FORCE_QACT;
> + writel(reg, regs_base + EXYNOS850_DRD_LINKCTRL);
> +
> + /* Start PHY Reset (POR=high) */
> + reg = readl(regs_base + EXYNOS850_DRD_CLKRST);
> + reg |= CLKRST_PHY_SW_RST;
> + writel(reg, regs_base + EXYNOS850_DRD_CLKRST);
> +
> + /* Enable UTMI+ */
> + reg = readl(regs_base + EXYNOS850_DRD_UTMI);
> + reg &= ~(UTMI_FORCE_SUSPEND | UTMI_FORCE_SLEEP | UTMI_DP_PULLDOWN |
> + UTMI_DM_PULLDOWN);
> + writel(reg, regs_base + EXYNOS850_DRD_UTMI);
> +
> + /* Set PHY clock and control HS PHY */
> + reg = readl(regs_base + EXYNOS850_DRD_HSP);
> + reg |= HSP_EN_UTMISUSPEND | HSP_COMMONONN;
> + writel(reg, regs_base + EXYNOS850_DRD_HSP);
> +
> + /* Set VBUS Valid and D+ pull-up control by VBUS pad usage */
> + reg = readl(regs_base + EXYNOS850_DRD_LINKCTRL);
> + reg |= FIELD_PREP(LINKCTRL_BUS_FILTER_BYPASS, 0xf);
> + writel(reg, regs_base + EXYNOS850_DRD_LINKCTRL);
> +
> + reg = readl(regs_base + EXYNOS850_DRD_UTMI);
> + reg |= UTMI_FORCE_BVALID | UTMI_FORCE_VBUSVALID;
> + writel(reg, regs_base + EXYNOS850_DRD_UTMI);
> +
> + reg = readl(regs_base + EXYNOS850_DRD_HSP);
> + reg |= HSP_VBUSVLDEXT | HSP_VBUSVLDEXTSEL;
> + writel(reg, regs_base + EXYNOS850_DRD_HSP);
> +
> + reg = readl(regs_base + EXYNOS850_DRD_SSPPLLCTL);
> + reg &= ~SSPPLLCTL_FSEL;
> + switch (phy_drd->extrefclk) {
> + case EXYNOS5_FSEL_50MHZ:
> + reg |= FIELD_PREP(SSPPLLCTL_FSEL, 7);
> + break;
> + case EXYNOS5_FSEL_26MHZ:
> + reg |= FIELD_PREP(SSPPLLCTL_FSEL, 6);
> + break;
> + case EXYNOS5_FSEL_24MHZ:
> + reg |= FIELD_PREP(SSPPLLCTL_FSEL, 2);
> + break;
> + case EXYNOS5_FSEL_20MHZ:
> + reg |= FIELD_PREP(SSPPLLCTL_FSEL, 1);
> + break;
> + case EXYNOS5_FSEL_19MHZ2:
> + reg |= FIELD_PREP(SSPPLLCTL_FSEL, 0);
> + break;
> + default:
> + dev_warn(phy->dev, "unsupported ref clk: %#.2x\n",
> + phy_drd->extrefclk);
> + break;
> + }
> + writel(reg, regs_base + EXYNOS850_DRD_SSPPLLCTL);
> +
> + /* Power up PHY analog blocks */
> + reg = readl(regs_base + EXYNOS850_DRD_HSP_TEST);
> + reg &= ~HSP_TEST_SIDDQ;
> + writel(reg, regs_base + EXYNOS850_DRD_HSP_TEST);
> +
> + /* Finish PHY reset (POR=low) */
> + udelay(10); /* required before doing POR=low */
> + reg = readl(regs_base + EXYNOS850_DRD_CLKRST);
> + reg &= ~(CLKRST_PHY_SW_RST | CLKRST_PORT_RST);
> + writel(reg, regs_base + EXYNOS850_DRD_CLKRST);
> + udelay(75); /* required after POR=low for guaranteed PHY clock */
> +
> + /* Disable single ended signal out */
> + reg = readl(regs_base + EXYNOS850_DRD_HSP);
> + reg &= ~HSP_FSV_OUT_EN;
> + writel(reg, regs_base + EXYNOS850_DRD_HSP);
> +}
> +
> +static void exynos850_usbdrd_utmi_exit(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> + void __iomem *regs_base = phy_drd->reg_phy;
> + u32 reg;
> +
> + /* Set PHY clock and control HS PHY */
> + reg = readl(regs_base + EXYNOS850_DRD_UTMI);
> + reg &= ~(UTMI_DP_PULLDOWN | UTMI_DM_PULLDOWN);
> + reg |= UTMI_FORCE_SUSPEND | UTMI_FORCE_SLEEP;
> + writel(reg, regs_base + EXYNOS850_DRD_UTMI);
> +
> + /* Power down PHY analog blocks */
> + reg = readl(regs_base + EXYNOS850_DRD_HSP_TEST);
> + reg |= HSP_TEST_SIDDQ;
> + writel(reg, regs_base + EXYNOS850_DRD_HSP_TEST);
> +
> + /* Link reset */
> + reg = readl(regs_base + EXYNOS850_DRD_CLKRST);
> + reg |= CLKRST_LINK_SW_RST;
> + writel(reg, regs_base + EXYNOS850_DRD_CLKRST);
> + udelay(10); /* required before doing POR=low */
> + reg &= ~CLKRST_LINK_SW_RST;
> + writel(reg, regs_base + EXYNOS850_DRD_CLKRST);
> +}
> +
> +static int exynos_usbdrd_phy_init(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> + int ret;
> +
> + ret = clk_prepare_enable(phy_drd->clk);
> + if (ret)
> + return ret;
> +
> + exynos850_usbdrd_utmi_init(phy);
> +
> + clk_disable_unprepare(phy_drd->clk);
> +
> + return 0;
> +}
> +
> +static int exynos_usbdrd_phy_exit(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> + int ret;
> +
> + ret = clk_prepare_enable(phy_drd->clk);
> + if (ret)
> + return ret;
> +
> + exynos850_usbdrd_utmi_exit(phy);
> +
> + clk_disable_unprepare(phy_drd->clk);
> +
> + return 0;
> +}
> +
> +static int exynos_usbdrd_phy_power_on(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> + int ret;
> +
> + dev_dbg(phy->dev, "Request to power_on usbdrd_phy phy\n");
> +
> + ret = clk_prepare_enable(phy_drd->core_clk);
> + if (ret)
> + return ret;
> +
> + /* Power-on PHY */
> + exynos_usbdrd_phy_isol(phy_drd->reg_pmu, false);
> +
> + return 0;
> +}
> +
> +static int exynos_usbdrd_phy_power_off(struct phy *phy)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(phy->dev);
> +
> + dev_dbg(phy->dev, "Request to power_off usbdrd_phy phy\n");
> +
> + /* Power-off the PHY */
> + exynos_usbdrd_phy_isol(phy_drd->reg_pmu, true);
> +
> + clk_disable_unprepare(phy_drd->core_clk);
> +
> + return 0;
> +}
> +
> +static int exynos_usbdrd_phy_init_clk(struct udevice *dev)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(dev);
> + unsigned long ref_rate;
> + int err;
> +
> + phy_drd->clk = devm_clk_get(dev, "phy");
> + if (IS_ERR(phy_drd->clk)) {
> + err = PTR_ERR(phy_drd->clk);
> + dev_err(dev, "Failed to get phy clock (err=%d)\n", err);
> + return err;
> + }
> +
> + phy_drd->core_clk = devm_clk_get(dev, "ref");
> + if (IS_ERR(phy_drd->core_clk)) {
> + err = PTR_ERR(phy_drd->core_clk);
> + dev_err(dev, "Failed to get ref clock (err=%d)\n", err);
> + return err;
> + }
> +
> + ref_rate = clk_get_rate(phy_drd->core_clk);
> + err = exynos_rate_to_clk(ref_rate, &phy_drd->extrefclk);
> + if (err) {
> + dev_err(dev, "Clock rate %lu not supported\n", ref_rate);
> + return err;
> + }
> +
> + return 0;
> +}
> +
> +static int exynos_usbdrd_phy_probe(struct udevice *dev)
> +{
> + struct exynos_usbdrd_phy *phy_drd = dev_get_priv(dev);
> + int err;
> +
> + phy_drd->reg_phy = dev_read_addr_ptr(dev);
> + if (!phy_drd->reg_phy)
> + return -EINVAL;
> +
> + err = exynos_usbdrd_phy_init_clk(dev);
> + if (err)
> + return err;
> +
> + phy_drd->reg_pmu = syscon_regmap_lookup_by_phandle(dev,
> + "samsung,pmu-syscon");
> + if (IS_ERR(phy_drd->reg_pmu)) {
> + err = PTR_ERR(phy_drd->reg_pmu);
> + dev_err(dev, "Failed to lookup PMU regmap\n");
> + return err;
> + }
> +
> + return 0;
> +}
> +
> +static const struct phy_ops exynos_usbdrd_phy_ops = {
> + .init = exynos_usbdrd_phy_init,
> + .exit = exynos_usbdrd_phy_exit,
> + .power_on = exynos_usbdrd_phy_power_on,
> + .power_off = exynos_usbdrd_phy_power_off,
> +};
> +
> +static const struct udevice_id exynos_usbdrd_phy_of_match[] = {
> + {
> + .compatible = "samsung,exynos850-usbdrd-phy",
> + },
> + { }
> +};
> +
> +U_BOOT_DRIVER(exynos_usbdrd_phy) = {
> + .name = "exynos-usbdrd-phy",
> + .id = UCLASS_PHY,
> + .of_match = exynos_usbdrd_phy_of_match,
> + .probe = exynos_usbdrd_phy_probe,
> + .ops = &exynos_usbdrd_phy_ops,
> + .priv_auto = sizeof(struct exynos_usbdrd_phy),
> +};
> --
> 2.39.5
More information about the U-Boot
mailing list