☰
深入剖析dw_mci_probe:Linux MMC/SD控制器驱动初始化全流程
2026/10/14 15:59:14 网站建设 项目流程

1. 先弄清楚 dw_mci_probe 到底在干什么

如果你做过 Linux 下的存储类驱动,或者接触过嵌入式平台上的 SD 卡/eMMC 启动问题,多半绕不开 dw_mci 这套东西。它是新思(Synopsys)DesignWare 系列里负责 MMC / SD / SDIO 控制器的驱动框架,在很多主控平台上都能看到它的身影。而 dw_mci_probe 就是这块驱动生命周期里最先被内核调用的那个函数,它负责把一个挂在平台总线上的存储控制器,从“硬件存在”变成“系统可用的 MMC 设备”。

用一句大白话讲:板子上电后,内核知道你有这块控制器,但不知道它该怎么初始化、怎么响应中断、怎么搬运数据、怎么被上层的文件系统使用。probe 函数要做的就是把这些事情全部安顿好——申请资源、配置时钟、初始化 DMA、注册中断、填充 mmc_host 结构体,最后把设备添加到 MMC 核心层。probe 一旦成功返回 0,你的 SD 卡才能被识别、分区、挂载;probe 失败,内核日志里通常只会留一行孤零零的报错,卡住你半天查不下去。

这篇文章不是给你抄代码用的,而是要帮你看懂 dw_mci_probe 的每一段逻辑背后的动机:为什么要分这么多阶段?每个阶段失败会有什么后果?哪些细节是常规文档里不会写、但实际调试时一定会踩的坑?如果你正被“sdhci 也不像、dw_mci 也不熟”的驱动代码折磨,或者马上要开始移植这块驱动到新平台,这篇文章应该能帮你省掉不少弯路。

2. 整体设计思路:先搞清楚这块驱动在系统里的位置

2.1 probe 之前的框架铺垫

在展开 dw_mci_probe 之前,得先说说它所在的分层。MMC 子系统在内核里其实分了三层:最下面是硬件控制器驱动(dw_mci 就属于这一层),中间是 MMC 核心层(mmc core),最上面是块设备层和文件系统。控制器驱动不需要知道你的 SD 卡是什么文件系统,它只负责和硬件寄存器打交道;mmc core 负责卡检测、协议协商、请求调度;块设备层把 read/write 请求转成 struct mmc_request。

所以 dw_mci_probe 的职责可以精确地定义为:把一个 platform device 转换成 mmc core 能用的 host adapter。换句话说,它干的是“适配层”的活儿,核心数据结构有三个:

  • struct dw_mci:内部状态,几乎所有的寄存器地址、时钟、DMA、请求队列都挂在这个结构体上。
  • struct dw_mci_platform_data:平台层传入的配置,比如卡检测引脚、总线宽度、capabilities 标志。
  • struct mmc_host:MMC 核心层使用的对外接口,probe 的最终成果就是把这个结构体注册给 mmc core。

从驱动的角度看,dw_mci_probe 挂在一个platform_driver的.probe回调上。内核的总线机制会在设备树节点或者 platform device 与驱动匹配成功时自动调用它。所以理解这个函数的第一把钥匙就是:它不是在真空中运行的,它的输入是这个设备在系统里已经注册好的资源(寄存器物理地址、中断号、时钟),以及设备树里描述的能力(bus-width、non-removable 等)。

2.2 probe 的五个阶段为什么这么划分

如果你把 dw_mci_probe 从头读一遍,会发现它的逻辑其实可以拆成五个明确阶段,这个划分不是随意的,而是内核驱动开发里非常典型的一套“安全初始化”流程:

  1. 基础资源获取:寄存器、中断、时钟,这些是硬性前提,缺一个后面全白搭。所以这段代码在函数最前面,失败就立刻返回。
  2. 控制器私有数据与能力初始化:拿到资源后,把 dw_mci 结构体里的字段填好,读硬件寄存器确认控制器 ID、版本,设置 DMA mask。
  3. 中断与 DMA 通道准备:这里要把中断处理函数挂到系统里,同时把数据传输的通路配好,否则后面每笔请求都会卡死。
  4. MMC host 的分配与参数填充:调用 mmc_alloc_host 创建上层接口,填入 f_min、f_max、caps 等能力位。
  5. 最终注册:mmc_add_host 把设备对外暴露,之后 mmc core 就会开始发命令、检测卡、协商模式。

