mmc相关知识以及dw_mci驱动的一些解析

发布于:2024-07-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

MMC相关知识

what is this?

eMMC是embedded MultiMediaCard的简称,即嵌入式多媒体卡,是一种闪存卡的标准,它定义了基于嵌入式多媒体卡的存储系统的物理架构和访问接口及协议,具体由电子设备工程联合委员会JEDEC订立和发布。它是对MMC的一个拓展,具有体积小,功耗低,容量大等优点,非常适合用作智能手机、平板电脑、移动互联网设备等消费类电子设备的存储介质。

组成

以三星的KLM8G1GETF-B041为例:
由nand flash(闪存),NAND I/O Block(闪存控制芯片),MMC I/O Block组成(标准接口封装)
在这里插入图片描述
eMMC则在其内部集成了 Flash Controller,包括了协议、擦写均衡、坏块管理、ECC校验、电源管理、时钟管理、数据存取等功能。相比于直接将NAND Flash接入到Host 端,eMMC屏蔽了 NAND Flash 的物理特性,可以减少 Host 端软件的复杂度,让 Host 端专注于上层业务,省去对 NAND Flash 进行特殊的处理。同时,eMMC通过使用Cache、Memory Array 等技术,在读写性能上也比 NAND Flash要好很多。

接口

eMMC接口主要实现将eMMC接入到Host的MMC总线上,与Host进行通信,实现eMMC的协议逻辑。
eMMC接口与Host之间的连接如下图:
在这里插入图片描述
其中:
CLK
CLK 信号用于从 Host 端输出时钟信号,进行数据传输的同步和设备运作的驱动。
在一个时钟周期内,CMD 和 DAT0-7 信号上都可以支持传输 1 个比特,即 SDR (Single Data Rate) 模式。此外,DAT0-7 信号还支持配置为 DDR (Double Data Rate) 模式,在一个时钟周期内,可以传输 2 个比特。
Host 可以在通讯过程中动态调整时钟信号的频率(注,频率范围需要满足 Spec 的定义)。通过调整时钟频率,可以实现省电或者数据流控(避免 Over-run 或者 Under-run)功能。 在一些场景中,Host 端还可以关闭时钟,例如 eMMC 处于 Busy 状态时,或者接收完数据,进入 Programming State 时。
CMD
CMD 信号主要用于 Host 向 eMMC 发送 Command 和 eMMC 向 Host 发送对于的 Response。
DAT0-7
DAT0-7 信号主要用于 Host 和 eMMC 之间的数据传输。在 eMMC 上电或者软复位后,只有 DAT0 可以进行数据传输,完成初始化后,可配置 DAT0-3 或者 DAT0-7 进行数据传输,即数据总线可以配置为 4 bits 或者 8 bits 模式。
Data Strobe (数据选通)
Data Strobe 时钟信号由 eMMC 发送给 Host,频率与 CLK 信号相同,用于 Host 端进行数据接收的同步。Data Strobe 信号只能在 HS400 模式下配置启用,启用后可以提高数据传输的稳定性省去总线 tuning 过程

NOTE: Extended CSD byte[183] BUS_WIDTH 寄存器用于配置总线宽度和 Data Strobe

eMMC总线模型

eMMC 总线中,可以有一个 Host,多个 eMMC Devices。总线上的所有通讯都由 Host 端以一个 Command 开始发起,Host 一次只能与一个 eMMC Device 通讯。

系统在上电启动后,Host 会为所有 eMMC Device 逐个分配地址(RCA,Relative device Address)。当 Host 需要和某一个 eMMC Device 通讯时,会先根据 RCA 选中该 eMMC Device,只有被选中的 eMMC Device 才会响应 Host 的 Command。

速率模式

随着 eMMC 协议的版本迭代,eMMC 总线的速率越来越高。为了兼容旧版本的 eMMC Device,所有 Devices 在上电启动或者 Reset 后,都会先进入兼容速率模式(Backward Compatible Mode)。在完成 eMMC Devices 的初始化后,Host 可以通过特定的流程,让 Device 进入其他高速率模式,目前支持以下的几种速率模式。
在这里插入图片描述

NOTE: Extended CSD byte[185] HS_TIMING 寄存器可以配置总线速率模式 Extended CSD
byte[183] BUS_WIDTH 寄存器用于配置总线宽度和 Data Strobe

关键参数

供电

