本文关注于 SD/EMMC 的驱动开发,协议相关的内容,请阅读:
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 内核必须同时支持两种模式:
- 标准模式:针对符合 SDHCI 规范的控制器,使用通用的 sdhci.c 驱动。
- 私有模式:针对厂商自定义的控制器(如 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来停止传输,这个指针就指向那个停止命令。
- 对于多块传输(如 CMD18/CMD25),传输结束后需要发送
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 机制无缝拼接,实现了"一次编写,到处运行"的目标。