| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Axiado eMMC PHY driver |
| * |
| * Copyright (C) 2017 Arasan Chip Systems Inc. |
| * Copyright (C) 2022-2026 Axiado Corporation (or its affiliates). |
| * |
| * Based on Arasan Driver (sdhci-pci-arasan.c) |
| * sdhci-pci-arasan.c - Driver for Arasan PCI Controller with integrated phy. |
| */ |
| #include <linux/bitfield.h> |
| #include <linux/delay.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/phy/phy.h> |
| #include <linux/platform_device.h> |
| |
| /* Arasan eMMC 5.1 - PHY configuration registers */ |
| #define CAP_REG_IN_S1_MSB 0x04 |
| #define PHY_CTRL_1 0x38 |
| #define PHY_CTRL_2 0x3c |
| #define PHY_CTRL_3 0x40 |
| #define STATUS 0x50 |
| |
| #define DLL_ENBL BIT(26) |
| #define RTRIM_EN BIT(21) |
| #define PDB_ENBL BIT(23) |
| #define RETB_ENBL BIT(1) |
| |
| #define REN_STRB BIT(27) |
| #define REN_CMD_EN GENMASK(20, 12) |
| |
| /* Pull-UP Enable on CMD Line */ |
| #define PU_CMD_EN GENMASK(11, 3) |
| |
| /* Selection value for the optimum delay from 1-32 output tap lines */ |
| #define OTAP_DLY 0x02 |
| /* DLL charge pump current trim default [1000] */ |
| #define DLL_TRM_ICP 0x08 |
| /* Select the frequency range of DLL Operation */ |
| #define FRQ_SEL 0x01 |
| |
| #define OTAP_SEL_MASK GENMASK(10, 7) |
| #define DLL_TRM_MASK GENMASK(25, 22) |
| #define DLL_FRQSEL_MASK GENMASK(27, 25) |
| |
| #define OTAP_SEL(x) (FIELD_PREP(OTAP_SEL_MASK, x) | OTAPDLY_EN) |
| #define DLL_TRM(x) (FIELD_PREP(DLL_TRM_MASK, x) | DLL_ENBL) |
| #define DLL_FRQSEL(x) FIELD_PREP(DLL_FRQSEL_MASK, x) |
| |
| #define OTAPDLY_EN BIT(11) |
| |
| #define SEL_DLY_RXCLK BIT(18) |
| #define SEL_DLY_TXCLK BIT(19) |
| |
| #define CALDONE_MASK 0x40 |
| #define DLL_RDY_MASK 0x1 |
| #define MAX_CLK_BUF0 BIT(20) |
| #define MAX_CLK_BUF1 BIT(21) |
| #define MAX_CLK_BUF2 BIT(22) |
| |
| #define CLK_MULTIPLIER 0xc008e |
| #define POLL_TIMEOUT_MS 3000 |
| #define POLL_DELAY_US 100 |
| |
| struct axiado_emmc_phy { |
| void __iomem *reg_base; |
| struct device *dev; |
| }; |
| |
| static int axiado_emmc_phy_init(struct phy *phy) |
| { |
| struct axiado_emmc_phy *ax_phy = phy_get_drvdata(phy); |
| struct device *dev = ax_phy->dev; |
| u32 val; |
| int ret; |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_1); |
| writel(val | RETB_ENBL | RTRIM_EN, ax_phy->reg_base + PHY_CTRL_1); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| writel(val | PDB_ENBL, ax_phy->reg_base + PHY_CTRL_3); |
| |
| ret = readl_poll_timeout(ax_phy->reg_base + STATUS, val, |
| val & CALDONE_MASK, POLL_DELAY_US, |
| POLL_TIMEOUT_MS * 1000); |
| if (ret) { |
| dev_err(dev, "PHY calibration timeout\n"); |
| return ret; |
| } |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_1); |
| writel(val | REN_CMD_EN | PU_CMD_EN, ax_phy->reg_base + PHY_CTRL_1); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_2); |
| writel(val | REN_STRB, ax_phy->reg_base + PHY_CTRL_2); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| writel(val | MAX_CLK_BUF0 | MAX_CLK_BUF1 | MAX_CLK_BUF2, |
| ax_phy->reg_base + PHY_CTRL_3); |
| |
| writel(CLK_MULTIPLIER, ax_phy->reg_base + CAP_REG_IN_S1_MSB); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| writel(val | SEL_DLY_RXCLK | SEL_DLY_TXCLK, |
| ax_phy->reg_base + PHY_CTRL_3); |
| |
| return 0; |
| } |
| |
| static int axiado_emmc_phy_power_on(struct phy *phy) |
| { |
| struct axiado_emmc_phy *ax_phy = phy_get_drvdata(phy); |
| struct device *dev = ax_phy->dev; |
| u32 val; |
| int ret; |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_1); |
| writel(val | RETB_ENBL, ax_phy->reg_base + PHY_CTRL_1); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| writel(val | PDB_ENBL, ax_phy->reg_base + PHY_CTRL_3); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_2); |
| writel(val | OTAP_SEL(OTAP_DLY), ax_phy->reg_base + PHY_CTRL_2); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_1); |
| writel(val | DLL_TRM(DLL_TRM_ICP), ax_phy->reg_base + PHY_CTRL_1); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| writel(val | DLL_FRQSEL(FRQ_SEL), ax_phy->reg_base + PHY_CTRL_3); |
| |
| ret = read_poll_timeout(readl, val, val & DLL_RDY_MASK, POLL_DELAY_US, |
| POLL_TIMEOUT_MS * 1000, false, |
| ax_phy->reg_base + STATUS); |
| if (ret) { |
| dev_err(dev, "DLL ready timeout\n"); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int axiado_emmc_phy_power_off(struct phy *phy) |
| { |
| struct axiado_emmc_phy *ax_phy = phy_get_drvdata(phy); |
| u32 val; |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_1); |
| val &= ~(DLL_TRM_MASK | DLL_ENBL); |
| writel(val, ax_phy->reg_base + PHY_CTRL_1); |
| |
| val = readl(ax_phy->reg_base + PHY_CTRL_3); |
| val &= ~(DLL_FRQSEL_MASK | PDB_ENBL); |
| writel(val, ax_phy->reg_base + PHY_CTRL_3); |
| |
| return 0; |
| } |
| |
| static const struct phy_ops axiado_emmc_phy_ops = { |
| .init = axiado_emmc_phy_init, |
| .power_on = axiado_emmc_phy_power_on, |
| .power_off = axiado_emmc_phy_power_off, |
| .owner = THIS_MODULE, |
| }; |
| |
| static const struct of_device_id axiado_emmc_phy_of_match[] = { |
| { .compatible = "axiado,ax3000-emmc-phy" }, |
| { /* sentinel */ }, |
| }; |
| MODULE_DEVICE_TABLE(of, axiado_emmc_phy_of_match); |
| |
| static int axiado_emmc_phy_probe(struct platform_device *pdev) |
| { |
| struct axiado_emmc_phy *ax_phy; |
| struct phy_provider *phy_provider; |
| struct device *dev = &pdev->dev; |
| struct phy *generic_phy; |
| |
| if (!dev->of_node) |
| return -ENODEV; |
| |
| ax_phy = devm_kzalloc(dev, sizeof(*ax_phy), GFP_KERNEL); |
| if (!ax_phy) |
| return -ENOMEM; |
| |
| ax_phy->reg_base = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(ax_phy->reg_base)) |
| return PTR_ERR(ax_phy->reg_base); |
| |
| ax_phy->dev = dev; |
| |
| generic_phy = devm_phy_create(dev, dev->of_node, &axiado_emmc_phy_ops); |
| if (IS_ERR(generic_phy)) |
| return dev_err_probe(dev, PTR_ERR(generic_phy), |
| "failed to create PHY\n"); |
| |
| phy_set_drvdata(generic_phy, ax_phy); |
| phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate); |
| |
| return PTR_ERR_OR_ZERO(phy_provider); |
| } |
| |
| static struct platform_driver axiado_emmc_phy_driver = { |
| .probe = axiado_emmc_phy_probe, |
| .driver = { |
| .name = "axiado-emmc-phy", |
| .of_match_table = axiado_emmc_phy_of_match, |
| }, |
| }; |
| module_platform_driver(axiado_emmc_phy_driver); |
| |
| MODULE_DESCRIPTION("AX3000 eMMC PHY Driver"); |
| MODULE_AUTHOR("Axiado Corporation"); |
| MODULE_LICENSE("GPL"); |