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)
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);
}
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;
描述符链的工作流程是这样的:
- CPU在内存中构建一个描述符数组
- 将第一个描述符的地址写入DMA的DES_BASE寄存器
- DMA自动读取第一个描述符,执行传输
- 传输完成后,根据next_des字段找到下一个描述符
- 重复直到遇到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);
}
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控制器的硬件细节,今天就讲到这里。下一节我们会深入描述符链的进阶用法,包括如何实现乒乓缓冲和零拷贝传输。到时候我会拿一个实际的相机预览案例来拆解,敬请期待。