Add an SDHCI host controller driver (MMC_SDHCI_AXIADO) for the Arasan
SDHCI controller found on the Axiado AX3005 SoC. The driver builds on
the generic SDHCI framework and adds SoC-specific handling for signal
voltage switching, HS200 tuning and eMMC PHY power sequencing through
the generic PHY API.

Signed-off-by: Siu Ming Tong <[email protected]>
---
 MAINTAINERS                |   1 +
 drivers/mmc/Kconfig        |   7 +
 drivers/mmc/Makefile       |   1 +
 drivers/mmc/axiado_sdhci.c | 311 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 320 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index eece4e5ebd0..02a0c171840 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -232,6 +232,7 @@ F:  arch/arm/dts/ax3005*
 F:     arch/arm/mach-axiado/
 F:     board/axiado/scm3005/
 F:     configs/ax3005_scm3005_defconfig
+F:     drivers/mmc/axiado_sdhci.c
 F:     drivers/phy/phy-axiado-emmc.c
 F:     include/configs/ax3005-scm3005.h
 
diff --git a/drivers/mmc/Kconfig b/drivers/mmc/Kconfig
index f9f7aa5cf97..4ee56465c0f 100644
--- a/drivers/mmc/Kconfig
+++ b/drivers/mmc/Kconfig
@@ -114,6 +114,13 @@ config MMC_QUIRKS
          are enabled by default, other may require additional flags or are
          enabled by the host driver.
 
+config MMC_SDHCI_AXIADO
+       bool "Axiado eMMC sdhci driver support"
+       depends on MMC
+       help
+         Enable this to support the Axiado eMMC sdhci found in the Ax3005 SoC.
+         This sdhci provides functionality for interface with sdhci driver.
+
 config SYS_MMC_MAX_BLK_COUNT
        int "Block count limit"
        default 65535
diff --git a/drivers/mmc/Makefile b/drivers/mmc/Makefile
index a23336d7d8d..f4a40ee10d5 100644
--- a/drivers/mmc/Makefile
+++ b/drivers/mmc/Makefile
@@ -56,6 +56,7 @@ obj-$(CONFIG_NEXELL_DWMMC) += nexell_dw_mmc.o
 obj-$(CONFIG_MMC_SDHCI)                        += sdhci.o
 obj-$(CONFIG_MMC_SDHCI_ASPEED)         += aspeed_sdhci.o
 obj-$(CONFIG_MMC_SDHCI_ATMEL)          += atmel_sdhci.o
+obj-$(CONFIG_MMC_SDHCI_AXIADO)         += axiado_sdhci.o
 obj-$(CONFIG_MMC_SDHCI_BCM2835)                += bcm2835_sdhci.o
 obj-$(CONFIG_MMC_SDHCI_BCMSTB)         += bcmstb_sdhci.o
 obj-$(CONFIG_MMC_SDHCI_CADENCE)                += sdhci-cadence.o
