从设备树到驱动源码:揭秘嵌入式 Linux 中 MMC 子系统的统一与差异

本文关注于 SD/EMMC 的驱动开发,协议相关的内容,请阅读:

SD卡协议详解:从物理结构到通信流程

1、引言:一个令人困惑的现象

在嵌入式 Linux 开发中,我们经常在设备树(Device Tree)中看到 sd、emmc、mmc 等相关的节点定义。

例如,在 Allwinner 或 Rockchip 的设备树中,你可能会看到如下配置:

c 复制代码
	aliases {
		sunxi-mmc0 = &sdc0;
		sunxi-mmc2 = &sdc2;
		......
	}
	
	sdc0: sdmmc@01c0f000 {
		compatible = "allwinner,sunxi-mmc-v4p1x";
		device_type = "sdc0";
		reg = <0x0 0x01c0f000 0x0 0x1000>; /* only sdmmc0 */
		interrupts = <GIC_SPI 32 0x0104>;  /*  */
		......
	}
	
	sdc1: sdmmc@1C10000 {
		compatible = "allwinner,sunxi-mmc-v4p1x";
		device_type = "sdc1";
		reg = <0x0 0x1C10000 0x0 0x1000>;
		interrupts = <GIC_SPI 33 0x0104>;  /*  */
		......
	}

注:由芯片手册可知,这里的 sdc0 节点为 sd 卡控制器;sdc1 节点为 eMMC 控制器


初学者往往会产生疑问:

👉 SD 卡和 eMMC 明明是两种不同的存储设备,为什么在设备树中却共用一种控制器?共享一套驱动代码?

要理解这个问题,可以从一个核心抽象入手:

存储设备访问 = Host Controller(硬件) + Protocol(协议)

  • Host Controller:负责"怎么发信号"
  • Protocol:决定"发什么命令"

2、技术根源:同根同源的演进史

SD 卡和 eMMC 能够复用 Host Controller,最根本的原因在于它们拥有共同的"祖先"------ MMC(MultiMediaCard)


早期的存储标准是 MMC(MultiMediaCard),其定义了一套基础的:

  • 命令机制(CMD)
  • 数据传输方式(DATA)
  • 时序规范

在此基础上,逐渐演化出两个主要分支:

(1)SD(Secure Digital)

  • 由 SD 协会主导发展
  • 在 MMC 基础上扩展而来
  • 增加了:
    • 更丰富的命令体系(如 ACMD)
    • 卡识别与安全机制
    • 广泛用于可插拔存储(SD 卡)

(2)eMMC(embedded MMC)

  • 由 JEDEC 标准组织定义
  • 本质上是 MMC 的"嵌入式版本"
  • 特点:
    • 焊接在板子上(不可插拔)
    • 集成控制器(Flash + Controller)
    • 增加高级特性:
      • Boot 分区
      • RPMB(安全存储)
      • 高速模式(HS200 / HS400)

可以这样理解三者关系:

MMC 是"祖先",SD 和 eMMC 是两个分支演化方向。

2.1 硬件接口的高度一致性

无论是 SD 还是 eMMC,它们在物理层(Physical Layer)和链路层(Link Layer)都保留了 MMC 的核心基因:

  • 信号线一致:都包含命令线(CMD)、时钟线(CLK)和数据线(DATA)。
  • 总线拓扑一致:都采用主从模式(Master-Slave),由 Host 发起通信。
  • 时序模型一致:都依赖于时钟同步数据传输。

因此,硬件厂商设计的 Host Controller,本质上是为了驱动"MMC 总线"而设计的。只要物理接口引脚定义兼容,同一个控制器就可以挂载 SD 卡或 eMMC 芯片。

3、软件抽象:SDHCI 标准化接口

3.1 现实的挑战:厂商自定义控制器

虽然我们在上一章提到 MMC/SD/eMMC 在理论上拥有共同的接口规范,但在实际的芯片设计中,硬件控制器的最终定义权掌握在 SoC 厂商手中

并不是所有的厂商都会严格遵循 SDHCI 标准,原因通常包括:

  • 追求极致性能:厂商可能设计了私有的 DMA 引擎或流水线优化,标准的 SDHCI 寄存器无法描述这些高级特性。
  • 历史遗留或架构差异:某些早期的 IP 核或特定架构(如某些 ARM PL081 DMA 配合 MMC)并不兼容 SDHCI 的寄存器映射。
  • 成本与面积考量:为了节省芯片面积,厂商可能会裁剪掉 SDHCI 中某些"非必需"的功能位。