根据“组成”一节的图片中描述:
VCCQ is for Controller power
VCC is for flash power
即:VCC为MMC Controller/Flash Controller的供电电压,VCCQ为Memory和Controller之间I/O的供电。

一个典型硬件电路的设计:
在这里插入图片描述
上电初始化阶段MMC时钟频率为400KHz,需要等电压调整到它要求的VCC时(host去获取OCR中记录的电压值,上面有说),MMC时钟才会调整到更高的正常工作频率。
OCR寄存器描述:
OCR寄存器值操作条件寄存器,存储eMMC的Vccq电压配置,以及包含了状态信息bit,这个状态在eMMC上电进程结束之后被设置。
也即,先后过程是:上电初始化阶段MMC时钟频率为400KHz -> 上电完毕 -> 调整到OCR寄存器中指示的电压 -> 切换到更高频率

对于emmc无需在运行时切换电压,而SD Card比较严格,有这个需求,所以设计时应该加入voltage ragulator。linux内核支持在dts中描述voltage ragulator,并在probe时获取并注册,在上电流程中使用-切换。
在这里插入图片描述

时钟

与SDHC控制器相关的时钟:BIU, CIU, HIU
BIU与CIU
在这里插入图片描述

在这里插入图片描述

HIU
在这里插入图片描述
可以理解为,BIU是读写寄存器和从FIFIO读数据的时钟,而CIU是控制card-specific协议的时钟,提供了时钟控制。
dts中应当配置这些时钟:

&sdhci0 {
	clocks = <&ts_clk TS_CLK_SDHC0_CCLK>, <&ts_clk TS_CLK_SDHC0_AHB_CLK>, <&ts_clk TS_CLK_SDHC0_SMPL_CLK>;
	clock-names = "ciu","hiu", "smpl_clk";
	assigned-clocks = <&ts_clk TS_CLK_SDHC0_CCLK>;
	assigned-clock-rates = <200000000>;
};

其中smpl_clk是sample phase调节时所需要的时钟,平台相关的CLK手册里应该有实现这种时钟,probe时获取,后期tuning时需要使用它。

DW_MCI驱动的初始化流程

从dw_mci_probe开始

