[PATCH 2/8] phy: axiado: add Arasan eMMC-PHY for Axiado

Tzu-Hao Wei
2025-12-22T08:45:40Z

From: SriNavmani A <srinavmani@axiado.com>

Add support for the Arasan eMMC PHY found on the AX3000 SoC. This
external PHY is required to achieve HS200 mode operation (200 MHz)
on the eMMC interface.

The existing sdhci-of-arasan.c driver supports internal PHY
configurations, but the AX3000 uses an external PHY that requires
different configuration. This driver was written based on the
reference implementation in sdhci-pci-arasan.c as suggested by the
Arasan team to provide the necessary PHY configuration for HS200
high-speed mode support.

Signed-off-by: SriNavmani A <srinavmani@axiado.com>
Signed-off-by: Tzu-Hao Wei <twei@axiado.com>
---
drivers/phy/Kconfig | 1 +
drivers/phy/Makefile | 1 +
drivers/phy/axiado/Kconfig | 15 ++
drivers/phy/axiado/Makefile | 1 +
drivers/phy/axiado/phy-axiado-emmc.c | 260 +++++++++++++++++++++++++++++++++++
5 files changed, 278 insertions(+)

diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
index 678dd0452f0aa0597773433f04d2a9ba77474d2a..b802274ea45a84bd36d7c0b7fb90e368a5c018b4 100644
--- a/drivers/phy/Kconfig
+++ b/drivers/phy/Kconfig
@@ -103,6 +103,7 @@ config PHY_NXP_PTN3222