👉 因此,Linux 内核必须同时支持两种模式:

  1. 标准模式:针对符合 SDHCI 规范的控制器,使用通用的 sdhci.c 驱动。
  2. 私有模式:针对厂商自定义的控制器(如 DesignWare-based 的非 SDHCI 兼容模式),需要编写特定的 Host Driver(如 dw_mmc.c)。

3.2 理想的解决方案:SDHCI 标准化接口

为了解决驱动碎片化的问题,SD 协会推出了 SDHCI(SD Host Controller Interface)

SDHCI 的核心价值在于"标准化" :它定义了一套统一的寄存器布局(Register Map)。只要硬件厂商愿意遵循这一规范(例如 RK3568 的部分控制器),Linux 就可以使用通用的 drivers/mmc/host/sdhci.c 驱动,真正做到"一次开发,处处运行"。

3.3 内核的兼容策略

Linux MMC 子系统采用了"通用核心 + 多种 Host 适配层"的策略:

  • Core 层:处理协议(SD/eMMC 协议差异)。
  • Host 层:
    • sdhci 驱动:用于符合 SDHCI 标准的硬件(标准化)。
    • dw_mmc / sunxi-mmc 等驱动:用于非标准的厂商自定义硬件(私有化)。

4、案例实战:RK3568 的控制器配置

为了更直观地理解,我们以 RK3568 平台为例。RK3568 集成了多个 MMC 控制器,其中既有遵循 SDHCI 标准的,也有非标准的。

arch\arm64\boot\dts\rockchip.rk3568-evb1-ddr4-v10-linux.dtb.dts.tmp

dts 复制代码
 aliases {
	mmc0 = &sdhci;
	mmc1 = &sdmmc0;
	mmc2 = &sdmmc1;
	mmc3 = &sdmmc2;
 }; 

4.1 标准 SDHCI 路线 (sdhci)

在设备树中,sdhci 节点通常用于连接 eMMC,因为它支持更宽的总线:

dts 复制代码
 sdhci: sdhci@fe310000 {
  compatible = "rockchip,rk3568-dwcmshc", "rockchip,dwcmshc-sdhci";
  reg = <0x0 0xfe310000 0x0 0x10000>;
  interrupts = <0 19 4>;
  ......
 };

&sdhci {
	bus-width = <8>; 	/* 8-bit 总线,典型 eMMC 配置 */
	no-sdio;   			/* 非 SDIO */
	no-sd;				/* 非 SD 卡控制器 */
	non-removable;		/* 不可插拔,更像板载 eMMC */
	max-frequency = <200000000>;
	full-pwr-cycle-in-suspend;
	status = "okay";
};

这里最关键的是:

dts 复制代码
compatible = "rockchip,rk3568-dwcmshc", "rockchip,dwcmshc-sdhci";

其控制器驱动,本质上是 Synopsys DWC MSHC。但我们通过 RK3568 芯片手册,发现它走的是 SDHCI 兼容框架。因此走通用驱动路径。

对应驱动是:

c 复制代码
drivers/mmc/host/sdhci-of-dwcmshc.c

4.2 非标准路线 (dw_mmc)

RK3568 还有其他三路控制器(sdmmc0/1/2),它们使用的是 dw_mmc 驱动框架:

dts 复制代码
sdmmc0: dwmmc@fe2b0000 {
	compatible = "rockchip,rk3568-dw-mshc",
		     "rockchip,rk3288-dw-mshc";
	...
};

sdmmc1: dwmmc@fe2c0000 {
	compatible = "rockchip,rk3568-dw-mshc",
		     "rockchip,rk3288-dw-mshc";
	...
};

sdmmc2: dwmmc@fe000000 {
	compatible = "rockchip,rk3568-dw-mshc",
		     "rockchip,rk3288-dw-mshc";
	...
};

这些控制器虽然也是基于 Synopsys IP,但寄存器接口未完全遵循 SDHCI 标准,因此 Linux 使用了专门的 dw_mmc 通用驱动(dw_mmc.c + dw_mmc-rockchip.c)。

  • 通用核心:drivers/mmc/host/dw_mmc.c
  • Rockchip 扩展:drivers/mmc/host/dw_mmc-rockchip.c

