DMA映射

参考资料:
《宋宝华:Linux设备驱动开发详解》
 

VA和PA的相互转换:

系统启动时,MMU便会建立映射表,将DRAM物理地址和虚拟地址进行映射,在linux内核中,可以使用下面的函数完成VA和PA的转换
#define virt_to_bus(virt)    (virt_to_phys(virt))

void *phys_to_virt(phys_addr_t address);
phys_addr_t virt_to_phys(const volatile void *address);

// 参数说明
address:要转换的虚拟地址

#define phys_to_virt(vaddr) ((void *)((unsigned long)(vaddr)+PAGE_OFFSET))
但从代码发现,phys_to_virt和virt_to_phys都获取到的内核地址仅做了简单的偏移,例如0x817000000经过phys_to_virt获取到的内核地址为0xffff000817000000
 

DMA地址掩码:

设备不一定能够在所有的内存地址上进行DMA操作,在这这种情况下应该通过下列函数执行DMA地址掩码:
int dma_set_mask(strcut device *dev, u64 mask);
例如,对于只能在32位地址上执行DMA操作的设备而言,就应该调用dma_set_mask(dev, 0xffffffff)。
其实该API的本质就是修改了device结构体中的dma_mask成员,如下:
int arm_dma_set_mask(struct device* dev, u64 dma_mask)
{
    if(!dev->dma_mask || !dma_supported(dev, dma_mask))
        return -EIO;
    *dev->dma_mask = dma_mask;
    
    return 0;
}
在device结构体中,除了有dma_mask之外,还有一个coherent_dma_mask成员,dma_mask是设备DMA可以寻址的范围,而coherent_dma_mask作用用于申请一致性的DMA缓冲区:
 

一致性DMA缓冲区:

DMA映射包括两个部分的工作:分配一片DMA缓冲区,为这片缓冲区产生设备可访问的地址。同时,DMA映射必须也要考虑cache一致性的问题。内核提供如下函数以分配DMA一致性的内存区域:
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); 

//分配DMA缓存区
//返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,需要释放,避免内存泄漏
//参数如下:
  //*dev:指针,这里填0,表示这个申请的缓冲区里没有内容
  //size:分配的地址大小(字节单位)
  //*handle:申请到的物理起始地址
  //gfp:分配出来的内存参数,标志定义在<linux/gfp.h>,常用标志如下:
        //GFP_ATOMIC    用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
        //GFP_KERNEL    内核内存的正常分配. 可能睡眠.
      //GFP_USER      用来为用户空间页来分配内存; 它可能睡眠. 
dma_alloc_coherent函数返回值表示kernel看到的地址,返回值表示dma设备看到的地址
 
对应的释放函数如下:
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)    //释放DMA缓存,与dma_alloc_coherent ()对应

//size:释放长度
//cpu_addr:虚拟地址,
//handle:物理地址
两者的区别:
  1. dma_alloc_writecombine
  • dma_alloc_writecombine 函数用于分配一块内存区域,该内存区域可以通过 DMA 进行写操作(如数据传输到设备)。
  • 分配的内存区域通常是通过写结合(write-combining)机制来实现的,这种机制可以提高性能,减少内存复制和刷新的开销。
  • 适用于一些需要高性能写操作的场景,比如网络设备的数据传输。
  1. dma_alloc_coherent
  • dma_alloc_coherent 函数也用于分配一块内存区域,但是它分配的内存区域是直接连续的物理内存,适合 DMA 读写操作。
  • 内核会保证返回的内存区域是物理上连续的,以便于 DMA 控制器直接访问这块内存进行数据传输。
  • 适用于需要进行 DMA 读写操作的场景,如一些设备对内存访问要求严格的情况。
 
dma_alloc_writecombine对应的释放函数:dma_free_writecombine
dma_free_writecombine函数定义如下:
void *dma_free_writecombine(struct device *dev, size_t size, cpu_addr, handle)        \
    dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle)
