找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

431

积分

0

好友

61

主题
发表于 昨天 03:54 | 查看: 1| 回复: 0

在Linux内核驱动开发中编写DMA代码,开发者必须处理好两个核心挑战。

第一,跨越地址的鸿沟:

  • CPU视角: 使用的是虚拟地址(Virtual Address),即我们常见的 void * 类型指针。
  • DMA硬件视角: 使用的是总线地址(Bus Address,类型为 dma_addr_t)。
    通常情况下,总线地址近似于物理地址。切记,绝不能将 kmalloc 等函数返回的虚拟地址指针直接写入DMA硬件寄存器,必须通过内核提供的API进行地址映射和转换。

第二,维护缓存一致性:
CPU读写内存时通常会经过高速缓存(Cache),而DMA引擎则直接访问物理RAM,这可能导致数据不一致:

  1. DMA写入场景: DMA将新数据写入RAM,但CPU随后读取该地址时,可能直接从Cache中拿到了旧数据。
  2. CPU写入场景: CPU修改了数据,但新数据还停留在Cache中未刷入RAM,此时DMA从RAM读取数据并发送,导致发出了旧数据。
    因此,必须使用Linux内核提供的专用DMA API,由内核自动处理Cache的刷新(Clean)与失效(Invalidate)操作,这也是Linux内核开发中保证硬件协同的关键。

Linux DMA映射的两种方式

根据使用场景和性能需求,Linux内核主要提供了两种DMA内存映射方式。

1. 一致性映射 (Coherent / Consistent Mapping)

特点: 映射建立后,CPU和DMA看到的内存内容始终保持一致。内核通常会为此段内存关闭Cache,或由硬件保证一致性。优点是省心,无需手动同步;缺点是因无Cache加速,CPU访问速度较慢。适用于驱动与硬件间需要长期共存、频繁访问的控制结构,如描述符环形缓冲区、状态寄存器等。其生命周期常与驱动模块绑定。

典型代码流程:

/* 1. 分配并映射 */
// cpu_addr: 给CPU用的虚拟地址
// dma_handle: 给硬件用的总线地址
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    /* 错误处理 */
}

/* 2. 配置硬件 */
// 将 dma_handle 写入硬件的DMA地址寄存器
writel(dma_handle, my_dev_reg_base + REG_DMA_START_ADDR);

/* 3. 使用 */
// CPU可直接读写 cpu_addr,无需担心Cache一致性

/* 4. 释放 */
dma_free_coherent(dev, size, cpu_addr, dma_handle);
2. 流式映射 (Streaming Mapping)

特点: 性能更高,允许使用Cache,但在每次DMA传输前后,必须手动调用API来同步Cache。适用于数据量大、生命周期短的场景,如网络数据包(sk_buff)、视频帧缓冲区等。使用时需明确指定数据流向(DMA_TO_DEVICEDMA_FROM_DEVICE)。

典型代码流程:

/* 假设 buffer 是已分配好的一块内存 */
void *buffer = kmalloc(size, GFP_KERNEL);
dma_addr_t dma_handle;

/* 1. 映射 (Map) */
// 此操作会处理Cache刷新,之后CPU暂时不应再访问buffer
dma_handle = dma_map_single(dev, buffer, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
    /* 错误处理 */
}

/* 2. 启动DMA传输 */
writel(dma_handle, my_dev_reg_base + REG_DATA_ADDR);
writel(START_CMD, my_dev_reg_base + REG_CMD);

/* ... 等待中断,确认传输完成 ... */

/* 3. 取消映射 (Unmap) */
// 此后,CPU才能再次安全访问 buffer
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);

使用DMA Engine通用API

对于支持DMA Engine框架的控制器,驱动可以作为“客户端”来申请和使用DMA通道,流程更为抽象和统一。

#include <linux/dmaengine.h>
struct dma_chan *chan;
struct dma_async_tx_descriptor *tx;

// 1. 申请通道 (通常在probe函数中)
chan = dma_request_chan(dev, "tx"); // “tx”需对应设备树中的定义

// 2. 准备数据 (使用流式映射)
dma_addr_t dma_buf = dma_map_single(dev, buffer, len, DMA_MEM_TO_DEV);

// 3. 配置从设备参数并准备传输描述符
struct dma_slave_config cfg = { ... };
dmaengine_slave_config(chan, &cfg);
tx = dmaengine_prep_slave_single(chan, dma_buf, len, DMA_MEM_TO_DEV, flags);

// 4. 设置传输完成回调函数
tx->callback = my_dma_callback;

// 5. 提交描述符并启动DMA传输
dmaengine_submit(tx);
dma_async_issue_pending(chan);

关键注意事项与常见“坑”

  1. 禁止映射栈内存:
    切勿对函数内的局部变量(栈内存)进行DMA映射,因为其物理地址可能不连续,且函数返回后内存会失效。

    void bad_func() {
        char buf[100]; // 栈内存
        // 错误!行为未定义
        dma_map_single(..., buf, ...);
    }

    必须使用 kmallocdma_alloc_coherent 分配的堆内存

  2. 设置DMA地址掩码:
    现代SoC多为64位CPU,但外设DMA控制器可能只支持32位寻址。驱动中必须设置正确的DMA掩码:

    if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
        /* 设置失败,硬件无法访问全部内存 */
    }

    若设置失败,则意味着存在硬件无法访问的内存区域。

  3. 切勿混淆数据传输方向:
    DMA_TO_DEVICE(内存 -> 硬件)与 DMA_FROM_DEVICE(硬件 -> 内存)的方向一旦搞反,内核执行的Cache同步操作将完全错误,导致数据损毁。




上一篇:百度后端实习面试高频问题深度剖析:MySQL索引、Redis缓存与MQ实战
下一篇:RedInk图文生成工具实战:基于AI的一句话生成与Docker开源部署指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-9 00:09 , Processed in 0.081393 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表