@@ -654,6 +654,17 @@ config MMC_SDHCI_F_SDH30
If you have a controller with this interface, say Y here.
If unsure, say N.
+config MMC_SDHCI_METANOIA
+ bool "SDHCI support for Metanoia MT2824 SoC"
+ depends on MMC_SDHCI
+ depends on OF_CONTROL
+ help
+ Support for SDHCI host controller on Metanoia MT2824 SoC platform.
+
+ If you have a controller with this interface, say Y here.
+
+ If unsure, say N.
+
config MMC_SDHCI_MSM
bool "Qualcomm SDHCI controller"
depends on MMC_SDHCI && ARCH_SNAPDRAGON
@@ -63,6 +63,7 @@ obj-$(CONFIG_MMC_SDHCI_CADENCE) += sdhci-cadence6.o
obj-$(CONFIG_MMC_SDHCI_CV1800B) += cv1800b_sdhci.o
obj-$(CONFIG_MMC_SDHCI_AM654) += am654_sdhci.o
obj-$(CONFIG_MMC_SDHCI_IPROC) += iproc_sdhci.o
+obj-$(CONFIG_MMC_SDHCI_METANOIA) += metanoia_sdhci.o
obj-$(CONFIG_MMC_SDHCI_MSM) += msm_sdhci.o
obj-$(CONFIG_MMC_SDHCI_MV) += mv_sdhci.o
obj-$(CONFIG_MMC_SDHCI_NPCM) += npcm_sdhci.o
new file mode 100644
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2026 Metanoia Communications Inc.,
+ * Jun Chang, Metanoia Communications Inc. <jun.chang@metanoia-comm.com>
+ * Wilson Kang, Metanoia Communications Inc. <wilson.kang@metanoia-comm.com>
+ */
+
+#include <config.h>
+#include <clk.h>
+#include <dm.h>
+#include <linux/delay.h>
+#include <linux/bitfield.h>
+#include <linux/iopoll.h>
+#include <linux/sizes.h>
+#include <sdhci.h>
+
+#define DWCMSHC_CTRL_HS400 0x7
+#define EMMC_MIN_FREQ 400000
+
+/* PHY register area pointer */
+#define DWC_MSHC_PHY_REGS 0x300
+
+#define DWC_MSHC_PHY_CNFG_REG (DWC_MSHC_PHY_REGS + 0x00)
+#define PHY_CNFG_RSTN_DEASSERT 0x1 /* Deassert PHY reset */
+#define PHY_PAD_SN_MASK GENMASK(23, 20)
+#define PHY_PAD_SN_SD 0x8
+#define PHY_PAD_SN_EMMC 0x8
+#define PHY_PAD_SP_MASK GENMASK(19, 16)
+#define PHY_PAD_SP_SD 0x9
+#define PHY_PAD_SP_EMMC 0x8
+#define PHY_PWRGOOD BIT(1)
+#define PHY_RSTN BIT(0)
+
+#define PHY_CMDPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x04)
+#define PHY_DATPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x06)
+#define PHY_CLKPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x08)
+#define PHY_STBPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x0a)
+#define PHY_RSTNPAD_CNFG_REG (DWC_MSHC_PHY_REGS + 0x0c)
+#define PHY_SMPLDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x20)
+#define PHY_ATDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x21)
+#define PHY_DLL_CTRL_REG (DWC_MSHC_PHY_REGS + 0x24)
+#define PHY_DLL_CNFG1_REG (DWC_MSHC_PHY_REGS + 0x25)
+#define PHY_DLLDL_CNFG_REG (DWC_MSHC_PHY_REGS + 0x28)
+#define PHY_DLL_STATUS_REG (DWC_MSHC_PHY_REGS + 0x2e)
+
+#define PHY_RXSEL_MASK GENMASK(2, 0)
+#define PHY_RXSEL_180V 0x01
+#define PHY_RXSEL_330V 0x02
+
+#define PHY_TXSLEW_CTRL_N_MASK GENMASK(12, 9)
+#define PHY_TXSLEW_CTRL_N_EMMC 0x2
+#define PHY_TXSLEW_CTRL_N_SD 0x1
+#define PHY_TXSLEW_CTRL_P_MASK GENMASK(8, 5)
+#define PHY_TXSLEW_CTRL_P_EMMC 0x2
+#define PHY_TXSLEW_CTRL_P_SD 0x1
+#define PHY_TXSLEW_CTRL_P_DS 0x3
+
+#define PHY_WEAKPULL_EN_PULLUP 0x08
+#define PHY_WEAKPULL_EN_PULLDOWN 0x10
+
+/* Vendor specific Registers */
+#define VENDOR_SPECIFIC_AREA_BASE 0x500
+
+#define DWCMSHC_HOST_CTRL3_REG (VENDOR_SPECIFIC_AREA_BASE + 0x08)
+#define DWCMSHC_CMD_CONFLICT_CHECK BIT(0)
+#define DWCMSHC_NEGEDGE_DATAOUT_EN BIT(1)
+#define NEDGE_SMPL_EN BIT(7)
+
+#define DWCMSHC_EMMC_CTRL_REG (VENDOR_SPECIFIC_AREA_BASE + 0x2c)
+#define ENH_STROBE_ENABLE BIT(8)
+#define CARD_IS_EMMC BIT(0)
+
+#define DWCMSHC_AT_CTRL_REG (VENDOR_SPECIFIC_AREA_BASE + 0x40)
+#define TUNE_CLK_STOP_EN BIT(16)
+
+static inline int metanoia_boundary_ok(dma_addr_t addr, int len)
+{
+ return ((addr | (SZ_128M - 1)) == ((addr + len - 1) | (SZ_128M - 1)));
+}
+
+struct metanoia_sdhc_plat {
+ struct mmc_config cfg;
+ struct mmc mmc;
+};
+
+struct metanoia_sdhc {
+ struct sdhci_host host;
+};
+
+static void metanoia_sdhci_sw_reset(struct sdhci_host *host, u8 mask)
+{
+ u8 val;
+ int retries = 100;
+
+ sdhci_writeb(host, mask, SDHCI_SOFTWARE_RESET);
+ do {
+ udelay(1000);
+ val = sdhci_readb(host, SDHCI_SOFTWARE_RESET);
+ if (!(val & mask))
+ break;
+ } while (--retries > 0);
+
+ if (retries <= 0)
+ pr_warn("%s: Reset 0x%x never completed.\n", __func__,
+ (int)mask);
+}
+
+static bool metanoia_sdhci_is_emmc(struct mmc *mmc)
+{
+ u32 bus_width;
+
+ if (IS_MMC(mmc))
+ return true;
+ if (IS_SD(mmc))
+ return false;
+
+ /*
+ * Card type may be unknown early (before op-cond completes). Fall back
+ * to board description (e.g. non-removable in DT -> eMMC).
+ */
+ if (mmc->dev) {
+ bus_width = dev_read_u32_default(mmc->dev, "bus-width", 0);
+ if (bus_width == 8)
+ return true;
+ if (dev_read_bool(mmc->dev, "non-removable"))
+ return true;
+ }
+
+ return false;
+}
+
+void metanoia_sdhci_adma_write_desc(struct sdhci_host *host, void **desc,
+ dma_addr_t addr, int len, bool end)
+{
+ int tmplen, offset;
+
+ if (likely(!len || metanoia_boundary_ok(addr, len))) {
+ sdhci_adma_write_desc(host, desc, addr, len, end);
+ return;
+ }
+
+ offset = addr & (SZ_128M - 1);
+ tmplen = SZ_128M - offset;
+ sdhci_adma_write_desc(host, desc, addr, tmplen, false);
+
+ addr += tmplen;
+ len -= tmplen;
+ sdhci_adma_write_desc(host, desc, addr, len, end);
+}
+
+static int metanoia_sdhci_of_to_plat(struct udevice *dev)
+{
+ struct metanoia_sdhc_plat *plat = dev_get_plat(dev);
+ struct sdhci_host *host = dev_get_priv(dev);
+ struct mmc_config *cfg = &plat->cfg;
+ int ret;
+
+ host->name = dev->name;
+ host->ioaddr = dev_read_addr_ptr(dev);
+ ret = mmc_of_parse(dev, cfg);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int metanoia_sdhci_bind(struct udevice *dev)
+{
+ struct metanoia_sdhc_plat *plat = dev_get_plat(dev);
+
+ return sdhci_bind(dev, &plat->mmc, &plat->cfg);
+}
+
+static void metanoia_emmc_phy_pad_cfg(struct sdhci_host *host, bool is_emmc)
+{
+ u16 val;
+ u16 txslew_n = 0;
+ u16 txslew_p = 0;
+ u16 rx_sel = PHY_RXSEL_330V;
+
+ if (host->mmc->signal_voltage == MMC_SIGNAL_VOLTAGE_180)
+ rx_sel = PHY_RXSEL_180V;
+
+ if (is_emmc) {
+ txslew_n = PHY_TXSLEW_CTRL_N_EMMC;
+ txslew_p = PHY_TXSLEW_CTRL_P_EMMC;
+ } else {
+ txslew_n = PHY_TXSLEW_CTRL_N_SD;
+ txslew_p = PHY_TXSLEW_CTRL_P_SD;
+ }
+
+ val = PHY_WEAKPULL_EN_PULLUP;
+ val |= FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, txslew_n);
+ val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, txslew_p);
+ val |= FIELD_PREP(PHY_RXSEL_MASK, rx_sel);
+
+ sdhci_writew(host, val, PHY_CMDPAD_CNFG_REG);
+ sdhci_writew(host, val, PHY_DATPAD_CNFG_REG);
+ sdhci_writew(host, val, PHY_RSTNPAD_CNFG_REG);
+
+ val = FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, txslew_n);
+ val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, txslew_p);
+ sdhci_writew(host, val, PHY_CLKPAD_CNFG_REG);
+
+ if (CONFIG_IS_ENABLED(MMC_HS400_SUPPORT)) {
+ val = PHY_WEAKPULL_EN_PULLDOWN;
+ val |= FIELD_PREP(PHY_TXSLEW_CTRL_N_MASK, PHY_TXSLEW_CTRL_N_SD);
+ val |= FIELD_PREP(PHY_TXSLEW_CTRL_P_MASK, PHY_TXSLEW_CTRL_P_DS);
+ val |= FIELD_PREP(PHY_RXSEL_MASK, PHY_RXSEL_180V);
+ sdhci_writew(host, val, PHY_STBPAD_CNFG_REG);
+ }
+}
+
+static void metanoia_emmc_phy_dl_cfg(struct sdhci_host *host)
+{
+ sdhci_writeb(host, 0x0C, PHY_SMPLDL_CNFG_REG);
+ sdhci_writeb(host, 0x0C, PHY_ATDL_CNFG_REG);
+}
+
+static void metanoia_sdhci_apply_phy_type(struct sdhci_host *host, bool is_emmc)
+{
+ u32 val;
+
+ u32 phy_pad_sn = is_emmc ? PHY_PAD_SN_EMMC : PHY_PAD_SN_SD;
+ u32 phy_pad_sp = is_emmc ? PHY_PAD_SP_EMMC : PHY_PAD_SP_SD;
+
+ val = sdhci_readl(host, DWC_MSHC_PHY_CNFG_REG);
+ val &= ~(PHY_PAD_SN_MASK | PHY_PAD_SP_MASK);
+ val |= FIELD_PREP(PHY_PAD_SN_MASK, phy_pad_sn);
+ val |= FIELD_PREP(PHY_PAD_SP_MASK, phy_pad_sp);
+ sdhci_writel(host, val, DWC_MSHC_PHY_CNFG_REG);
+
+ metanoia_emmc_phy_pad_cfg(host, is_emmc);
+}
+
+static void metanoia_sdhci_apply_hs400_phy(struct sdhci_host *host)
+{
+ u16 clk;
+ u8 val;
+ int retries = 10;
+
+ /* configure DLL as user guide suggest */
+ clk = sdhci_readw(host, SDHCI_CLOCK_CONTROL);
+ clk &= ~SDHCI_CLOCK_CARD_EN;
+ sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL);
+
+ sdhci_writeb(host, 0x20, PHY_DLL_CNFG1_REG);
+ sdhci_writeb(host, 0x60, PHY_DLLDL_CNFG_REG);
+
+ clk = sdhci_readw(host, SDHCI_CLOCK_CONTROL);
+ clk |= SDHCI_CLOCK_CARD_EN;
+ sdhci_writew(host, clk, SDHCI_CLOCK_CONTROL);
+ udelay(10);
+
+ sdhci_writeb(host, 0x1, PHY_DLL_CTRL_REG);
+
+ do {
+ udelay(1);
+ val = sdhci_readb(host, PHY_DLL_STATUS_REG);
+ if ((val & 0x1) == 0x1)
+ break;
+ } while (--retries);
+
+ if (!retries)
+ pr_err("eMMC dll has not locked\n");
+
+ val = sdhci_readb(host, PHY_DLL_STATUS_REG);
+ if (val & 0x2)
+ pr_err("eMMC dll has locked and has errors\n");
+
+ metanoia_sdhci_sw_reset(host, 0x3 << 1);
+}
+
+static int metanoia_sdhci_phy_init(struct sdhci_host *host)
+{
+ u32 val;
+ int ret;
+
+ /* Wait powergood before deassert phy reset */
+ ret = readl_poll_timeout(host->ioaddr + DWC_MSHC_PHY_CNFG_REG, val,
+ val & PHY_PWRGOOD, 20000);
+ if (ret) {
+ pr_warn("%s: Internal clock never stabilised.\n", __func__);
+ return ret;
+ }
+
+ val = sdhci_readl(host, DWC_MSHC_PHY_CNFG_REG);
+ if (!(val & PHY_RSTN)) {
+ val |= PHY_RSTN;
+ sdhci_writel(host, val, DWC_MSHC_PHY_CNFG_REG);
+ }
+
+ /* Set TUNE_CLK_STOP_EN to prevent glitching */
+ val = sdhci_readl(host, DWCMSHC_AT_CTRL_REG);
+ val |= TUNE_CLK_STOP_EN;
+ sdhci_writel(host, val, DWCMSHC_AT_CTRL_REG);
+
+ return 0;
+}
+
+static int metanoia_sdhci_apply_phy(struct sdhci_host *host)
+{
+ struct mmc *mmc = host->mmc;
+ bool is_emmc = metanoia_sdhci_is_emmc(mmc);
+ bool is_hs400 = is_emmc && (mmc->selected_mode == MMC_HS_400 ||
+ mmc->selected_mode == MMC_HS_400_ES);
+ u32 emmc_ctrl;
+ int ret;
+
+ metanoia_emmc_phy_dl_cfg(host);
+ metanoia_sdhci_apply_phy_type(host, is_emmc);
+
+ emmc_ctrl = sdhci_readl(host, DWCMSHC_EMMC_CTRL_REG);
+ if (is_emmc)
+ emmc_ctrl |= CARD_IS_EMMC;
+ else
+ emmc_ctrl &= ~CARD_IS_EMMC;
+ sdhci_writel(host, emmc_ctrl, DWCMSHC_EMMC_CTRL_REG);
+
+ /*
+ * PHY pad/voltage settings are programmed above based on the current
+ * ios. Ensure the PHY is out of reset afterwards; otherwise voltage
+ * switching (e.g. SD 3.3V -> 1.8V) can fail.
+ */
+ ret = metanoia_sdhci_phy_init(host);
+ if (ret)
+ return ret;
+
+ if (is_hs400)
+ metanoia_sdhci_apply_hs400_phy(host);
+
+ return 0;
+}
+
+static int metanoia_sdhci_set_ios_post(struct sdhci_host *host)
+{
+ struct mmc *mmc = host->mmc;
+ u32 reg;
+ int ret;
+
+ /* Disable cmd conflict check, especially for clk 200MHz */
+ reg = sdhci_readb(host, DWCMSHC_HOST_CTRL3_REG);
+ reg &= ~DWCMSHC_CMD_CONFLICT_CHECK;
+ if (host->mmc->ddr_mode && (mmc->selected_mode == MMC_DDR_52 ||
+ mmc->selected_mode == UHS_DDR50)) {
+ /*
+ * Databook claims this bit only for HS400, however, it does
+ * not work on real silicon.
+ */
+ reg |= DWCMSHC_NEGEDGE_DATAOUT_EN;
+ } else {
+ reg &= ~DWCMSHC_NEGEDGE_DATAOUT_EN;
+ }
+
+ if (mmc->selected_mode == MMC_HS_400 ||
+ mmc->selected_mode == MMC_HS_400_ES) {
+ /* Samples CMD/DATA with respect to negative edge of cclk_rx */
+ reg |= NEDGE_SMPL_EN;
+ } else {
+ reg &= ~NEDGE_SMPL_EN;
+ }
+
+ sdhci_writeb(host, reg, DWCMSHC_HOST_CTRL3_REG);
+
+ ret = metanoia_sdhci_apply_phy(host);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int metanoia_sdhci_set_enhanced_strobe(struct sdhci_host *host)
+{
+ struct mmc *mmc = host->mmc;
+ u32 val;
+
+ val = sdhci_readl(host, DWCMSHC_EMMC_CTRL_REG);
+ if (mmc->selected_mode == MMC_HS_400_ES)
+ val |= ENH_STROBE_ENABLE;
+ else
+ val &= ~ENH_STROBE_ENABLE;
+ sdhci_writel(host, val, DWCMSHC_EMMC_CTRL_REG);
+
+ return 0;
+}
+
+static void metanoia_sdhci_set_control_reg(struct sdhci_host *host)
+{
+ struct mmc *mmc = host->mmc;
+ u32 val;
+
+ if (mmc->selected_mode == MMC_HS || mmc->selected_mode == MMC_HS_52) {
+ pr_debug("force 1.8v for MMC\n");
+ mmc->signal_voltage = MMC_SIGNAL_VOLTAGE_180;
+ }
+
+ sdhci_set_control_reg(host);
+
+ if (mmc->selected_mode == MMC_HS_400 ||
+ mmc->selected_mode == MMC_HS_400_ES) {
+ val = sdhci_readw(host, SDHCI_HOST_CONTROL2);
+ val &= ~SDHCI_CTRL_UHS_MASK;
+ val |= DWCMSHC_CTRL_HS400;
+ sdhci_writew(host, val, SDHCI_HOST_CONTROL2);
+ }
+
+ /*
+ * Reinitializing the device tries to set it to lower-speed modes
+ * first, which fails if the Enhanced Strobe bit is set, making
+ * the device impossible to use. Set the correct value here to
+ * let reinitialization attempts succeed.
+ */
+ if (CONFIG_IS_ENABLED(MMC_HS400_ES_SUPPORT))
+ metanoia_sdhci_set_enhanced_strobe(host);
+}
+
+static const struct sdhci_ops metanoia_sdhci_ops = {
+ .set_ios_post = metanoia_sdhci_set_ios_post,
+ .set_control_reg = metanoia_sdhci_set_control_reg,
+ .set_enhanced_strobe = metanoia_sdhci_set_enhanced_strobe,
+#if CONFIG_IS_ENABLED(MMC_SDHCI_ADMA_HELPERS)
+ .adma_write_desc = metanoia_sdhci_adma_write_desc,
+#endif
+};
+
+static int metanoia_sdhci_probe(struct udevice *dev)
+{
+ struct mmc_uclass_priv *upriv = dev_get_uclass_priv(dev);
+ struct metanoia_sdhc_plat *plat = dev_get_plat(dev);
+ struct metanoia_sdhc *priv = dev_get_priv(dev);
+ struct mmc_config *cfg = &plat->cfg;
+ struct sdhci_host *host = &priv->host;
+ struct clk clk = {};
+ ulong clk_rate = 0;
+ int ret;
+
+ ret = clk_get_by_index(dev, 0, &clk);
+ if (!ret) {
+ clk_rate = clk_get_rate(&clk);
+ if (IS_ERR_VALUE(clk_rate))
+ clk_rate = 0;
+ }
+
+ if (!clk_rate) {
+ u32 max_freq;
+
+ dev_read_u32(dev, "max-frequency", &max_freq);
+ clk_rate = max_freq;
+ }
+
+ if (clk_rate) {
+ host->max_clk = clk_rate;
+ if (!cfg->f_max || cfg->f_max > clk_rate)
+ cfg->f_max = clk_rate;
+ } else {
+ host->max_clk = cfg->f_max;
+ }
+
+ host->ops = &metanoia_sdhci_ops;
+ host->quirks = SDHCI_QUIRK_WAIT_SEND_CMD | SDHCI_QUIRK_USE_WIDE8;
+ host->mmc = &plat->mmc;
+ host->mmc->priv = &priv->host;
+ host->mmc->dev = dev;
+ upriv->mmc = host->mmc;
+
+ ret = metanoia_sdhci_apply_phy(host);
+ if (ret)
+ return ret;
+
+ ret = sdhci_setup_cfg(cfg, host, cfg->f_max, EMMC_MIN_FREQ);
+ if (ret)
+ return ret;
+
+ cfg->host_caps |= MMC_MODE_8BIT | MMC_CAP(MMC_DDR_52);
+
+ return sdhci_probe(dev);
+}
+
+static const struct udevice_id metanoia_sdhci_ids[] = {
+ { .compatible = "metanoia,mt2824-dwcmshc" },
+ { /* sentinel */ }
+};
+
+U_BOOT_DRIVER(metanoia_sdhci_drv) = {
+ .name = "metanoia_sdhci",
+ .id = UCLASS_MMC,
+ .of_match = metanoia_sdhci_ids,
+ .of_to_plat = metanoia_sdhci_of_to_plat,
+ .ops = &sdhci_ops,
+ .bind = metanoia_sdhci_bind,
+ .probe = metanoia_sdhci_probe,
+ .priv_auto = sizeof(struct metanoia_sdhc),
+ .plat_auto = sizeof(struct metanoia_sdhc_plat),
+};
Add an SDHCI driver for the DesignWare Mobile Storage Host Controller (DWC MSHC) integrated in the Metanoia MT2824 SoC. It supports eMMC up to HS400/HS400ES and SD cards, and implements the MMC driver model. The controller is a DWC MSHC instance, but its PHY programming, voltage/speed-mode handling and tuning model are silicon-specific and differ from the existing DWC MSHC drivers. The PHY pad drive strength, receiver select and weak-pull settings depend on the card type (eMMC vs SD) and are re-applied on every ios, whereas snps_sdhci.c uses fixed pad values. High-speed modes force 1.8V signalling and HS400 uses an explicit PHY DLL lock sequence (set_control_reg()/set_ios_post()), which snps_sdhci.c does not implement. Tuning relies on the generic SDHCI core tuner, whereas snps_sdhci.c uses the hardware auto-tuning block. Keeping a dedicated driver avoids per-variant branches in the shared DWC MSHC drivers, matching how snps_sdhci.c, adi_sdhci.c and cv1800b_sdhci.c are already split in U-Boot. Signed-off-by: Benoît Monin <benoit.monin@bootlin.com> --- drivers/mmc/Kconfig | 11 + drivers/mmc/Makefile | 1 + drivers/mmc/metanoia_sdhci.c | 494 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 506 insertions(+)