这类控制器可以理解为:

它也是 Synopsys 的一类 MMC Host Controller,但寄存器接口不是按 SDHCI 通用层来走,所以 Linux 需要单独的一套 host driver。

5、源码深度解析

在 Linux MMC 子系统中,Host Driver(主机驱动)与 Core 层的交互是通过一组标准的回调函数集实现的。我们以 sdhci 驱动为例,深入探究其内部机制。

5.1 驱动接口:mmc_host_ops

drivers/mmc/host/sdhci.c 中,定义了 sdhci_ops 结构体。这是驱动向 Core 层提供的"操作说明书",它告诉 Core 层:"如果需要执行某个操作(如发送请求、设置时钟),请调用我这里指定的函数"。

核心代码:

c 复制代码
static const struct mmc_host_ops sdhci_ops = {
	.request	= sdhci_request,		// 核心:处理读写请求
	.post_req	= sdhci_post_req,		// 请求后处理(如 DMA 同步)
	.pre_req	= sdhci_pre_req,		// 请求前处理(如 DMA 映射)
	
	.set_ios	= sdhci_set_ios, 		// 核心:配置总线参数 (时钟、电压、电源、总线宽度)
	
	.get_cd		= sdhci_get_cd, 		// 查询卡是否存在
	.get_ro		= sdhci_get_ro,			// 查询写保护状态
	.hw_reset	= sdhci_hw_reset, 		// 硬件复位
	
	.enable_sdio_irq = sdhci_enable_sdio_irq,	// 使能 SDIO 中断
	.ack_sdio_irq    = sdhci_ack_sdio_irq,		// 确认 SDIO 中断
	
	.start_signal_voltage_switch	= sdhci_start_signal_voltage_switch, 	// 电压切换
	
	 // 高速模式的关键:时序校准
	.prepare_hs400_tuning		= sdhci_prepare_hs400_tuning, // (可选) 为 HS400 模式准备 Tuning
	.execute_tuning			= sdhci_execute_tuning,			  // 核心:高速模式下的采样点校准

	.card_event			= sdhci_card_event,	// 处理卡插拔事件
	.card_busy	= sdhci_card_busy,			// 查询卡是否处于 Busy 状态 (DAT0 拉低)
};

架构流程:

驱动通过 mmc_add_host 接口将 mmc_host_ops 注册到 MMC Core 层。

bash 复制代码
dwcmshc_probe
	--> __sdhci_add_host  
		--> mmc_add_host

5.2 核心数据结构:mmc_request

既然 sdhci_request 是入口,那么它处理的"货物"是什么呢?答案是 struct mmc_request。这是 Core 层传递给 Host Driver 的"任务单"。

include/linux/mmc/core.h 中,一次 MMC/SD 通信被抽象为一个完整的事务(Transaction),通常包含四个部分:

  • sbc (Set Block Count):可选,如 CMD23。
  • cmd (Command):主命令,如读命令 CMD17 或写命令 CMD24。
  • data (Data):数据传输描述符,包含缓冲区、块大小和数量。
  • stop (Stop):传输结束命令,如 CMD12。
c 复制代码
struct mmc_request {
	struct mmc_command	*sbc;		/* SET_BLOCK_COUNT for multiblock */
	struct mmc_command	*cmd;
	struct mmc_data		*data;
	struct mmc_command	*stop;

	struct completion	completion;
	struct completion	cmd_completion;
	void			(*done)(struct mmc_request *);/* completion function */
	......
};
5.2.1 命令描述符:mmc_command

mmc_request 只是外壳,真正的指令细节由 mmc_command 承载。在 include/linux/mmc/core.h 中,它定义了一条具体命令的所有属性:

c 复制代码
struct mmc_command {
    u32 opcode;             // 命令索引
    u32 arg;                // 命令参数
    u32 resp[4];            // 卡返回的响应
    unsigned int flags;     // 关键:指定响应类型和命令类型
    unsigned int retries;   // 重试次数
    int error;              // 错误码
    unsigned int busy_timeout; // 忙等待超时
    struct mmc_data *data;  // 关联的数据段
    struct mmc_request *mrq; // 所属的请求
};