dw_mci_probe
	dw_mci_parse_dt
		if (!device_property_read_u32(dev, "clock-frequency", &clock_frequency))
		device_property_read_u32(dev, "card-detect-delay",
						 &pdata->detect_delay_ms);
						pdata->bus_hz = clock_frequency;
	host->hiu_clk = devm_clk_get(host->dev, "hiu");
	host->ciu_clk = devm_clk_get(host->dev, "ciu");
	clk_prepare_enable(host->ciu_clk);
	if (host->pdata->bus_hz) {
		ret = clk_set_rate(host->ciu_clk, host->pdata->bus_hz);
	host->bus_hz = clk_get_rate(host->ciu_clk);
	setup_timer(&host->cmd11_timer,          //专用于cmd11的执行
		    dw_mci_cmd11_timer, (unsigned long)host);

	setup_timer(&host->cto_timer,
		    dw_mci_cto_timer, (unsigned long)host);

	setup_timer(&host->dto_timer,
		    dw_mci_dto_timer, (unsigned long)host);
	INIT_LIST_HEAD(&host->queue);
	tasklet_init(&host->tasklet, dw_mci_tasklet_func, (unsigned long)host);      //host->tasklet
	ret = devm_request_irq(host->dev, host->irq, dw_mci_interrupt,     //请求cd脚的中断
			       host->irq_flags, "dw-mci", host);
	
	dw_mci_init_slot(host);
		mmc_alloc_host(sizeof(struct dw_mci_slot), host->dev);
			INIT_DELAYED_WORK(&host->detect, mmc_rescan);      //host->detect延时任务绑定到了mmc_rescan
			
			INIT_DELAYED_WORK(&host->sdio_irq_work, sdio_irq_work);
			setup_timer(&host->retune_timer, mmc_retune_timer, (unsigned long)host);  //tune定时器任务 
				-->该定时器任务的回调函数只是会将host->need_tune置1
		
		mmc->ops = &dw_mci_ops;
		mmc_regulator_get_supply(mmc);
		mmc_of_parse(mmc);
			//初始化host->caps以及host->caps2
			if (device_property_read_bool(dev, "cap-sd-highspeed"))
				host->caps |= MMC_CAP_SD_HIGHSPEED;
			if (device_property_read_bool(dev, "cap-mmc-highspeed"))
				host->caps |= MMC_CAP_MMC_HIGHSPEED;
			if (device_property_read_bool(dev, "sd-uhs-sdr12"))
				host->caps |= MMC_CAP_UHS_SDR12;
			if (device_property_read_bool(dev, "sd-uhs-sdr25"))
		dw_mci_init_slot_caps(slot);
			int gpio_cd = mmc_gpio_get_cd(mmc);
			present = gpio_cd;
			return present;
				static const struct mmc_host_ops dw_mci_ops = {
					.get_cd			= dw_mci_get_cd,
				}
	dw_mci_enable_cd(host);
		if (host->slot->mmc->caps & MMC_CAP_NEEDS_POLL) //如果需要poll模式探测,则直接退出
				return;
		mmc_gpio_get_cd(host->slot->mmc) 
			gpiod_get_value_cansleep(ctx->cd_gpio);
		temp = mci_readl(host, INTMASK);          //打开CD中断
		temp  |= SDMMC_INT_CD;
		mci_writel(host, INTMASK, temp);
		
	dw_mci_interrupt
		dw_mci_handle_cd
			mmc_detect_change
				_mmc_detect_change
					mmc_schedule_delayed_work(&host->detect, delay);
queue_delayed_work(system_freezable_wq, work, delay);

从mmc_rescan开始:

static const unsigned freqs[] = { 400000, 300000, 200000, 100000 };
//四次尝试:
mmc_rescan
	for (i = 0; i < ARRAY_SIZE(freqs); i++) {
		if (!mmc_rescan_try_freq(host, max(freqs[i], host->f_min))) //如无特别指定f_min=0
			break;
		if (freqs[i] <= host->f_min)
			break;
	}
	
mmc_rescan_try_freq
	从host->caps判断MMC_CAP类型,执行相应card的attach动作
	if (host->caps & MMC_CAP_NEEDS_POLL)
		mmc_schedule_delayed_work(&host->detect, HZ);
		
		/* Order's important: probe SDIO, then SD, then MMC */
		if (!(host->caps2 & MMC_CAP2_NO_SDIO))
			if (!mmc_attach_sdio(host))
				return 0;
	
		if (!(host->caps2 & MMC_CAP2_NO_SD))
			if (!mmc_attach_sd(host))
				return 0;
	
		if (!(host->caps2 & MMC_CAP2_NO_MMC))
			if (!mmc_attach_mmc(host))   //实际的MMC Card检测到attach上了
				return 0;
		
		
	mmc_attach_mmc
		mmc_attach_bus(host)
			host->bus_ops = ops;
		mmc_host_is_spi(host) //看host是否是spi?..
		mmc_select_voltage(host, ocr);
			找到ocr第一个是1的bit,其前三个bit置1
			mmc_power_cycle //下电再上电
		mmc_init_card(host, rocr, NULL);  //older == NULL 探测并初始化卡
			mmc_send_cid(host, cid);
			card = mmc_alloc_card(host, &mmc_type);
			card->ocr = ocr;
			card->type = MMC_TYPE_MMC;
			card->rca = 1;
			memcpy(card->raw_cid, cid, sizeof(card->raw_cid));
			mmc_send_csd(card, card->raw_csd);
			mmc_decode_csd(card); //Card-Specific Data register
				mmc_send_cxd_native(card->host, card->rca << 16,	csd,
							MMC_SEND_CSD);
					cmd.opcode = opcode;  //填充cmd结构,实际读写host寄存器了
					cmd.arg = arg;
					cmd.flags = MMC_RSP_R2 | MMC_CMD_AC;
					mmc_wait_for_cmd(host, &cmd, MMC_CMD_RETRIES);
						struct mmc_request mrq = {};
						mmc_wait_for_req(host, &mrq);
							__mmc_start_req
								//使用了completion信号量
								init_completion(&mrq->completion);
								mrq->done = mmc_wait_done;
								init_completion(&mrq->cmd_completion);
								
								mmc_start_request(host, mrq);
									__mmc_start_request
										mmc_retune(host);  //判断host->need_tune值以确定是否需要tune
										mmc_request_done
											mmc_complete_cmd
												completion_done(&mrq->cmd_completion)
			mmc_decode_cid(card);   //cid主要是一些identity相关的信息
				根据card->csd.mmca_vsn,设置其他mmc_card的字段
					card->cid.manfid	= UNSTUFF_BITS(resp, 104, 24);
					card->cid.prod_name[0]	= UNSTUFF_BITS(resp, 96, 8);
					card->cid.prod_name[1]	= UNSTUFF_BITS(resp, 88, 8);
			mmc_read_ext_csd(card)  //ext_csd,额外的csd信息
				mmc_get_ext_csd(card, &ext_csd);
				mmc_decode_ext_csd(card, ext_csd);
				      通过ext_csd数组对card->ext_csd结构成员的初始化
				mmc_select_card_type:
				      通过判断host->caps与host->caps2,设置avail_type以及hs_max_dtr
					………
					if (caps2 & MMC_CAP2_HS200_1_8V_SDR &&
					    card_type & EXT_CSD_CARD_TYPE_HS200_1_8V) {
						hs200_max_dtr = MMC_HS200_MAX_DTR;
						avail_type |= EXT_CSD_CARD_TYPE_HS200_1_8V;
					}
				
					if (caps2 & MMC_CAP2_HS200_1_2V_SDR &&
					    card_type & EXT_CSD_CARD_TYPE_HS200_1_2V) {
						hs200_max_dtr = MMC_HS200_MAX_DTR;
						avail_type |= EXT_CSD_CARD_TYPE_HS200_1_2V;
					}
					…………
					card->ext_csd.hs_max_dtr = hs_max_dtr;
					card->ext_csd.hs200_max_dtr = hs200_max_dtr;
					card->mmc_avail_type = avail_type;
					
			err = mmc_select_timing(card);
				//通过判断mmc_avail_type支持哪些模式,去初始化各个模式下的timing 
				if (card->mmc_avail_type & EXT_CSD_CARD_TYPE_HS400ES)
					err = mmc_select_hs400es(card);
						//修改ext_csd寄存器
						-->__mmc_switch(card, EXT_CSD_CMD_SET_NORMAL,
									   EXT_CSD_HS_TIMING, EXT_CSD_TIMING_HS,
									   card->ext_csd.generic_cmd6_time, MMC_TIMING_MMC_HS,
									   true, true, true);
							--> mmc_wait_for_cmd
							-->mmc_poll_for_busy
							-->mmc_set_timing
								
					else if (card->mmc_avail_type & EXT_CSD_CARD_TYPE_HS200)
						err = mmc_select_hs200(card);
					else if (card->mmc_avail_type & EXT_CSD_CARD_TYPE_HS)
						err = mmc_select_hs(card);
					哪里初始化了card->mmc_avail_type?
	
			if (mmc_card_hs200(card)) {
				err = mmc_hs200_tuning(card); //hs200需要tunning 看另一章节的调用解释
				if (err)
					goto free_card;
		
				err = mmc_select_hs400(card);
				if (err)
					goto free_card;
			} else if (!mmc_card_hs400es(card)) {
				/* Select the desired bus width optionally */
				err = mmc_select_bus_width(card);
				if (err > 0 && mmc_card_hs(card)) {
					err = mmc_select_hs_ddr(card);
					if (err)
						goto free_card;
				}
			}

DW_MCI 如何配置不同mmc模式

根据上面的初始化过程, 不难看出,最终调用哪个mmc_select_xxx来切换哪种模式,取决于card->mmc_avail_type,而card->mmc_avail_type是在mmc_select_card_type接口中,根据host->caps,host->caps2以及card->ext_csd.raw_card_type来决定的。
其中,host->caps,host->caps2来自对dts的parse,mmc_of_parse接口。而card->ext_csd.raw_card_type,则来自mmc_read_ext_csd(card) //ext_csd,额外的csd信息 --> mmc_get_ext_csd(card, &ext_csd); --> mmc_decode_ext_csd(card, ext_csd);这个过程,从mmc中读取ext_csd字段到内存中,然后decode解码到host的ext_csd成员的各个域中。
因此,配置mmc的模式,是由dts的配置,以及mmc卡本身特性,即ext_csd中的内容决定的。
在dw_mci控制器驱动下,以三星的KLM8G1GETF-B041为例:
在这里插入图片描述
由其datasheet中,DEVICE_TYPE[196]的描述可以看到这款emmc所支持的所有模式。

默认配置下,mmc跑在sdr 25M下:

&sdhci0 {
	status = "okay";
};

如果要配置sdr 52M,则使用:

&sdhci0 {
	status = "okay";
    cap-mmc-highspeed;     //for SDR 52M
};

如果要配置为sdr 200M,则使用:

&sdhci0 {
	status = "okay";
    mmc-hs200-1_8v;          //for hs200 200M
    pinctrl-names = "default";
    pinctrl-0 = <&emmc0_pinctrl>;
    ts,default-sample-phase = <0>;
    ts,desired-num-phases = <360>;
};

sdr200模式,需要执行tune。如果tuning接口没有实现,启动时会报错,以下是mmc_hs200_tunning的调用过程:
在这里插入图片描述

DW_MCI驱动 tune过程

Sampling Tuning 是用于计算 Host 最佳采样时间点的流程,大致的流程如下:

  1. Host 将采样时间点重置为默认值
  2. Host 向 eMMC Device 发送 Send Tuning Block 命令
  3. eMMC Device 向 Host 发送固定的 Tuning Block 数据
  4. Host 接收到 Tuning Block 并进行校验
  5. Host 修改采样时点,重新从第 2 步开始执行,直到 Host 获取到一个有效采样时间点区间
  6. Host 取有效采样时间点区间的中间值作为采样时间点,并推出 Tuning 流程

在dw mobile storage host databook里关于tuning的描述:
在这里插入图片描述
对于SDR104和HS200模式,来自最大输出延迟card可以达到2UI(这里的UI是一个CIU时钟周期),为了采样数据的正确,采样点必须使用tuning来寻找。固件必须使用tuning信息,去调节cclk_in和cclk_in_sample之间的延迟。延迟可以使用phase shifter来实现,最小的phase-shift精度为90°. phase_shift输入必须基于tuning,由host controller应用提供
note里说,建议使用最小22.5°的phase-shift精度,更精确,更好。。(咱就是说,上下文能否不要啪啪打脸)

在这里插入图片描述
tuning commands 和 tuning data
在这里插入图片描述
在这里插入图片描述
手册里给出了更详细的tunning描述,如何调节采样点?这应该和各个平台的实现不同,一个寄存器demo:
在这里插入图片描述

这里给出一个tuning的时序图:
在这里插入图片描述
此处sd_cclk_smpl的移位,由sd_cclk_smpl_ph决定,sd_cclk_smpl_ph的值由上面展示的寄存器的值决定。sd_cclk_smpl每加一,sd_cclk_phase的波形偏移一个pll0的时钟周期(就是图中sd_cclk_src的一个周期,它来自PLL0)。其note注释中提到,sd_cclk_smpl取值不能大于sd_cclk分频系数。如果sd_cclk分频系数给的很小,那么cclk_smpl寄存器可选的值就更小,tune精度就更小。(我理解,如果cclk_smple上限为10,那么tune精度就是360/10 = 36°)

需要了解的是,tuning block data:
Tuning Block 是专门为了 Tuning 而设计的一组特殊数据。相对于普通的数据,这组特殊数据在传输过程中,会更高概率的出现 high SSO noise、deterministic jitter、ISI、timing errors 等问题。这组数据的具体内容如下所示:
在这里插入图片描述
在这里插入图片描述

实际的tune实现

根据前面mmc_hs200_tunning的调用过程,我们需要实现execute_tuning接口:
在这里插入图片描述
这个接口里,需要调用dw_mci_drv_data私有数据结构里提供的tunning接口

static int dw_mci_execute_tuning(struct mmc_host *mmc, u32 opcode)
{
	struct dw_mci_slot *slot = mmc_priv(mmc);
	struct dw_mci *host = slot->host;
	const struct dw_mci_drv_data *drv_data = host->drv_data;
    
	int err = -EINVAL;
	if (drv_data && drv_data->execute_tuning)
		err = drv_data->execute_tuning(slot, opcode);
	return err;
}

该结构会在probe时获取相应data成员,并进行注册
在这里插入图片描述
这里移植了RK的tuning过程,适配到我自己的平台上:

int dw_mci_XXXXX_execute_tuning(struct dw_mci_slot *slot, u32 opcode)
{
#ifdef TS_SDHC_EMMC_RUN_HS200

	struct dw_mci *host = slot->host;
	struct dw_mci_ts_priv_data *priv = host->priv;
	struct mmc_host *mmc = slot->mmc;
	int ret = 0;
    uint32_t sample_clk_rate, parent_of_sample_rate;
	int i;
	bool v, prev_v = 0, first_v;
	struct range_t {
		int start;
		int end; /* inclusive */
	};
	struct range_t *ranges;
	unsigned int range_count = 0;
	int longest_range_len = -1;
	int longest_range = -1;
	int middle_phase;

    parent_of_sample_rate = (2000000000UL); //temp, not elegent
    sample_clk_rate = clk_get_rate(priv->sample_clk);

	if (IS_ERR(priv->sample_clk)) {
		dev_err(host->dev, "Tuning clock (sample_clk) not defined.\n");
		return -EIO;
	}

	ranges = kmalloc_array(priv->num_phases / 2 + 1,
			       sizeof(*ranges), GFP_KERNEL);
	if (!ranges)
		return -ENOMEM;

	/* Try each phase and extract good ranges */
	for (i = 0; i < priv->num_phases; ) {
		clk_set_phase(priv->sample_clk,
			      TUNING_ITERATION_TO_PHASE(i, priv->num_phases));

		v = !mmc_send_tuning(mmc, opcode, NULL);
		if (i == 0)
			first_v = v;

		if ((!prev_v) && v) {
			range_count++;
			ranges[range_count-1].start = i;
		}
		if (v) {
			ranges[range_count-1].end = i;
			i++;
		} else if (i == priv->num_phases - 1) {
			/* No extra skipping rules if we're at the end */
			i++;
		} else {
			/*
			 * No need to check too close to an invalid
			 * one since testing bad phases is slow.  Skip
			 * 20 degrees.
			 */
             pr_debug("parent_of_sample_rate / sample_clk_rate:%d\n", parent_of_sample_rate / sample_clk_rate);
			i += DIV_ROUND_UP((parent_of_sample_rate / sample_clk_rate) * priv->num_phases, 360);

			/* Always test the last one */
			if (i >= priv->num_phases)
				i = priv->num_phases - 1;
		}

		prev_v = v;
	}

	if (range_count == 0) {
		dev_warn(host->dev, "All phases bad!");
		ret = -EIO;
		goto free;
	}

	/* wrap around case, merge the end points */
	if ((range_count > 1) && first_v && v) {
		ranges[0].start = ranges[range_count-1].start;
		range_count--;
	}

	if (ranges[0].start == 0 && ranges[0].end == priv->num_phases - 1) {
		clk_set_phase(priv->sample_clk, priv->default_sample_phase);
		dev_info(host->dev, "All phases work, using default phase %d.",
			 priv->default_sample_phase);
		goto free;
	}

	/* Find the longest range */
	for (i = 0; i < range_count; i++) {
		int len = (ranges[i].end - ranges[i].start + 1);

		if (len < 0)
			len += priv->num_phases;

		if (longest_range_len < len) {
			longest_range_len = len;
			longest_range = i;
		}

		dev_dbg(host->dev, "Good phase range %d-%d (%d len)\n",
			TUNING_ITERATION_TO_PHASE(ranges[i].start,
						  priv->num_phases),
			TUNING_ITERATION_TO_PHASE(ranges[i].end,
						  priv->num_phases),
			len
		);
	}

	dev_dbg(host->dev, "Best phase range %d-%d (%d len)\n",
		TUNING_ITERATION_TO_PHASE(ranges[longest_range].start,
					  priv->num_phases),
		TUNING_ITERATION_TO_PHASE(ranges[longest_range].end,
					  priv->num_phases),
		longest_range_len
	);

	middle_phase = ranges[longest_range].start + longest_range_len / 2;
	middle_phase %= priv->num_phases;
	dev_info(host->dev, "Successfully tuned phase to %d\n",
		 TUNING_ITERATION_TO_PHASE(middle_phase, priv->num_phases));

	clk_set_phase(priv->sample_clk,
		      TUNING_ITERATION_TO_PHASE(middle_phase,
						priv->num_phases));
free:
	kfree(ranges);
	return ret;
#endif

    return 0;
}

该算法会编译所有采样点,通过clk_get_rate去调节sample_clk,往mmc发送tuning block data并进行校验,找到一个最长的可用采样点的分区,以其中间值来设置最终的sample_clk寄存器。

主要注意一下clk_get_rate(priv->sample_clk);接口,该接口需要CCF框架的支持,对CCF框架的理解可以阅读我另一篇博客:
该接口最后就是要根据tunning流中传下来的degrees,计算出一个phase值,写入上面的sample_ph寄存器。
该接口的实现:
在这里插入图片描述

DW_MCI驱动 如何发送命令与数据

TODO: