[toc]

在这里插入图片描述

MMCI(ARM PrimeCell PL18x 主机控制器驱动):全面了解与深度解析

文件路径 / 技术中文名 / 功能概述

  • 文件路径:drivers/mmc/host/mmci.c
  • 技术中文名:MMCI(ARM PrimeCell PL180/PL181 及衍生控制器的 MMC/SD/SDIO Host 驱动)
  • 功能概述:该文件是 Linux MMC host 层驱动实现,负责把 MMC core 发下来的命令与数据请求,转换成 MMCI/SDMMC 控制器寄存器操作,覆盖命令发送、响应接收、数据通路控制、PIO 与 DMA 传输、中断处理、时钟与电源控制,以及不同硬件变体的差异适配。

介绍

drivers/mmc/host/mmci.c 属于 Linux MMC 子系统里的 Host Controller Driver。它不负责块层调度,也不负责文件系统,而是直接驱动一类具体的 MMC/SD/SDIO 控制器 IP。

从主线实现看,这个文件不是“单一芯片专用驱动”,而是一个覆盖面较广的家族驱动骨架。它最初面向 ARM PrimeCell PL180/PL181,后续又吸收了 ST、Ux500、STM32、Qualcomm 等衍生实现。也就是说,mmci.c 的核心价值不只是“驱动一个控制器”,而是通过统一骨架管理多种相似但不完全一致的控制器变体。

本文分析范围以 Linux 主线 drivers/mmc/host/mmci.c 和相邻头文件 drivers/mmc/host/mmci.h 为主,重点解释:

  • 驱动在 MMC 子系统中的位置
  • 寄存器语义与执行路径
  • 变体抽象方式
  • PIO / DMA / busy detect 等关键机制
  • sdhci.cdw_mmc.cmmc_spi.c 的差异

本文不展开 MMC 协议本身的所有细节,也不展开块层与文件系统行为。

历史与背景

1. 诞生背景

这类驱动出现的根本原因,是 PL180/PL181 这类控制器并不遵循 SDHCI 标准寄存器模型,不能直接套用 sdhci.c 这类通用主机驱动。Linux 需要一个专门面向这类控制器寄存器布局和状态机设计的 Host 驱动。

在它出现之前,Linux 生态中解决 MMC/SD 控制器支持问题的方式,通常是:

  • 对标准 SD Host Controller 使用 SDHCI 驱动
  • 对 Synopsys DesignWare 控制器使用 dw_mmc
  • 对没有原生 MMC Host 的场景,用 mmc_spi 之类的替代路径
  • 对专有控制器单独写 Host 驱动

旧方案的痛点不是“没有存储卡支持”,而是控制器模型高度异构:

  • 寄存器布局不同
  • 时钟控制方式不同
  • 数据通路状态机不同
  • DMA 路径不同
  • busy 检测和 SDIO 支持差异较大

因此,PL18x 这一类控制器必须有自己的驱动实现。

2. 发展历程

从源码版权和当前主线实现结构看,mmci.c 是一条历史较长的驱动线。它早期主要服务 ARM PrimeCell PL180/PL181,后续逐步吸收了 ST-Ericsson、Ux500、STM32 和 Qualcomm 等变体支持。

对今天使用方式影响最大的演进最大的演进有三类:

第一类是变体抽象的形成。
当前实现不是为每个 SoC 重写一份驱动,而是通过 variant_data 把差异抽象出来。不同变体可以有不同的:

  • FIFO 大小
  • datalength_bits
  • datactrl_blocksz
  • 时钟寄存器位定义
  • 是否支持任意 block size
  • 是否支持 busy detect
  • 是否支持 SDIO IRQ
  • DMA 方式与限制
  • 最大频率上限

这使得 mmci.c 从“一个驱动一个控制器”演进成“一个骨架驱动多个相近控制器”。

第二类是设备树绑定的规范化。
早期很多板级信息依赖 platform data 或早期 DT 约定,后来逐步转向统一的绑定描述。当前绑定文档明确把 PL180、PL181、PL18x 通配以及 STM32 的相关兼容项纳入统一 schema,约束了寄存器窗口、中断数量、时钟数量、DMA 通道名等资源表达方式。

第三类是厂商增强逻辑外提。
主线中已经存在与 mmci.c 配套的变体文件,例如 STM32 和 Qualcomm 的增强逻辑文件。这说明主线维护方向不是把所有厂商特性继续堆进一个大文件,而是保留 mmci.c 作为公共骨架,再把强变体特性拆出。

3. 社区与生态现状

从主线文档和当前源码组织方式看,这条驱动线仍在维护,并未废弃。

它的典型应用场景主要在:

  • ARM 参考设计平台
  • ST / Ux500 / Nomadik 一类历史 ARM SoC
  • STM32MP 及相关 SDMMC 路线
  • 某些 Qualcomm 衍生控制器场景
  • 嵌入式 Linux、工业控制、板级 BSP

它不是云平台或通用服务器环境里最常被直接讨论的“平台技术选型点”,但在嵌入式 Linux 和 SoC BSP 层面仍然非常实际。

核心原理与设计

1. 核心工作原理

1.1 在 Linux 系统中的位置

mmci.c 位于 MMC 子系统的 host 层。它接收 MMC core 传下来的请求,并将其翻译成具体寄存器操作。核心职责包括:

  • 设置命令参数与命令寄存器
  • 启动命令路径状态机
  • 配置数据长度、方向、块大小与 DMA 位
  • 启动数据路径状态机
  • 响应命令中断、数据中断、FIFO 中断
  • 在 PIO 和 DMA 之间切换
  • 在请求结束时回收状态并通知 MMC core

因此,这个驱动的本质是“MMC core 与具体控制器硬件之间的桥接层”。

1.2 寄存器模型

mmci.h 可以看出,这个驱动的寄存器模型非常直接,关键寄存器包括:

  • MMCIPOWER:电源控制
  • MMCICLOCK:时钟控制
  • MMCIARGUMENT:命令参数
  • MMCICOMMAND:命令寄存器
  • MMCIDATATIMER:数据超时
  • MMCIDATALENGTH:数据长度
  • MMCIDATACTRL:数据控制
  • MMCISTATUS:状态寄存器
  • MMCICLEAR:中断与状态清除
  • MMCIMASK0 / MMCIMASK1:中断屏蔽

其中两个状态机最关键:

  • CPSM:Command Path State Machine,负责命令发送与响应接收
  • DPSM:Data Path State Machine,负责数据读写路径

驱动围绕这两个状态机组织命令与数据请求,整体逻辑很贴近硬件。

1.3 关键执行流程

结合文件中的核心辅助函数,可以把主路径概括为:

  1. 上层提交一个 mmc_request
  2. 驱动校验数据请求是否合法
  3. 根据请求类型配置命令与数据参数
  4. 写入参数寄存器、数据长度寄存器、数据控制寄存器
  5. 启动命令路径状态机
  6. 如有数据传输,则通过 PIO 或 DMA 推进数据通路
  7. 由命令中断、数据中断、FIFO 中断驱动状态推进
  8. 结束数据路径,清理中断屏蔽与状态位
  9. 将请求结果回送给 MMC core

