4. MTK DMA控制器硬件详解:以GCE和MDP_DMA为例

各位同学,今天我们来啃一块硬骨头——MTK的DMA控制器硬件。说实话,我刚接触MTK平台时,也被这些寄存器、描述符链搞得头大。但搞懂了之后你会发现,DMA其实就是个聪明的搬运工,你告诉它搬什么、搬到哪里、搬多少,它自己就干完了。

我们以两个典型代表为例:GCE(Global Command Engine)MDP_DMA(Media Data Path DMA)。前者是通用型的,后者专为显示通路优化。我当年在调一个双摄预览卡顿问题时,就是靠深扒GCE的寄存器才找到症结的。

4.1 寄存器布局:DMA的大脑

每个DMA控制器都有一组控制寄存器,说白了就是它的指令集。MTK的DMA寄存器布局大致分三类:

寄存器类型 典型寄存器 作用
全局控制 DMA_GLOBAL_CTRL 使能DMA、复位、时钟门控
通道配置 DMA_CHx_CTRL 每个通道的独立控制
描述符基址 DMA_CHx_DES_BASE 指向描述符链的起始地址

以GCE为例,它的基地址通常在0x10280000附近(不同芯片略有差异)。我个人习惯先把基地址宏定义好:

#define GCE_BASE                0x10280000
#define GCE_GLOBAL_CTRL         (GCE_BASE + 0x0000)
#define GCE_CH0_CTRL            (GCE_BASE + 0x0100)
#define GCE_CH0_DES_BASE        (GCE_BASE + 0x0104)
#define GCE_CH0_SRC_ADDR        (GCE_BASE + 0x0108)
#define GCE_CH0_DST_ADDR        (GCE_BASE + 0x010C)
#define GCE_CH0_LEN             (GCE_BASE + 0x0110)
注意:不同MTK芯片的寄存器偏移可能不同。我曾经在MT6765上踩过坑,照着MT6739的寄存器表配,结果DMA死活不工作。后来发现是GCE_CH0_CTRL的偏移从0x0100改成了0x0200。所以,一定要以当前芯片的datasheet为准

4.2 通道配置:每个通道都是一个独立工人

GCE有8个通道,MDP_DMA通常有4个。每个通道可以独立配置,互不干扰。你想想看,这就像你有8个搬运工,可以同时干不同的活。

通道配置的核心寄存器是DMA_CHx_CTRL,它的位域定义大致如下:

位域 名称 说明
[0] EN 通道使能,1启动传输
[3:1] MODE 传输模式:000=单次,001=循环,010=描述符链
[7:4] BURST_SIZE 突发传输大小:0000=1字节,0001=4字节,0010=8字节...
[15:8] INT_EN 中断使能位,每位对应不同中断事件

配置一个通道的基本流程,我一般这样写:

// 以GCE通道0为例,配置为描述符链模式
void gce_channel_config(void)
{
    uint32_t ctrl_val = 0;
    
    // 1. 先复位通道
    writel(0x0, GCE_CH0_CTRL);
    
    // 2. 设置模式为描述符链 (MODE=010)
    ctrl_val |= (0x2 << 1);
    
    // 3. 设置突发大小为8字节 (BURST_SIZE=0010)
    ctrl_val |= (0x2 << 4);
    
    // 4. 使能传输完成中断
    ctrl_val |= (1 << 8);
    
    // 5. 写入控制寄存器
    writel(ctrl_val, GCE_CH0_CTRL);
    
    // 6. 设置描述符基址
    writel((uint32_t)descriptor_chain, GCE_CH0_DES_BASE);
    
    // 7. 最后使能通道
    ctrl_val |= 0x1;
    writel(ctrl_val, GCE_CH0_CTRL);
}
我的经验:配置完成后,建议先读回寄存器确认写入成功。有些芯片的寄存器有写保护,需要先解锁。我遇到过写进去读出来是0的情况,排查了半天才发现是时钟没开。

4.3 描述符链机制:DMA的待办清单

描述符链(Descriptor Chain)是DMA最核心的机制。说白了,就是给DMA一个任务列表,它按顺序一个一个执行。每个描述符就是一个任务项。

描述符的结构体定义通常长这样:

typedef struct {
    uint32_t src_addr;      // 源地址
    uint32_t dst_addr;      // 目的地址
    uint32_t length;        // 传输长度
    uint32_t flags;         // 控制标志(中断、链指针等)
    uint32_t next_des;      // 下一个描述符地址(关键!)
    uint32_t reserved;      // 保留
} dma_descriptor_t;

描述符链的工作流程是这样的:

  1. CPU在内存中构建一个描述符数组
  2. 将第一个描述符的地址写入DMA的DES_BASE寄存器
  3. DMA自动读取第一个描述符,执行传输
  4. 传输完成后,根据next_des字段找到下一个描述符
  5. 重复直到遇到next_des为NULL

为什么会用描述符链?我举个例子:你要从摄像头采集一帧图像,数据分散在3个不同的buffer里,需要搬到连续的显存中。用描述符链,你只需要建3个描述符,DMA自动帮你搬完。如果没有描述符链,你得等一次传输完再配置下一次,效率差远了。

构建描述符链的代码示例:

#define DES_COUNT 3

dma_descriptor_t des[DES_COUNT] __attribute__((aligned(32)));

void build_descriptor_chain(void)
{
    // 任务1:搬第一个buffer
    des[0].src_addr = (uint32_t)buffer1;
    des[0].dst_addr = (uint32_t)dest1;
    des[0].length = 1024;
    des[0].flags = 0;  // 不产生中断
    des[0].next_des = (uint32_t)&des[1];  // 指向下一个
    
    // 任务2:搬第二个buffer
    des[1].src_addr = (uint32_t)buffer2;
    des[1].dst_addr = (uint32_t)dest2;
    des[1].length = 2048;
    des[1].flags = 0;
    des[1].next_des = (uint32_t)&des[2];
    
    // 任务3:最后一个,产生中断通知CPU
    des[2].src_addr = (uint32_t)buffer3;
    des[2].dst_addr = (uint32_t)dest3;
    des[2].length = 512;
    des[2].flags = DES_FLAG_INT_EN;  // 使能中断
    des[2].next_des = 0;  // 链结束
    
    // 确保描述符已写入内存(cache一致性)
    dma_cache_sync(des, sizeof(des), DMA_TO_DEVICE);
}
关键点:描述符必须放在非cacheable的内存区域,或者确保cache一致性。否则DMA读到的是cache里的旧数据,而不是你刚写的新描述符。我当年调试一个花屏问题,查了三天才发现是描述符在cache里没刷出去。

4.4 GCE vs MDP_DMA:选谁更合适?

这两个控制器虽然都是DMA,但各有侧重:

特性 GCE MDP_DMA
通道数 8个 4个
最大传输长度 64KB 1MB
支持格式 普通内存拷贝 支持YUV/RGB格式转换
典型用途 音频、传感器数据搬运 显示帧缓冲、图像处理

我的建议是:通用数据搬运用GCE,显示相关的用MDP_DMA。MDP_DMA内部有硬件格式转换器,可以直接把YUV转成RGB,省掉CPU的算力。但要注意,MDP_DMA的配置比GCE复杂,多了很多格式相关的寄存器。

4.5 避坑指南:我踩过的那些雷

最后分享几个实战中容易出问题的地方:

  • 地址对齐:源地址和目的地址必须按突发大小对齐。比如BURST_SIZE=8字节,地址必须是8的倍数。不对齐的话,DMA会报错或者传输错误数据。
  • 描述符链的循环:如果把最后一个描述符的next_des指向第一个,就形成了循环链。这在音频播放中很常用,但要注意中断处理,别让DMA跑飞了。
  • 中断处理:DMA传输完成中断要尽快处理,否则可能丢失后续中断。我习惯在中断里只做标记,实际处理放到工作队列里。
  • 时钟和电源:DMA模块的时钟和电源域要提前打开。有些低功耗场景下,DMA会被自动关掉,需要重新配置。

嗯,关于MTK DMA控制器的硬件细节,今天就讲到这里。下一节我们会深入描述符链的进阶用法,包括如何实现乒乓缓冲和零拷贝传输。到时候我会拿一个实际的相机预览案例来拆解,敬请期待。