关键字段解析:

  • opcode
    即 SD/MMC 协议中的命令索引。例如:
    • CMD17 (READ_SINGLE_BLOCK)
    • CMD18 (READ_MULTIPLE_BLOCK)
    • CMD24 (WRITE_BLOCK)
    • CMD25 (WRITE_MULTIPLE_BLOCK)
  • arg
    命令的参数。对于读写命令,这通常是数据的块地址。
  • resp[4]
    命令发送后,卡会返回响应(Response)。驱动会将硬件读取到的响应值保存在这里,供 Core 层解析(
    如解析卡的状态寄存器 OCR, R1, R2 等)。
  • flags
    这是驱动最关注的字段,它告诉硬件控制器"期望什么样的响应"以及"这是什么类型的命令"。
    • 响应类型:如 MMC_RSP_R1 (短响应,带CRC)、MMC_RSP_R1B (带忙信号)、MMC_RSP_R2 (长响应,136位)。
    • 命令类型:如 MMC_CMD_ADTC (地址+数据传输)、MMC_CMD_AC (仅地址,无数据)。
5.2.2 数据描述符:mmc_data

如果 mmc_command 是"发号施令",那么 mmc_data 就是"物资运输"。它描述了数据传输阶段的详细参数:

c 复制代码
struct mmc_data {
    unsigned int blksz;         // 块大小 (通常为 512 字节)
    unsigned int blocks;        // 块数量
    unsigned int blk_addr;      // 起始块地址
    int error;                  // 数据错误码
    unsigned int flags;         // 关键:读/写方向
    unsigned int bytes_xfered;  // 实际传输的字节数
    struct mmc_command *stop;   // 停止命令 (如 CMD12)
    struct mmc_request *mrq;    // 所属的请求
    struct scatterlist *sg;     // 内存缓冲区描述符
    int sg_count;               // 散列列表条目数
};

关键字段解析:

  • blksz & blocks
    定义了传输的规模。例如读取 4KB 数据,通常是 blksz=512, blocks=8。
  • flags
    明确指示数据传输的方向,驱动会根据此标志配置硬件控制器的读写位:
    • MMC_DATA_READ:从卡读取数据到内存。
    • MMC_DATA_WRITE:从内存写入数据到卡。
  • sg (Scatterlist):
  • 这是 Linux 内核处理 I/O 的标准方式。数据在内存中不一定是连续的,sg 链表描述了数据在物理内存中的实际分布,硬件 DMA 引擎
  • 根据这个链表自动搬运数据,无需驱动进行内存拷贝。
  • stop
    • 对于多块传输(如 CMD18/CMD25),传输结束后需要发送 CMD12 来停止传输,这个指针就指向那个停止命令。
5.2.3 一次典型的通信事务

以"多块读"为例,可以把一次通信理解成:

text 复制代码
sbc  = CMD23,可选,先告诉卡后面要传多少块
cmd  = CMD18,开始多块读
data = 描述要读多少块、每块多大、数据缓冲区在哪里
stop = CMD12,必要时停止传输

以"多块写"为例则类似:

text 复制代码
sbc  = CMD23
cmd  = CMD25
data = 写数据缓冲区
stop = CMD12

所以从软件抽象角度看:

一次 SD 通信,就是把命令阶段和数据阶段装进 mmc_request,再交给 host controller driver 执行。

5.3 请求处理流程 (sdhci_request)

驱动的核心函数 sdhci_request 负责将 mmc_request 转化为硬件操作。

c 复制代码
void sdhci_request(struct mmc_host *mmc, struct mmc_request *mrq)
{
    struct sdhci_host *host = mmc_priv(mmc);
    struct mmc_command *cmd;
    unsigned long flags;
    bool present;

    // 1. 状态检查:首先检查卡是否存在
    present = mmc->ops->get_cd(mmc); 
    spin_lock_irqsave(&host->lock, flags);
    sdhci_led_activate(host); // 点亮 LED 指示灯

    if (sdhci_present_error(host, mrq->cmd, present))
        goto out_finish; // 如果卡不存在或状态异常,直接结束请求

    // 2. 命令预处理:处理 CMD23 (Set Block Count)
    // 某些控制器支持手动发送 CMD23,这里决定是否将其作为第一个命令发送
    cmd = sdhci_manual_cmd23(host, mrq) ? mrq->sbc : mrq->cmd;

    // 3. 发送命令
    // sdhci_send_command_retry 会尝试发送命令,并处理可能的传输错误重试
    if (!sdhci_send_command_retry(host, cmd, flags))
        goto out_finish;

    // 4. 成功提交
    spin_unlock_irqrestore(&host->lock, flags);
    return;

out_finish:
    // 5. 错误处理:如果在上述任何步骤出错,直接标记请求完成并返回错误
    sdhci_finish_mrq(host, mrq);
    spin_unlock_irqrestore(&host->lock, flags);
}