对该文件来说,真正重要的不是某一个单独函数,而是“命令通路 + 数据通路 + 中断推进 + 结束回调”这一整条链。

1.4 变体抽象方式

这是 mmci.c 最核心的设计点。

驱动没有把所有控制器差异硬编码在大量 if (soc == ...) 里,而是通过 variant_data 描述硬件差异,通过 mmci_host_ops 描述可替换行为。

variant_data 里直接体现的差异包括:

  • FIFO 容量与半满阈值
  • 最大时钟上限
  • 数据长度位宽
  • 块大小位域
  • 是否允许任意块大小
  • 是否支持 busy detect
  • 是否支持 SDIO IRQ
  • 是否 STM32 特有时钟分频
  • 是否存在 QCOM 特有时钟/数据位
  • 是否存在 DMA LLI 能力

这意味着阅读 mmci.c 时,不能把某一条路径当成全部硬件都成立的“统一真相”。许多行为都依赖 variant_data

1.5 数据路径约束

mmci_validate_data() 明确体现了一个关键硬件边界:

  • 如果变体没有声明支持任意 block size,那么 block size 必须是 2 的幂
  • 否则驱动会直接返回 -EINVAL

这说明很多“请求失败”并不是 MMC core 的抽象问题,而是控制器寄存器编码能力本身的限制。

同时,不同变体的 datalength_bitsdatactrl_blocksz 也不同,直接影响:

  • 单次请求最大可描述数据长度
  • 块大小编码上限
  • 总块数能力边界

这是生产环境做大块传输、DMA 分段和性能分析时必须考虑的硬件前提。

1.6 PIO 与 DMA

mmci.c 并不强制 DMA,它保留了 PIO 和 DMA 两条路径。

DMA 相关实现显示,这个驱动会尝试申请 "rx""tx" 两个 DMA 通道。若资源不足,DMA 可能被关闭,回退到 PIO。这里有几个实际工程意义很强的点:

  • 如果只有 RX 通道存在,驱动会尝试复用它
  • 如果 RX/TX 资源不足,DMA 可能整体不可用
  • DMA finalize 阶段会检查 FIFO 里是否还残留数据
  • 如果发现 DMA 控制器无法正确处理某些 scatter-gather 场景,驱动会记录为错误,并采取“放弃 DMA、转回 PIO”的保守策略

这说明 mmci.c 在 DMA 路径上不是“激进追求理论性能”,而是优先保证正确性与可回退性。

1.7 busy detect

Ux500 / STM32 相关变体的一个重要增强能力是 busy detect。

mmci.c 中的 busy 处理逻辑可以看出,某些变体会监视 DAT0 忙状态,并通过状态机跟踪:

  • 是否进入忙状态
  • 是否等到忙开始中断
  • 是否等到忙结束中断
  • 是否发生忙等待超时

这类逻辑的存在,说明驱动不仅处理普通命令完成,还要处理响应后设备继续拉低 DAT0 的时序场景。R1B 类命令、某些 SDIO/存储器操作,以及厂商硬件差异,都会影响这里的行为。

1.8 时钟与电源控制

mmci.h 表明,这类控制器的时钟和电源寄存器语义在不同变体之间差异明显:

  • 原始 ARM 变体使用一套较早的时钟/电源位定义
  • ST 变体复用了部分位做方向控制
  • STM32 变体的时钟和电源寄存器含义又发生变化

所以 set_ios 这一类操作在这个驱动中不是“简单设置频率和总线宽度”,而是需要结合变体位定义生成寄存器值。对板级 bring-up 来说,这一点很关键,因为同样的 bus-width、时钟和电压切换需求,在不同变体上的寄存器动作不相同。

2. 设计目标

mmci.c 的设计目标可以概括为:

  1. 兼容性
    用一个驱动骨架覆盖 PL180/PL181 及多个衍生实现。

  2. 可维护性
    通过 variant_data 和可替换操作减少重复驱动代码。

  3. 资源控制与性能平衡
    支持 DMA,但保留 PIO 回退;优先保证可靠性。

  4. 适配现实硬件差异
    明确吸收不同厂商在 FIFO、busy detect、时钟位、DMA 行为上的差异。

  5. 可观测性与可排障性
    错误路径会保留较明确的日志与失败回退行为,便于定位 DMA、块大小、FIFO、忙检测问题。

3. 核心优势

优势一:适合做家族化控制器适配

对于 PL18x 系列及其衍生控制器,这个驱动的复用性很强。BSP 团队不需要为每个 SoC 分叉一份完整驱动,只需在合理范围内扩展变体数据和少量变体操作。

优势二:寄存器语义清晰,调试路径直接

它不经过一层行业标准寄存器抽象,而是直接使用控制器原生语义组织命令和数据通路。对于硬件调试、时序排障、寄存器对照分析来说,这种结构是有利的。

优势三:对硬件边界表达明确

这个驱动对块大小、FIFO、DMA、busy detect 等硬件边界表达得比较直接。虽然这会增加代码分支,但也让很多异常行为能快速回到硬件约束本身,而不是停留在“MMC 请求失败”这种模糊层面。

优势四:保守而实用的 DMA 策略

DMA 路径遇到不可控问题时,驱动会主动降级,而不是继续冒险。这种设计在嵌入式量产环境里通常比追求极限吞吐更有价值。

4. 局限性与缺点

局限一:变体复杂度高

variant_data 虽然提升了复用性,但也让代码理解成本显著上升。阅读时如果忽略当前实际变体,很容易把某个变体的行为误认为通用行为。

局限二:硬件能力边界明显

这类控制器在以下方面往往存在天然限制:

  • 块大小编码方式
  • 数据长度位数
  • DMA 能力不一致
  • FIFO 深度有限
  • busy detect 并非所有变体都有
  • 时钟与电源寄存器语义不统一

这意味着它并不是一类“能力完全现代化、统一化”的主机控制器驱动。

局限三:板级依赖较重

时钟、复位、供电、GPIO、DMA 通道、中断线、pinctrl 描述稍有偏差,就可能出现:

  • 探测成功但读写不稳定
  • DMA 不生效
  • PIO 中断异常
  • 高速模式不稳定
  • 电压切换失败

局限四:默认参数容易引发误判

源码里模块参数 fmax 的默认值很保守。如果板级资源没有正确提供最大频率信息,驱动可能工作但性能异常低,容易被误判为总线质量问题或 DMA 故障。

5. 不适用场景

场景一:控制器本身是标准 SDHCI

如果硬件是标准 SD Host Controller,优先使用 SDHCI 驱动。此时使用 MMCI 思路没有意义,因为两者的寄存器模型和能力组织方式不同。

场景二:控制器是 DesignWare MMC

如果硬件是 Synopsys DesignWare 路线,应优先使用 dw_mmc,因为它有自己完整的寄存器模型、IDMAC/EDMAC 逻辑和平台适配层。

场景三:系统没有原生 MMC Host,只能走 SPI

这时应使用 mmc_spi 一类驱动。mmci.c 不适合这种场景。

场景四:需要高度依赖更统一的现代标准能力

如果项目特别强调标准控制器能力、通用平台兼容性和更大生态复用,通常优先考虑 SDHCI 家族,而不是 MMCI 家族。

使用场景

1. 适合使用的场景

场景一:基于 PL180 / PL181 的参考设计或历史 ARM 平台

  • 场景背景:控制器与 ARM PrimeCell PL180/PL181 兼容
  • 为什么适合:这是该驱动最原生的适配对象
  • 实际收益:驱动路径短,寄存器映射清晰,bring-up 成本低
  • 注意问题:先确认 DT 或平台资源中的时钟、电源与中断信息完整

场景二:ST / Ux500 / Nomadik 派生平台

  • 场景背景:控制器在 PL18x 基础上加入 ST 方向控制、busy detect、扩展位
  • 为什么适合:主线已经存在对应变体抽象
  • 实际收益:无需长期维护大规模厂商私有分叉
  • 注意问题:DT 属性和方向控制相关资源要准确描述

场景三:STM32 SDMMC 路线

  • 场景背景:控制器沿用 MMCI 家族思路,但寄存器和能力已有较大扩展
  • 为什么适合:主线 mmci.c 已纳入 STM32 v1/v2/v3 相关变体支持
  • 实际收益:能直接利用主线对 FIFO、busy detect、IDMA/LLI 等差异的处理
  • 注意问题:STM32 能力不能外推为所有 MMCI 控制器都具备

场景四:老平台量产维护

  • 场景背景:系统已经大规模部署,关注的是稳定性和内核升级可控性
  • 为什么适合:主线对这条驱动线的兼容性维护较长
  • 实际收益:升级时可继续沿主线修复思路推进
  • 注意问题:升级时必须重点回归 DMA、频率、电压与中断路径

2. 不推荐使用的场景

  • 标准 SDHCI 控制器平台
  • DesignWare MMC 平台
  • 只通过 SPI 接卡的平台
  • 追求统一标准主机能力矩阵的平台
  • 板级资源描述长期不规范、维护成本过高的平台

原因通常来自:

  • 实现模型不匹配
  • 性能路径不匹配
  • 运维复杂度不划算
  • 硬件边界与项目需求不匹配

3. 生产实践建议

配置关注点

生产环境最应该优先检查的配置点是:

  • max-frequency
  • bus-width
  • 中断数量与 wiring
  • vmmc-supply / vqmmc-supply
  • DMA 通道定义
  • pinctrl 与方向控制相关资源
  • reset 资源
  • 变体专用属性

监控指标

建议至少关注:

  • 中断计数是否持续增长
  • DMA 是否真正启用
  • 是否频繁回退到 PIO
  • 命令超时、数据超时、CRC 错误数量
  • 高频读写时的稳定性
  • 实际工作频率是否符合预期

排障方法

建议按以下顺序排查:

  1. 看启动日志是否正确识别控制器
  2. 看板级时钟和供电是否到位
  3. /proc/interrupts 中命令 IRQ / FIFO IRQ 是否增长
  4. 判断当前走的是 DMA 还是 PIO
  5. 用读写压力测试复现
  6. 再回头核查 block size、scatter-gather、FIFO 与 busy detect 相关边界

风险控制

  • 升级内核时优先灰度验证 DMA 路径
  • 不要只测小文件读写,要测长时间顺序读写和高并发 I/O
  • 对 SDIO 设备要单独验证中断与 busy 行为
  • 对历史平台要保留 PIO 回退验证路径

4. 命令、配置与排障示例

常用命令

1
2
3
dmesg | grep -iE 'mmc|mmci'
grep -iE 'mmc|sd' /proc/interrupts
ls /sys/bus/mmc/devices/

常见验证命令

1
2
cat /sys/kernel/debug/clk/clk_summary | grep -i mmc
cat /sys/kernel/debug/mmc0/ios

使用 mmc_test 做 Host 驱动验证

1
2
3
4
5
6
7
modprobe mmc_test
ls /sys/bus/mmc/devices/
echo 'mmc0:0001' > /sys/bus/mmc/drivers/mmcblk/unbind
echo 'mmc0:0001' > /sys/bus/mmc/drivers/mmc_test/bind
cat /sys/kernel/debug/mmc0/mmc0:0001/testlist
echo 4 > /sys/kernel/debug/mmc0/mmc0:0001/test
dmesg | grep mmc0

典型 DT 关注点

1
2
3
4
5
6
7
8
9
10
11
12
mmc@... {
compatible = "arm,pl18x", "arm,primecell";
reg = <...>;
interrupts = <...>, <...>;
clocks = <...>, <...>;
dmas = <...>, <...>;
dma-names = "rx", "tx";
bus-width = <4>;
max-frequency = <100000000>;
vmmc-supply = <...>;
vqmmc-supply = <...>;
};

常见误区

  • 驱动加载成功就认为时钟、电压、DMA 都正确
  • 把某个变体能力当成所有 MMCI 变体通用能力
  • 忽略 max-frequency
  • 只测 PIO 小流量,不测 DMA 长时间压力
  • 只看卡能枚举,不看高速模式是否稳定

对比分析

这里把 mmci.csdhci.cdw_mmc.cmmc_spi.c 做横向比较。

1. 实现方式

MMCI
基于 PL18x 家族寄存器模型实现,是一个“公共骨架 + 变体数据 + 变体操作”的专用驱动。

SDHCI
基于行业标准 SD Host Controller Interface 寄存器模型实现,平台侧通常通过 glue 层适配。

DesignWare MMC
基于 Synopsys 专有控制器实现,通常带有自己的一整套 DMA、描述符与平台适配逻辑。

MMC over SPI
本质不是原生 MMC Host 控制器,而是通过 SPI 协议胶水层去访问存储卡。

2. 性能开销

MMCI
性能上限较依赖具体变体和 DMA 质量。DMA 成熟时可用性较好,但 PIO 回退代价明显。

SDHCI
通常在主流平台上性能和标准能力更均衡,尤其在 ADMA 等能力完整时更有优势。

DesignWare MMC
在 DMA 体系完善的平台上性能通常较好,但驱动路径更重,调试复杂度也更高。

MMC over SPI
性能明显弱于原生 Host 模式,更适合作为替代方案而不是高性能方案。

3. 资源占用

MMCI
驱动本身常驻开销不高,但对 DMA 通道、FIFO、中断与板级资源依赖较强。

SDHCI
资源占用通常也不高,但现代平台上常叠加 PHY、tuning、平台 glue。

DesignWare MMC
常见地会引入更复杂的 DMA 描述符和平台数据结构,整体更重。

MMC over SPI
不需要原生 MMC Host IP,但会占用 SPI 控制器与总线时序资源。

4. 隔离级别

