4. DMA通道管理:申请、释放、状态查询与回调注册
DMA通道管理,说白了就是怎么跟硬件要资源、怎么还回去、怎么知道它干到哪了、干完了怎么通知你。这四个动作,是DMA驱动开发最基础也最容易出坑的地方。
我刚开始写DMA驱动时,觉得申请通道不就是调个函数嘛,有啥难的?结果第一次做多通道传输,通道冲突、回调丢失、状态查询返回乱码……各种问题全来了。嗯,从那以后我养成了一个习惯:先把通道管理这层理清楚,再碰传输逻辑。
4.1 申请DMA通道
ARM架构下,DMA控制器通常有多个通道。比如STM32的DMA1有8个通道,i.MX系列的eDMA有32个通道。申请通道,就是告诉内核:我要用哪个通道,用来干什么。
Linux内核提供了统一的DMA引擎API。核心函数是:
struct dma_chan *dma_request_channel(dma_cap_mask_t mask,
dma_filter_fn filter_fn,
void *filter_param);
参数说明:
| 参数 | 含义 | 我踩过的坑 |
|---|---|---|
mask |
通道能力掩码,指定需要的DMA传输类型 | 掩码没配好,申请到的通道不支持你要的模式 |
filter_fn |
过滤回调,用于匹配具体硬件通道 | 回调写错了,永远申请不到通道 |
filter_param |
传给过滤回调的私有参数 | 参数类型不匹配,编译不报错,运行才崩溃 |
我个人习惯的做法是:先定义好能力掩码,再写过滤函数。举个例子:
dma_cap_mask_t mask;
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask); // 我要做内存拷贝
struct dma_chan *chan = dma_request_channel(mask, my_filter, &my_dev);
这里有个细节:dma_cap_set可以多次调用,设置多种能力。但别贪多,只设你真正需要的。设多了,内核匹配范围变窄,可能申请不到。
4.2 释放DMA通道
有借有还,再借不难。释放通道的API很简单:
void dma_release_channel(struct dma_chan *chan);
但简单不代表可以随便写。我曾经犯过一个错误:在设备还在传输时释放了通道。结果DMA控制器还在搬运数据,通道却被标记为空闲,另一个驱动申请到了同一个通道……数据错乱,系统崩溃。
正确的释放流程应该是:
- 停止所有正在进行的DMA传输
- 等待正在执行的描述符完成(或强制终止)
- 释放通道
- 将通道指针置为NULL
if (chan) {
dmaengine_terminate_sync(chan); // 等待传输停止
dma_release_channel(chan);
chan = NULL;
}
dmaengine_terminate_sync而不是dmaengine_terminate_all。前者会等待硬件真正停止,后者只是发个停止命令就返回。你想想看,命令发出去了,硬件还没停,你这边就把通道释放了,不出问题才怪。
4.3 通道状态查询
传输进行到哪了?还剩多少?有没有出错?这些信息都得靠状态查询接口拿。
Linux DMA引擎提供了两种查询方式:
| 接口 | 返回信息 | 适用场景 |
|---|---|---|
dmaengine_tx_status |
传输状态、剩余字节数、已完成的描述符数 | 轮询模式、进度查询 |
dmaengine_pause/resume |
暂停/恢复传输 | 流控、暂停后查询状态 |
核心函数原型:
enum dma_status dmaengine_tx_status(struct dma_chan *chan,
dma_cookie_t cookie,
struct dma_tx_state *state);
state结构体包含三个关键字段:
residue:剩余未传输的字节数used:已使用的描述符数last:最后一个完成的描述符cookie
我在项目中遇到过一个问题:用dmaengine_tx_status查状态,返回的residue一直是0。查了半天,发现是DMA控制器不支持硬件残值计算。有些低端MCU的DMA,传输完成后不会更新剩余计数寄存器。这时候只能靠描述符完成回调来推算进度。
经验之谈:状态查询的精度取决于硬件能力。别假设所有DMA控制器都能精确报告剩余字节。写驱动时,先查芯片手册,看DMA控制器是否支持硬件残值跟踪。不支持的话,自己维护一个进度计数器。
4.4 DMA回调函数注册
回调函数是DMA传输完成后的通知机制。没有回调,你就得轮询状态,浪费CPU。有了回调,传输完成自动触发,高效又优雅。
注册回调的核心是填充描述符的回调字段:
struct dma_async_tx_descriptor *desc;
desc->callback = my_dma_callback;
desc->callback_param = my_private_data;
回调函数原型:
void my_dma_callback(void *param);
这里有个容易忽略的点:回调是在什么上下文中执行的?
- 中断上下文:大多数DMA控制器在传输完成时触发中断,回调在中断处理中执行。这意味着回调里不能调用可能睡眠的函数(比如
kmalloc(GFP_KERNEL))。 - 任务上下文:有些DMA引擎(如dmatest)会在内核线程中调用回调。这种情况较少见。
我曾经在回调里调了mutex_lock,结果死锁了。因为中断上下文不能睡眠,而mutex_lock可能导致睡眠。从那以后,我养成了一个习惯:回调里只做轻量级操作,比如设置标志位、唤醒等待队列。真正的数据处理,放到工作队列或tasklet里做。
static void my_dma_callback(void *param)
{
struct my_device *dev = param;
// 只做轻量操作:设置标志,唤醒等待队列
dev->transfer_done = 1;
wake_up(&dev->wait_queue);
// 不要在这里做复杂处理!
// process_data(dev); // 错误做法
}
dma_release_channel、dmaengine_terminate_*等可能引发重入的函数。我在调试一个音频驱动时,在回调里释放了通道,结果DMA引擎内部链表被破坏,系统直接oops。正确的做法是:在回调中设置标志,在进程上下文中释放通道。
4.5 通道管理全流程
把上面四个步骤串起来,就是完整的通道管理流程。我画了一张图,帮你理清关系:
从图中可以看到,通道管理不是孤立的。申请通道后,要配置传输、启动传输、等待完成,最后释放通道。状态查询可以在传输过程中随时进行。回调函数在传输完成时自动触发。
4.6 多通道管理要点
当系统中有多个DMA通道同时工作时,管理复杂度会上升。我总结了几条经验:
- 通道优先级:高优先级通道优先获得总线访问权。音频、视频等实时性要求高的传输,用高优先级通道。
- 通道隔离:不同外设使用不同通道,避免互相干扰。我见过一个案例,UART和SPI共用同一个DMA通道,结果数据混在一起。
- 资源池化:对于通用DMA传输(如内存拷贝),可以维护一个通道池,按需分配。
/sys/kernel/debug/dmaengine下查看通道状态。这个虚拟文件系统会显示每个通道的当前状态、传输描述符数量、错误计数等信息。我调试多通道音频驱动时,全靠这个目录定位问题。
好了,通道管理这部分就讲到这里。记住四个核心动作:申请、释放、查询、回调。每个动作都有它的坑,但只要你理解了背后的硬件原理,写出来的驱动就会稳如磐石。