这个顺序其实暗合了硬件的依赖链:时钟没起来,你去读写寄存器就是空转;中断没注册,控制器就算收到了卡插入事件也没人响应;DMA 没初始化,数据传输请求就没法完成。每一步的开销都不小,所以正确性优先于效率,宁可逐步检查,也不能图省事一把梭。

我在实际调试中遇到过一个典型的反例:某位开发者为了省事,把时钟配置放在 probe 的最后才做,结果前面读到的硬件版本号全是 0xFFFFFFFF,花了半天才发现是时钟没开。这种问题,光看代码逻辑很难发现,但它很能说明阶段划分的意义。

3. 核心机制拆解:从资源到寄存器的每一步都有讲究

3.1 资源获取:别急着读寄存器,先看时钟和物理地址

probe 的第一步是从 platform device 里拿资源,这部分代码在几乎所有 dw_mci 平台驱动里长得都差不多:

static int dw_mci_probe(struct platform_device *pdev) { struct dw_mci *host; struct resource *regs; int ret; host = devm_kzalloc(&pdev->dev, sizeof(*host), GFP_KERNEL); if (!host) return -ENOMEM; regs = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!regs) return -ENXIO; host->regs = devm_ioremap_resource(&pdev->dev, regs); if (IS_ERR(host->regs)) return PTR_ERR(host->regs); ... }

有几个细节要专门点一下:

  • platform_get_resource拿到的 resource 是物理地址范围,不能直接用,必须经过 ioremap。这里用devm_ioremap_resource是推荐做法,它内部会检查资源是否被占用,返回值用 IS_ERR 判断。这个函数在资源冲突时会打印清晰的错误信息,比你自己用 request_mem_region 再 ioremap 省心。
  • devm_kzalloc的意思是“设备管理的内存”,驱动卸载或 probe 失败时内会儿自动释放。这对错误路径处理帮助极大,你不需要在每个 err 标签里逐个 kfree。
  • 时钟获取在 dw_mci 里通常是devm_clk_get(&pdev->dev, "ciu")和devm_clk_get(&pdev->dev, "biu")。ciu 是 Card Interface Unit 时钟,biu 是 Bus Interface Unit 时钟。这两个名字虽然在不同平台设备树里略有差异,但语义是一样的:ciu 时钟决定卡端的工作频率,biu 时钟决定控制器内部总线频率。在 probe 阶段必须先把它们开启,否则寄存器读写全是空转。

我在一个项目中遇到的情况是设备树里 ciu 时钟频率写的是 200MHz,但实际晶振只有 24MHz,导致 mmc core 在协商卡频率时算出来的分频系数完全不对,卡偶尔能识别、偶尔失败。排查到最后才发现是设备树源文件写错了。所以拿到一个新平台,第一件事就是把 dts 里的时钟频率和实际量到的波形对上,再往下走。

3.2 时钟频率与能力位的填充逻辑

时钟获取之后,紧接着就是填充 host->bus_hz。这个值决定了后面 mmc core 计算时钟分频的基础。很多平台驱动里有类似这样的代码:

host->bus_hz = clk_get_rate(host->ciu_clk); if (!host->bus_hz) { ret = -EINVAL; goto err_clk; }

看到!host->bus_hz要格外敏感。clk_get_rate 返回 0 通常意味着时钟树没有正确配置,或者父时钟没起来。这时候继续往下走是毫无意义的,赶紧回去查时钟框架。

接下来是读取控制器的能力寄存器,dw_mci 控制器里有一组寄存器专门描述硬件能力,比如SDMMC_GET_64ADDR、SDMMC_GET_HDATA_WIDTH、SDMMC_GET_USE_DMA等。这些能力位拿到之后会写进 dw_mci 结构体的caps字段,并且最终会在初始化 mmc_host 时被翻译成 MMC core 认识的能力标志,比如MMC_CAP_SD_HIGHSPEED、MMC_CAP_MMC_HIGHSPEED、MMC_CAP_UHS_SDR104。

这块有个容易犯迷糊的地方:dw_mci 的能力寄存器只能告诉你“控制器硬件支持到什么程度”,但板级设计可能限制了实际能力。比如控制器硬件支持 8 位总线,但你的 PCB 只连了 4 根数据线。这时候:

  • 设备树里需要写bus-width = <4>;
  • 平台代码需要把MMC_CAP_8_BIT_DATA清除。

