| /* |
| * Exynos Specific Extensions for Synopsys DW Multimedia Card Interface driver |
| * |
| * Copyright (C) 2012, Samsung Electronics Co., Ltd. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/clk.h> |
| #include <linux/mmc/host.h> |
| #include <linux/mmc/dw_mmc.h> |
| #include <linux/mmc/mmc.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| #include <linux/slab.h> |
| |
| #include "dw_mmc.h" |
| #include "dw_mmc-pltfm.h" |
| |
| #define NUM_PINS(x) (x + 2) |
| |
| #define SDMMC_CLKSEL 0x09C |
| #define SDMMC_CLKSEL_CCLK_SAMPLE(x) (((x) & 7) << 0) |
| #define SDMMC_CLKSEL_CCLK_DRIVE(x) (((x) & 7) << 16) |
| #define SDMMC_CLKSEL_CCLK_DIVIDER(x) (((x) & 7) << 24) |
| #define SDMMC_CLKSEL_GET_DRV_WD3(x) (((x) >> 16) & 0x7) |
| #define SDMMC_CLKSEL_TIMING(x, y, z) (SDMMC_CLKSEL_CCLK_SAMPLE(x) | \ |
| SDMMC_CLKSEL_CCLK_DRIVE(y) | \ |
| SDMMC_CLKSEL_CCLK_DIVIDER(z)) |
| #define SDMMC_CLKSEL_WAKEUP_INT BIT(11) |
| |
| #define EXYNOS4210_FIXED_CIU_CLK_DIV 2 |
| #define EXYNOS4412_FIXED_CIU_CLK_DIV 4 |
| |
| /* Block number in eMMC */ |
| #define DWMCI_BLOCK_NUM 0xFFFFFFFF |
| |
| #define SDMMC_EMMCP_BASE 0x1000 |
| #define SDMMC_MPSECURITY (SDMMC_EMMCP_BASE + 0x0010) |
| #define SDMMC_MPSBEGIN0 (SDMMC_EMMCP_BASE + 0x0200) |
| #define SDMMC_MPSEND0 (SDMMC_EMMCP_BASE + 0x0204) |
| #define SDMMC_MPSCTRL0 (SDMMC_EMMCP_BASE + 0x020C) |
| |
| /* SMU control bits */ |
| #define DWMCI_MPSCTRL_SECURE_READ_BIT BIT(7) |
| #define DWMCI_MPSCTRL_SECURE_WRITE_BIT BIT(6) |
| #define DWMCI_MPSCTRL_NON_SECURE_READ_BIT BIT(5) |
| #define DWMCI_MPSCTRL_NON_SECURE_WRITE_BIT BIT(4) |
| #define DWMCI_MPSCTRL_USE_FUSE_KEY BIT(3) |
| #define DWMCI_MPSCTRL_ECB_MODE BIT(2) |
| #define DWMCI_MPSCTRL_ENCRYPTION BIT(1) |
| #define DWMCI_MPSCTRL_VALID BIT(0) |
| |
| #define EXYNOS_CCLKIN_MIN 50000000 /* unit: HZ */ |
| |
| /* Variations in Exynos specific dw-mshc controller */ |
| enum dw_mci_exynos_type { |
| DW_MCI_TYPE_EXYNOS4210, |
| DW_MCI_TYPE_EXYNOS4412, |
| DW_MCI_TYPE_EXYNOS5250, |
| DW_MCI_TYPE_EXYNOS5420, |
| DW_MCI_TYPE_EXYNOS5420_SMU, |
| }; |
| |
| /* Exynos implementation specific driver private data */ |
| struct dw_mci_exynos_priv_data { |
| enum dw_mci_exynos_type ctrl_type; |
| u8 ciu_div; |
| u32 sdr_timing; |
| u32 ddr_timing; |
| u32 cur_speed; |
| }; |
| |
| static struct dw_mci_exynos_compatible { |
| char *compatible; |
| enum dw_mci_exynos_type ctrl_type; |
| } exynos_compat[] = { |
| { |
| .compatible = "samsung,exynos4210-dw-mshc", |
| .ctrl_type = DW_MCI_TYPE_EXYNOS4210, |
| }, { |
| .compatible = "samsung,exynos4412-dw-mshc", |
| .ctrl_type = DW_MCI_TYPE_EXYNOS4412, |
| }, { |
| .compatible = "samsung,exynos5250-dw-mshc", |
| .ctrl_type = DW_MCI_TYPE_EXYNOS5250, |
| }, { |
| .compatible = "samsung,exynos5420-dw-mshc", |
| .ctrl_type = DW_MCI_TYPE_EXYNOS5420, |
| }, { |
| .compatible = "samsung,exynos5420-dw-mshc-smu", |
| .ctrl_type = DW_MCI_TYPE_EXYNOS5420_SMU, |
| }, |
| }; |
| |
| static int dw_mci_exynos_priv_init(struct dw_mci *host) |
| { |
| struct dw_mci_exynos_priv_data *priv = host->priv; |
| |
| if (priv->ctrl_type == DW_MCI_TYPE_EXYNOS5420_SMU) { |
| mci_writel(host, MPSBEGIN0, 0); |
| mci_writel(host, MPSEND0, DWMCI_BLOCK_NUM); |
| mci_writel(host, MPSCTRL0, DWMCI_MPSCTRL_SECURE_WRITE_BIT | |
| DWMCI_MPSCTRL_NON_SECURE_READ_BIT | |
| DWMCI_MPSCTRL_VALID | |
| DWMCI_MPSCTRL_NON_SECURE_WRITE_BIT); |
| } |
| |
| return 0; |
| } |
| |
| static int dw_mci_exynos_setup_clock(struct dw_mci *host) |
| { |
| struct dw_mci_exynos_priv_data *priv = host->priv; |
| unsigned long rate = clk_get_rate(host->ciu_clk); |
| |
| host->bus_hz = rate / (priv->ciu_div + 1); |
| return 0; |
| } |
| |
| #ifdef CONFIG_PM_SLEEP |
| static int dw_mci_exynos_suspend(struct device *dev) |
| { |
| struct dw_mci *host = dev_get_drvdata(dev); |
| |
| return dw_mci_suspend(host); |
| } |
| |
| static int dw_mci_exynos_resume(struct device *dev) |
| { |
| struct dw_mci *host = dev_get_drvdata(dev); |
| |
| dw_mci_exynos_priv_init(host); |
| return dw_mci_resume(host); |
| } |
| |
| /** |
| * dw_mci_exynos_resume_noirq - Exynos-specific resume code |
| * |
| * On exynos5420 there is a silicon errata that will sometimes leave the |
| * WAKEUP_INT bit in the CLKSEL register asserted. This bit is 1 to indicate |
| * that it fired and we can clear it by writing a 1 back. Clear it to prevent |
| * interrupts from going off constantly. |
| * |
| * We run this code on all exynos variants because it doesn't hurt. |
| */ |
| |
| static int dw_mci_exynos_resume_noirq(struct device *dev) |
| { |
| struct dw_mci *host = dev_get_drvdata(dev); |
| u32 clksel; |
| |
| clksel = mci_readl(host, CLKSEL); |
| if (clksel & SDMMC_CLKSEL_WAKEUP_INT) |
| mci_writel(host, CLKSEL, clksel); |
| |
| return 0; |
| } |
| #else |
| #define dw_mci_exynos_suspend NULL |
| #define dw_mci_exynos_resume NULL |
| #define dw_mci_exynos_resume_noirq NULL |
| #endif /* CONFIG_PM_SLEEP */ |
| |
| static void dw_mci_exynos_prepare_command(struct dw_mci *host, u32 *cmdr) |
| { |
| /* |
| * Exynos4412 and Exynos5250 extends the use of CMD register with the |
| * use of bit 29 (which is reserved on standard MSHC controllers) for |
| * optionally bypassing the HOLD register for command and data. The |
| * HOLD register should be bypassed in case there is no phase shift |
| * applied on CMD/DATA that is sent to the card. |
| */ |
| if (SDMMC_CLKSEL_GET_DRV_WD3(mci_readl(host, CLKSEL))) |
| *cmdr |= SDMMC_CMD_USE_HOLD_REG; |
| } |
| |
| static void dw_mci_exynos_set_ios(struct dw_mci *host, struct mmc_ios *ios) |
| { |
| struct dw_mci_exynos_priv_data *priv = host->priv; |
| unsigned int wanted = ios->clock; |
| unsigned long actual; |
| u8 div = priv->ciu_div + 1; |
| |
| if (ios->timing == MMC_TIMING_UHS_DDR50) { |
| mci_writel(host, CLKSEL, priv->ddr_timing); |
| /* Should be double rate for DDR mode */ |
| if (ios->bus_width == MMC_BUS_WIDTH_8) |
| wanted <<= 1; |
| } else { |
| mci_writel(host, CLKSEL, priv->sdr_timing); |
| } |
| |
| /* Don't care if wanted clock is zero */ |
| if (!wanted) |
| return; |
| |
| /* Guaranteed minimum frequency for cclkin */ |
| if (wanted < EXYNOS_CCLKIN_MIN) |
| wanted = EXYNOS_CCLKIN_MIN; |
| |
| if (wanted != priv->cur_speed) { |
| int ret = clk_set_rate(host->ciu_clk, wanted * div); |
| if (ret) |
| dev_warn(host->dev, |
| "failed to set clk-rate %u error: %d\n", |
| wanted * div, ret); |
| actual = clk_get_rate(host->ciu_clk); |
| host->bus_hz = actual / div; |
| priv->cur_speed = wanted; |
| host->current_speed = 0; |
| } |
| } |
| |
| static int dw_mci_exynos_parse_dt(struct dw_mci *host) |
| { |
| struct dw_mci_exynos_priv_data *priv; |
| struct device_node *np = host->dev->of_node; |
| u32 timing[2]; |
| u32 div = 0; |
| int idx; |
| int ret; |
| |
| priv = devm_kzalloc(host->dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) { |
| dev_err(host->dev, "mem alloc failed for private data\n"); |
| return -ENOMEM; |
| } |
| |
| for (idx = 0; idx < ARRAY_SIZE(exynos_compat); idx++) { |
| if (of_device_is_compatible(np, exynos_compat[idx].compatible)) |
| priv->ctrl_type = exynos_compat[idx].ctrl_type; |
| } |
| |
| if (priv->ctrl_type == DW_MCI_TYPE_EXYNOS4412) |
| priv->ciu_div = EXYNOS4412_FIXED_CIU_CLK_DIV - 1; |
| else if (priv->ctrl_type == DW_MCI_TYPE_EXYNOS4210) |
| priv->ciu_div = EXYNOS4210_FIXED_CIU_CLK_DIV - 1; |
| else { |
| of_property_read_u32(np, "samsung,dw-mshc-ciu-div", &div); |
| priv->ciu_div = div; |
| } |
| |
| ret = of_property_read_u32_array(np, |
| "samsung,dw-mshc-sdr-timing", timing, 2); |
| if (ret) |
| return ret; |
| |
| priv->sdr_timing = SDMMC_CLKSEL_TIMING(timing[0], timing[1], div); |
| |
| ret = of_property_read_u32_array(np, |
| "samsung,dw-mshc-ddr-timing", timing, 2); |
| if (ret) |
| return ret; |
| |
| priv->ddr_timing = SDMMC_CLKSEL_TIMING(timing[0], timing[1], div); |
| host->priv = priv; |
| return 0; |
| } |
| |
| static inline u8 dw_mci_exynos_get_clksmpl(struct dw_mci *host) |
| { |
| return SDMMC_CLKSEL_CCLK_SAMPLE(mci_readl(host, CLKSEL)); |
| } |
| |
| static inline void dw_mci_exynos_set_clksmpl(struct dw_mci *host, u8 sample) |
| { |
| u32 clksel; |
| clksel = mci_readl(host, CLKSEL); |
| clksel = (clksel & ~0x7) | SDMMC_CLKSEL_CCLK_SAMPLE(sample); |
| mci_writel(host, CLKSEL, clksel); |
| } |
| |
| static inline u8 dw_mci_exynos_move_next_clksmpl(struct dw_mci *host) |
| { |
| u32 clksel; |
| u8 sample; |
| |
| clksel = mci_readl(host, CLKSEL); |
| sample = (clksel + 1) & 0x7; |
| clksel = (clksel & ~0x7) | sample; |
| mci_writel(host, CLKSEL, clksel); |
| return sample; |
| } |
| |
| static s8 dw_mci_exynos_get_best_clksmpl(u8 candiates) |
| { |
| const u8 iter = 8; |
| u8 __c; |
| s8 i, loc = -1; |
| |
| for (i = 0; i < iter; i++) { |
| __c = ror8(candiates, i); |
| if ((__c & 0xc7) == 0xc7) { |
| loc = i; |
| goto out; |
| } |
| } |
| |
| for (i = 0; i < iter; i++) { |
| __c = ror8(candiates, i); |
| if ((__c & 0x83) == 0x83) { |
| loc = i; |
| goto out; |
| } |
| } |
| |
| out: |
| return loc; |
| } |
| |
| static int dw_mci_exynos_execute_tuning(struct dw_mci_slot *slot, u32 opcode, |
| struct dw_mci_tuning_data *tuning_data) |
| { |
| struct dw_mci *host = slot->host; |
| struct mmc_host *mmc = slot->mmc; |
| const u8 *blk_pattern = tuning_data->blk_pattern; |
| u8 *blk_test; |
| unsigned int blksz = tuning_data->blksz; |
| u8 start_smpl, smpl, candiates = 0; |
| s8 found = -1; |
| int ret = 0; |
| |
| blk_test = kmalloc(blksz, GFP_KERNEL); |
| if (!blk_test) |
| return -ENOMEM; |
| |
| start_smpl = dw_mci_exynos_get_clksmpl(host); |
| |
| do { |
| struct mmc_request mrq = {NULL}; |
| struct mmc_command cmd = {0}; |
| struct mmc_command stop = {0}; |
| struct mmc_data data = {0}; |
| struct scatterlist sg; |
| |
| cmd.opcode = opcode; |
| cmd.arg = 0; |
| cmd.flags = MMC_RSP_R1 | MMC_CMD_ADTC; |
| |
| stop.opcode = MMC_STOP_TRANSMISSION; |
| stop.arg = 0; |
| stop.flags = MMC_RSP_R1B | MMC_CMD_AC; |
| |
| data.blksz = blksz; |
| data.blocks = 1; |
| data.flags = MMC_DATA_READ; |
| data.sg = &sg; |
| data.sg_len = 1; |
| |
| sg_init_one(&sg, blk_test, blksz); |
| mrq.cmd = &cmd; |
| mrq.stop = &stop; |
| mrq.data = &data; |
| host->mrq = &mrq; |
| |
| mci_writel(host, TMOUT, ~0); |
| smpl = dw_mci_exynos_move_next_clksmpl(host); |
| |
| mmc_wait_for_req(mmc, &mrq); |
| |
| if (!cmd.error && !data.error) { |
| if (!memcmp(blk_pattern, blk_test, blksz)) |
| candiates |= (1 << smpl); |
| } else { |
| dev_dbg(host->dev, |
| "Tuning error: cmd.error:%d, data.error:%d\n", |
| cmd.error, data.error); |
| } |
| } while (start_smpl != smpl); |
| |
| found = dw_mci_exynos_get_best_clksmpl(candiates); |
| if (found >= 0) |
| dw_mci_exynos_set_clksmpl(host, found); |
| else |
| ret = -EIO; |
| |
| kfree(blk_test); |
| return ret; |
| } |
| |
| /* Common capabilities of Exynos4/Exynos5 SoC */ |
| static unsigned long exynos_dwmmc_caps[4] = { |
| MMC_CAP_UHS_DDR50 | MMC_CAP_1_8V_DDR | |
| MMC_CAP_8_BIT_DATA | MMC_CAP_CMD23, |
| MMC_CAP_CMD23, |
| MMC_CAP_CMD23, |
| MMC_CAP_CMD23, |
| }; |
| |
| static const struct dw_mci_drv_data exynos_drv_data = { |
| .caps = exynos_dwmmc_caps, |
| .init = dw_mci_exynos_priv_init, |
| .setup_clock = dw_mci_exynos_setup_clock, |
| .prepare_command = dw_mci_exynos_prepare_command, |
| .set_ios = dw_mci_exynos_set_ios, |
| .parse_dt = dw_mci_exynos_parse_dt, |
| .execute_tuning = dw_mci_exynos_execute_tuning, |
| }; |
| |
| static const struct of_device_id dw_mci_exynos_match[] = { |
| { .compatible = "samsung,exynos4412-dw-mshc", |
| .data = &exynos_drv_data, }, |
| { .compatible = "samsung,exynos5250-dw-mshc", |
| .data = &exynos_drv_data, }, |
| { .compatible = "samsung,exynos5420-dw-mshc", |
| .data = &exynos_drv_data, }, |
| { .compatible = "samsung,exynos5420-dw-mshc-smu", |
| .data = &exynos_drv_data, }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, dw_mci_exynos_match); |
| |
| static int dw_mci_exynos_probe(struct platform_device *pdev) |
| { |
| const struct dw_mci_drv_data *drv_data; |
| const struct of_device_id *match; |
| |
| match = of_match_node(dw_mci_exynos_match, pdev->dev.of_node); |
| drv_data = match->data; |
| return dw_mci_pltfm_register(pdev, drv_data); |
| } |
| |
| const struct dev_pm_ops dw_mci_exynos_pmops = { |
| SET_SYSTEM_SLEEP_PM_OPS(dw_mci_exynos_suspend, dw_mci_exynos_resume) |
| .resume_noirq = dw_mci_exynos_resume_noirq, |
| .thaw_noirq = dw_mci_exynos_resume_noirq, |
| .restore_noirq = dw_mci_exynos_resume_noirq, |
| }; |
| |
| static struct platform_driver dw_mci_exynos_pltfm_driver = { |
| .probe = dw_mci_exynos_probe, |
| .remove = __exit_p(dw_mci_pltfm_remove), |
| .driver = { |
| .name = "dwmmc_exynos", |
| .of_match_table = dw_mci_exynos_match, |
| .pm = &dw_mci_exynos_pmops, |
| }, |
| }; |
| |
| module_platform_driver(dw_mci_exynos_pltfm_driver); |
| |
| MODULE_DESCRIPTION("Samsung Specific DW-MSHC Driver Extension"); |
| MODULE_AUTHOR("Thomas Abraham <thomas.ab@samsung.com"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("platform:dwmmc-exynos"); |