5.4 命令发送细节 (sdhci_send_command)

sdhci_request 决定发送命令时,会调用底层的发送函数:

c 复制代码
static bool sdhci_send_command(struct sdhci_host *host, struct mmc_command *cmd)
{
    int flags;
    u32 mask;
    unsigned long timeout;

    WARN_ON(host->cmd); // 调试:确保没有挂起的命令

    cmd->error = 0; // 初始化错误码

    // --- 关键点 1:SDHCI 硬件状态机检查 ---
    // 在发送命令前,必须检查硬件状态寄存器 (PRESENT_STATE)。
    // mask 定义了哪些状态会阻止新命令的发送。
    mask = SDHCI_CMD_INHIBIT; // 命令线忙
    if (sdhci_data_line_cmd(cmd)) 
        mask |= SDHCI_DATA_INHIBIT; // 数据线忙

    // 读取当前硬件状态。如果命令线或数据线被占用,返回 false,上层会稍后重试。
    if (sdhci_readl(host, SDHCI_PRESENT_STATE) & mask)
        return false; 

    host->cmd = cmd; // 将当前命令指针保存在 host 结构中,供中断处理函数使用

    // --- 关键点 2:数据传输设置 ---
    // 如果是数据传输命令(读/写),需要设置超时时间和传输模式
    if (sdhci_data_line_cmd(cmd)) {
        host->data_cmd = cmd;
        sdhci_set_timeout(host, cmd); // 根据数据量设置数据超时寄存器
    }

    // --- 关键点 3:DMA/PIO 数据准备 ---
    // 如果命令包含数据(读或写)
    if (cmd->data) {
        if (host->use_external_dma)
            sdhci_external_dma_prepare_data(host, cmd);
        else
            sdhci_prepare_data(host, cmd); // 准备 Scatterlist,设置 DMA 或设置 PIO 模式
    }

    // --- 关键点 4:写入参数与命令 ---
    // 将命令参数写入 ARGUMENT 寄存器
    sdhci_writel(host, cmd->arg, SDHCI_ARGUMENT);

    // 设置传输模式(如 4-bit, 8-bit, DMA 使能等)
    sdhci_set_transfer_mode(host, cmd);

    // --- 关键点 5:构建命令寄存器 ---
    // 根据 flags (响应类型、CRC 校验、索引等) 构建最终的命令值
    if (!(cmd->flags & MMC_RSP_PRESENT))
        flags = SDHCI_CMD_RESP_NONE;
    else if (cmd->flags & MMC_RSP_136)
        flags = SDHCI_CMD_RESP_LONG;
    // ... 其他响应类型判断 ...

    // 如果是数据命令(包含读写数据),设置 DATA_PRESENT 位
    if (cmd->data || tuning_cmd)
        flags |= SDHCI_CMD_DATA;

    // --- 关键点 6:启动定时器与发送 ---
    // 设置软件超时定时器,防止硬件死锁
    timeout = jiffies + 10 * HZ; 
    sdhci_mod_timer(host, cmd->mrq, timeout);

    // 最终一步:将命令写入 COMMAND 寄存器,硬件开始执行!
    sdhci_writew(host, SDHCI_MAKE_CMD(cmd->opcode, flags), SDHCI_COMMAND);

    return true; // 命令发送成功
}

5.5 中断驱动与请求完成:事务的"下半场"

在上一节中,我们通过 sdhci_send_command 将命令写入了硬件寄存器,按下了"启动键"。但接下来发生了什么?硬件是如何通知驱动"数据读完了"或者"出错了"?

答案是:中断

sdhci.c 驱动中,请求的生命周期分为"启动"和"完成"两个阶段,中间由中断状态机推进。

