3. DMA驱动框架:Linux内核DMA引擎子系统、dmaengine API、dma_device与dma_chan结构体

好,咱们进入第三章。这一章我打算把Linux内核里的DMA驱动框架给你拆开揉碎了讲清楚。很多工程师写DMA驱动时,上来就对着寄存器一顿操作,结果发现跟内核框架拧巴在一起,调试起来特别痛苦。我个人习惯是,先理解框架再动手,这样写出来的代码才能跟内核“和平共处”。

3.1 Linux DMA引擎子系统:为什么需要它?

说白了,DMA引擎子系统就是内核给你搭好的一套“标准化接口”。你想想看,不同厂家的SoC,DMA控制器的寄存器、通道数、传输模式千差万别。如果没有一个统一的框架,每个驱动都得自己操作DMA寄存器,那代码复用性几乎为零。

我在项目中遇到过最典型的情况:一个平台用着TI的DMA,另一个平台换成了Xilinx的AXI DMA。如果都按裸机方式写,移植起来简直要命。DMA引擎子系统就是为了解决这个问题——它把DMA控制器抽象成dma_device,把传输通道抽象成dma_chan,然后提供一套标准的API(即dmaengine API)给上层驱动调用。

核心思想: DMA引擎子系统 = 硬件抽象层 + 标准API + 通道管理。上层驱动(比如音频、视频、存储)不需要知道DMA控制器具体怎么操作寄存器,只需要调用dmaengine_prep_slave_sg()这类接口就行了。

3.2 dma_device结构体:DMA控制器的“身份证”

每个DMA控制器驱动,都要向内核注册一个dma_device结构体。这个结构体描述了控制器的能力、支持的传输类型、以及各种回调函数。我刚开始看这个结构体时,觉得它字段特别多,其实核心就几类。

struct dma_device {
    unsigned int    chancnt;        // 通道数量
    unsigned int    max_burst;      // 最大突发长度
    enum dma_transaction_type  cap_mask; // 支持的能力位图
    struct list_head channels;      // 通道链表

    // 关键回调函数
    int (*device_alloc_chan_resources)(struct dma_chan *chan);
    void (*device_free_chan_resources)(struct dma_chan *chan);
    struct dma_async_tx_descriptor *(*device_prep_slave_sg)(
        struct dma_chan *chan, struct scatterlist *sgl,
        unsigned int sg_len, enum dma_transfer_direction dir,
        unsigned long flags, void *context);
    int (*device_issue_pending)(struct dma_chan *chan);
    enum dma_status (*device_tx_status)(struct dma_chan *chan,
        dma_cookie_t cookie, struct dma_tx_state *txstate);
};

这里我重点说几个你一定会用到的:

  • chancnt:这个DMA控制器有多少个物理通道。我见过有些芯片有8个,有些只有2个,别搞错了。
  • cap_mask:用位图表示支持哪些传输类型,比如DMA_SLAVE、DMA_MEMCPY、DMA_CYCLIC。音频驱动一般需要DMA_CYCLIC,因为要做环形缓冲区。
  • device_prep_slave_sg:这是最常用的回调——准备一个scatter-gather传输。上层驱动把数据分散在多个内存块里,DMA控制器要能把这些块串起来传输。

我的经验: 写DMA控制器驱动时,device_alloc_chan_resources里一定要做好硬件资源的分配和初始化。我曾经漏掉了中断使能,结果传输完成后一直等不到回调,排查了半天才发现是这里的问题。

3.3 dma_chan结构体:传输通道的“句柄”

dma_chan代表一个DMA传输通道。上层驱动通过dma_request_channel()来获取它,用完之后通过dma_release_channel()释放。这个结构体本身不大,但它的private字段指向了控制器驱动的私有数据。

struct dma_chan {
    struct dma_device *device;   // 所属的DMA控制器
    dma_cookie_t cookie;         // 当前传输的cookie
    void *private;               // 控制器私有数据
    struct list_head device_node;
};

嗯,这里要注意:cookie是内核用来跟踪传输状态的。每次提交一个传输描述符,内核都会分配一个cookie。上层驱动可以通过dma_async_is_tx_complete()查询传输是否完成。

