3. Linux内核内存管理机制
DMA,说白了就是让硬件自己搬数据,CPU 别插手。我做了这么多年驱动,见过太多人一上来就写 DMA 代码,结果不是数据错乱就是系统卡死。嗯,这一章咱们就把 DMA 驱动开发的三个核心环节讲透:控制器怎么初始化、描述符链怎么管、软硬件怎么同步。
3.1 DMA控制器初始化——先把地基打牢
初始化 DMA 控制器,不是简单配几个寄存器就完事。你得想清楚:系统里有多少个 DMA 通道?每个通道给谁用?中断怎么路由?
我个人习惯,先看芯片手册里的 DMA 控制器框图。搞清楚下面几件事:
- 通道数量:是 8 通道还是 16 通道?有些 SoC 把 DMA 通道分成了两组,每组有自己的仲裁器。
- 传输粒度:支持 8/16/32/64 位?burst 长度最大多少?
- 地址对齐要求:源地址和目标地址有没有对齐限制?
- 中断映射:每个通道完成中断挂到哪个 GIC 中断号上?
核心要点:初始化 DMA 控制器时,必须先把时钟和复位搞定。我遇到过一块芯片,DMA 寄存器死活写不进去,查了两天才发现是时钟门控没开。
下面是一个典型的 DMA 控制器初始化流程:
/* DMA控制器初始化示例 */
static int dma_controller_init(struct platform_device *pdev)
{
struct dma_device *dma_dev = platform_get_drvdata(pdev);
void __iomem *base;
int i, ret;
/* 1. 获取寄存器基地址 */
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
/* 2. 使能时钟和复位 */
ret = clk_prepare_enable(dma_dev->clk);
if (ret)
return ret;
reset_control_deassert(dma_dev->rstc);
/* 3. 配置全局控制寄存器 */
writel(DMA_GLOBAL_CFG_ENABLE | DMA_GLOBAL_CFG_ARB_ROUND_ROBIN,
base + DMA_GLOBAL_CFG);
/* 4. 初始化每个通道 */
for (i = 0; i < dma_dev->num_channels; i++) {
struct dma_chan *chan = &dma_dev->channels[i];
chan->idx = i;
chan->base = base + DMA_CHAN_OFFSET(i);
chan->state = DMA_CHAN_IDLE;
/* 配置通道中断 */
writel(DMA_CHAN_CFG_IE | DMA_CHAN_CFG_TC_IE,
chan->base + DMA_CHAN_CFG);
}
/* 5. 注册DMA引擎 */
ret = dma_async_device_register(dma_dev);
if (ret)
dev_err(&pdev->dev, "DMA device register failed\n");
return ret;
}
3.2 描述符链管理——DMA的“任务清单”
描述符链,就是 DMA 要干活的清单。每个描述符告诉 DMA:从哪搬数据、搬到哪、搬多少、搬完下一个任务是什么。
你想想看,如果每次只发一个描述符,CPU 就得频繁参与。描述符链的好处是:一次提交一串任务,DMA 自己按顺序执行,CPU 可以干别的事。
经验之谈:描述符链最好用环形缓冲区管理。我曾经在一个项目中用了链表,结果描述符释放时出现了内存碎片,导致 DMA 卡死。换成环形缓冲区后,问题就解决了。
描述符的结构通常长这样:
/* DMA描述符结构 */
struct dma_descriptor {
dma_addr_t src_addr; /* 源地址 */
dma_addr_t dst_addr; /* 目标地址 */
uint32_t length; /* 传输长度 */
uint32_t ctrl; /* 控制位:中断使能、链使能等 */
dma_addr_t next_desc; /* 下一个描述符地址(物理地址) */
uint32_t status; /* 状态:由DMA硬件写入 */
} __attribute__((packed));
描述符链的管理,核心是三个操作:
- 分配描述符:从描述符池中取一个空闲的描述符,填充传输参数。
- 链接描述符:把新描述符的 next_desc 指向上一个描述符,形成链。
- 提交描述符链:把链首地址写入 DMA 控制器的描述符地址寄存器,触发传输。
注意:描述符必须放在非 Cacheable 的内存区域,或者确保 Cache 一致性。否则 DMA 读到的描述符可能是过时的数据。我见过有人把描述符放在栈上,结果 DMA 读到的是垃圾数据。
描述符链的典型管理流程:
/* 提交描述符链 */
int dma_submit_chain(struct dma_chan *chan, struct dma_descriptor *chain_head)
{
unsigned long flags;
int ret = 0;
spin_lock_irqsave(&chan->lock, flags);
/* 检查通道状态 */
if (chan->state != DMA_CHAN_IDLE) {
ret = -EBUSY;
goto out;
}
/* 确保描述符在DMA可见的内存区域 */
dma_sync_single_for_device(chan->dev,
virt_to_phys(chain_head),
sizeof(*chain_head) * chain_head->count,
DMA_TO_DEVICE);
/* 把链首地址写入DMA控制器 */
writel(virt_to_phys(chain_head),
chan->base + DMA_CHAN_DESC_ADDR);
/* 启动DMA传输 */
writel(DMA_CHAN_CTRL_START | DMA_CHAN_CTRL_CHAIN_EN,
chan->base + DMA_CHAN_CTRL);
chan->state = DMA_CHAN_BUSY;
out:
spin_unlock_irqrestore(&chan->lock, flags);
return ret;
}
3.3 硬件与软件同步机制——别让数据“飞了”
DMA 是异步的。你提交了任务,DMA 在后台搬数据,CPU 继续跑。那问题来了:你怎么知道 DMA 搬完了?搬完的数据 CPU 能看到吗?
同步机制,说白了就是解决两个问题:
- 传输完成通知:DMA 搬完了怎么告诉 CPU?
- 数据一致性:DMA 写的数据,CPU 的 Cache 里有没有?
3.3.1 中断同步——最常用的方式
DMA 传输完成时触发中断,CPU 在中断处理函数里收尾。这是最直接的方式。
/* DMA完成中断处理 */
irqreturn_t dma_done_irq(int irq, void *dev_id)
{
struct dma_chan *chan = dev_id;
struct dma_descriptor *desc;
unsigned long flags;
spin_lock_irqsave(&chan->lock, flags);
/* 读取状态寄存器,确认是哪个通道完成 */
uint32_t status = readl(chan->base + DMA_CHAN_STATUS);
if (!(status & DMA_STATUS_TC)) {
spin_unlock_irqrestore(&chan->lock, flags);
return IRQ_NONE;
}
/* 清除中断标志 */
writel(DMA_STATUS_TC, chan->base + DMA_CHAN_STATUS);
/* 获取当前完成的描述符 */
desc = chan->active_desc;
/* 同步Cache:让CPU看到DMA写的数据 */
dma_sync_single_for_cpu(chan->dev,
desc->dst_addr,
desc->length,
DMA_FROM_DEVICE);
/* 回调通知上层 */
if (chan->callback)
chan->callback(chan->callback_param);
chan->state = DMA_CHAN_IDLE;
chan->active_desc = NULL;
spin_unlock_irqrestore(&chan->lock, flags);
return IRQ_HANDLED;
}
3.3.2 轮询同步——低延迟场景的选择
有些场景中断延迟太大,比如音频 DMA。这时候可以用轮询:CPU 不断检查 DMA 的状态寄存器。
我的建议:轮询适合短传输、高频率的场景。但要注意,轮询会占 CPU,别在主线程里死等。我曾经在一个项目中用轮询等 DMA 完成,结果把整个系统的实时性拖垮了。
/* 轮询等待DMA完成 */
int dma_poll_completion(struct dma_chan *chan, int timeout_us)
{
uint32_t status;
int timeout = timeout_us;
while (timeout-- > 0) {
status = readl(chan->base + DMA_CHAN_STATUS);
if (status & DMA_STATUS_TC) {
writel(DMA_STATUS_TC, chan->base + DMA_CHAN_STATUS);
return 0;
}
udelay(1);
}
return -ETIMEDOUT;
}
3.3.3 数据一致性——Cache 是个“捣蛋鬼”
DMA 直接读写内存,但 CPU 有 Cache。如果 DMA 写了一块内存,而 CPU 的 Cache 里还存着旧数据,CPU 读到的就是错的。反过来也一样:CPU 写了数据到 Cache,还没刷回内存,DMA 就去搬,搬的是旧数据。
解决办法就两个:
- 使用一致性内存:dma_alloc_coherent() 分配的内存,硬件保证 Cache 和内存一致。
- 手动同步:用 dma_sync_single_for_cpu() 和 dma_sync_single_for_device() 在合适的时机刷 Cache。
避坑指南:我曾经在一个项目中,DMA 从网卡收数据,CPU 处理时总丢包。查了三天,发现是 Cache 没刷。DMA 写完了数据,CPU 的 Cache 里还是旧数据,一读就错。加上 dma_sync_single_for_cpu() 后,问题立刻消失。
3.4 知识体系总览
下面这张图,把 DMA 驱动开发的三个核心环节串起来了:
3.5 常见问题与调试技巧
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| DMA传输未启动 | 时钟未使能、描述符地址错误 | 检查时钟寄存器、打印描述符物理地址 |
| 数据错位 | 地址对齐不满足要求 | 检查源/目标地址对齐方式 |
| 中断丢失 | 中断标志未清除、中断优先级问题 | 在中断处理函数开头打印状态寄存器 |
| 数据不一致 | Cache未同步 | 检查 dma_sync_single_for_cpu/device 调用 |
| 描述符链断裂 | next_desc 指向了无效地址 | 遍历描述符链,打印每个节点的 next_desc |
最后提醒一句:调试 DMA 驱动时,别只盯着代码。用逻辑分析仪抓一下 DMA 控制器的信号,往往能发现寄存器配置的问题。我遇到过好几次,代码看着没问题,但示波器一抓,发现时序完全不对。