forked from Minki/linux
adc40a5179
As commit ef6b75671b
("mmc: sdhci-cadence: send tune request twice to
work around errata") stated, this IP has an errata. This commit applies
the second workaround for the SD mode.
Due to the errata, it is not possible to use the hardware tuning provided
by SDHCI_HOST_CONTROL2.
Use the software-controlled tuning like the eMMC mode.
Set sdhci_host_ops::platform_execute_tuning instead of overriding
mmc_host_ops::execute_tuning.
Signed-off-by: Masahiro Yamada <yamada.masahiro@socionext.com>
Link: https://lore.kernel.org/r/20200720061141.172944-1-yamada.masahiro@socionext.com
Signed-off-by: Ulf Hansson <ulf.hansson@linaro.org>
477 lines
12 KiB
C
477 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Copyright (C) 2016 Socionext Inc.
|
|
* Author: Masahiro Yamada <yamada.masahiro@socionext.com>
|
|
*/
|
|
|
|
#include <linux/bitfield.h>
|
|
#include <linux/bits.h>
|
|
#include <linux/iopoll.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mmc/host.h>
|
|
#include <linux/mmc/mmc.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
|
|
#include "sdhci-pltfm.h"
|
|
|
|
/* HRS - Host Register Set (specific to Cadence) */
|
|
#define SDHCI_CDNS_HRS04 0x10 /* PHY access port */
|
|
#define SDHCI_CDNS_HRS04_ACK BIT(26)
|
|
#define SDHCI_CDNS_HRS04_RD BIT(25)
|
|
#define SDHCI_CDNS_HRS04_WR BIT(24)
|
|
#define SDHCI_CDNS_HRS04_RDATA GENMASK(23, 16)
|
|
#define SDHCI_CDNS_HRS04_WDATA GENMASK(15, 8)
|
|
#define SDHCI_CDNS_HRS04_ADDR GENMASK(5, 0)
|
|
|
|
#define SDHCI_CDNS_HRS06 0x18 /* eMMC control */
|
|
#define SDHCI_CDNS_HRS06_TUNE_UP BIT(15)
|
|
#define SDHCI_CDNS_HRS06_TUNE GENMASK(13, 8)
|
|
#define SDHCI_CDNS_HRS06_MODE GENMASK(2, 0)
|
|
#define SDHCI_CDNS_HRS06_MODE_SD 0x0
|
|
#define SDHCI_CDNS_HRS06_MODE_MMC_SDR 0x2
|
|
#define SDHCI_CDNS_HRS06_MODE_MMC_DDR 0x3
|
|
#define SDHCI_CDNS_HRS06_MODE_MMC_HS200 0x4
|
|
#define SDHCI_CDNS_HRS06_MODE_MMC_HS400 0x5
|
|
#define SDHCI_CDNS_HRS06_MODE_MMC_HS400ES 0x6
|
|
|
|
/* SRS - Slot Register Set (SDHCI-compatible) */
|
|
#define SDHCI_CDNS_SRS_BASE 0x200
|
|
|
|
/* PHY */
|
|
#define SDHCI_CDNS_PHY_DLY_SD_HS 0x00
|
|
#define SDHCI_CDNS_PHY_DLY_SD_DEFAULT 0x01
|
|
#define SDHCI_CDNS_PHY_DLY_UHS_SDR12 0x02
|
|
#define SDHCI_CDNS_PHY_DLY_UHS_SDR25 0x03
|
|
#define SDHCI_CDNS_PHY_DLY_UHS_SDR50 0x04
|
|
#define SDHCI_CDNS_PHY_DLY_UHS_DDR50 0x05
|
|
#define SDHCI_CDNS_PHY_DLY_EMMC_LEGACY 0x06
|
|
#define SDHCI_CDNS_PHY_DLY_EMMC_SDR 0x07
|
|
#define SDHCI_CDNS_PHY_DLY_EMMC_DDR 0x08
|
|
#define SDHCI_CDNS_PHY_DLY_SDCLK 0x0b
|
|
#define SDHCI_CDNS_PHY_DLY_HSMMC 0x0c
|
|
#define SDHCI_CDNS_PHY_DLY_STROBE 0x0d
|
|
|
|
/*
|
|
* The tuned val register is 6 bit-wide, but not the whole of the range is
|
|
* available. The range 0-42 seems to be available (then 43 wraps around to 0)
|
|
* but I am not quite sure if it is official. Use only 0 to 39 for safety.
|
|
*/
|
|
#define SDHCI_CDNS_MAX_TUNING_LOOP 40
|
|
|
|
struct sdhci_cdns_phy_param {
|
|
u8 addr;
|
|
u8 data;
|
|
};
|
|
|
|
struct sdhci_cdns_priv {
|
|
void __iomem *hrs_addr;
|
|
bool enhanced_strobe;
|
|
unsigned int nr_phy_params;
|
|
struct sdhci_cdns_phy_param phy_params[];
|
|
};
|
|
|
|
struct sdhci_cdns_phy_cfg {
|
|
const char *property;
|
|
u8 addr;
|
|
};
|
|
|
|
static const struct sdhci_cdns_phy_cfg sdhci_cdns_phy_cfgs[] = {
|
|
{ "cdns,phy-input-delay-sd-highspeed", SDHCI_CDNS_PHY_DLY_SD_HS, },
|
|
{ "cdns,phy-input-delay-legacy", SDHCI_CDNS_PHY_DLY_SD_DEFAULT, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr12", SDHCI_CDNS_PHY_DLY_UHS_SDR12, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr25", SDHCI_CDNS_PHY_DLY_UHS_SDR25, },
|
|
{ "cdns,phy-input-delay-sd-uhs-sdr50", SDHCI_CDNS_PHY_DLY_UHS_SDR50, },
|
|
{ "cdns,phy-input-delay-sd-uhs-ddr50", SDHCI_CDNS_PHY_DLY_UHS_DDR50, },
|
|
{ "cdns,phy-input-delay-mmc-highspeed", SDHCI_CDNS_PHY_DLY_EMMC_SDR, },
|
|
{ "cdns,phy-input-delay-mmc-ddr", SDHCI_CDNS_PHY_DLY_EMMC_DDR, },
|
|
{ "cdns,phy-dll-delay-sdclk", SDHCI_CDNS_PHY_DLY_SDCLK, },
|
|
{ "cdns,phy-dll-delay-sdclk-hsmmc", SDHCI_CDNS_PHY_DLY_HSMMC, },
|
|
{ "cdns,phy-dll-delay-strobe", SDHCI_CDNS_PHY_DLY_STROBE, },
|
|
};
|
|
|
|
static int sdhci_cdns_write_phy_reg(struct sdhci_cdns_priv *priv,
|
|
u8 addr, u8 data)
|
|
{
|
|
void __iomem *reg = priv->hrs_addr + SDHCI_CDNS_HRS04;
|
|
u32 tmp;
|
|
int ret;
|
|
|
|
ret = readl_poll_timeout(reg, tmp, !(tmp & SDHCI_CDNS_HRS04_ACK),
|
|
0, 10);
|
|
if (ret)
|
|
return ret;
|
|
|
|
tmp = FIELD_PREP(SDHCI_CDNS_HRS04_WDATA, data) |
|
|
FIELD_PREP(SDHCI_CDNS_HRS04_ADDR, addr);
|
|
writel(tmp, reg);
|
|
|
|
tmp |= SDHCI_CDNS_HRS04_WR;
|
|
writel(tmp, reg);
|
|
|
|
ret = readl_poll_timeout(reg, tmp, tmp & SDHCI_CDNS_HRS04_ACK, 0, 10);
|
|
if (ret)
|
|
return ret;
|
|
|
|
tmp &= ~SDHCI_CDNS_HRS04_WR;
|
|
writel(tmp, reg);
|
|
|
|
ret = readl_poll_timeout(reg, tmp, !(tmp & SDHCI_CDNS_HRS04_ACK),
|
|
0, 10);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static unsigned int sdhci_cdns_phy_param_count(struct device_node *np)
|
|
{
|
|
unsigned int count = 0;
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(sdhci_cdns_phy_cfgs); i++)
|
|
if (of_property_read_bool(np, sdhci_cdns_phy_cfgs[i].property))
|
|
count++;
|
|
|
|
return count;
|
|
}
|
|
|
|
static void sdhci_cdns_phy_param_parse(struct device_node *np,
|
|
struct sdhci_cdns_priv *priv)
|
|
{
|
|
struct sdhci_cdns_phy_param *p = priv->phy_params;
|
|
u32 val;
|
|
int ret, i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(sdhci_cdns_phy_cfgs); i++) {
|
|
ret = of_property_read_u32(np, sdhci_cdns_phy_cfgs[i].property,
|
|
&val);
|
|
if (ret)
|
|
continue;
|
|
|
|
p->addr = sdhci_cdns_phy_cfgs[i].addr;
|
|
p->data = val;
|
|
p++;
|
|
}
|
|
}
|
|
|
|
static int sdhci_cdns_phy_init(struct sdhci_cdns_priv *priv)
|
|
{
|
|
int ret, i;
|
|
|
|
for (i = 0; i < priv->nr_phy_params; i++) {
|
|
ret = sdhci_cdns_write_phy_reg(priv, priv->phy_params[i].addr,
|
|
priv->phy_params[i].data);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void *sdhci_cdns_priv(struct sdhci_host *host)
|
|
{
|
|
struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
|
|
|
|
return sdhci_pltfm_priv(pltfm_host);
|
|
}
|
|
|
|
static unsigned int sdhci_cdns_get_timeout_clock(struct sdhci_host *host)
|
|
{
|
|
/*
|
|
* Cadence's spec says the Timeout Clock Frequency is the same as the
|
|
* Base Clock Frequency.
|
|
*/
|
|
return host->max_clk;
|
|
}
|
|
|
|
static void sdhci_cdns_set_emmc_mode(struct sdhci_cdns_priv *priv, u32 mode)
|
|
{
|
|
u32 tmp;
|
|
|
|
/* The speed mode for eMMC is selected by HRS06 register */
|
|
tmp = readl(priv->hrs_addr + SDHCI_CDNS_HRS06);
|
|
tmp &= ~SDHCI_CDNS_HRS06_MODE;
|
|
tmp |= FIELD_PREP(SDHCI_CDNS_HRS06_MODE, mode);
|
|
writel(tmp, priv->hrs_addr + SDHCI_CDNS_HRS06);
|
|
}
|
|
|
|
static u32 sdhci_cdns_get_emmc_mode(struct sdhci_cdns_priv *priv)
|
|
{
|
|
u32 tmp;
|
|
|
|
tmp = readl(priv->hrs_addr + SDHCI_CDNS_HRS06);
|
|
return FIELD_GET(SDHCI_CDNS_HRS06_MODE, tmp);
|
|
}
|
|
|
|
static int sdhci_cdns_set_tune_val(struct sdhci_host *host, unsigned int val)
|
|
{
|
|
struct sdhci_cdns_priv *priv = sdhci_cdns_priv(host);
|
|
void __iomem *reg = priv->hrs_addr + SDHCI_CDNS_HRS06;
|
|
u32 tmp;
|
|
int i, ret;
|
|
|
|
if (WARN_ON(!FIELD_FIT(SDHCI_CDNS_HRS06_TUNE, val)))
|
|
return -EINVAL;
|
|
|
|
tmp = readl(reg);
|
|
tmp &= ~SDHCI_CDNS_HRS06_TUNE;
|
|
tmp |= FIELD_PREP(SDHCI_CDNS_HRS06_TUNE, val);
|
|
|
|
/*
|
|
* Workaround for IP errata:
|
|
* The IP6116 SD/eMMC PHY design has a timing issue on receive data
|
|
* path. Send tune request twice.
|
|
*/
|
|
for (i = 0; i < 2; i++) {
|
|
tmp |= SDHCI_CDNS_HRS06_TUNE_UP;
|
|
writel(tmp, reg);
|
|
|
|
ret = readl_poll_timeout(reg, tmp,
|
|
!(tmp & SDHCI_CDNS_HRS06_TUNE_UP),
|
|
0, 1);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* In SD mode, software must not use the hardware tuning and instead perform
|
|
* an almost identical procedure to eMMC.
|
|
*/
|
|
static int sdhci_cdns_execute_tuning(struct sdhci_host *host, u32 opcode)
|
|
{
|
|
int cur_streak = 0;
|
|
int max_streak = 0;
|
|
int end_of_streak = 0;
|
|
int i;
|
|
|
|
/*
|
|
* Do not execute tuning for UHS_SDR50 or UHS_DDR50.
|
|
* The delay is set by probe, based on the DT properties.
|
|
*/
|
|
if (host->timing != MMC_TIMING_MMC_HS200 &&
|
|
host->timing != MMC_TIMING_UHS_SDR104)
|
|
return 0;
|
|
|
|
for (i = 0; i < SDHCI_CDNS_MAX_TUNING_LOOP; i++) {
|
|
if (sdhci_cdns_set_tune_val(host, i) ||
|
|
mmc_send_tuning(host->mmc, opcode, NULL)) { /* bad */
|
|
cur_streak = 0;
|
|
} else { /* good */
|
|
cur_streak++;
|
|
if (cur_streak > max_streak) {
|
|
max_streak = cur_streak;
|
|
end_of_streak = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!max_streak) {
|
|
dev_err(mmc_dev(host->mmc), "no tuning point found\n");
|
|
return -EIO;
|
|
}
|
|
|
|
return sdhci_cdns_set_tune_val(host, end_of_streak - max_streak / 2);
|
|
}
|
|
|
|
static void sdhci_cdns_set_uhs_signaling(struct sdhci_host *host,
|
|
unsigned int timing)
|
|
{
|
|
struct sdhci_cdns_priv *priv = sdhci_cdns_priv(host);
|
|
u32 mode;
|
|
|
|
switch (timing) {
|
|
case MMC_TIMING_MMC_HS:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_SDR;
|
|
break;
|
|
case MMC_TIMING_MMC_DDR52:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_DDR;
|
|
break;
|
|
case MMC_TIMING_MMC_HS200:
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_HS200;
|
|
break;
|
|
case MMC_TIMING_MMC_HS400:
|
|
if (priv->enhanced_strobe)
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_HS400ES;
|
|
else
|
|
mode = SDHCI_CDNS_HRS06_MODE_MMC_HS400;
|
|
break;
|
|
default:
|
|
mode = SDHCI_CDNS_HRS06_MODE_SD;
|
|
break;
|
|
}
|
|
|
|
sdhci_cdns_set_emmc_mode(priv, mode);
|
|
|
|
/* For SD, fall back to the default handler */
|
|
if (mode == SDHCI_CDNS_HRS06_MODE_SD)
|
|
sdhci_set_uhs_signaling(host, timing);
|
|
}
|
|
|
|
static const struct sdhci_ops sdhci_cdns_ops = {
|
|
.set_clock = sdhci_set_clock,
|
|
.get_timeout_clock = sdhci_cdns_get_timeout_clock,
|
|
.set_bus_width = sdhci_set_bus_width,
|
|
.reset = sdhci_reset,
|
|
.platform_execute_tuning = sdhci_cdns_execute_tuning,
|
|
.set_uhs_signaling = sdhci_cdns_set_uhs_signaling,
|
|
};
|
|
|
|
static const struct sdhci_pltfm_data sdhci_cdns_uniphier_pltfm_data = {
|
|
.ops = &sdhci_cdns_ops,
|
|
.quirks2 = SDHCI_QUIRK2_PRESET_VALUE_BROKEN,
|
|
};
|
|
|
|
static const struct sdhci_pltfm_data sdhci_cdns_pltfm_data = {
|
|
.ops = &sdhci_cdns_ops,
|
|
};
|
|
|
|
static void sdhci_cdns_hs400_enhanced_strobe(struct mmc_host *mmc,
|
|
struct mmc_ios *ios)
|
|
{
|
|
struct sdhci_host *host = mmc_priv(mmc);
|
|
struct sdhci_cdns_priv *priv = sdhci_cdns_priv(host);
|
|
u32 mode;
|
|
|
|
priv->enhanced_strobe = ios->enhanced_strobe;
|
|
|
|
mode = sdhci_cdns_get_emmc_mode(priv);
|
|
|
|
if (mode == SDHCI_CDNS_HRS06_MODE_MMC_HS400 && ios->enhanced_strobe)
|
|
sdhci_cdns_set_emmc_mode(priv,
|
|
SDHCI_CDNS_HRS06_MODE_MMC_HS400ES);
|
|
|
|
if (mode == SDHCI_CDNS_HRS06_MODE_MMC_HS400ES && !ios->enhanced_strobe)
|
|
sdhci_cdns_set_emmc_mode(priv,
|
|
SDHCI_CDNS_HRS06_MODE_MMC_HS400);
|
|
}
|
|
|
|
static int sdhci_cdns_probe(struct platform_device *pdev)
|
|
{
|
|
struct sdhci_host *host;
|
|
const struct sdhci_pltfm_data *data;
|
|
struct sdhci_pltfm_host *pltfm_host;
|
|
struct sdhci_cdns_priv *priv;
|
|
struct clk *clk;
|
|
unsigned int nr_phy_params;
|
|
int ret;
|
|
struct device *dev = &pdev->dev;
|
|
static const u16 version = SDHCI_SPEC_400 << SDHCI_SPEC_VER_SHIFT;
|
|
|
|
clk = devm_clk_get(dev, NULL);
|
|
if (IS_ERR(clk))
|
|
return PTR_ERR(clk);
|
|
|
|
ret = clk_prepare_enable(clk);
|
|
if (ret)
|
|
return ret;
|
|
|
|
data = of_device_get_match_data(dev);
|
|
if (!data)
|
|
data = &sdhci_cdns_pltfm_data;
|
|
|
|
nr_phy_params = sdhci_cdns_phy_param_count(dev->of_node);
|
|
host = sdhci_pltfm_init(pdev, data,
|
|
struct_size(priv, phy_params, nr_phy_params));
|
|
if (IS_ERR(host)) {
|
|
ret = PTR_ERR(host);
|
|
goto disable_clk;
|
|
}
|
|
|
|
pltfm_host = sdhci_priv(host);
|
|
pltfm_host->clk = clk;
|
|
|
|
priv = sdhci_pltfm_priv(pltfm_host);
|
|
priv->nr_phy_params = nr_phy_params;
|
|
priv->hrs_addr = host->ioaddr;
|
|
priv->enhanced_strobe = false;
|
|
host->ioaddr += SDHCI_CDNS_SRS_BASE;
|
|
host->mmc_host_ops.hs400_enhanced_strobe =
|
|
sdhci_cdns_hs400_enhanced_strobe;
|
|
sdhci_enable_v4_mode(host);
|
|
__sdhci_read_caps(host, &version, NULL, NULL);
|
|
|
|
sdhci_get_of_property(pdev);
|
|
|
|
ret = mmc_of_parse(host->mmc);
|
|
if (ret)
|
|
goto free;
|
|
|
|
sdhci_cdns_phy_param_parse(dev->of_node, priv);
|
|
|
|
ret = sdhci_cdns_phy_init(priv);
|
|
if (ret)
|
|
goto free;
|
|
|
|
ret = sdhci_add_host(host);
|
|
if (ret)
|
|
goto free;
|
|
|
|
return 0;
|
|
free:
|
|
sdhci_pltfm_free(pdev);
|
|
disable_clk:
|
|
clk_disable_unprepare(clk);
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_PM_SLEEP
|
|
static int sdhci_cdns_resume(struct device *dev)
|
|
{
|
|
struct sdhci_host *host = dev_get_drvdata(dev);
|
|
struct sdhci_pltfm_host *pltfm_host = sdhci_priv(host);
|
|
struct sdhci_cdns_priv *priv = sdhci_pltfm_priv(pltfm_host);
|
|
int ret;
|
|
|
|
ret = clk_prepare_enable(pltfm_host->clk);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = sdhci_cdns_phy_init(priv);
|
|
if (ret)
|
|
goto disable_clk;
|
|
|
|
ret = sdhci_resume_host(host);
|
|
if (ret)
|
|
goto disable_clk;
|
|
|
|
return 0;
|
|
|
|
disable_clk:
|
|
clk_disable_unprepare(pltfm_host->clk);
|
|
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
static const struct dev_pm_ops sdhci_cdns_pm_ops = {
|
|
SET_SYSTEM_SLEEP_PM_OPS(sdhci_pltfm_suspend, sdhci_cdns_resume)
|
|
};
|
|
|
|
static const struct of_device_id sdhci_cdns_match[] = {
|
|
{
|
|
.compatible = "socionext,uniphier-sd4hc",
|
|
.data = &sdhci_cdns_uniphier_pltfm_data,
|
|
},
|
|
{ .compatible = "cdns,sd4hc" },
|
|
{ /* sentinel */ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, sdhci_cdns_match);
|
|
|
|
static struct platform_driver sdhci_cdns_driver = {
|
|
.driver = {
|
|
.name = "sdhci-cdns",
|
|
.pm = &sdhci_cdns_pm_ops,
|
|
.of_match_table = sdhci_cdns_match,
|
|
},
|
|
.probe = sdhci_cdns_probe,
|
|
.remove = sdhci_pltfm_unregister,
|
|
};
|
|
module_platform_driver(sdhci_cdns_driver);
|
|
|
|
MODULE_AUTHOR("Masahiro Yamada <yamada.masahiro@socionext.com>");
|
|
MODULE_DESCRIPTION("Cadence SD/SDIO/eMMC Host Controller Driver");
|
|
MODULE_LICENSE("GPL");
|