// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2025 Linaro Ltd. * Sam Protsenko * * Samsung Exynos SoC series USB DRD PHY driver. * Based on Linux kernel PHY driver: drivers/phy/samsung/phy-exynos5-usbdrd.c */ #include #include #include #include #include #include #include #include #include #include /* 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) #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), };