这几类技术都不是安全隔离机制。
它们都运行在内核态,所谓差异主要体现在控制器功能边界、DMA 路径和总线模型,而不是进程级或硬件级安全隔离。

5. 启动速度

MMCI
初始化路径相对直接,尤其在老平台和基础变体上较轻量。

SDHCI
初始化也较快,但现代控制器可能附带更多能力探测和参数配置。

DesignWare MMC
初始化通常更复杂,尤其涉及 DMA 和平台 glue 时。

MMC over SPI
通常不以启动快见长,且受 SPI 交互模式限制。

6. 运维复杂度

MMCI
中等偏高。问题不在代码量本身,而在变体差异和板级资源正确性。

SDHCI
通常中等。标准化程度高,生态经验更丰富。

DesignWare MMC
通常偏高。平台差异和 DMA 复杂度都更明显。

MMC over SPI
实现边界简单,但性能与总线共享问题限制很大。

7. 适用场景差异

  • 优先选 MMCI:硬件明确属于 PL18x 家族或其衍生实现
  • 优先选 SDHCI:硬件兼容标准 SDHCI
  • 优先选 DesignWare:硬件是 Synopsys DesignWare MMC
  • 优先选 MMC over SPI:没有原生 MMC Host,只能走 SPI

总结表

对比维度 MMCI SDHCI DesignWare MMC MMC over SPI
实现方式 PL18x 家族专用骨架 + 变体抽象 标准寄存器模型 + 平台 glue 专用控制器驱动 + 自有 DMA 体系 SPI 协议胶水层
性能开销 依赖变体与 DMA 质量,PIO 回退代价明显 通常较均衡,标准能力成熟 通常较强,但实现更重 较高,原生模式通常更优
资源占用 中等 中等 中等偏高 占用 SPI 总线资源
隔离级别 内核态驱动,无额外安全隔离 内核态驱动,无额外安全隔离 内核态驱动,无额外安全隔离 内核态驱动,无额外安全隔离
启动速度 较快 较快到中等 中等 中等
运维复杂度 中等偏高 中等 偏高 中等
推荐场景 PL18x 及衍生控制器 标准 SDHCI 控制器 DesignWare 控制器 无原生 MMC Host 的替代路径

总结

1. 关键特性总结

mmci.c 最关键的特性可以总结为:

  • 它是 Linux 主线中面向 PL18x 家族的 Host 驱动骨架
  • 它的核心设计不是单一控制器驱动,而是变体抽象
  • 它直接围绕 CPSM / DPSM 与状态寄存器组织执行路径
  • 它同时支持 PIO 与 DMA,并保留保守回退策略
  • 它非常依赖板级资源描述的正确性
  • 它的很多行为都必须在具体变体前提下理解

mmci_probe mmci_remove mmci_save mmci_restore mmci_runtime_suspend mmci_runtime_resume mmci_dev_pm_ops mmci_ids MODULE_DEVICE_TABLE mmci_driver module_amba_driver module_param MODULE_DESCRIPTION MODULE_LICENSE 探测建链、变体分流、运行时电源管理与 AMBA 注册路径梳理

作用与实现要点

这段代码把 MMCI 主机的“设备出现后如何接入 Linux MMC 子系统”拆成四层:
先由 AMBA 总线用 mmci_ids 做匹配,把命中的 id->data 交给 mmci_probe() 作为 variantprobe()
再把时钟、复位、寄存器映射、OCR、电气能力、IRQ、DMA、PM 参数和 mmc_host 串起来;
mmci_dev_pm_ops 把运行时挂起/恢复与系统休眠复用到同一组保存/恢复寄存器路径;
module_amba_driver(mmci_driver) 则把整个 mmci_driver 对象注册进 AMBA 框架。
struct amba_idmask 决定比较哪些硬件 ID 位,data 是驱动私有数据,命中后会成为后续能力分支的总开关;
struct dev_pm_ops 同时承载系统休眠与运行时 PM 回调;
device_driver.probe_type = PROBE_PREFER_ASYNCHRONOUS 表示该驱动倾向异步探测以减少启动阻塞。
PrimeCell 设备在 Devicetree 下也会以 amba_device 形式出现,因此这里同时接受平台数据和 DT 路径。([Linux内核文档][1])

代码里还有一个容易忽略的点:mmc->opshost->mmc_ops 都指向全局 mmci_ops
probe() 又会按变体去写 mmci_ops.card_busyenable_sdio_irqack_sdio_irq
这意味着这里不是只给当前 host 绑定私有回调,而是在修改一个全局回调表,再由后续 MMC 核心经 mmc->ops 进入忙检测和 SDIO IRQ 控制路径。
另一条重要回退链是 module_param(fmax, uint, 0444):当板级或 DT 没有给出 mmc->f_max 时,probe() 会退回到 fmax
再和输入时钟或变体上限比较后取较小值。模块参数既可通过内核命令行携带,也可在模块装载时通过 modprobe 传入;
MODULE_DEVICE_TABLE() 会把匹配表导出给自动装载链路;MODULE_LICENSE() 则给模块装载器和用户态工具提供许可信息。([Linux内核文档][2])

