搞了这么久嵌入式SD卡驱动这块总算是琢磨透了。这篇文章专门讲Linux下SD卡和SDIO驱动的核心部分内容比较细从框架到代码再到我踩过的那些坑一次说清楚。1. 先把整个SDIO子系统的大框架摸清楚刚接触SD/SDIO驱动的时候最容易犯的毛病就是一头扎进协议文档里死磕SD卡的命令时序结果折腾半天连驱动该往哪儿挂都不知道。其实在Linux里SD卡和SDIO设备走的是一条路——mmc子系统整个框架分得特别清楚就三层。最底层是host controller驱动对应你板子上的SDIO控制器一般是芯片内部的IP也可能走USB读卡器转出来的。中间那层是mmc core处理协议和状态机对上提供统一的接口。最上层是card驱动和块设备层SD卡走到block层变成/dev/mmcblk0SDIO WiFi或者蓝牙设备则走到各自的总线驱动去。这三层的关系用个比喻挺好理解的。host controller是银行柜台mmc core是银行大厅的引导流程card驱动就是你要办的业务。你不需要跟柜台后面的人直接喊话一切都走流程。这个架构设计的好处非常明显对驱动开发者来说真的是福音。各个芯片厂商的SDIO控制器硬件寄存器千差万别但只要按照host controller的接口把底层搞定上面那些协议处理、分区管理、卡检测机制全都复用内核现成的。所以你做驱动的核心工作集中在两点把host controller驱动写对把设备侧驱动的probe回调实现好。中间那个mmc core绝大多数情况下你只调用它的接口不用动它。2. 驱动入口的注册流程和设备树配置2.1 host controller驱动的注册核心看代码先从入口开始。一个标准的mmc host controller驱动注册路径大概是这样的static const struct of_device_id mymmc_of_match[] { { .compatible vendor,my-sdio-controller }, { } }; static struct platform_driver mymmc_driver { .probe mymmc_probe, .remove mymmc_remove, .driver { .name my-sdio, .of_match_table mymmc_of_match, .pm mymmc_pm_ops, }, }; module_platform_driver(mymmc_driver);这里有个细节.name字段和compatible字段不一定要一样但强烈建议保持一致。因为你调试的时候用ls /sys/bus/platform/drivers/看驱动列表名字对得上会省很多事。module_platform_driver这个宏其实就是把module_init和module_exit封装了等价于注册一个platform驱动。接着看probe函数这里面的流程就是个流水线static int mymmc_probe(struct platform_device *pdev) { struct sdhci_host *host; struct resource *res; struct mymmc_priv *priv; int ret; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; host sdhci_pltfm_init(pdev, my_sdhci_pdata, sizeof(*priv)); if (IS_ERR(host)) { dev_err(pdev-dev, sdhci pltfm init failed: %ld\n, PTR_ERR(host)); return PTR_ERR(host); } ... }注意这个sdhci_pltfm_init它是标准SDHCI控制器驱动的通用初始化入口。如果你的芯片用的是标准SDHCI IP直接用这一套就行能省掉很多麻烦。但如果你用的是私有协议或者simplified MMC控制器那就要手动去分配mmc_host结构了完全另一条路。probe里面几个关键的初始化动作一个都不能省使能时钟包括AHB总线的时钟和卡时钟。卡时钟频率初始化时一般先设为400KHz这是SD协议规定的初始化识别时钟频率等卡进入传输模式后才切到高速。申请中断或者使能DMA能力。SDIO控制器一般都支持SDMA或者ADMA2DMA这块建议直接把ADMA2配好性能差异非常大。填充sdhci_host的ops回调这些是真正干活的底层函数。调用sdhci_setup_host和sdhci_add_host把host添加到mmc core。注册完成后你在/sys/bus/platform/devices/目录对应的设备节点下就能看到mmc host的注册信息了。2.2 设备树配置的关键节点设备树这块是实际调试中最容易出问题的地方。一个基本的SDIO控制器节点长这样sdhci0 { compatible vendor,my-sdio-controller; reg 0x0 0xff370000 0x0 0x1000; interrupts 0 96 4; bus-width 8; non-removable; no-sdio; no-sd; cap-mmc-highspeed; mmc-hs200-1_8v; mmc-hs400-1_8v; vmmc-supply vmmc_regulator; vqmmc-supply vqmmc_regulator; status okay; };这里每个属性都在告诉内核你的板子长什么样子。bus-width是8说明板子上拉了8根数据线走的是eMMCnon-removable表示eMMC是焊死的不能拔no-sdio和no-sd则告诉内核这个控制器只接eMMC别把SD卡和SDIO设备的协议分支也开着能省不少初始化时间。关键是vqmmc-supply这是IO电平的供电。如果你的eMMC要跑HS200或者HS400模式IO电压需要切到1.8V这个regulator必须有。如果没有这个属性内核默认认为不能切电压就最高跑到HS模式性能上不去。提示改完设备树之后别只检查语法一定得看probe有没有真正跑起来。很多问题不出在你写的设备树语法上而是regulator没配置好导致probe失败。3. 请求链路的核心机制与数据结构流转3.1 mmc request的完整生命周期搞清楚框架之后最值得深入钻研的就是一个读写请求是怎么从用户态一路走到寄存器的。这条链路是SDIO驱动的核心脉络。先从应用层发一个read操作说起。VFS把请求交给块设备层块设备层合并、排序后生成一个struct request然后通过mmc block驱动转成struct mmc_request这里面的关键组件有struct mmc_command、struct mmc_data、struct mmc_host这几个结构体。其中mmc_command代表一条SD命令比如CMD17是单块读CMD18是多块读。mmc_data则描述数据传输的方向、块数、块大小、DMA内存地址等信息。这些结构体打包在一起交给host controller驱动去执行。执行过程是异步的。host controller拿到request后配置DMA描述符、设置寄存器然后写命令寄存器触发传输。传输完成后触发中断中断处理函数里调用mmc_cmd_done或者mmc_data_done一路回调上去最后唤醒等待的进程。这里要注意的是mmc core的请求队列是串行的同一时刻只有一个request在执行。这点和网络子系统不一样。它的设计是为了保证SD卡操作的顺序性因为你没法让卡同时执行两条命令。3.2 timeout值的把控和遗留中断问题实际写host驱动时timeout处理是最容易整出毛病的地方。内核每个request都带timeout时间如果硬件没能在预期时间内完成操作就要调用超时回调。你要做的就是在struct mmc_host_ops里把request和timeout这两个回调实现好。static void mymmc_request(struct mmc_host *mmc, struct mmc_request *mrq) { struct mymmc_host *host mmc_priv(mmc); unsigned long flags; spin_lock_irqsave(host-lock, flags); /* 填充DMA描述符 */ /* 配置控制器寄存器 */ /* 触发命令 */ spin_unlock_irqrestore(host-lock, flags); }这里一个很反直觉的细节是这个函数在执行时可能已经持有spinlock了所以回调里面你不能再调用任何可能睡眠的函数。我之前就踩过这个坑在request回调里顺手调了msleep等待前一条命令完成结果直接死锁。正确做法是如果硬件还在忙就返回BUSY让mmc core稍后再重试。还有遗留中断的问题。新板子首次跑驱动的时候probe完成之后可能会立刻收到一个莫名其妙的中断。这种通常是上电时序导致的中断状态寄存器残留你必须在初始化时完全清空中断状态。检查一下sdhci_clear_set_irqs这个调用是否生效它的第二个参数要传ALL_INT_ENABLE确保把所有历史中断都清掉。3.3 DMA映射的精细处理DMA这块也比较阴。很多SDIO控制器支持的是ADMA2它的描述符格式有要求——每项描述符要设置valid、end标志位而且地址必须按8字节对齐。如果对齐不符合要求控制器就卡死在那里表现为请求提交后一直不产生完成中断。找到这个问题的过程很有代表性。最开始以为中断没配好从头到尾查了两遍中断控制器配置还是老样子。后来把DMA描述符地址打印出来才发现那块内存没对齐。可以这样修struct mmc_data *data mrq-data; if (data-sg_count 1) { /* 单段DMA直接映射 */ >dmesg | grep mmc如果你看到mmc0: error -110 whilst initialising SD card这种错误-110是ETIMEDOUT说明卡没有应答。这时候优先查时钟和供电是否正常。我调试时一般这样验证host控制器是否注册成功ls /sys/class/mmc_host/ ls /sys/bus/platform/drivers/my-sdio/ cat /sys/class/mmc_host/mmc0/device/uevent有时还会看/sys/kernel/debug/mmc0/下的信息需要先挂载debugfs。mmc-utils这个工具也很关键其中的mmc extcsd read可以读取eMMC的扩展寄存器mmc status get可以获取卡状态。你可以通过这些命令确认卡处于什么状态再对照数据手册排查问题。4.3 确认控制器工作模式的一些手段如果你不确定当前SD卡跑在哪个模式下面在Linux 5.x时代可以用/sys/kernel/debug/mmc0/ios查看当前的时序、电压、总线宽度cat /sys/kernel/debug/mmc0/ios它会输出类似clock: 200000000 Hz vdd: 21 (3.3 ~ 3.4 V) bus mode: 2 (push-pull) chip select: 0 (dont care) power mode: 2 (on) bus width: 2 (8-bit) timing spec: 9 (mmc hs400) signal voltage: 0 (1.8 V)看到timing spec: 9 (mmc hs400)和bus width: 2 (8-bit)就说明eMMC已经跑在HS400模式了。如果只有HS200说明mode切换没成功基本是信号完整性问题。5. SDIO设备侧的初始化与枚举机制5.1 设备驱动的匹配和probe执行顺序SDIO设备的驱动写起来跟普通驱动有些区别。它不挂在platform总线上而是挂在sdio总线上由mmc core管理。驱动结构一般是这样的static const struct sdio_device_id mywifi_id_table[] { { SDIO_DEVICE(0x6666, 0x1111) }, { } }; static struct sdio_driver mywifi_driver { .probe mywifi_probe, .remove mywifi_remove, .name mywifi, .id_table mywifi_id_table, }; module_driver(mywifi_driver, sdio_register_driver, sdio_unregister_driver);注意这里匹配的是VID和DID对应SDIO卡内部的FBRFunction Base Register里读出来的信息。每次复位后mmc core会重新扫描总线发现设备ID匹配就会执行probe。5.2 probe里不为人注意的IO延迟问题SDIO设备probe时最常见的坑是设备刚上电还没稳定你就去读写它的寄存器结果操作失败。解决办法是添加必要的延迟static int mywifi_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct mywifi_dev *dev; int ret; /* 等待设备完成内部初始化 */ msleep(50); dev kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; sdio_claim_host(func); ret sdio_enable_func(func); if (ret) goto err; ret sdio_set_block_size(func, 512); if (ret) goto err; sdio_release_host(func); /* 读设备信息确认通信正常 */ sdio_claim_host(func); ret sdio_readb(func, 0x00, err); sdio_release_host(func); ... }这里必须强调SDIO设备的所有I/O操作都要用sdio_claim_host和sdio_release_host包起来。这是因为SDIO_function是共享同一个物理host控制器的如果不claim就可能和卡访问冲突。很多刚入行的同事总是忘掉这一步probe有时灵有时不灵其实就是这个原因。sdio_set_block_size这一步也容易被忽略。默认的block size是512字节但某些SDIO设备要求以特定大小传输不设置会导致数据错误。根据设备手册配置正确的块大小这是在probe里必须完成的事情。5.3 IRQ线程化机制的取舍SDIO设备处理中断时一般用工作队列或者线程化IRQ。如果直接用request_irq中断上下文里只能做短暂操作大量数据传输还是要放到下半部。Linux对SDIO IRQ有一种特殊的机制——你可以通过sdio_claim_irq注册一个回调这个回调会在mmc core的专用线程里执行可以安全地调用sdio_readb、sdio_writeb这些函数。这个特性用起来非常方便。举个例子SDIO WiFi的中断往往是电平触发型的收到中断后必须主动读设备的中断状态寄存器才能清掉。直接用request_irq在中断上下文里操作SDIO函数寄存器会带来巨大的复杂度。换成sdio_claim_irq直接在线程里读寄存器轻松很多。但要注意不要两个一起注册否则会冲突导致probe失败。6. 几个典型问题的排查思路记录驱动开发里面最值钱的经验就是排错的方法论。根据我多次折腾SD卡和SDIO驱动的经历列几个典型的故障场景每个都附上排查思路大家可以直接当checklist来用。6.1 板子启动后完全没有mmc设备生成这种问题不用怀疑就是host controller层面没通。按这个顺序排查第一步确认probe有没有执行。在probe函数入口加一句dev_info打印看看驱动加载没有。如果没执行查设备树节点有没有写对compatible和驱动match表是否对应。第二步如果probe执行了但是后面报错看中断有没有正确注册。有些SDIO控制器的中断是wire到GIC的如果设备树里interrupt属性写错了等卡插入时无法感知设备就不会枚举。第三步确认时钟有没有使能。规范要求初始化时卡时钟是400KHz如果没使能或者频率不对卡完全不响应表现为mmc0: error -110 whilst initialising SD card。这种问题还有一种常见隐藏原因——供电。SD卡的VDD和VDDQ分开供电如果VDDQ没供上卡的IO口没有电平等于不存在。6.2 读卡速度上不去只能跑到默认速率这个现象一般是两个原因设备树没配置对应的speed mode属性或者硬件信号质量不够。先查设备树mmc-hs200-1_8v、mmc-hs400-1_8v这些属性一个都不能少。再查初始化日志里卡是否识别到了HS200/HS400。如果识别到了但实际速度上不去十有八九是PCB走线或者上拉电阻的问题。这里有个很有意思的经验SD卡的卡检测虽然也有时钟线但它能启动时靠的是CMD线的上拉。如果上拉电阻焊错了位置甚至漏焊卡也能枚举成功但跑到高速模式时会偶发出错。这类问题特别难查因为刚开始低速模式一切正常一上高速就随机挂。物理检查时重点看这几条线CLK线尽量短DATA线和CMD线不要交叉CLK线旁边不要走高频信号线。还有一点1.8V电平转换芯片的摆率不能太慢否则VCCO切换后时序余量不够跑HS400就挂。6.3 系统睡眠唤醒后设备丢失这种情况经常让没有电源管理经验的同事头疼不已。启动时一切正常跑起来读写也没问题但只要系统suspend唤醒后/dev/mmcblk0就消失了或者SDIO WiFi连不上了。排查方向放在两点一是检查驱动有没有实现pm_ops。标准驱动需要实现suspend和resume回调在resume的时候重新初始化硬件。很多芯片在睡眠掉电后之前配置的寄存器内容全丢了不重新初始化就完全没法用。另一个是供电链路的睡眠策略。如果vmmc-regulator在睡眠时被关掉了而resume时又没有重新拉起来设备自然就消失了。有一个容易忽略的细节mmc core有个功能叫runtime PM如果驱动的runtime suspend/resume没实现好系统suspend时也会触发runtime流程导致时序混乱。解决办法很简单要么完整实现runtime PM回调要么在设备树里加上mmc-pm-ignore-removable这类属性来绕过runtime检查。6.4 数据传输偶发CRC错误这个问题的两个主要方向时钟频率和信号质量。先降低时钟频率试一下。如果降到50MHz就稳定说明信号质量确实有瓶颈。如果50MHz也随机出错那要考虑DMA描述符的地址对齐问题前面提到过的ADMA2对齐要求。还可以看下卡的供电是否稳定特别是电流瞬态响应。可以用示波器抓一下VDD波形如果传输时电压跌落超过10%那就是电源问题加大电容或者换低内阻的LDO。我遇到的几次CRC错误最后发现几乎都是电源的锅。调试手段小结与体会写了这么长最后再说说调试工具的使用心得。除了/sys/kernel/debug/mmc0/ios之外抓协议分析波形我常用逻辑分析仪放在CLK/CMD/DAT0这几根线上。SD协议2.0以上的模式都比较容易解析逻辑分析仪价格也不高算是性价比最高的调试器。调试时尽量保持一个变量的原则不要同时改设备树、驱动代码和电压配置。改完一个层面验证一个层面不然出了问题没法定位。另外如果你手头有两种不同的SD卡或者eMMC颗粒建议都插上去试试。不同厂商的卡对时序的容错性不同。有的卡严格按规范来有点余量不足就失败有的卡容错性好能让问题晚暴露。多试几片卡能有效验证你的驱动是否可靠。SDIO驱动开发这活入门门槛其实不高但要真正调稳定了是非常吃经验的。框架理解清楚了原语用对了加上一套系统的排错思路大部分问题都会变得有迹可循。希望这篇内容能帮你少走点弯路。