1. 中断处理:状态机的推进者

当命令或数据传输启动后,SDHCI 控制器会异步工作。一旦硬件检测到响应返回或数据传输完成,就会触发中断。

sdhci.c 中,核心中断处理路径通常分为两部分(取决于是否使用了线程化中断):

  • sdhci_irq():中断上半部。主要任务是读取 SDHCI_INT_STATUS 寄存器,判断中断来源(是命令完成?数据完成?还是卡移除?),并清除中断标志。
  • sdhci_thread_irq():中断下半部(线程上下文)。处理耗时的操作,如通过 DMA 搬运剩余数据、处理复杂的错误恢复等。

核心理解:

从驱动调试的角度看,request

函数只解决了"开始 "的问题。真正的"过程推进 "(如数据块一个个传输)和"结束判定",完全依赖于中断状态机的流转。

2. 请求完成路径:sdhci_request_done

无论请求是成功完成还是因错误终止,驱动都需要进行统一的资源清理和状态回收。在 sdhci.c 中,这一逻辑封装在 sdhci_request_done 中。

这是一个关键的"收尸"函数,它的主要职责包括:

  • 错误处理与复位:如果检测到错误(sdhci_needs_reset),它会复位控制器的内部状态机(CMD/DATA 复位)。
  • DMA 资源清理:如果使用了 DMA,无论传输是否成功,都需要在这里进行 dma_unmap 或终止 DMA 通道,防止内存泄漏。
  • 交付结果:最后将结果(包含错误码)交还给上层。
c 复制代码
static bool sdhci_request_done(struct sdhci_host *host)
{
    struct mmc_request *mrq = host->mrq;
    
    /*
	 * The controller needs a reset of internal state machines
	 * upon error conditions.
	 */
	if (sdhci_needs_reset(host, mrq)) {
		......
	}

	/*
	 * Always unmap the data buffers if they were mapped by
	 * sdhci_prepare_data() whenever we finish with a request.
	 * This avoids leaking DMA mappings on error.
	 */
	if (host->flags & SDHCI_REQ_USE_DMA) {
		......
	}

    // 检查是否有平台特定的完成回调
    if (host->ops->request_done)
        host->ops->request_done(host, mrq);
    else
        // 默认路径:交还给 MMC Core
        mmc_request_done(host->mmc, mrq);
        
    return true;
}

这里体现了 Linux 驱动的灵活性:

  • 默认路径:直接调用 mmc_request_done,告诉核心层"活干完了"。
  • 平台插桩:如果特定平台(如我们提到的 RK3568 的 dwcmshc)有特殊的收尾工作(比如特殊的 DMA 缓存一致性处理),可以通过 host->ops->request_done 插入自己的逻辑,处理完后再调回核心层。

5.6 平台差异化实现:RK3568 的板级适配

在上一节 sdhci_request_done 的源码中,我们看到了 host->ops->request_done 这一行关键代码。这引发了一个疑问:host->ops 这个函数指针表究竟是如何被初始化为 RK3568 特有的版本的?

Linux 驱动通过分层设计(Layered Design)解决了通用性与差异性的矛盾。RK3568 的驱动逻辑位于标准 SDHCI 层之上,通过"覆盖"和"修补"的方式实现定制。

  • 标准层 (sdhci.c):处理符合 SDHCI 规范的通用逻辑。
  • 平台层 (sdhci-pltfm.c):提供通用的平台设备初始化框架。
  • 厂商层 (sdhci-of-dwcmshc.c):针对 RK3568 的特定硬件进行"修补"和"定制"。

1. 核心结构:sdhci_pltfm_data

sdhci-of-dwcmshc.c 中,RK3568 定义了一份"配置清单"------ sdhci_pltfm_data。这份清单在驱动初始化时被传递给通用框架,决定了硬件的行为模式:

c 复制代码
static const struct sdhci_pltfm_data sdhci_dwcmshc_rk35xx_pdata = {
    // 1. 操作函数集覆盖
    // 指向 RK3568 特有的 ops 结构体,覆盖通用的 sdhci_ops
    .ops = &sdhci_dwcmshc_rk35xx_ops,
    
    // 2. 硬件缺陷标识 (Quirks)
    // 告诉通用驱动规避已知的硬件问题
    .quirks = SDHCI_QUIRK_BROKEN_TIMEOUT_VAL, // 超时寄存器失效
    
    // 3. 扩展缺陷标识
    .quirks2 = SDHCI_QUIRK2_PRESET_VALUE_BROKEN | // 预设值寄存器异常
               SDHCI_QUIRK2_CLOCK_DIV_ZERO_BROKEN, // 分频器 0 值异常
};