这个约束通常通过dw_mci_platform_data里的caps和caps2字段完成。probe 的逻辑一般是:

host->caps = SDMMC_GET_CAPS(host); if (pdata->caps) host->caps |= pdata->caps; if (pdata->caps2) host->caps2 |= pdata->caps2;

我跟你讲,这里有个很坑的场景:有的平台 pdata 里清了某个能力位,但代码忘记把它从硬件读出的默认 caps 里去掉,结果内核在协商 UHS-I 模式时选了一个板级根本不支持的速度档,数据线信号质量又不够,最后表现为“卡能识别,但一跑大文件就 IO error”。排查这种问题,最直接的办法是看内核日志里的mmc0: new ultra high speed SDR104 SDIO card at address 0001,如果发现模式比你期望的高,优先怀疑 caps 位没清干净。

3.3 DMA 与 IDMAC:不是所有平台都走同一条路

dw_mci 支持两种 DMA 模式:内部 DMA(IDMAC)和外部 DMA 引擎。区分点在设备树和平台数据里,probe 代码会根据配置决定走哪条路:

  • 如果使用 IDMAC,驱动分配描述符表,每个描述符对应一个物理段,传输时把数据地址和长度填进描述符,硬件会自动搬运。IDMAC 的优点是延迟低、实现简单,缺点是描述符表本身要内存连续。
  • 如果使用外部 DMA 引擎,probe 会调用dma_request_chan获取 rx/tx 通道,然后将传输请求包装成 dmaengine API 给 DMA 控制器。这种方式更灵活,可以绕过控制器的内部 DMA 限制,适合某些需要高带宽或特殊内存布局的平台。

针对 IDMAC,probe 中有个细节:dw_mci_init_dma(host)会设置 DMA mask。如果平台支持 64 位地址,会调用dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)),否则回退到 32 位。这里有一个常见的翻车点:某些 64 位控制器的主控 DMA 能力其实有限,如果盲目设置 64 位 mask,后面 dma_alloc_coherent 分配的内存可能超出硬件实际寻址范围,导致传输时数据被写到错误的地方。我的经验是,先看控制器的HCON寄存器里 ADDR_CONFIG 字段,确认真实地址宽度,再决定 mask 怎么设。

static int dw_mci_init_dma(struct dw_mci *host) { int ret; if (host->use_dma == TRANS_MODE_IDMAC) { ... ret = dma_set_mask_and_coherent(host->dev, DMA_BIT_MASK(64)); if (ret) ret = dma_set_mask_and_coherent(host->dev, DMA_BIT_MASK(32)); ... } ... }

如果你在调试时发现 dma_alloc_coherent 失败或者地址异常,少不了要回头检查这一段。另外,设备树里也可以用dmas和dma-names属性强制走外部 DMA,probe 会优先检查外部 DMA 通道是否存在,存在则优先使用外部 DMA。有次我在一个平台上发现 IDMAC 性能和预期差得远,后来换到外部 DMA,吞吐量直接翻倍,原因是那个控制器的 IDMAC 只支持 4KB 描述符边界,大块传输被硬生生拆成很多小段,效率自然上不去。

3.4 中断注册:共享中断的深水区

dw_mci 的中断处理是整个驱动的心脏,所有命令完成、数据完成、错误事件、SDIO 中断都汇聚在同一个 IRQ 上。probe 里的注册代码大概是:

ret = devm_request_irq(&pdev->dev, host->irq, dw_mci_interrupt, IRQF_SHARED, "dw-mci", host);

这里两个细节值得注意:

第一,IRQF_SHARED是必需的。DW MMC 控制器在很多 SoC 上和其他外设共享一个中断线(比如通过中断控制器级联或者共用 GIC 中断),如果不加共享标志,request_irq 会返回 -EBUSY,probe 直接失败。加了共享标志后,中断处理函数需要正确判断硬件是否真的有中断 pending,否则会把别人的中断也吃掉。dw_mci_interrupt 里会读取INTSTAT寄存器,如果为 0 就直接返回 IRQ_NONE,这就是共享中断的标准写法。

第二,中断处理线程化。dw_mci 的中断处理分两部分:hardirq 里只做紧急处理和状态记录,真正耗时的请求完成工作通过tasklet或者irq_thread延后执行。probe 中你会看到tasklet_init的调用,它把 request 完成回调放到软中断上下文,避免在硬中断里做太多事阻塞系统。实际调试中,如果发现中断上下文耗时过高,多半是这里处理逻辑太重,可以考虑改用mmc->threaded_fn方式。