mmci_probe 设备探测、资源建链与主机注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/**
* @brief 为匹配到的 AMBA MMCI 控制器建立主机实例并接入 MMC 子系统
* @param dev 当前命中的 AMBA 设备,提供资源、IRQ、平台数据与 DT 节点
* @param id 当前命中的 AMBA 表项,id->data 会给出本次硬件变体描述
* @return 0 表示主机注册成功,负错误码表示资源获取、时钟/复位、IRQ、
* GPIO、寄存器映射或主机注册过程中失败
*
* 补充说明:
* - 这里把 mmc_host、mmci_host、variant、时钟/复位/中断/DMA/PM 串成同一条生命周期
* - 这里把全局 mmci_ops 绑定到 mmc->ops,并按 variant 改写其中部分回调入口
*/
static int mmci_probe(struct amba_device *dev,
const struct amba_id *id)
{
struct mmci_platform_data *plat = dev->dev.platform_data;
struct device_node *np = dev->dev.of_node;
struct variant_data *variant = id->data;
struct mmci_host *host;
struct mmc_host *mmc;
int ret;

if (!plat && !np) /* 没有平台数据也没有 DT 时,后续拿不到板级连线与能力来源,继续探测只会得到不完整主机 */
{
dev_err(&dev->dev, "No plat data or DT found\n");
return -EINVAL;
}

if (!plat) {
plat = devm_kzalloc(&dev->dev, sizeof(*plat), GFP_KERNEL);
if (!plat) /* 只有 DT 路径时仍要补一份平台数据容器,后面统一从 plat 读取回退项 */
return -ENOMEM;
}

mmc = devm_mmc_alloc_host(&dev->dev, sizeof(*host));
if (!mmc) /* 主机对象分配失败时还没完成任何硬件启用,直接返回即可 */
return -ENOMEM;

host = mmc_priv(mmc);
host->mmc = mmc;
host->mmc_ops = &mmci_ops; /* 这里绑定的是全局 mmci_ops,后面对 mmci_ops 字段的写入会影响经该表进入的后续请求路径 */
mmc->ops = &mmci_ops; /* MMC 核心后续通过这张表调用请求、忙检测和 SDIO IRQ 相关入口 */

ret = mmci_of_parse(np, mmc);
if (ret) /* DT 解析失败后,频率、电源、卡检测等后续信息都不可信,必须立即退出 */
return ret;

if (!variant->opendrain) {
host->pinctrl = devm_pinctrl_get(&dev->dev);
if (IS_ERR(host->pinctrl))
return dev_err_probe(&dev->dev, PTR_ERR(host->pinctrl),
"failed to get pinctrl\n");

host->pins_opendrain = pinctrl_lookup_state(host->pinctrl,
MMCI_PINCTRL_STATE_OPENDRAIN);
if (IS_ERR(host->pins_opendrain)) /* 这类变体没有硬件 open-drain 位,只能靠 pinctrl 状态切换来完成总线电气模式 */
return dev_err_probe(&dev->dev, PTR_ERR(host->pins_opendrain),
"Can't select opendrain pins\n");
}

host->hw_designer = amba_manf(dev);
host->hw_revision = amba_rev(dev);
dev_dbg(mmc_dev(mmc), "designer ID = 0x%02x\n", host->hw_designer);
dev_dbg(mmc_dev(mmc), "revision = 0x%01x\n", host->hw_revision);

host->clk = devm_clk_get(&dev->dev, NULL);
if (IS_ERR(host->clk))
return PTR_ERR(host->clk);

ret = clk_prepare_enable(host->clk);
if (ret) /* 从这里开始硬件已被真正拉起,后面所有失败都要走 clk_disable 回收 */
return ret;

if (variant->qcom_fifo)
host->get_rx_fifocnt = mmci_qcom_get_rx_fifocnt; /* FIFO 计数方式由变体决定,后续收数路径都走这里 */
else
host->get_rx_fifocnt = mmci_get_rx_fifocnt;

host->plat = plat;
host->variant = variant; /* id->data 在这里落到 host->variant,后面时钟、busy detect、SDIO IRQ、寄存器宽度都从这里分流 */
host->mclk = clk_get_rate(host->clk);
if (host->mclk > variant->f_max) {
ret = clk_set_rate(host->clk, variant->f_max); /* 输入时钟高于该变体上限时先往下调,避免后面分频仍落在超规格区间 */
if (ret < 0)
goto clk_disable; /* 已经 enable 过时钟,这里统一退回关钟路径,避免探测失败后把外设留在工作态 */
host->mclk = clk_get_rate(host->clk);
dev_dbg(mmc_dev(mmc), "eventual mclk rate: %u Hz\n",
host->mclk);
}

host->phybase = dev->res.start;
host->base = devm_ioremap_resource(&dev->dev, &dev->res);
if (IS_ERR(host->base)) {
ret = PTR_ERR(host->base);
goto clk_disable; /* 寄存器窗口拿不到时后面所有 writel/readl 都无从谈起,这里回收已开启的时钟 */
}

if (variant->init)
variant->init(host); /* 变体私有寄存器/能力补丁在这里落地,后续通用路径默认认为这些差异已经准备好 */

if (variant->st_clkdiv)
mmc->f_min = DIV_ROUND_UP(host->mclk, 257); /* ST 分频公式不同,最小时钟必须按该公式估算 */
else if (variant->stm32_clkdiv)
mmc->f_min = DIV_ROUND_UP(host->mclk, 2046); /* STM32 SDMMC 再走一套不同分频范围,避免把最小时钟估得过高 */
else if (variant->explicit_mclk_control)
mmc->f_min = clk_round_rate(host->clk, 100000); /* 这类变体由时钟框架直接找最接近 100kHz 的输入频点 */
else
mmc->f_min = DIV_ROUND_UP(host->mclk, 512);

if (mmc->f_max)
mmc->f_max = variant->explicit_mclk_control ?
min(variant->f_max, mmc->f_max) :
min(host->mclk, mmc->f_max); /* 上层已给出最大频率时,仍要再和变体或输入时钟上限取较小值 */
else
mmc->f_max = variant->explicit_mclk_control ?
fmax : min(host->mclk, fmax); /* 没给板级上限时退回模块参数 fmax,这就是 module_param 影响 probe 的实际落点 */

dev_dbg(mmc_dev(mmc), "clocking block at %u Hz\n", mmc->f_max);

host->rst = devm_reset_control_get_optional_exclusive(&dev->dev, NULL);
if (IS_ERR(host->rst)) {
ret = PTR_ERR(host->rst);
goto clk_disable;
}
ret = reset_control_deassert(host->rst);
if (ret)
dev_err(mmc_dev(mmc), "failed to de-assert reset\n"); /* 这里只报错不中断,说明有些平台即便拿不到复位线释放也可能继续工作 */

ret = mmc_regulator_get_supply(mmc);
if (ret) /* 供电影响 OCR 与上电流程,拿不到电源描述时不能把主机交给 MMC 核心 */
goto clk_disable;

if (!mmc->ocr_avail)
mmc->ocr_avail = plat->ocr_mask; /* DT 没给 OCR 时退回平台数据,决定后续卡电压能力暴露 */
else if (plat->ocr_mask)
dev_warn(mmc_dev(mmc), "Platform OCR mask is ignored\n");

mmc->caps |= MMC_CAP_CMD23;

if (variant->busy_detect) {
mmci_ops.card_busy = mmci_card_busy; /* 这里改的是全局 ops 上的 busy 回调,后续等待 busy 清除的请求会经它判断卡是否仍忙 */
if (variant->busy_dpsm_flag)
mmci_write_datactrlreg(host,
host->variant->busy_dpsm_flag); /* 这类硬件需要先在 DPSM 打开 busy 观察位,否则回调存在也看不到硬件忙信号 */
mmc->caps |= MMC_CAP_WAIT_WHILE_BUSY;
}

if (variant->supports_sdio_irq && host->mmc->caps & MMC_CAP_SDIO_IRQ) {
mmc->caps2 |= MMC_CAP2_SDIO_IRQ_NOTHREAD;

mmci_ops.enable_sdio_irq = mmci_enable_sdio_irq; /* 绑定全局 SDIO IRQ 控制入口后,MMC/SDIO 栈才能显式开关设备侧中断 */
mmci_ops.ack_sdio_irq = mmci_ack_sdio_irq; /* 绑定应答入口,避免 SDIO 中断触发后无法正确清除状态 */

mmci_write_datactrlreg(host,
host->variant->datactrl_mask_sdio); /* 某些变体要先把 SDIO 相关位写进数据控制寄存器,中断路径才真正可用 */
}

if (variant->busy_timeout)
mmc->caps |= MMC_CAP_NEED_RSP_BUSY; /* 硬件强依赖 busy timeout 时,命令响应类型必须带 R1B,避免协议层过早认为命令已结束 */

host->stop_abort.opcode = MMC_STOP_TRANSMISSION;
host->stop_abort.arg = 0;
host->stop_abort.flags = MMC_RSP_R1B | MMC_CMD_AC; /* 给某些变体预先备好 CMD12,中止传输时可直接清 DPSM */

mmc->pm_caps |= MMC_PM_KEEP_POWER; /* 休眠能力对上层可见,表示该主机支持在 PM 路径里保持卡供电 */

mmc->max_segs = NR_SG;

mmc->max_req_size = (1 << variant->datalength_bits) - 1; /* 单次请求大小受数据长度寄存器有效位数限制,先在这里收紧边界 */
mmc->max_seg_size = mmc->max_req_size; /* 还没进入 DMA 特化前,段大小上限与单次请求上限一致 */
mmc->max_blk_size = 1 << variant->datactrl_blocksz; /* 块长编码由寄存器位宽决定,最大块长因此跟变体字段绑定 */
mmc->max_blk_count = mmc->max_req_size >> variant->datactrl_blocksz; /* 把最大请求尺寸反推成最大块数,避免块数*块长溢出寄存器 */

spin_lock_init(&host->lock);

writel(0, host->base + MMCIMASK0); /* 刚接管硬件时先关中断屏蔽,避免旧状态或上电毛刺在驱动尚未就绪前闯入 */
if (variant->mmcimask1)
writel(0, host->base + MMCIMASK1);
writel(0xfff, host->base + MMCICLEAR); /* 清历史中断状态,防止后面 request_irq 后立刻吃到脏中断 */

if (!np) {
ret = mmc_gpiod_request_cd(mmc, "cd", 0, false, 0);
if (ret == -EPROBE_DEFER) /* 卡检测 GPIO 资源还没好时必须延后重探,硬探下去会把插卡状态固定错 */
goto clk_disable;

ret = mmc_gpiod_request_ro(mmc, "wp", 0, 0);
if (ret == -EPROBE_DEFER)
goto clk_disable;
}

ret = devm_request_threaded_irq(&dev->dev, dev->irq[0], mmci_irq,
mmci_irq_thread, IRQF_SHARED,
DRIVER_NAME " (cmd)", host);
if (ret) /* 命令/主中断拿不到时主机无法推进请求完成,不能注册到 MMC 核心 */
goto clk_disable;

if (!dev->irq[1])
host->singleirq = true; /* 只有一根 IRQ 时,后面 PIO 中断要并到主中断掩码路径处理 */
else {
ret = devm_request_irq(&dev->dev, dev->irq[1], mmci_pio_irq,
IRQF_SHARED, DRIVER_NAME " (pio)", host);
if (ret)
goto clk_disable;
}

if (host->variant->busy_detect)
INIT_DELAYED_WORK(&host->ux500_busy_timeout_work,
ux500_busy_timeout_work); /* busy 观察依赖延迟工作兜底,后面超时处理会走这条后台路径 */

writel(MCI_IRQENABLE | variant->start_err, host->base + MMCIMASK0); /* 资源齐全后再打开正式中断屏蔽,避免在半初始化状态接收事件 */

amba_set_drvdata(dev, mmc); /* 把 mmc_host 挂到 amba_device,remove/runtime PM 以后都从这里回取私有状态 */

dev_info(&dev->dev, "%s: PL%03x manf %x rev%u at 0x%08llx irq %d,%d (pio)\n",
mmc_hostname(mmc), amba_part(dev), amba_manf(dev),
amba_rev(dev), (unsigned long long)dev->res.start,
dev->irq[0], dev->irq[1]);

mmci_dma_setup(host); /* DMA 能力准备放在注册前完成,保证后续请求路径看到的是最终传输能力 */

pm_runtime_set_autosuspend_delay(&dev->dev, 50);
pm_runtime_use_autosuspend(&dev->dev); /* 打开 autosuspend 后,空闲设备会按 50ms 延时进入 runtime suspend */

ret = mmc_add_host(mmc);
if (ret) /* 主机注册失败时不能留下打开的时钟和已映射硬件状态 */
goto clk_disable;

pm_runtime_put(&dev->dev); /* probe 结束把运行时 PM 引用放掉,让设备可在空闲后进入 autosuspend */
return 0;

clk_disable:
clk_disable_unprepare(host->clk); /* 统一回收 probe 中开启的输入时钟,避免失败后外设继续跑在活动态 */
return ret;
}