字段 说明 使用场景
device 指向所属的dma_device 获取控制器能力、调用回调
cookie 当前传输的标识符 查询传输状态
private 控制器驱动的私有数据 存储硬件寄存器基址、中断号等
device_node 链表节点 挂在dma_device的channels链表上

3.4 dmaengine API:上层驱动的“瑞士军刀”

dmaengine API是上层驱动(比如I2S、SPI、MMC)用来发起DMA传输的标准接口。我把它分成三类:通道管理、传输准备、传输控制。

3.4.1 通道管理

struct dma_chan *dma_request_channel(dma_cap_mask_t mask,
    dma_filter_fn fn, void *fn_param);
void dma_release_channel(struct dma_chan *chan);

dma_request_channel的第一个参数是能力掩码,告诉内核你需要什么样的DMA通道。比如你要做内存到外设的传输,就设置DMA_SLAVE位。第二个参数是过滤函数,用来从多个符合条件的通道中选出你想要的。我在项目中经常用dma_request_slave_channel()这个封装,它通过设备树直接获取通道,省去了写过滤函数的麻烦。

3.4.2 传输准备

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg(
    struct dma_chan *chan, struct scatterlist *sgl,
    unsigned int sg_len, enum dma_transfer_direction dir,
    unsigned long flags, void *context);

这个函数返回一个传输描述符。描述符里包含了你要传输的数据、方向、以及完成后的回调函数。你可以在描述符上注册回调:

desc->callback = my_dma_callback;
desc->callback_param = my_data;

曾经踩过的坑: 回调函数是在中断上下文执行的,千万别在里面调用msleep()或者mutex_lock()。我见过有人直接在回调里申请内存,结果系统直接挂了。回调里应该只做最轻量级的工作,比如设置一个标志位或者唤醒一个等待队列。

3.4.3 传输控制

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
void dma_async_issue_pending(struct dma_chan *chan);
enum dma_status dma_async_is_tx_complete(struct dma_chan *chan,
    dma_cookie_t cookie, struct dma_tx_state *state);

流程很简单:submit把描述符提交到硬件队列,issue_pending告诉DMA控制器可以开始传输了,is_tx_complete查询传输状态。我习惯在submit之后立即调用issue_pending,除非你需要攒一批传输再统一触发。

3.5 整体框架关系图

下面这张图展示了DMA引擎子系统的核心架构。你可以看到上层驱动、dmaengine API、DMA控制器驱动、以及硬件之间的关系。

DMA引擎子系统架构图 DMA引擎子系统架构图 上层驱动(音频、SPI、MMC、UART等) 调用 dmaengine API 发起 DMA 传输 dmaengine API dma_request_channel / dmaengine_prep_slave_sg / dmaengine_submit DMA控制器驱动 注册 dma_device,实现回调函数 device_prep_slave_sg / device_issue_pending / device_tx_status DMA控制器硬件 寄存器 / 通道 / 中断控制器 dma device dma chan 回调 函数 私有 数据

3.6 典型传输流程

最后,我把一个典型的DMA传输流程串起来,让你看看这些API在实际中是怎么配合的:

  1. 请求通道:chan = dma_request_slave_channel(dev, "rx");
  2. 配置通道参数:设置源地址、目的地址、数据宽度、突发长度等。这一步通过dmaengine_slave_config()完成。
  3. 准备传输描述符:desc = dmaengine_prep_slave_sg(chan, sgl, sg_len, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT, NULL);
  4. 注册回调:desc->callback = my_complete_cb; desc->callback_param = my_data;
  5. 提交传输:cookie = dmaengine_submit(desc);
  6. 启动传输:dma_async_issue_pending(chan);
  7. 等待完成:在回调中设置完成标志,或者用dma_async_is_tx_complete()轮询。
  8. 释放通道:dma_release_channel(chan);

个人建议: 调试DMA驱动时,先用dma_async_is_tx_complete()轮询方式验证基本功能,确认传输正常后再切换到中断回调方式。这样能减少中断处理带来的调试复杂度。