SDIO 中断是另一个单独的话题,它通过sdio_signal_irq机制工作。卡的 SDIO 中断引脚会和数据线 1 复用,所以只有在 SDIO 设备使能中断后,控制器才会监控这根线的电平变化。probe 阶段这块通常只做能力声明,真正的开关是在enable_sdio_irq回调里。你只需要知道,SDIO 中断在注册层面没有额外动作,但它的出现会影响中断共享的判断逻辑:如果硬件没有把 SDIO 中断状态反映在 INTSTAT 寄存器里,驱动的dw_mci_interrupt可能永远认为没有中断,导致 sdio_irq 线程饿死。

3.5 mmc_host 的分配与参数填充

资源、时钟、DMA、中断都齐了,接下来的核心动作是创建 MMC 核心层的 host 对象:

mmc = mmc_alloc_host(sizeof(struct dw_mci_slot), &pdev->dev); if (!mmc) { ret = -ENOMEM; goto err_dma; } host->mmc = mmc;

mmc_alloc_host的参数里有个sizeof(struct dw_mci_slot),这是 MMC 核心层给控制器驱动预留的私有数据区。所以 dw_mci 驱动注册的其实是两个对象:一个是 platform 层面的 dw_mci(全局控制器状态),另一个是 MMC core 层面的 dw_mci_slot(每个槽位一个)。一个控制器可以对应多个槽位,但大多数平台上只有 0 号槽位在使用。

填充 mmc_host 字段时,有几个值必须仔细斟酌:

  • mmc->f_min和mmc->f_max:这两个值决定了 MMC core 可以选择的工作频率范围。f_max 一般取 ciu 时钟频率或者它的一半,f_min 通常设 400kHz(MMC 规范要求的最低初始化频率)。如果你发现某些卡在低速模式下初始化失败,先检查 f_min 是否合理。
  • mmc->ocr_avail:表示支持的电压范围。大多数平台用MMC_VDD_32_33 | MMC_VDD_33_34,如果你的板子用的是 1.8V 信号,需要改成对应的 VDD 位。这个值设错了,卡的电压协商就会失败,表现为“卡完全无法识别”。
  • mmc->caps:把前面从硬件能力和平台数据里拼好的能力位传进来。
  • mmc->ops:指向 dw_mci 自己实现的一堆回调,包括 request、set_ios、get_ro、get_cd、enable_sdio_irq 等。这些回调会在 MMC core 的各个阶段被调用,是驱动真正干活的入口。

这中间最容易被忽略的是mmc->max_segs、mmc->max_blk_size、mmc->max_blk_count、mmc->max_req_size这四个值。它们决定了上层能提交多大的 I/O 请求。如果设置得过小,性能会受限;设置得过大,底层 DMA 描述符可能不够用。dw_mci 的 IDMAC 描述符表大小是编译时决定的(通常是 128 个描述符),所以 max_segs 不能超过这个数。常见的设置是:

mmc->max_segs = 128; mmc->max_blk_size = 65536; mmc->max_blk_count = 512; mmc->max_req_size = 512 * 1024 * 1024;

3.6 最后的注册和槽位绑定

参数的填充到位之后,就到了dw_mci_add_slot。这个函数在 dw_mci 驱动里负责创建 MMC host 并绑定槽位信息。它会读取设备树里bus-width对应的DW_MCI_QUIRK_系列标志,设置mmc->caps中的MMC_CAP_4_BIT_DATA、MMC_CAP_8_BIT_DATA。同时,如果设备树标了non-removable,它会设置MMC_CAP_NONREMOVABLE,告诉 MMC core 不要做卡检测,这种设置常见于 eMMC 和板上焊接的 SDIO 设备。

dw_mci_add_slot的最后调用了mmc_add_host(mmc)。一个很容易被忽略的点是:mmc_add_host 之后,MMC core 会通过mmc_detect_change触发一次延迟的卡检测。这意味着probe 返回之前,你已经可以看到内核日志里出现 mmc0 相关的检测信息了。如果你的驱动在 mmc_add_host 之后还对 host 状态有依赖,要小心竞态——有时候卡检测线程跑得比你还快。

下面是 slot 初始化里几个关键步骤的简化版:

