| // 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"); |