mmci_remove 设备解绑与硬件静默

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief 从 MMC 子系统移除主机并让 MMCI 硬件回到静默状态
* @param dev 当前被解绑的 AMBA 设备
*
* 补充说明:
* - remove 通过 amba_get_drvdata() 取回 probe 保存的 mmc_host
* - 这里先把 runtime PM 引用拉回活动态,再做寄存器访问和主机注销
*/
static void mmci_remove(struct amba_device *dev)
{
struct mmc_host *mmc = amba_get_drvdata(dev);

if (mmc) {
struct mmci_host *host = mmc_priv(mmc);
struct variant_data *variant = host->variant;

pm_runtime_get_sync(&dev->dev); /* probe 末尾做过 pm_runtime_put,这里先拉回活动态,避免在硬件已挂起时直接访问 primecell 寄存器 */

mmc_remove_host(mmc); /* 先从 MMC 核心摘掉主机,阻止后续新请求再进入本控制器 */

writel(0, host->base + MMCIMASK0); /* 先关主中断,避免拆资源期间仍有 IRQ 进来 */
if (variant->mmcimask1)
writel(0, host->base + MMCIMASK1);

writel(0, host->base + MMCICOMMAND); /* 清命令通路,防止控制器继续驱动命令状态机 */
writel(0, host->base + MMCIDATACTRL); /* 清数据通路,防止 PIO/DMA 侧残余传输继续推进 */

mmci_dma_release(host);
clk_disable_unprepare(host->clk); /* remove 收尾时把 probe 打开的输入时钟关掉,避免设备解绑后仍耗电或响应 */
}
}

mmci_save 运行时挂起前保存并静默寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @brief 在进入低功耗前保存并关闭 MMCI 的活动寄存器状态
* @param host 当前主机私有状态
*
* 补充说明:
* - 该函数由运行时 PM/系统休眠路径复用
* - 锁保护的是寄存器序列,避免 IRQ 上下文在保存过程中插入读写
*/
static void mmci_save(struct mmci_host *host)
{
unsigned long flags;

spin_lock_irqsave(&host->lock, flags);

writel(0, host->base + MMCIMASK0); /* 挂起前先停主中断,避免控制器在保存窗口里继续抛事件 */
if (host->variant->pwrreg_nopower) {
writel(0, host->base + MMCIDATACTRL); /* 这类变体进入低功耗前要先停数据状态机,否则恢复时容易带着旧传输状态回来 */
writel(0, host->base + MMCIPOWER); /* 明确拉低电源相关寄存器,防止外设保持半上电状态 */
writel(0, host->base + MMCICLOCK); /* 最后关时钟寄存器,让外设静默 */
}
mmci_reg_delay(host); /* 给前面的寄存器写入一个真正落到外设的窗口,再离开临界区 */

spin_unlock_irqrestore(&host->lock, flags);
}