static int dw_mci_add_slot(struct dw_mci *host, unsigned int id) { struct mmc_host *mmc = host->mmc; struct dw_mci_slot *slot; int ret; slot = mmc_priv(mmc); slot->host = host; slot->id = id; mmc->ops = &dw_mci_ops; mmc->f_min = 400000; mmc->f_max = host->bus_hz / 2; mmc->ocr_avail = MMC_VDD_32_33 | MMC_VDD_33_34; mmc->caps = host->caps; mmc->caps2 = host->caps2; ret = mmc_add_host(mmc); if (ret) return ret; return 0; }

4. 实操流程:从零开始的完整 probe 实现思路

4.1 前置条件:设备树和平台驱动注册

在写 probe 之前,你得先确认这个平台驱动真的会被内核找到。dw_mci 驱动通常被组织成一个核心模块加多个平台变体,例如:

static const struct of_device_id dw_mci_of_match[] = { { .compatible = "snps,dw-mshc", .data = &dw_mci_pltfm_ops }, { .compatible = "rockchip,rk3399-dw-mshc", .data = &rk3399_dw_mci_ops }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, dw_mci_of_match); static struct platform_driver dw_mci_platform_driver = { .probe = dw_mci_probe, .remove = dw_mci_remove, .driver = { .name = "dwmmc", .of_match_table = dw_mci_of_match, .pm = &dw_mci_pmops, }, }; module_platform_driver(dw_mci_platform_driver);

设备树里对应的节点至少要包含:

sdmmc: sdmmc@fe310000 { compatible = "snps,dw-mshc"; reg = <0x0 0xfe310000 0x0 0x1000>; interrupts = <0x0 0x42 0x4>; clocks = <&cru SDMMC_BIU>, <&cru SDMMC_CIU>; clock-names = "biu", "ciu"; bus-width = <4>; cap-mmc-highspeed; cap-sd-highspeed; status = "okay"; };

注意reg和interrupts必须与硬件实际设计一致,clock-names要和 probe 里 devm_clk_get 用的名字对应,否则 clk 获取直接失败。

4.2 完整 probe 骨架示例

综合前面的分析,一个简化的、可参考的 probe 函数大概是这样的:

static int dw_mci_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; struct dw_mci_platform_data *pdata = dev_get_platdata(&pdev->dev); struct dw_mci *host; struct resource *regs; struct mmc_host *mmc; int ret, irq; host = devm_kzalloc(&pdev->dev, sizeof(*host), GFP_KERNEL); if (!host) return -ENOMEM; host->dev = &pdev->dev; spin_lock_init(&host->lock); INIT_LIST_HEAD(&host->queue); tasklet_init(&host->tasklet, dw_mci_tasklet_func, (unsigned long)host); regs = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!regs) return -ENXIO; host->regs = devm_ioremap_resource(&pdev->dev, regs); if (IS_ERR(host->regs)) return PTR_ERR(host->regs); irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; host->irq = irq; host->biu_clk = devm_clk_get(&pdev->dev, "biu"); if (IS_ERR(host->biu_clk)) return PTR_ERR(host->biu_clk); host->ciu_clk = devm_clk_get(&pdev->dev, "ciu"); if (IS_ERR(host->ciu_clk)) return PTR_ERR(host->ciu_clk); ret = clk_prepare_enable(host->biu_clk); if (ret) return ret; ret = clk_prepare_enable(host->ciu_clk); if (ret) goto err_biu; host->bus_hz = clk_get_rate(host->ciu_clk); if (!host->bus_hz) { ret = -EINVAL; goto err_ciu; } host->quirks = (enum dw_mci_quirks)device_get_match_data(&pdev->dev); ret = dw_mci_init_slot_caps(host); if (ret) goto err_ciu; ret = dw_mci_init_dma(host); if (ret) goto err_ciu; ret = devm_request_irq(&pdev->dev, irq, dw_mci_interrupt, IRQF_SHARED, "dw-mci", host); if (ret) goto err_ciu; mmc = mmc_alloc_host(sizeof(struct dw_mci_slot), &pdev->dev); if (!mmc) { ret = -ENOMEM; goto err_ciu; } host->mmc = mmc; ret = dw_mci_add_slot(host, 0); if (ret) { mmc_free_host(mmc); goto err_ciu; } platform_set_drvdata(pdev, host); return 0; err_ciu: clk_disable_unprepare(host->ciu_clk); err_biu: clk_disable_unprepare(host->biu_clk); return ret; }