需要注意的一点是,dma_alloc_xxx()函数虽然是dma_alloc_开头的,但是其申请的区域不一定在DMA区域中。以32位ARM处理器为例,当coherent_dma_mask小于0xffffffff时,才会设置GPA_DMA标记,并从DMA区域去申请内存。
当使用ARM时,像GPU、 isp等设备需要预留大量连续的内存,这部分内存平时不用,但是一般的做法又必须先预留。CMA机制,可以做到不预留内存,这些内存是平时可用的,只是当有需要的时候才会被分配给isp设备。
 

流式DMA映射:

需要流式映射的原因:

并不是所有的DMA缓冲区都是驱动申请的,如果是驱动申请的,用一致性DMA映射自然最方便,一致性直接考虑了cache一致性问题了。但是,缓冲区来自内核的较上层(例如网络报文,块设备要写入设备的数据),上层很可能使用的是kmalloc()等方法去申请的,这时就需要用到流式DMA映射。
 

流式映射相对于一致性映射有更多的限制点,以下原因不同于一致性映射:

1、映射需要使用已经分配的缓冲区
2、映射可以接收几个分散的不连续缓冲区
3、映射的缓冲区属于设备而不属于cpu。cpu使用缓冲区之前,应该首先解除映射(在dma_unmap_single()或者dma_unmap_sg()之后)。这是为了缓存
4、对于写入事务(CPU到设备),驱动程序应该在映射之前将数据放入缓冲区
5、必须指定数据移动的方向,只能基于该方向使用数据
为啥在取消映射之前不能访问缓冲区呢?原因很简单:CPU映射是可缓存的。用于流式映射的dma_map_xxx()系列函数将首先清理与缓冲区相关的缓存使之无效,在出现相应的dma_unmap_xxx()之前,CPU不能访问。
 
流式DMA映射本质上大多就是进行cache的使无效或清除操作
 

流式映射有两种形式:

1、单缓冲区映射,只允许单页映射
2、分散/聚集映射,允许传递多个缓冲区(分散在内存中)
对于这两种形式,都必须指定方向:
enum dam_data_direction {
    DMA_BIDIRECTIONAL = 0,
    DMA_TO_DEVICE = 1;
    DMA_FROM_DEVICE = 2;
    DMA_NONE = 3;
};
 

单缓冲区映射:

适用于偶然使用的映射,可用dma_map_single实现流式DMA映射,dma_unmap_single取消流式映射:
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);
// 参数和返回值说明
dev:指向代表要进行 DMA 操作的设备的 struct device 结构体的指针。
ptr:指向要映射的内存区域的起始虚拟地址。
size:要映射的内存区域的大小。
direction:数据传输方向,可以是 DMA_TO_DEVICE(表示数据从内存传输到设备)、DMA_FROM_DEVICE(表示数据从设备传输到内存)或者 DMA_BIDIRECTIONAL(表示双向数据传输)。
dma_addr_t:表示映射后的物理地址(即 DMA 地址)。如果映射成功,则返回映射后的物理地址;如果映射失败,则返回一个特定的错误码。

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
// 参数说明
dev:指向代表进行 DMA 操作的设备的 struct device 结构体的指针。
dma_addr:要取消映射的 DMA 地址,即之前调用 dma_map_single 返回的物理地址。
size:取消映射的内存区域的大小,应与之前映射时传入的大小相同。
direction:数据传输方向,应与之前映射时传入的方向参数相同。
通常情况下,设备驱动不应该访问unmap的流式映射DMA缓冲区,如果一定要这么做,可以先使用下列函数获得DMA缓冲区的拥有权:
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction);