mmci_restore 恢复寄存器并重新打开中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @brief 从低功耗返回后恢复 MMCI 的关键寄存器与中断屏蔽
* @param host 当前主机私有状态
*
* 补充说明:
* - 该函数与 mmci_save() 成对出现
* - 恢复顺序先回写控制寄存器,再恢复中断使能,避免一恢复就吃到半配置状态下的 IRQ
*/
static void mmci_restore(struct mmci_host *host)
{
unsigned long flags;

spin_lock_irqsave(&host->lock, flags);

if (host->variant->pwrreg_nopower) {
writel(host->clk_reg, host->base + MMCICLOCK); /* 先把时钟配置写回去,后续数据/电源寄存器才有一致的时序基准 */
writel(host->datactrl_reg, host->base + MMCIDATACTRL); /* 再恢复数据路径配置,避免恢复后丢失块长/方向等状态 */
writel(host->pwr_reg, host->base + MMCIPOWER); /* 最后恢复电源寄存器,让控制器回到挂起前的可工作状态 */
}
writel(MCI_IRQENABLE | host->variant->start_err,
host->base + MMCIMASK0); /* 等关键寄存器都回位后再开中断,避免恢复窗口里的脏状态直接触发 IRQ */
mmci_reg_delay(host);

spin_unlock_irqrestore(&host->lock, flags);
}

mmci_runtime_suspend 运行时挂起入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief 运行时 PM 挂起回调,切到睡眠引脚状态并关闭主时钟
* @param dev PM 核心传入的 device
* @return 始终返回 0
*
* 补充说明:
* - 该函数由 mmci_dev_pm_ops 经 PM 核心调用
* - 它依赖 probe 里通过 amba_set_drvdata() 保存的 mmc_host
*/
static int mmci_runtime_suspend(struct device *dev)
{
struct amba_device *adev = to_amba_device(dev);
struct mmc_host *mmc = amba_get_drvdata(adev);

if (mmc) {
struct mmci_host *host = mmc_priv(mmc);
pinctrl_pm_select_sleep_state(dev); /* 先把引脚切到睡眠态,避免关钟后引脚仍保持工作驱动方式 */
mmci_save(host); /* 保存寄存器并让控制器静默 */
clk_disable_unprepare(host->clk); /* 最后关输入时钟,真正进入低功耗 */
}

return 0;
}

mmci_runtime_resume 运行时恢复入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief 运行时 PM 恢复回调,恢复时钟、寄存器与默认引脚状态
* @param dev PM 核心传入的 device
* @return 始终返回 0
*
* 补充说明:
* - 恢复顺序与挂起相反
* - 先有时钟,再回寄存器,最后切回默认 pinctrl,避免引脚和控制器状态错位
*/
static int mmci_runtime_resume(struct device *dev)
{
struct amba_device *adev = to_amba_device(dev);
struct mmc_host *mmc = amba_get_drvdata(adev);

if (mmc) {
struct mmci_host *host = mmc_priv(mmc);
clk_prepare_enable(host->clk); /* 没有输入时钟时恢复寄存器没有意义,先把时钟拉起来 */
mmci_restore(host); /* 把挂起前保存的控制器状态写回 */
pinctrl_select_default_state(dev); /* 控制器恢复后再切回默认引脚,保证总线电气状态与寄存器配置同步 */
}

return 0;
}

mmci_dev_pm_ops 电源管理回调表

这张表会通过 mmci_driver.drv.pm 挂到驱动对象上,由 PM 核心在系统休眠和运行时 PM 阶段读取。SYSTEM_SLEEP_PM_OPS() 让系统休眠复用 runtime force suspend/resume 封装,RUNTIME_PM_OPS() 则把日常空闲挂起/恢复明确绑定到 mmci_runtime_suspend()mmci_runtime_resume()struct dev_pm_ops 正是 Linux 设备电源管理操作的标准承载体。([Linux内核文档][4])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 这段代码单元负责声明 MMCI 驱动的系统休眠与运行时 PM 入口。
* 它会被 mmci_driver.drv.pm 挂到驱动对象上,由 PM 核心读取。
* 它在设备完成绑定后、进入 system sleep 或 runtime PM 阶段时生效。
* 它决定系统休眠时是否复用 runtime PM 路径,以及空闲时由谁执行挂起与恢复。
*
* 补充说明:
* - SYSTEM_SLEEP_PM_OPS 把系统休眠路径接到 PM 核心提供的 force_suspend/force_resume 封装
* - RUNTIME_PM_OPS 把日常 runtime suspend/resume 明确绑到 mmci_runtime_suspend/mmci_runtime_resume
*/
static const struct dev_pm_ops mmci_dev_pm_ops = {
SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume) /* 系统休眠时复用 runtime PM 保存/恢复语义,避免维护两套寄存器路径 */
RUNTIME_PM_OPS(mmci_runtime_suspend, mmci_runtime_resume, NULL) /* 设备空闲后由 PM 核心调用这两个入口,runtime_idle 留空 */
};

mmci_ids AMBA 设备匹配表