diff --git a/drivers/mmc/axiado_sdhci.c b/drivers/mmc/axiado_sdhci.c
new file mode 100644
index 00000000000..a1cf22ad4f5
--- /dev/null
+++ b/drivers/mmc/axiado_sdhci.c
@@ -0,0 +1,311 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2021-26 Axiado Corporation (or its affiliates).
+ */
+
+#include <clk.h>
+#include <dm.h>
+#include <fdtdec.h>
+#include "mmc_private.h"
+#include <dm/device.h>
+#include <linux/err.h>
+#include <linux/libfdt.h>
+#include <malloc.h>
+#include <sdhci.h>
+#include <generic-phy.h>
+#include <dm/device_compat.h>
+#include <linux/delay.h>
+
+#define SDHCI_HOST_CTRL2 0x3E
+#define SDHCI_CTRL_VDD_180 0x0008
+#define SDHCI_CTRL_EXEC_TUNING 0x0040
+#define SDHCI_TUNING_LOOP_COUNT 40
+#define EMMC_MIN_FREQ 400000
+#define EMMC_MAX_FREQ 200000000
+
+DECLARE_GLOBAL_DATA_PTR;
+
+struct axiado_sdhci_plat {
+       struct mmc_config cfg;
+       struct mmc mmc;
+       unsigned int f_max;
+       struct phy phy;
+       bool phy_is_on;
+};
+
+struct axiado_sdhci_priv {
+       struct sdhci_host *host;
+};
+
+static void axiado_sdhci_set_control_reg(struct sdhci_host *host)
+{
+       struct mmc *mmc = (struct mmc *)host->mmc;
+       u32 reg;
+
+       switch (mmc->signal_voltage) {
+       case MMC_SIGNAL_VOLTAGE_180:
+               reg = sdhci_readw(host, SDHCI_HOST_CTRL2);
+               reg |= SDHCI_CTRL_VDD_180; /* Set 1.8V mode */
+               sdhci_writew(host, reg, SDHCI_HOST_CTRL2);
+               break;
+
+       case MMC_SIGNAL_VOLTAGE_330:
+               reg = sdhci_readw(host, SDHCI_HOST_CTRL2);
+               reg &= ~SDHCI_CTRL_VDD_180; /* Clear 1.8V mode (3.3V) */
+               sdhci_writew(host, reg, SDHCI_HOST_CTRL2);
+               break;
+
+       default:
+               debug("axiado_sdhci: Unsupported voltage: %d\n",
+                     mmc->signal_voltage);
+               break;
+       }
+
+       sdhci_set_uhs_timing(host);
+}
+
+static int axiado_sdhci_set_ios_post(struct sdhci_host *host)
+{
+       struct udevice *dev = host->mmc->dev;
+       struct axiado_sdhci_plat *plat = dev_get_plat(dev);
+       unsigned int speed = host->mmc->clock;
+       u16 clk;
+
+       /* Reset SD Clock Enable */
+       clk = sdhci_readw(host, SDHCI_CLOCK_CONTROL);
+       clk &= ~SDHCI_CLOCK_CARD_EN;
+       sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL);
+
+       /* Power off PHY */
+       if (plat->phy_is_on) {
+               debug("Powering off eMMC PHY\n");
+               generic_phy_power_off(&plat->phy);
+               plat->phy_is_on = false;
+       }
+
+       /* Restart SD Clock */
+       sdhci_set_clock(host->mmc, speed);
+
+       /* Power on PHY if required */
+       if (speed > EMMC_MIN_FREQ) {
+               debug("Speed is high enough, powering on PHY...\n");
+               generic_phy_power_on(&plat->phy);
+               plat->phy_is_on = true;
+       }
+
+       return 0;
+}
+
+static int axiado_sdhci_execute_tuning(struct mmc *mmc, u8 opcode)
+{
+       struct mmc_cmd cmd;
+       struct mmc_data data;
+       u32 ctrl;
+       struct sdhci_host *host;
+       char tuning_loop_counter = SDHCI_TUNING_LOOP_COUNT;
+
+       host = mmc->priv;
+
+       ctrl = sdhci_readw(host, SDHCI_HOST_CTRL2);
+       ctrl |= SDHCI_CTRL_EXEC_TUNING;
+       sdhci_writew(host, ctrl, SDHCI_HOST_CTRL2);
+
+       sdhci_writel(host, SDHCI_INT_DATA_AVAIL, SDHCI_INT_ENABLE);
+       sdhci_writel(host, SDHCI_INT_DATA_AVAIL, SDHCI_SIGNAL_ENABLE);
+
+       do {
+               cmd.cmdidx = opcode;
+               cmd.resp_type = MMC_RSP_R1;
+               cmd.cmdarg = 0;
+
+               data.blocksize = 64;
+               data.blocks = 1;
+               data.flags = MMC_DATA_READ;
+
+               if (tuning_loop_counter-- == 0)
+                       break;
+
+               if (cmd.cmdidx == MMC_CMD_SEND_TUNING_BLOCK_HS200 &&
+                   mmc->bus_width == 8)
+                       data.blocksize = 128;
+
+               sdhci_writew(host,
+                            SDHCI_MAKE_BLKSZ(SDHCI_DEFAULT_BOUNDARY_ARG, 
data.blocksize),
+                            SDHCI_BLOCK_SIZE);
+               sdhci_writew(host, data.blocks, SDHCI_BLOCK_COUNT);
+               sdhci_writew(host, SDHCI_TRNS_READ, SDHCI_TRANSFER_MODE);
+
+               mmc_send_cmd(mmc, &cmd, NULL);
+
+               ctrl = sdhci_readw(host, SDHCI_HOST_CTRL2);
+
+               if (cmd.cmdidx == MMC_CMD_SEND_TUNING_BLOCK)
+                       udelay(1);
+
+       } while (ctrl & SDHCI_CTRL_EXEC_TUNING);
+
+       if (tuning_loop_counter < 0) {
+               ctrl &= ~SDHCI_CTRL_TUNED_CLK;
+               sdhci_writew(host, ctrl, SDHCI_HOST_CTRL2);
+       }
+
+       if (!(ctrl & SDHCI_CTRL_TUNED_CLK)) {
+               printf("%s: Tuning failed\n", __func__);
+               return -1;
+       }
+
+       /* Enable only interrupts served by the SD controller */
+       sdhci_writel(host, SDHCI_INT_DATA_MASK | SDHCI_INT_CMD_MASK,
+                    SDHCI_INT_ENABLE);
+       /* Mask all sdhci interrupt sources */
+       sdhci_writel(host, 0x0, SDHCI_SIGNAL_ENABLE);
+
+       return 0;
+}
+
+static struct sdhci_ops axiado_ops = {
+       .set_ios_post = &axiado_sdhci_set_ios_post,
+       .platform_execute_tuning = &axiado_sdhci_execute_tuning,
+       .set_control_reg = &axiado_sdhci_set_control_reg,
+};
+
+static int axiado_sdhci_probe(struct udevice *dev)
+{
+       struct axiado_sdhci_plat *plat = dev_get_plat(dev);
+       struct mmc_uclass_priv *upriv = dev_get_uclass_priv(dev);
+       struct axiado_sdhci_priv *priv = dev_get_priv(dev);
+       struct sdhci_host *host;
+       unsigned long clock = 0;
+       int ret;
+       struct clk clk;
+
+       host = priv->host;
+       /* Get clock frequency */
+       ret = clk_get_by_index(dev, 0, &clk);
+       if (ret < 0) {
+               dev_err(dev, "failed to get clock\n");
+               return ret;
+       }
+
+       clock = clk_get_rate(&clk);
+       if (IS_ERR_VALUE(clock)) {
+               dev_err(dev, "failed to get rate\n");
+               return clock;
+       }
+
+       ret = clk_enable(&clk);
+       /* U-Boot commonly uses ENOSYS for "function not implemented" */
+       if (ret && ret != -ENOSYS) {
+               dev_err(dev, "failed to enable clock\n");
+               return ret;
+       }
+
+       if (device_is_compatible(dev, "axiado,ax3005-sdhci")) {
+               /* Get and init phy */
+               ret = generic_phy_get_by_name(dev, "phy_arasan", &plat->phy);
+               if (ret) {
+                       dev_err(dev, "can't get phy from DT\n");
+                       return ret;
+               }
+
+               ret = generic_phy_init(&plat->phy);
+               if (ret) {
+                       dev_err(dev, "couldn't initialize MMC PHY\n");
+                       return ret;
+               }
+
+               plat->phy_is_on = false;
+       }
+
+       host->ops = &axiado_ops;
+       host->quirks = SDHCI_QUIRK_WAIT_SEND_CMD;
+
+       host->max_clk = clock;
+       host->bus_width = dev_read_u32_default(dev, "bus-width", 4);
+
+       if (host->bus_width == 8)
+               host->host_caps |= MMC_MODE_8BIT;
+       else if (host->bus_width == 4)
+               host->host_caps |= MMC_MODE_4BIT;
+
+       host->mmc = &plat->mmc;
+       host->mmc->dev = dev;
+       host->mmc->priv = host;
+
+       ret = sdhci_setup_cfg(&plat->cfg, host, plat->f_max, EMMC_MIN_FREQ);
+       if (ret)
+               goto err;
+       upriv->mmc = host->mmc;
+
+       ret = sdhci_probe(dev);
+       if (ret)
+               goto err;
+
+       return 0;
+
+err:
+       clk_disable(&clk);
+       return ret;
+}
+
+static int axiado_sdhci_ofdata_to_platdata(struct udevice *dev)
+{
+       struct axiado_sdhci_plat *plat = dev_get_plat(dev);
+       struct axiado_sdhci_priv *priv = dev_get_priv(dev);
+
+       priv->host = calloc(1, sizeof(struct sdhci_host));
+       if (!priv->host) {
+               dev_err(dev, "sdhci_host malloc failed\n");
+               return -ENOMEM;
+       }
+
+       priv->host->name = dev->name;
+
+       priv->host->ioaddr = dev_read_addr_ptr(dev);
+       if (!priv->host->ioaddr) {
+               dev_err(dev, "failed to get I/O address\n");
+               return -EINVAL;
+       }
+
+       plat->f_max = dev_read_u32_default(dev, "mmc-clk", EMMC_MAX_FREQ);
+
+       return 0;
+}
+
+static int axiado_sdhci_bind(struct udevice *dev)
+{
+       struct axiado_sdhci_plat *plat = dev_get_plat(dev);
+
+       return sdhci_bind(dev, &plat->mmc, &plat->cfg);
+}
+
+static const struct udevice_id axiado_sdhci_ids[] = {
+       { .compatible = "axiado,ax3005-sdhci" },
+       {}
+};
+
+static int axiado_sdhci_remove(struct udevice *dev)
+{
+       struct axiado_sdhci_plat *plat = dev_get_plat(dev);
+
+       if (device_is_compatible(dev, "axiado,ax3005-sdhci")) {
+               if (plat->phy_is_on)
+                       generic_phy_power_off(&plat->phy);
+               generic_phy_exit(&plat->phy);
+       }
+
+       return 0;
+}
+
+U_BOOT_DRIVER(axiado_sdhci_drv) = {
+       .name = "axiado_sdhci",
+       .id = UCLASS_MMC,
+       .of_match = axiado_sdhci_ids,
+       .of_to_plat = axiado_sdhci_ofdata_to_platdata,
+       .ops = &sdhci_ops,
+       .bind = axiado_sdhci_bind,
+       .probe = axiado_sdhci_probe,
+       .remove = axiado_sdhci_remove,
+       .priv_auto = sizeof(struct axiado_sdhci_priv),
+       .plat_auto = sizeof(struct axiado_sdhci_plat),
+};

-- 
2.34.1

Reply via email to