关键点解析:

  • .ops:这是驱动的扩展点。它指向一个完全由 RK3568 定义的 sdhci_ops 结构体。一旦注册,通用框架调用的 set_clock、reset 等函数将全部指向 RK3568 的实现。
  • .quirks:这是驱动的安全机制。硬件设计往往存在无法通过软件修复的 Bug,Quirks 机制允许驱动在运行时动态关闭相关功能,防止系统崩溃。

2. 厂商定制函数集

让我们看看 RK3568 到底定制了哪些函数:

c 复制代码
static const struct sdhci_ops sdhci_dwcmshc_rk35xx_ops = {
    // 定制时钟设置:RK3568 的时钟树可能比较特殊,需要专用逻辑
    .set_clock      = dwcmshc_rk3568_set_clock,
    
    // 复用标准实现:设置总线宽度是通用的,直接复用
    .set_bus_width  = sdhci_set_bus_width,
    
    // 定制 UHS 信号:高速模式下的信号切换需要特定操作
    .set_uhs_signaling = dwcmshc_set_uhs_signaling,
    
    // 定制复位逻辑:RK3568 的复位可能涉及额外的寄存器
    .reset          = rk35xx_sdhci_reset,
    
    // 定制 DMA 描述符:ADMA 引擎的实现细节可能不同
    .adma_write_desc = dwcmshc_adma_write_desc,
    
    // 定制完成回调:我们在 5.5 节提到的特殊处理就在这里
    .request_done   = sdhci_dwcmshc_request_done,
};

3. 初始化流程:sdhci_pltfm_init

在探测函数 (probe) 中,驱动会将上述的"定制清单"传递给通用框架:

c 复制代码
// 1. 获取厂商定义的定制数据
pltfm_data = drv_data->pdata; // 即上面的 sdhci_dwcmshc_rk35xx_pdata

// 2. 初始化平台驱动
// 这个函数会:
// A. 分配 host 结构体
// B. 将 pltfm_data->ops 赋值给 host->ops (实现函数覆盖)
// C. 将 pltfm_data->quirks 赋值给 host->quirks (标记硬件缺陷)
host = sdhci_pltfm_init(pdev, pltfm_data, sizeof(struct dwcmshc_priv));

总结:

这种设计体现了 Linux 内核驱动的优雅:通用代码(sdhci.c)保持绝对的纯净和稳定,而厂商代码(sdhci-of-dwcmshc.c)仅需关注那 10% 的硬件差异 。两者通过 sdhci_pltfm_init 机制无缝拼接,实现了"一次编写,到处运行"的目标。

相关推荐
Paxon Zhang2 小时前
JavaEE初阶学习web开发的第一步**计算机组成原理,操作系统,进程(基础扫盲)**
java·后端·学习·java-ee
Full Stack Developme2 小时前
Linux 软连接与硬连接比较
linux·运维·服务器
zore_c2 小时前
【C++】C++类和对象实现日期类项目——时间计算器!!!
java·c语言·数据库·c++·笔记·算法·排序算法
云边有个稻草人2 小时前
【Linux系统】第九节—进程状态续集+进程优先级+进程切换
linux·进程状态·进程优先级·linux进程调度算法·linux进程切换·死循环进程如何运行·pri and ni
草莓熊Lotso2 小时前
Linux 线程同步与互斥(二):线程同步从条件变量到生产者消费者模型全解,原理 + 源码彻底吃透
linux·运维·服务器·c语言·开发语言·数据库·c++
沐多2 小时前
IgH EtherCAT主站详解(十六)--Igh主站实时网卡驱动开发指南(以r8169为例)
驱动开发·ethercat·实时linux
海绵宝宝的月光宝盒2 小时前
3-机加工工艺
经验分享·笔记·其他·自动化·学习方法
weixin_443478512 小时前
Flutter学习之自定义组件
javascript·学习·flutter
23471021274 小时前
4.18 学习笔记
软件测试·笔记·python·学习