这个骨架刻意省略了很多平台差异代码,但结构上已经把核心流程都串起来了。你在实际平台里看到的 dw_mci_probe 通常还会多出dw_mci_parse_dt、dw_mci_pltfm_register、dw_mci_pltfm_init之类的封装,但核心基调不变。

4.3 错误路径的清理顺序有讲究

一个容易被忽略但极其重要的点是错误路径的清理顺序。可以这样想:probe 是一个“按顺序创建资源”的过程,所以失败时应该“按逆序销毁资源”。比如 clk_prepare_enable 开启的时钟,必须在返回错误前 disable;mmc_alloc_host 分配的对象,必须在失败时 mmc_free_host。而 devm 系列接口管理的资源不需要手动释放,但时钟是例外——devm_clk_get 虽然自动管理 clk 的引用,但 clk_prepare_enable 之后的状态不会自动恢复,必须显式调用 clk_disable_unprepare。

实际项目中我看到过不少低级错误:某个 err 标签路径上漏了 clk_disable_unprepare,导致 probe 失败后时钟一直开着,系统进入低功耗模式时电流多出几十毫安;还有的路径上重复调用 mmc_free_host,导致内核 panic。所以写错误路径的时候一定要“慢下来”,对照着每一步资源申请,逐个检查逆序清理。

调试时还有一个技巧:probe 函数的每个错误返回点之前最好都加上 dev_err 打印,明确失败的阶段。比如:

err_ciu: clk_disable_unprepare(host->ciu_clk); dev_err(&pdev->dev, "dw_mci probe failed: %d\n", ret); return ret;

这样一旦 probe 失败,dmesg 里就能直接定位到哪个资源出了问题,省去重新编译加打印的麻烦。

5. 常见问题与排查技巧实录

5.1 probe 失败但没打印:先查资源和时钟

第一次移植 dw_mci 驱动时,最常见的现象是:启动日志里根本看不到任何 mmc0 相关输出,probe 似乎没执行。排查思路按优先级排列:

  • 确认设备树节点状态是 “okay” 而不是 “disabled”。
  • 确认驱动确实匹配了:在 dw_mci_probe 入口临时加个 dev_info,看看有没有执行。
  • 确认 ioremap 没有失败:如果 dts 里 reg 范围写错,或者和别的节点冲突,devm_ioremap_resource 会报错。
  • 确认时钟框架没问题:使用clk_summary接口查看 ciu 和 biu 时钟是否 enable、频率是否为 0。

如果 probe 确实执行了但中途失败,dmesg 一般会有明确报错。看到 -ENXIO 基本就是 resource 获取失败,-ENOMEM 是内存分配失败,-EBUSY 可能是中断冲突或者资源被别人占用。

5.2 中断注册失败:共享中断的隐藏陷阱

上面提到 IRQF_SHARED 是必需的。但还有一种情况:分配中断号时,platform_get_irq返回的是 0。在内核里 0 是合法中断号吗?不是。新的内核版本里platform_get_irq返回负错误码表示失败,0 表示无效,需要额外判断。如果你用老内核,有些平台把 0 当作“没有中断”,所以调用前一定要确认:

irq = platform_get_irq(pdev, 0); if (irq < 0) return irq;

如果devm_request_irq返回 -EBUSY,先看是不是有别的驱动已经占用了这根中断线且没有设置 IRQF_SHARED。然后是更隐蔽的:中断控制器的层级关系,比如 IOMMU 或 GIC 的级联配置,可能导致中断号对不上。实际排查时,可以先用/proc/interrupts看看各个中断的注册情况,再用cat /proc/irq/<irq>/spurious看是否有 spurious interrupt。

5.3 卡能识别但读写超时:DMA 描述符或者时钟分频的问题

这类问题最让人头疼,因为 probe 成功了,系统也识别到了卡,但一跑 I/O 就超时或者 CRC error。我遇到过的情况可以归成三类:

第一,DMA 描述符表没有正确初始化。IDMAC 模式下,如果描述符地址没有按硬件要求的对齐方式设置(通常是 4KB 对齐),或者描述符的 OWN 位没有正确置 1,硬件会一直等待 DMA 就绪,最后命令超时。排查时可以通过寄存器DMAC_CTRL查看 DMA 状态,确认是否处于 idle。

