核心判断
源码拆解的重点不是“把代码读完”,而是把设计取舍读出来:它为什么这么分层,为什么这样切状态,为什么这里用 coherent、那里用 streaming,为什么 reset 放在这个点做。看不出这些,读再多源码也只是路过文本。
这一章解决什么问题
前面九章已经把 DMA 的核心原则都立住了。现在该回到真实代码世界,看成熟驱动是怎么把这些原则落下去的。
这一章不追求覆盖所有驱动,而是建立一套 源码阅读抓手。以后你不管看音频、网络、视频还是采集驱动,都能按同一套问题框架下刀。
看 DMA 源码,先别急着读函数细节
很多人一打开源码就开始:
- 顺着调用栈看
- 看见 API 就查文档
- 看见结构体就往下翻
然后十分钟后脑内一团浆糊。
正确姿势应该先问四个问题:
- 数据面在哪里
- 控制面在哪里
- 生命周期怎么推进
- 异常路径怎么收口
这四个问题一旦立住,源码就从“树林”变成“地图”。
推荐拆解顺序:先骨架,后血肉
第一步:看 probe / init
先确认:
- 设备上下文怎么建
- DMA 能力怎么声明
- buffer / descriptor 怎么分配
- 中断和完成路径在哪里挂上
probe 是整套系统的建筑图纸,不是无聊样板间。
第二步:看 runtime data path
也就是:
- submit 在哪
- completion 在哪
- reclaim 在哪
- ring 怎么推进
这里要抓的不是每一行代码,而是:
谁在什么时候接手 buffer。
第三步:看 error path
包括:
- timeout
- reset
- stop / close
- remove
如果一个驱动连 error path 都没写像样,那它再能跑,也只是 happy path 表演艺术。
一套你可以直接复用的源码阅读模板
阅读维度 | 要问什么 |
资源初始化 | DMA 资源、ring、irq、上下文在哪里建立? |
地址与内存 | 哪些结构用 coherent,哪些路径用 streaming? |
状态机 | slot / descriptor 的 owner 和 done 语义在哪? |
提交路径 | 什么时候把 buffer 正式交给设备? |
完成路径 | 完成后如何通知上层、如何回收? |
异常路径 | 超时、reset、stop 时怎么作废旧状态? |
用户态边界 | 数据如何暴露给上层?谁负责消费确认? |
为什么是这三类案例,而不是随便挑三篇源码
之所以选 音频周期流 + 网卡 ring + 持续采集 这三类,不是为了凑数量,而是为了把 DMA 世界里三种最关键的工程矛盾拆开看:
案例类型 | 最值得看的矛盾 | 你会学到什么 |
音频周期流 | 节拍稳定性 vs 吞吐 | period、watermark、cyclic 语义怎么落到真实硬件节奏 |
网卡 ring | 高吞吐并发 vs 回收秩序 | descriptor ring、批量 completion、producer/consumer 推进 |
持续采集 | 长期运行 vs 消费协议 | circular DMA、采样窗口、old_pos -> pos 消费边界 |
也就是说,这三类案例分别对应:
- 节拍型系统
- 吞吐型系统
- 持续输入型系统
把这三类都看透,你对 DMA 的理解才不会只停在某一个垂直场景里自我感动。
推荐选 2~3 类不同风格的驱动拆
1. 周期流驱动
比如音频、ADC、持续采样类。
重点看:
- cyclic / period 语义
- completion 节奏
- ring / period 边界
2. 高吞吐 ring 驱动
比如网络、视频采集、私有 ring 设备。
重点看:
- descriptor queue
- ownership 迁移
- producer / consumer 指针
- 批量 completion / reclaim
3. 持续采集类驱动
比如 ADC 周期采样、UART circular DMA 接收、各类持续输入型采集设备。
重点看:
- trigger / sampling window / latency budget
- IDLE / HT / TC 这类事件点怎么参与交接
- 环形缓冲区回卷与 old_pos -> pos 的消费协议
具体案例 A:i.MX6UL fsl-sai 应该怎么读
你现有专题里已经有一篇旧稿可作为案例入口:
这个案例特别值钱,因为它天然能讲清三件事:
- 真实 DMA 和“软件模拟 DMA”不是一个量级的问题
- 音频类周期流为什么高度依赖 cyclic / period 语义
- CPU DAI 驱动与 Platform DMA 层为什么要分工
看这个驱动时先抓角色定位
fsl-sai 不是“包办一切的 DMA 驱动”,它更像:
- CPU DAI 驱动
- SAI 硬件接口配置者
- DMA 参数提供者
真正的数据搬运常常落在:
- i.MX 平台私有 DMA 路径
- 或 DMAEngine / PCM 框架层
这个观察很重要,因为很多人第一次看音频驱动,最大误解就是:以为看到一个驱动文件,就以为所有 DMA 语义都在里面。
再抓数据面和控制面的边界
在 fsl-sai 这种驱动里:
- 控制面 往往是寄存器配置、FIFO watermark、DMA 参数组织
- 数据面 则由 DMA 控制器和 PCM buffer 通路去搬
也就是说,它的关键价值不是“亲手 memcpy 数据”,而是把:
- FIFO 寄存器地址
- burst 大小
- 通道方向
- 时钟与格式配置
正确交给 DMA 生态里的下一层
重点问题 1:为什么 FIFO watermark 这么重要
在音频这种周期流场景里,watermark 决定的是:
- 什么时候发 DMA 请求
- 一次搬多少
- 软件多久收到一次完成节奏
它背后的工程问题不是“寄存器写多少”,而是:
period 语义和实时性如何落到真实硬件节拍上。
重点问题 2:为什么这类驱动要高度关心 burst / maxburst
因为 burst 不只是吞吐参数,它还会影响:
- FIFO 触发节奏
- 总线访问颗粒度
- period 回调稳定性
- underrun / overrun 风险
所以看到
maxburst 这类参数,别把它当数字细节,要把它理解成“数据流拍点控制”。重点问题 3:为什么“真实 DMA vs 模拟 DMA”这个对比特别值钱
因为它能把两个世界一下拉开:
维度 | 模拟 DMA / 软件定时推进 | 真实 DMA |
数据搬运 | CPU / 定时器模拟推进 | DMA 控制器真实搬运 |
CPU 占用 | 高 | 低 |
节奏来源 | 软件时钟 | 硬件 FIFO / DMA 请求 |
适用场景 | 学习 / 仿真 / 测试 | 生产环境 |
这个对比能帮你真正理解:
真实 DMA 不是“把定时器回调换个名字”,而是把数据搬运责任交给了独立硬件通路。
具体案例 B:网卡 ring DMA 应该怎么读
作为对照案例,新增了一页:
它最适合补上另一种思维:
- 音频案例强调 周期流节拍
- 网卡案例强调 descriptor ring 并发推进、批量回收和 NAPI poll
看网卡案例时优先抓什么
- TX / RX ring 分别如何组织
- descriptor 上有哪些状态位
- producer / consumer 指针谁推进
- TX completion 后如何 unmap / free skb
- RX buffer 用完后如何补回 ring
- reset 后 ring 如何重建,旧 completion 如何作废
也就是说,网卡案例更像 DMA 世界里的 高吞吐并发治理课。
从案例里提炼的阅读抓手
当你看类似 fsl-sai 或网卡 ring 驱动这样的成熟实现时,优先抓这些问题:
- DMA 参数在哪里组出来
- FIFO / watermark 或 ring / tail 如何驱动 DMA 节奏
- completion 的节拍是谁提供
- 驱动层次分工边界在哪里
- stop / suspend / resume / reset 时数据流如何停稳
看源码时不要只盯 API,要盯“设计信号”
信号 1:为什么这块内存这么分
如果你看到:
- descriptor 一套
- payload 一套
- 状态页又一套
不要只问“它们在哪定义”,要问:
为什么作者不把它们混在一起。
信号 2:为什么 completion 在这里推进
如果 completion 到来后:
- 立刻更新状态
- 唤醒等待者
- 延迟回收 payload
要看的是背后的生命周期划分,而不是表面上的函数调用。
信号 3:为什么 reset 这么写
成熟驱动在 reset 前后,往往会:
- 停新请求
- 清旧状态
- 重建 ring
- 丢弃旧 completion
这不是作者啰嗦,而是因为 DMA 世界里“旧状态残留”足以把新会话一起带进沟里。
一个推荐的阅读姿势
第 1 遍:只看结构
- 设备结构体
- probe / remove
- irq handler
- 提交函数
- reset / stop 函数
第 2 遍:只画状态图
- 哪些状态存在
- 哪些迁移合法
- 何处切 owner
第 3 遍:只看异常路径
- 超时怎么收
- reset 怎么收
- remove 怎么收
不要三遍混在一遍里读。那样最容易把自己读成缓存不一致。
一套“实现 → 设计原因”映射法
看到一段实现时,别停在“它做了什么”,继续问:
看到的实现 | 要追问的设计原因 |
先写 descriptor 再 doorbell | 是不是在保证设备不会看到半成品? |
completion 后先同步再通知 | 是不是在保证 CPU / 用户读到最新数据? |
remove 前先停 DMA | 是不是在防止资源释放后硬件仍继续写? |
reset 后重建 ring | 是不是在消灭旧世界残留状态? |
watermark 触发 DMA 请求 | 是不是在把硬件 FIFO 节奏映射成软件可控 period? |
NAPI poll 批量回收 descriptor | 是不是在用吞吐换更复杂的 completion 边界? |
只要你开始这样读源码,就不会再把代码看成杂乱操作,而会看到背后的工程逻辑链。
最容易犯的源码阅读错误
错误 1:只盯函数,不盯状态机
结果读完一堆函数,还是不知道 slot 到底归谁。
错误 2:只盯 happy path,不看 reset / remove
结果你会误以为驱动写得很优雅,直到真实系统出异常时才发现作者根本没把坑填平。
错误 3:把所有奇怪写法都当历史包袱
有些代码确实是包袱,但很多“看起来绕”的写法,恰恰是在处理 DMA 的隐蔽边界问题。
本章结论
- 读 DMA 源码的重点,不是追函数,而是抓数据面、控制面、状态机和异常路径。
- probe、submit、completion、reset、remove 是最值得优先读的五个点。
- 像 fsl-sai 这类音频驱动,额外要抓 FIFO watermark、period 节拍和层次分工。
- 像网卡 ring 这类驱动,额外要抓 descriptor ring、批量回收和并发推进。
- 真正有价值的阅读方式,是把“实现细节”反推成“设计理由”。
下一章要干什么
下一章开始把这些知识沉淀成可表达的工程判断:
- 面试高频问答
- 场景题
- 高分回答框架
因为最终目标不只是“会写”,还要“会解释为什么这么写”。






