1. 为什么从 SD 卡驱动入手
想要理解 Linux 驱动开发,很多人第一反应是去啃字符设备、平台总线、设备树这些概念。但我个人的建议是:直接从 SD 卡/SDIO 驱动入手。理由很简单,SD 卡驱动几乎涵盖了 Linux 驱动开发的全部核心要素:总线协议、主机控制器、中断处理、DMA 传输、电源管理、热插拔检测、块设备层对接。把这些搞明白,再去写其他驱动会顺很多。
SD 卡协议本身并不复杂,它不像 PCIe 那样有复杂的枚举和链路训练过程,也不像 USB 需要处理各种描述符和端点。SD 卡就是一条时钟线、一条命令线、四条数据线,加上电源和地。命令响应的机制也很直白。正因为它简单,反而更容易把整个驱动框架看清楚。
这篇文章我打算从一个比较基础的角度入手,把 SD 卡/SDIO 驱动开发需要的前置知识讲明白。内容包括:SD 卡协议的命令结构、寄存器布局、初始化流程、Linux 内核中相关子系统怎么组织、驱动框架的层次划分、以及调试手段。零基础或者刚接触驱动开发的朋友可以把它当作敲门砖,有经验的朋友也可以借此把一些容易忽略的细节再捋一遍。
2. SD 卡协议基础速览
2.1 物理接口与信号定义
SD 卡在物理上分两种形态:一种是常见的 SD 大卡,一种是 Micro SD(TF 卡)。在电路连线层面,两者大同小异,常用的信号包括:
- CLK:时钟信号,由主机提供,决定了通信速率
- CMD:命令线,双向,用于发送命令和接收响应
- DAT0-DAT3:数据线,双向,可以工作在 1-bit 或 4-bit 模式
- VDD:供电,典型值 3.3V,部分场景也支持 1.8V 信号
- VSS:地
这里有一个很多人刚开始容易搞混的点:SD 卡的通信是主从模式,所有通信都由主机发起,卡本身绝对不会主动向主机发数据。这一点和 I2C 类似,但和 USB 那种设备可以发起异步通知完全不同。后续写中断相关逻辑时,这个特性会直接影响设计方案。
4-bit 模式比 1-bit 模式快四倍,所以绝大多数控制器都会优先启用 4-bit。但如果布线条件不好,或者为了兼容某些老设备,退回 1-bit 模式也是合理的降级方案。
2.2 SD 卡命令结构
SD 协议里的命令是 48 位:1 位起始位、1 位传输位、6 位命令索引、32 位参数、7 位 CRC 校验、1 位结束位。命令由主机发出,卡根据命令索引来执行对应操作并返回响应。
命令可以按功能大致分类:
- 基本命令:CMD0(复位)、CMD2(读 CID)、CMD3(读 RCA)等
- 读操作:CMD17(单块读)、CMD18(多块读)
- 写操作:CMD24(单块写)、CMD25(多块写)
- 擦除操作:CMD32、CMD33、CMD38
- 应用相关命令:ACMD 系列,需要先发送 CMD55 进入应用模式
这里需要注意:ACMD 不是一条独立命令,而是 CMD55 + 普通命令的组合。比如 ACMD41 的实际发送顺序是:先发 CMD55,再发 ACMD41。很多新手在这里卡住,逻辑分析仪上看不到数据,就是因为没有先发 CMD55。
CRC 校验在 SD 协议里是硬件自动完成的,软件层一般不需要关心。但有一条例外:CMD0 的 CRC 字段是硬编码的 0x95,不是计算出来的。这点在手动构造命令时要特别注意。
2.3 寄存器与卡识别流程
SD 卡内部有多个寄存器,每个都有明确作用:
- CID:卡标识,包含制造商 ID、产品名、序列号,出厂时写入,只读
- RCA:相对卡地址,在初始化时由卡自行分配,主机通过它来寻址
- CSD:卡描述,包含容量、最大传输速率、读写块大小等关键参数
- SCR:SD 配置寄存器,记录卡支持的比特位宽、命令系统版本等信息
- OCR:操作条件寄存器,记录卡支持的电压范围,ACMD41 会用到
初始化流程可以简化为:主机发送 CMD0 让卡进入空闲态,然后发送 CMD8 确认卡是否支持 SD 2.0 协议,接着发送 ACMD41 进行电压协商和卡初始化轮询,卡返回忙信号直到就绪。就绪之后,主机读 CID、读 RCA、发 CMD9 读 CSD,最后通过 CMD7 选中卡,再发 ACMD6 切换到 4-bit 模式。这个流程听起来不长,但每一步都有对应的状态机,调试时经常要一步一步确认卡有没有正确响应。
判断 SD 卡和 MMC 卡的差异,也是在这个阶段完成的。SD 卡支持 CMD8 和 ACMD41,MMC 卡不支持,只认 CMD1。所以驱动代码里通常先发 CMD8,如果卡没有响应,再尝试 MMC 的初始化路径。
3. Linux 内核里的 SD/SDIO 驱动框架
3.1 三层结构
Linux 内核把 SD/SDIO 驱动分成了三层:MMC 核心层、MMC 总线层、MMC 主机控制器驱动层。每一层的职责非常清楚。
- MMC 核心层负责协议层面的逻辑,包括卡的枚举、状态维护、命令序列编排,它不关心具体的硬件怎么操作
- MMC 总线层负责把卡抽象为总线设备,和 Linux 的设备模型对接,让卡的驱动和主机控制器驱动解耦
- 主机控制器驱动层才是真正和硬件打交道的部分,负责寄存器读写、DMA 描述符管理、时钟控制等
举个例子,SD 卡的初始化时序,核心层会调用主机控制器驱动提供的接口来发送命令、读取响应。但具体怎么往寄存器里填命令、怎么等待响应完成,这些细节完全属于主机控制器驱动层。
// drivers/mmc/core/core.c 中初始化流程的一个片段 static int mmc_sd_init_card(struct mmc_host *host, u32 ocr, struct mmc_card *oldcard) { // 发送 CMD0,让卡进入空闲态 err = mmc_send_cmd(host, &cmd, 0); // 发送 CMD8,确认是否支持 SD 2.0 err = mmc_send_if_cond(host, host->ocr_avail); // 发送 ACMD41,等待卡上电完成 err = mmc_sd_init_uhs_card(host); }这段代码暴露了一个核心设计思想:核心层拥有完整的协议状态机,主机控制器驱动只是执行者。这种分层让同一个核心代码能够无缝支持不同厂商的主机控制器,也是 Linux 驱动可移植性强的关键原因。
3.2 主机控制器驱动要实现的回调
每个具体的主机控制器驱动,都需要填充struct mmc_host_ops这组回调函数,它是 MMC 核心层和硬件之间的桥梁。
static const struct mmc_host_ops my_mmc_ops = { .request = my_mmc_request, // 发送命令/传输数据 .set_ios = my_mmc_set_ios, // 设置时钟、电压、位宽 .get_ro = my_mmc_get_ro, // 获取写保护状态 .get_cd = my_mmc_get_cd, // 获取卡检测状态 .enable_sdio_irq = my_mmc_enable_irq, // SDIO 中断使能 .start_signal_voltage_switch = my_mmc_switch_voltage, // 切换信号电压 };request回调是其中最繁忙的。核心层会把一条完整的命令(包含参数、期望的响应类型、数据缓冲区)打包成struct mmc_command传下来,驱动要负责把它翻译成硬件操作。响应类型有 R1、R2、R3、R6 等,驱动需要根据类型决定读取哪部分寄存器来拼装响应数据。
set_ios回调负责状态切换,比如上电时设置 3.3V,初始化阶段用 400kHz 低速时钟,初始化完成后切换到高速模式。这些细节写在协议规范里,但最终落地都是靠这个回调。
3.3 从零编写一个简化驱动
从头写一个完整的主机控制器驱动篇幅太长,这里给出一个极简的框架,帮助理解驱动注册的基本流程。
static struct platform_driver my_sd_driver = { .probe = my_sd_probe, .remove = my_sd_remove, .driver = { .name = "my-sd-controller", .of_match_table = my_sd_of_match, }, }; static int my_sd_probe(struct platform_device *pdev) { struct mmc_host *mmc; struct my_sd_host *host; // 1. 分配 mmc_host 结构体 mmc = mmc_alloc_host(sizeof(struct my_sd_host), &pdev->dev); host = mmc_priv(mmc); // 2. 配置 mmc_host 的能力参数 mmc->ops = &my_mmc_ops; mmc->caps = MMC_CAP_4_BIT_DATA | MMC_CAP_SD_HIGHSPEED; mmc->caps2 = MMC_CAP2_NO_MMC; // 3. 获取硬件资源:寄存器地址、中断号、时钟等 host->base = devm_platform_ioremap_resource(pdev, 0); host->irq = platform_get_irq(pdev, 0); // 4. 注册 mmc_host,触发卡检测流程 mmc_add_host(mmc); return 0; }mmc_alloc_host分配的是mmc_host及其私有数据,之后通过mmc_priv获取私有结构体。mmc_add_host会启动内核线程检测卡槽中是否有卡,如果有,MMC 核心层就开始走初始化流程。
睡眠和唤醒、电源管理相关的回调也要处理。系统休眠时,MMC 核心会调用驱动的suspend和resume回调,驱动需要保存和恢复寄存器和时钟状态,否则唤醒后 SD 卡可能无法正常工作。实测中 RAM 和 DMA 上下文不一致导致系统唤醒后 SD 卡读写卡死的案例非常常见。
4. 调试 SD 卡驱动的实战手段
4.1 常见调试方法和工具
写驱动离不开调试,SD 卡驱动的调试有自己的特点,因为卡是外部硬件,除了内核日志外,逻辑分析仪是最有效的工具。
- 内核日志:可以在 MMC 核心层打开 debug 选项,看到完整的命令交互记录,这是最直接的调试入口
- 逻辑分析仪:挂在 CLK、CMD、DAT0-DAT3 上,抓取真实波形,定位协议层面的问题
- 硬件探测:用示波器看电源是否稳定、时钟频率是否达到预期、波形是否有毛刺
在源码层面,Linux 提供/sys/kernel/debug/mmc0/目录,里面可以看到卡的信息和寄存器状态。查看当前卡的信息很实用:
cat /sys/kernel/debug/mmc0/ios clock: 400000 Hz vdd: 21 (3.3 ~ 3.4 V) bus mode: 2 (push-pull) chip select: 0 (don't care) power mode: 2 (on) bus width: 2 (4 bits) timing spec: 2 (sd high-speed) signal voltage: 0 (3.30 V)这个ios文件直观地写出了当前时钟频率、电压、位宽、时序模式。初始化过程中如果卡没有被识别,可以通过这个文件判断主机控制器到底有没有按照预期把参数配好。
4.2 典型故障排查步骤
驱动起不来的问题,九成以上集中在电源、时钟、命令交互三个环节。
先看电源。如果卡的 VDD 没有到 3.3V,或者上电时序不满足规格要求,卡就会无响应。此时逻辑分析仪常常什么都抓不到,因为卡根本没正常工作。电源问题的排查思路是:先确认供电电压稳定,再确认上电时序。
再看时钟。初始化阶段时钟必须是 400kHz 以内的低频,如果直接配置成 50MHz 高速时钟,卡会直接不响应。排查时先降低频率,再看波形是否完整、占空比是否正常。
最后看命令交互。发送 CMD0 后应该有 CMD0 的响应,发送 CMD8 后也应该有对应的响应。如果命令线电平不对、或者 CRC 处理有问题,卡会静默。这时候可以通过打开 MMC core 的CONFIG_MMC_DEBUG编译选项来获取详细的命令交互日志。
曾经遇到一个非常隐蔽的问题:SD 卡的 DAT3 引脚在协议里兼任卡检测功能,某些方案直接把 DAT3 用作卡检测,导致插入卡后发送 CMD0 时 DAT3 被拉低,命令通道被破坏。排查这种问题,必须依赖逻辑分析仪同时观察 CMD 和 DAT3,才能定位到根因。
4.3 定位时序相关问题的经验
有些问题不是逻辑错误,而是时序不满足要求。比如卡发送响应后,主机必须在一定时间内读取响应寄存器,如果中断响应延迟过大,后面的响应就会丢失。这种问题的特征是:有时能识别、有时不能识别、和系统负载有关。
解决思路很直接:把中断优先级调高,或者改用轮询模式,或者通过 DMA 减少 CPU 参与。实测效果最好的是把 SD 控制器中断注册为快速中断,并禁止在中断上下文做太多耗时操作。
request_irq(host->irq, my_sd_isr, IRQF_TRIGGER_RISING, "my-sd", host);同时要注意 DMA 缓冲区的对齐问题。SD 控制器的 DMA 引擎通常要求缓冲区 4 字节或 8 字节对齐,如果驱动里分配的缓冲区不对齐,DMA 写内存时会出错,表现是数据读出来是乱码。这种情况建议用dma_alloc_coherent分配 DMA 缓冲区,省心很多。
5. 扩展:SDIO、WIFI 与中断处理
5.1 SDIO 接口的特点
SDIO 是为了让 SD 总线承载 IO 功能而设计的协议,很多 WiFi 模块、蓝牙模块、指纹识别模块都走 SDIO 接口。它在 SD 协议基础上增加了一个额外功能:设备可以主动向主机发送中断信号,这对低功耗和实时响应非常重要。
SDIO 设备有多个 Function,每个 Function 对应不同的 IO 功能,通过 FBR(Function Basic Register)描述。驱动通过 CMD52 读写寄存器,通过 CMD53 进行数据传输。Linux 内核把 SDIO 设备也挂载在 MMC 总线上,所以 SDIO 驱动需要注册成 SDIO 驱动,而不是普通的平台驱动。
static struct sdio_driver my_wifi_driver = { .name = "my-sdio-wifi", .id_table = my_wifi_id_table, .probe = my_wifi_probe, .remove = my_wifi_remove, .irq_handler = my_wifi_irq, };SDIO 的中断和普通外设中断不太一样:它是带内中断,通过 DAT1 线传递。主机控制器检测到 DAT1 被拉低后,通过sdio_irq_thread来处理设备中断。驱动的中断处理函数要尽量短小,复杂的处理逻辑放到工作队列或线程里执行。
5.2 SDIO WiFi 驱动的数据路径
SDIO WiFi 设备的数据路径一般分两条:控制路径和数据路径。控制路径走寄存器读写,管理固件加载、状态查询;数据路径走批量 DMA,传递网络报文。两条路径最终都要通过 SDIO 协议转换成命令序列发送到总线上。
在实现数据路径时,效率是第一位的。SDIO 支持多块传输,一次 CMD53 可以携带多个数据块,所以驱动会把连续的网络数据打包成一次传输,减少命令交互次数。实测下来,一次 CMD53 默认能传 512 字节甚至更多,吞吐量提升非常明显。
static int my_wifi_send_packet(struct sdio_func *func, u8 *buf, int len) { // 打包成一个 CMD53 的写请求 ret = sdio_writesb(func, MY_WIFI_FIFO_ADDR, buf, len); if (ret) goto err_out; // 通知固件有新的数据 sdio_writeb(func, MY_WIFI_IRQ_TRIGGER, MY_WIFI_CTRL_REG, &ret); return 0; }很多 SDIO WiFi 驱动会维护一个发送队列,避免每条网络报文都触发一次 CMD53,这样能显著减少协议开销。这也是为什么有些驱动看起来代码量大,但吞吐量高的原因。
5.3 中断与并发问题
SDIO 驱动大概率会碰到并发问题,集中在中断上下文、工作队列、用户态访问三者之间。常见问题是:驱动在主动访问 SDIO 设备时,设备突然发起中断,两个流程同时操作总线导致命令交错。
解决思路是给总线访问加锁。Linux 的sdio_claim_host和sdio_release_host是专门用来做这件事的接口,任何访问 SDIO 设备的代码都要包在这两个函数之间。否则,日志里会出现奇怪的mmc0: req#6: timeout,卡却不掉线。
static void my_wifi_poll(struct work_struct *work) { sdio_claim_host(func); // 读取设备状态寄存器 val = sdio_readb(func, MY_WIFI_STATUS_REG, &ret); // 处理数据 sdio_release_host(func); }驱动写的越多,越能体会到:SD 总线的并发访问控制比寄存器配置难得多。链路层只是一个简单的状态机,但多进程多中断的并发访问,会让状态机变得无比复杂。这个教训是无数 bug 填出来的。
6. 我的实际体会
做驱动开发几年下来,有个很深刻的感受:写驱动的方法论其实是共通的。先把硬件的数据手册吃透,再把内核框架的层次关系搞明白,然后从最简单的功能开始验证,逐步添加复杂度,最终形成一个稳定的驱动。
SD 卡驱动是最好的入门练手项目。它不像网卡驱动那样有复杂的收包发包逻辑,也不像 GPU 驱动那样需要处理大块显存,但麻雀虽小、五脏俱全。总线协议、主机控制器、中断、DMA、电源管理、设备模型,一个都不少。把这些都弄透之后,再去看其他复杂的驱动,会发现框架思维完全一致,区别只是具体协议和硬件的不同。
最后分享一个小技巧:调试 SD 卡驱动时,把 MMC core 的调试日志打开,同时准备好逻辑分析仪,两个工具配合使用效率最高。只靠软件日志,很多时序问题看不出来;只看波形没有日志,又不知道该往哪个方向查。两者一对照,定位问题的速度能快好几倍。