DMA简介
DMA 是一种采用硬件实现存储器与存储器之间或存储器与外设之间直接进行高速数据传输的技术,传输过程无需 CPU 参与(但是CPU需要提前配置传输规则),可以大大减轻 CPU 的负担。
DMA 存储传输的过程如下:
- CPU 向 DMA 控制器配置传输规则,如源地址、目的地址、长度、地址是否自增等
- DMA 控制器根据配置进行数据搬移,此时 CPU 资源被释放
- DMA 数据搬移完成后向 CPU 发送中断
AXI DMA简介
AXI Direct Memory Access(AXI DMA)是 ZYNQ PL端的一个 IP 核,它提供了 CPU 内存(AXI4 内存映射)和 PL 端 AXI4-Stream 之间高速数据传输的功能,如下是 AXI DMA 的框图:
- AXI4 Memory Map Read:用于从 DDR3 中读取数据
- AXI4 Memory Map Write:用于向 DDR3 中写入数据
- AXI4 Stream Master(MM2S):接口用于向外设写入数据
- AXI4-Stream Slave(S2MM):接口用于从外设读取数据
- AXI Control stream (MM2S):是控制流接口,该接口的主要作用是 AXI DMA 对目标设备写入数据时进行节流
- AXI Stream (S2MM):是一个状态流接口,主要作用是将目标设备的状态传输到 AXI DMA 中的用户应用程序数据字段中
- AXI Memory Map Write/Read:接口的主要作用是在 S/G 模式下 AXI DMA 与外设进行数据的读写
ADX DMA工作模式
AXI DMA 提供 3 种模式,分别是 Direct Register 模式、Scatter/Gather 模式和 Cyclic DMA(循环 DMA)模式,其中 Direct Register 模式最常用,Scatter/Gather 模式和 Cyclic DMA 模式使用的相对较少
- Direct Register DMA 模式 :
Direct Register DMA 模式也就是 Simple DMA(简单 DMA)。Direct Register 模式提供了一种配置,用于在 MM2S 和 S2MM 通道上执行简单的 DMA 传输,这种传输只需要少量的 FPGA 资源。Simple DMA(简单 DMA)允许应用程序在 DMA 和 Device 之间定义单个事务。它有两个通道:一个从 DMA 到 Device,另一个从 Device 到 DMA。这里有个地方需要大家注意下,在编写 Simple DMA(简单 DMA)代码时必须设置缓冲区地址和长度字段以启动相应通道中的传输。 - Scatter/Gather DMA 模式 :
SGDMA(Scatter/Gather DMA)模式允许在单个 DMA 事务中将数据传输到多个存储区域(相当于将多个 Simple DMA 请求通过事务列表链接在一起)。SGDMA 允许应用程序在内存中定义事务列表,硬件将在应用程序没有进一步干预的情况下处理这些事务,在此期间应用程序可以继续添加更多事务以保持硬件工作,还可以通过轮询或中断来检查事务是否完成。 - Cyclic DMA 模式:
Cyclic DMA(循环 DMA)模式是在 Scatter/Gather 模式下的一种独特工作方式,在 Multichannel Mode 下不可用。正常情况下的 Scatter/Gather 模式在遇到 Tail BD(最后一个事务列表项)就应该结束当前的传输,但是如果使能了 Cyclic 模式的话,在遇到 Tail BD时会忽略 completed 位,并且回到 First BD(第一个事务列表项),这一过程会一直持续直到遇到错误或者人为中止。
VIVADO 工程搭建
使用 Direct Register DMA 模式搭建一个 DMA 回环测试的 VIVADO 工程,Vivado 工程框图如下:
-
添加 ZYNQ IP 核,并进行配置,使能flash接口、AXI GP0、AXI HP0、ENET、SD、DDR、PL中断、PL时钟等,其中AXI GP0、AXI HP0、PL 中断、PL 时钟、PL 中断是为了连接 PL 端的AXI DMA IP 核。
AXI GP0、AXI HP0配置如下:
PL 时钟配置如下:
PL中断配置如下:
-
添加 DMA IP 核
-
配置 DMA IP 核
-
添加 FIFO IP 核心
-
连接DMA IP和FIFO IP,将 DMA 的 MM2S 和 S2MM 接口通过一个 AXIS DATA FIFO 构成回环。
-
点击“Run Block Automation”和“Run Connection Automation”进行自动连线(可能会重复操作多次),在连线过程中会自动添加一些 IP 核。
-
添加 concat IP 核和 const IP 核
添加 concat IP:
添加 const IP:
-
配置 concat IP 核和 const IP 核
配置 concat IP:
配置 const IP:
-
链接中断信号
-
生成代码,然后编译并生成bit文件,最后导出 xsa 文件,导出时必须包含bit流
利用 Petalinux 构建根文件系统和 BOOT.BIN
此部分内容参考04 搭建linux驱动开发环境中的利用 Petalinux 构建根文件系统和 BOOT.BIN部分。
获取 Linux 设备树
此部分内容04 搭建linux驱动开发环境中的获取Linux设备树文件部分
可以发现在 pl.dtsi 文件中已经生成了AXI DMA IP核的设备树节点
编译 Linux 内核
此部分内容04 搭建linux驱动开发环境中的编译Linux内核部分。
Linux 中使用 DMA的要点
DMA驱动框架
DMA驱动框架可分为三层:
DMA slave驱动:使用DMA设备的驱动程序,如SPI控制器驱动、UART控制器驱动等
DMA核心:提供统一的编程接口,管理DMA slave驱动和DMA控制器驱动
DMA控制器驱动:驱动DMA控制器
在Linux系统中DMA核心已经包含在系统中,DMA控制器驱动一般有芯片厂家提供,普通开发人员一般只要掌握DMA slave驱动开发即可
DMA 区域
部分SOC的DMA只能访问特定的一段内存空间,而非整个内存空间,所以在使用kmalloc()、__get_free_pages()等类似函数申请可能用于DMA缓冲区的内存时需要在申请标志中增加GFP_DMA标志,此外系统也通过了一些快捷接口,如__get_dma_pages()、dma_mem_alloc()等。
提示
大多数嵌入式SOC其DMA都可以访问整个常规内存空间。
虚拟地址、物理地址、总线地址
虚拟地址:CPU所使用的地址就是虚拟地址(站在CPU角度来看)。
物理地址:站在MMU角度来看,即DRAM空间向MMU呈现出来的地址,它可通过MMU转换为虚拟地址。
总线地址:站在设备的角度来看,即DRAM空间向设备呈现出来的地址,部分SOC带有IOMMU,还可以对总线地址进行映射后在呈现给设备。
DMA地址掩码
部分SOC的DMA只能访问特定的一段内存空间(如系统总线32bit地址,而DMA只能访问低24bit地址,在这种情况下,外设在发起DMA操作的时候就只能访问16M以下的系统物理内存),因此需要设置DMA mask,以指示DMA能访问的地址空间,如DMA只能访问低24位地址则需要执行dma_set_mask(dev, DMA_BIT_MASK(24))和dev.coherent_dma_mask = DMA_BIT_MASK(24)操作,dma_set_mask(dev, DMA_BIT_MASK(24))设置的是DMA流式映射的寻址范围,dev.coherent_dma_mask = DMA_BIT_MASK(24)设置一致性 DMA 缓冲区的寻址范围,或者使用int dma_set_mask_and_coherent(dev, DMA_BIT_MASK(24))一次完成这两部操作。
一致性DMA缓冲区
一致性DMA缓冲区即DMA和CPU可以一致访问的缓冲区,不需要考虑cache的影响,一般通过关闭cache实现(部分SOC的DMA也支持从通过cache访问内存),一致性缓冲区分别使用如下函数申请或释放:
//分配一致性缓冲区
void *dmam_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)
void *dma_alloc_wc(struct device *dev, size_t size, dma_addr_t *dma_addr, gfp_t gfp)
//释放一致性缓冲区
void dmam_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle)
void dma_free_wc(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_addr)
single类型流式映射
有时候需要通过DMA传输的内存并非由DMA分配,针对这种情况可以采用流式映射,若来自其他驱动的是一个连续的内存区域可以使用如下函数进行映射和取消映射:
//映射
dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction dir)
//取消映射
void dma_unmap_single((struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir)
被映射后的内存在取消映射前原则上CPU不能访问,若CPU实在需要访问需要先申请拥有权,访问完后在释放拥有权:
//申请CPU拥有权
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir)
//释放CPU拥有权
void dma_sync_single_for_device(struct device *dev, dma_addr_t addr, size_t size, enum dma_data_direction dir)
SG类型流式映射
若来自其他驱动的内存区域在物理上是不连续的则需要进行分段映射,可以使用如下函数进行映射和取消映射:
//映射
#define dma_map_sg(d, s, n, r) dma_map_sg_attrs(d, s, n, r, 0)
int dma_map_sg_attrs(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, unsigned long attrs)
//取消映射
#define dma_unmap_sg(d, s, n, r) dma_unmap_sg_attrs(d, s, n, r, 0)
void dma_unmap_sg_attrs(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, unsigned long attrs)
同样对于被映射后的内存在取消映射前原则上CPU不能访问,若CPU实在需要访问需要先申请拥有权,访问完后在释放拥有权:
//申请CPU拥有权
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nelems, enum dma_data_direction dir)
//释放CPU拥有权
void dma_sync_sg_for_device(struct device *d