// 参数说明
dev:指向进行 DMA 操作的设备的 struct device 结构体的指针。
dma_handle:要同步的 DMA 地址,即之前通过 dma_map_single 函数获得的 DMA 地址。
size:要同步的内存区域的大小,应与之前映射时传入的大小相同。
direction:数据传输方向,通常应与之前映射时传入的方向参数相同,用于指示数据流向。
驱动访问完DMA缓冲区后,应该将其所有权返还给设备,如下:
dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction direction);

// 参数说明
dev:指向进行 DMA 操作的设备的 struct device 结构体的指针。
dma_handle:要同步的 DMA 地址,即之前通过 dma_map_single 函数获得的 DMA 地址。
size:要同步的内存区域的大小,应与之前映射时传入的大小相同。
direction:数据传输方向,通常应与之前映射时传入的方向参数相同,用于指示数据流向。
 

分散/聚集映射:

分散/聚集映射是一种特殊类型的流式DMA映射,可以在单个槽中传输多个缓冲区区域,而不是单独映射每个缓冲区并逐个传输。假设有几个缓冲区物理上可能不是连续的,所有这些缓冲区都需要同时传输到设备或者从设备传输,这些情况可能出现的原因:
1、readv或writev系统调用
2、磁盘I/O请求
 
内核将分散列表表示为:
struct scatterlist {
    struct scatterlist *next;   // 指向下一个 scatterlist 结构体的指针
    unsigned int        offset;    // 缓冲区在页中的偏移
    unsigned long       dma_address;  // DMA 地址
    unsigned int        dma_length;   // 数据长度
    unsigned int        dma_offset;   // 数据偏移量
};

 

为了设置分散列表映射,应该进行如下操作:
1、分配分散的缓冲区
2、创建分散列表数组,并使用sg_set_buf()分配的内存填充它。注:分散页表必须是页面大小(除结尾外)
3、在该分散列表上调用dma_map_sg();负责缓存一致性
4、一旦完成DMA映射,就调用dma_unmap_sg()来取消映射分散列表
 
例如:
u32 *wbuf, *wbuf2, *wbuf3;
wbuf = kzmalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf2 = kzmalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf3 = kzmalloc(SDMA_BUF_SIZE/2, GFP_DMA);

struct scatterlist sg[3];
sg_init_table(sg, 3);
sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE/2);
ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM);

 

 
 
单缓冲区映射使用demo:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/dma-mapping.h>

#define BUF_SIZE 4096

static struct device *dummy_device;
static dma_addr_t dma_handle;
static char *dma_buffer;

static int __init dma_sync_demo_init(void)
{
    // 分配内存作为 DMA 缓冲区
    dma_buffer = kmalloc(BUF_SIZE, GFP_KERNEL);
    if (!dma_buffer) {
        pr_err("Failed to allocate DMA buffer\n");
        return -ENOMEM;
    }

    // 映射 DMA 缓冲区
    dma_handle = dma_map_single(dummy_device, dma_buffer, BUF_SIZE, DMA_BIDIRECTIONAL);
    if (dma_mapping_error(dummy_device, dma_handle)) {
        pr_err("Failed to map DMA buffer\n");
        kfree(dma_buffer);
        return -ENOMEM;
    }

    // 修改 DMA 缓冲区数据
    memset(dma_buffer, 0xAA, BUF_SIZE);

    // 同步 DMA 内存到 CPU 缓存
    dma_sync_single_for_cpu(dummy_device, dma_handle, BUF_SIZE, DMA_BIDIRECTIONAL);

    // 在这里可以处理同步后的数据

    return 0;
}

static void __exit dma_sync_demo_exit(void)
{
    // 取消映射 DMA 缓冲区
    dma_unmap_single(dummy_device, dma_handle, BUF_SIZE, DMA_BIDIRECTIONAL);

    // 释放 DMA 缓冲区内存
    kfree(dma_buffer);
}

module_init(dma_sync_demo_init);
module_exit(dma_sync_demo_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("DMA Sync Demo");
posted @ 2024-03-24 18:00  lethe1203  阅读(120)  评论(0编辑  收藏  举报