第二,时钟分频系数不对。dw_mci 的时钟分频是(bus_hz / (2 * target_hz))这样的关系,如果 bus_hz 设置错误,实际卡时钟会偏离目标值很大。特别是支持 UHS 模式后,频率升高,信号完整性问题暴露出来。排查手段是示波器量 CLK 引脚,看实际频率是否和协议要求的匹配。

第三,数据线信号完整性问题。这个问题和驱动代码无关,完全由硬件决定。如果你在高速模式下遇到随机 CRC error,尝试把 bus-width 降为 1 位、把 timing 模式降为 high speed,看看问题是否消失。如果降速后正常,基本可以确定是 PCB 走线或者叠层设计的问题,而不是驱动的 bug。

这里我做了一张速查表,方便你排查问题时快速定位:

现象可能原因排查手段
probe 不执行dts 状态不是 okay、compatible 不匹配检查 dts status、of_match_table
clk_get_rate 返回 0父时钟没配好、时钟节点名称错误clk_summary、示波器量晶振
寄存器读写全为 -1ciu 时钟未使能、供电异常clk_prepare_enable 检查、万用表量电源
request_irq 返回 -EBUSY中断共享未设置、中断号冲突/proc/interrupts、检查 IRQF_SHARED
probe 成功但没检测到卡卡检测引脚配置错误、cd-gpios 缺失检查设备树 cd-gpios、gpio 状态
卡识别但读写超时IDMAC 描述符错误、时钟分频不对读 DMAC_CTRL、示波器量 CLK 引脚
大文件读写 IO errorUHS 模式信号质量差、caps 位未清干净降速测试、检查 caps2 配置

5.4 调试工具和方法推荐

调试 dw_mci 驱动,我常用的手段按“成本从低到高”排:

  • 内核日志的 mmc 部分:打开CONFIG_MMC_DEBUG,可以在 dmesg 里看到 MMC core 发出的每一条命令的细节,包括 cmd、arg、resp。这能帮你快速定位“命令发到了哪一步失败”。
  • 寄存器 dump:dw_mci 的寄存器布局相对规整,通过 devmem 工具直接读关键寄存器,比如CTRL、CMD、RSP、INTSTAT、DMAC_CTRL。如果连寄存器都读不到正确值,说明时钟或 ioremap 有问题。
  • ftrace 的 irq 事件:打开CONFIG_FUNCTION_TRACER,跟踪 dw_mci_interrupt 的调用频率和耗时,可以判断是否存在中断风暴或者中断丢失。
  • 示波器/逻辑分析仪:这些是硬件手段,但往往是最终裁决者。量 CLK、CMD、DATA 引脚的波形,能直接看出协议时序是否满足规范。

有一个我屡试不爽的排查思路:先关掉 DMA,走 PIO 模式,看看问题是否依然存在。如果 PIO 模式下一切正常,说明问题出在 DMA 路径上;如果 PIO 模式也失败,说明问题在命令/响应路径或者时钟问题上。这种二分法能节省大量时间。

6. 最后再分享一点我自己的经验

做了这么多年存储类驱动,我越来越觉得读 dw_mci_probe 这种函数,不能只看它每一行在干什么,而是要把它理解成一条“资源生命周期链”:从设备树到内核,从平台总线到 MMC core,每一环的成败都依赖于前一个环节的正确性。写驱动的时候心里要装着这条链,调 bug 的时候也要顺着这条链去排查。最忌讳的是凭感觉改参数、加延时,那样只会把问题弄得更乱。

另外有一点是我踩过不少坑之后总结出来的:probe 函数里的每一个返回值都值得认真思考。内核里那么多错误码,用哪个不是随手选的,它直接影响调用者怎么处理失败。比如 -EPROBE_DEFER 和 -ENXIO 的差别就很大——前者告诉内核“我依赖的某个设备还没准备好,请晚点再试一次”,后者则是“这个设备根本用不了,别再试了”。在 dw_mci 的 probe 里,时钟获取失败时往往需要返回 -EPROBE_DEFER,因为时钟提供者可能还没注册。如果你返回的是 -ENXIO,内核后续不会再试探这个设备,哪怕时钟稍后就好了,你这块控制器也永远起不来了。

希望这篇拆解能帮你在看 dw_mci 驱动时少走点弯路。最后留一个小建议:拿到一个新平台的 dw_mci 驱动,先别急着跑起来,花半个小时把 dts 里的时钟、中断、reg 范围和硬件手册对一遍,这半小时一定值回票价。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询