source "drivers/phy/allwinner/Kconfig"
source "drivers/phy/amlogic/Kconfig"
+source "drivers/phy/axiado/Kconfig"
source "drivers/phy/broadcom/Kconfig"
source "drivers/phy/cadence/Kconfig"
source "drivers/phy/freescale/Kconfig"
diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
index bfb27fb5a494283d7fd05dd670ebd1b12df8b1a1..f1b9e4a8673bcde3fdc0fdc06a3deddb5785ced1 100644
--- a/drivers/phy/Makefile
+++ b/drivers/phy/Makefile
@@ -15,6 +15,7 @@ obj-$(CONFIG_PHY_AIROHA_PCIE) += phy-airoha-pcie.o
obj-$(CONFIG_PHY_NXP_PTN3222) += phy-nxp-ptn3222.o
obj-y += allwinner/ \
amlogic/ \
+ axiado/ \
broadcom/ \
cadence/ \
freescale/ \
diff --git a/drivers/phy/axiado/Kconfig b/drivers/phy/axiado/Kconfig
new file mode 100644
index 0000000000000000000000000000000000000000..824114e6068da327308321b9884552ad33db9efc
--- /dev/null
+++ b/drivers/phy/axiado/Kconfig
@@ -0,0 +1,15 @@
+#
+# PHY drivers for Axiado platforms
+#
+
+config PHY_AX3000_EMMC
+ tristate "Axiado eMMC PHY driver"
+ select GENERIC_PHY
+ help
+ This enables support for the eMMC PHY block found on the
+ Axiado AX3000 SoCs. The PHY provides the physical layer
+ interface used by the Arasan SDHCI host controller for emmc
+ signaling and timing adjustment.
+
+ If you are building a kernel for AX3000 platform with
+ eMMC storage, say Y or N.
diff --git a/drivers/phy/axiado/Makefile b/drivers/phy/axiado/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..1e2b1ba016092eaffdbd7acbd9cdc8577d79b35c
--- /dev/null
+++ b/drivers/phy/axiado/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_PHY_AX3000_EMMC) += phy-axiado-emmc.o
diff --git a/drivers/phy/axiado/phy-axiado-emmc.c b/drivers/phy/axiado/phy-axiado-emmc.c
new file mode 100644
index 0000000000000000000000000000000000000000..a61a458c9a65915cd576b431bc6a0cf7e8b18add
--- /dev/null
+++ b/drivers/phy/axiado/phy-axiado-emmc.c
@@ -0,0 +1,260 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Axiado eMMC PHY driver
+ *
+ * Copyright (C) 2022-2025 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.
+ *
+ * Copyright (C) 2017 Arasan Chip Systems Inc.
+ */
+#include <linux/clk.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_LSB 0x00
+#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 BIT(12)
+#define REN_DAT0 BIT(13)
+#define REN_DAT1 BIT(14)
+#define REN_DAT2 BIT(15)
+#define REN_DAT3 BIT(16)
+#define REN_DAT4 BIT(17)
+#define REN_DAT5 BIT(18)
+#define REN_DAT6 BIT(19)
+#define REN_DAT7 BIT(20)
+#define REN_CMD_EN (REN_CMD | REN_DAT0 | REN_DAT1 | REN_DAT2 | \
+ REN_DAT3 | REN_DAT4 | REN_DAT5 | REN_DAT6 | REN_DAT7)
+
+/* Pull-UP Enable on CMD Line */
+#define PU_CMD BIT(3)
+#define PU_DAT0 BIT(4)
+#define PU_DAT1 BIT(5)
+#define PU_DAT2 BIT(6)
+#define PU_DAT3 BIT(7)
+#define PU_DAT4 BIT(8)
+#define PU_DAT5 BIT(9)
+#define PU_DAT6 BIT(10)
+#define PU_DAT7 BIT(11)
+#define PU_CMD_EN (PU_CMD | PU_DAT0 | PU_DAT1 | PU_DAT2 | PU_DAT3 | \
+ PU_DAT4 | PU_DAT5 | PU_DAT6 | PU_DAT7)
+
+/* Slection 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(x) (((x) << 7) | OTAPDLY_EN)
+#define DLL_TRM(x) (((x) << 22) | DLL_ENBL)
+#define DLL_FRQSEL(x) ((x) << 25)
+
+#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 LOOP_TIMEOUT 3000
+#define TIMEOUT_DELAY 100
+
+struct axiado_emmc_phy {
+ void __iomem *reg_base;
+};
+
+static void arasan_emmc_phy_write(struct axiado_emmc_phy *ax_phy, u32 offset, u32 data)
+{
+ writel(data, ax_phy->reg_base + offset);
+}
+
+static int arasan_emmc_phy_read(struct axiado_emmc_phy *ax_phy, u32 offset)
+{
+ u32 val = readl(ax_phy->reg_base + offset);
+
+ return val;
+}
+
+static int axiado_emmc_phy_init(struct phy *phy)
+{
+ u32 val;
+ ktime_t timeout;
+
+ struct axiado_emmc_phy *ax_phy = phy_get_drvdata(phy);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_1);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_1, val | RETB_ENBL | RTRIM_EN);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_3);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_3, val | PDB_ENBL);
+
+ /* Wait max 3000 ms */
+ timeout = ktime_add_ms(ktime_get(), LOOP_TIMEOUT);
+
+ while (1) {
+ bool timedout = ktime_after(ktime_get(), timeout);
+
+ if (arasan_emmc_phy_read(ax_phy, STATUS) & CALDONE_MASK)
+ break;
+
+ if (timedout) {
+ dev_err(&phy->dev, "CALDONE_MASK bit is not cleared.");
+ return -ETIMEDOUT;
+ }
+ udelay(TIMEOUT_DELAY);
+ }
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_1);
+
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_1, val | REN_CMD_EN | PU_CMD_EN);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_2);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_2, val | REN_STRB);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_3);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_3, val | MAX_CLK_BUF0 |
+ MAX_CLK_BUF1 | MAX_CLK_BUF2);
+
+ val = arasan_emmc_phy_read(ax_phy, CAP_REG_IN_S1_MSB);
+ arasan_emmc_phy_write(ax_phy, CAP_REG_IN_S1_MSB, CLK_MULTIPLIER);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_3);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_3, val | SEL_DLY_RXCLK |
+ SEL_DLY_TXCLK);
+
+ return 0;
+}
+
+static int axiado_emmc_phy_power_on(struct phy *phy)
+{
+ struct axiado_emmc_phy *ax_phy = phy_get_drvdata(phy);
+
+ u32 val;
+ ktime_t timeout;
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_1);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_1, val | RETB_ENBL);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_3);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_3, val | PDB_ENBL);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_2);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_2, val | OTAP_SEL(OTAP_DLY));
+
+ arasan_emmc_phy_read(ax_phy, PHY_CTRL_2);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_1);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_1, val | DLL_TRM(DLL_TRM_ICP));
+
+ arasan_emmc_phy_write(ax_phy, STATUS, 0x00);
+
+ val = arasan_emmc_phy_read(ax_phy, PHY_CTRL_3);
+ arasan_emmc_phy_write(ax_phy, PHY_CTRL_3, val | DLL_FRQSEL(FRQ_SEL));
+
+ /* Wait max 3000 ms */
+ timeout = ktime_add_ms(ktime_get(), LOOP_TIMEOUT);
+
+ while (1) {
+ bool timedout = ktime_after(ktime_get(), timeout);
+
+ if (arasan_emmc_phy_read(ax_phy, STATUS) & DLL_RDY_MASK)
+ break;
+
+ if (timedout) {
+ dev_err(&phy->dev, "DLL_RDY_MASK bit is not cleared.");
+ return -ETIMEDOUT;
+ }
+ udelay(TIMEOUT_DELAY);
+ }
+ return 0;
+}
+
+static const struct phy_ops axiado_emmc_phy_ops = {
+ .init = axiado_emmc_phy_init,
+ .power_on = axiado_emmc_phy_power_on,
+ .owner = THIS_MODULE,
+};
+
+static const struct of_device_id axiado_emmc_phy_of_match[] = {
+ { .compatible = "axiado,ax3000-emmc-phy"},
+ {}
+};
+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;
+ const struct of_device_id *id;
+ struct phy *generic_phy;
+ struct resource *res;
+
+ if (!dev->of_node)
+ return -ENODEV;
+
+ ax_phy = devm_kzalloc(dev, sizeof(*ax_phy), GFP_KERNEL);
+ if (!ax_phy)
+ return -ENOMEM;
+
+ res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+
+ ax_phy->reg_base = devm_ioremap_resource(&pdev->dev, res);
+
+ if (IS_ERR(ax_phy->reg_base))
+ return PTR_ERR(ax_phy->reg_base);
+
+ id = of_match_node(axiado_emmc_phy_of_match, pdev->dev.of_node);
+ if (!id) {
+ dev_err(dev, "failed to get match_node\n");
+ return -EINVAL;
+ }
+
+ generic_phy = devm_phy_create(dev, dev->of_node, &axiado_emmc_phy_ops);
+ if (IS_ERR(generic_phy)) {
+ dev_err(dev, "failed to create PHY\n");
+ return PTR_ERR(generic_phy);
+ }
+
+ 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");

--
2.48.1