mmci_ids 会被 AMBA 总线框架拿来做设备匹配;匹配规则是 ((hardware device ID) & mask) == id,而 data 是驱动私有数据,所以这里的 &variant_* 会直接决定 mmci_probe() 中的 variant 指针,进而改写时钟分频、busy detect、SDIO IRQ、open-drain、寄存器位宽和初始化函数等能力选择。PrimeCell 节点在 DT 下也会被注册成 amba_device,因此这张表既服务于传统 PrimeCell ID 匹配,也服务于 DT 创建设备后的 AMBA 身份。([Linux内核文档][1])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 这段代码单元负责描述本驱动支持哪些 AMBA/PrimeCell 硬件变体。
* 它会被 AMBA 总线框架读取,用来决定设备是否交给 mmci_driver 继续 probe。
* 它在设备枚举与驱动绑定阶段生效。
* 它通过 .data 把具体 variant 描述符传给 probe,进而影响时钟、IRQ、busy detect、SDIO IRQ、寄存器布局与能力选择。
*
* 补充说明:
* - .mask 决定比较 ID 时哪些位参与匹配
* - .data 会在 probe 中被取成 variant_data *,后面的能力分支都从这里展开
*/
static const struct amba_id mmci_ids[] = {
{
.id = 0x00041180, /* 命中该 PrimeCell ID 后,probe 会按 variant_arm 走能力配置 */
.mask = 0xff0fffff, /* 掩码决定比较时哪些位必须一致 */
.data = &variant_arm, /* 这条表项把 probe 的 variant 指到 ARM 基本型描述符 */
},
{
.id = 0x01041180,
.mask = 0xff0fffff,
.data = &variant_arm_extended_fifo,
},
{
.id = 0x02041180,
.mask = 0xff0fffff,
.data = &variant_arm_extended_fifo_hwfc,
},
{
.id = 0x00041181,
.mask = 0x000fffff,
.data = &variant_arm,
},
{
.id = 0x00180180,
.mask = 0x00ffffff,
.data = &variant_u300,
},
{
.id = 0x10180180,
.mask = 0xf0ffffff,
.data = &variant_nomadik,
},
{
.id = 0x00280180,
.mask = 0x00ffffff,
.data = &variant_nomadik,
},
{
.id = 0x00480180,
.mask = 0xf0ffffff,
.data = &variant_ux500,
},
{
.id = 0x10480180,
.mask = 0xf0ffffff,
.data = &variant_ux500v2,
},
{
.id = 0x00880180,
.mask = 0x00ffffff,
.data = &variant_stm32, /* 这里把 STM32 旧型 MMCI 变体接到 probe,后面 open-drain 与时钟分频分支都会随之变化 */
},
{
.id = 0x10153180,
.mask = 0xf0ffffff,
.data = &variant_stm32_sdmmc,
},
{
.id = 0x00253180,
.mask = 0xf0ffffff,
.data = &variant_stm32_sdmmcv2,
},
{
.id = 0x20253180,
.mask = 0xf0ffffff,
.data = &variant_stm32_sdmmcv2,
},
{
.id = 0x00353180,
.mask = 0xf0ffffff,
.data = &variant_stm32_sdmmcv3,
},
{
.id = 0x00051180,
.mask = 0x000fffff,
.data = &variant_qcom, /* Qualcomm 项命中后,FIFO 计数与最小时钟估算会走 qcom/explicit_mclk_control 分支 */
},
{ 0, 0 }, /* 终止表项,告诉 AMBA 匹配逻辑表到此结束 */
};

MODULE_DEVICE_TABLE 自动装载导出项

MODULE_DEVICE_TABLE(amba, mmci_ids) 会把这张 AMBA 匹配表导出给构建与自动装载链路,使用户态能根据设备别名找到这个模块。内核文档明确说明驱动通常会把设备 ID 表通过 MODULE_DEVICE_TABLE() 导出给用户空间;模块自动装载体系会利用这些映射。([Linux内核文档][1])

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责把 mmci_ids 导出给模块自动装载机制。
* 它会被构建系统与用户态热插拔/自动装载链路消费。
* 它在模块构建与设备热插拔匹配阶段生效。
* 它让设备出现时,用户态能够依据 AMBA 设备别名把本驱动模块装入,再进入 probe 路径。
*
* 补充说明:
* - 这里导出的就是上面的 mmci_ids
* - 没有它时,驱动即使能手工加载,也少了按设备别名自动关联的那一步
*/
MODULE_DEVICE_TABLE(amba, mmci_ids); /* 导出 AMBA 匹配表,使设备出现时模块自动装载链路能把它关联到本驱动 */

mmci_driver AMBA 驱动描述符

mmci_driver 是真正交给 AMBA 框架消费的驱动对象:.id_table = mmci_ids 决定谁能命中,.probe = mmci_probe 决定命中后怎么建主机,.remove = mmci_remove 决定解绑时怎么静默并回收,.drv.pm = pm_ptr(&mmci_dev_pm_ops) 决定 PM 核心在这个驱动上的挂起/恢复入口,.probe_type = PROBE_PREFER_ASYNCHRONOUS 表示驱动愿意异步探测。struct device_driverpmprobe_type 含义都由驱动核心统一解释。([Linux内核文档][5])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 这段代码单元负责描述一个完整的 AMBA MMCI 驱动对象。
* 它会被 module_amba_driver() 注册到 AMBA 总线框架中。
* 它在模块装载或内建驱动初始化阶段生效。
* 它把匹配表、probe/remove 回调、PM 表和探测策略绑定到同一个驱动描述符上,决定 probe/remove/PM/自动绑定路径。
*
* 补充说明:
* - .pm 把电源管理回调表接到驱动对象
* - .id_table、.probe、.remove 分别决定匹配、接管、解绑时走哪条函数路径
*/
static struct amba_driver mmci_driver = {
.drv = {
.name = DRIVER_NAME,
.pm = pm_ptr(&mmci_dev_pm_ops), /* 驱动对象的 PM 入口指向 mmci_dev_pm_ops,系统休眠与 runtime PM 都从这里挂进来 */
.probe_type = PROBE_PREFER_ASYNCHRONOUS, /* 允许探测优先异步执行,减少慢设备 probe 对启动路径的阻塞 */
},
.probe = mmci_probe, /* 设备命中匹配表后,由 AMBA 框架调用这里建立主机 */
.remove = mmci_remove, /* 设备解绑时,由 AMBA 框架调用这里静默硬件并回收资源 */
.id_table = mmci_ids, /* AMBA 框架用这张表决定是否把设备交给本驱动 */
};

module_amba_driver 驱动注册封装宏

module_amba_driver(mmci_driver) 不是新的驱动对象,而是把“注册/注销这个 mmci_driver”封成模块入口与退出代码。历史补丁说明这个宏正是为了省掉手写 amba_driver_register() / amba_driver_unregister()module_init() / module_exit() 的样板代码而加入的。([lists.infradead.org][6])

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责把 mmci_driver 的注册/注销封装成模块入口与退出流程。
* 它会被模块装载器和驱动核心消费。
* 它在模块插入或内建驱动初始化时生效,在模块卸载时负责反向注销。
* 它最终把 mmci_driver 接入 AMBA 总线框架,从而让上面的 .id_table/.probe/.remove/.pm 真正参与匹配与生命周期管理。
*
* 补充说明:
* - 这是注册封装,不会创建第二个 driver 对象
* - 它替代手写的 module_init/module_exit + amba_driver_register/unregister 样板
*/
module_amba_driver(mmci_driver); /* 最终把 mmci_driver 注册到 AMBA 总线;模块卸载时再走对应注销路径 */

module_param 模块参数入口

module_param(fmax, uint, 0444)fmax 暴露成模块参数。对这段代码来说,它最重要的影响不是“参数存在”,而是 mmci_probe() 里当 mmc->f_max 没有被板级或 DT 填出来时,会退回到 fmax。内核文档说明模块参数既可以从内核命令行传入,也可以在 modprobe 装载模块时传入。([Linux内核文档][7])

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责暴露驱动的最大时钟回退值参数 fmax。
* 它会被模块参数解析逻辑消费。
* 它在模块装载或内建内核启动解析参数时生效。
* 它会影响 probe 中 mmc->f_max 为空时采用哪一个默认上限,从而改变最终给控制器选择的工作频率。
*
* 补充说明:
* - 它对应的实际使用点在 mmci_probe() 里给 mmc->f_max 赋回退值的分支
* - 板级或 DT 已给出更具体频率时,这里不会覆盖那些显式配置
*/
module_param(fmax, uint, 0444); /* 暴露 fmax 模块参数,probe 在没有显式最大频率时会退回到这个值 */