浅谈DMA系统
Zane Lv4

DMA系统概述

什么是DMA?

DMA(Direct Memory Access,直接内存访问)系统是一种用于解放 CPU 的 I/O 技术,使得 CPU 不必占用太多时间来完成数据的传输,而是由 DMA 直接控制数据的传输。CPU需要做的仅仅是给 DMA 控制器下达指令。

DMA控制器

在DMA系统中,DMA控制器和CPU并行工作,CPU和DMA控制器之间通过I/O总线进行数据交互,而DMA控制器则直接和主存储器进行数据交换。DMA控制器从I/O设备传入内存中,或从内存传入I/O设备,使得CPU用于处理其他任务。

DMA原理简介

通俗的讲,当外设需要与内存进行数据传输时,外设首先向 DMA 控制器发送传输请求,DMA 控制器在得到请求后,使 CPU 从数据传输过程中解脱出来,将数据从外设中读取到DMA 缓冲区,然后将数据传输到内存中指定的位置。这个过程中,CPU 不参与数据的传输,而是在 DMA 控制器传输数据的同时,继续处理其他任务。当 DMA 控制器完成数据传输后,它会向 CPU 发送中断请求,通知 CPU 数据传输已经完成。

Linux下的dma_buf

在 Linux 内核中,dma_buf 充当一种通用的缓存机制,允许多设备之间的共享内存,避免不必要的数据复制。

简单来说,dma_buf 可以把一片底层驱动 A 的 buffer 导出到用户空间成为一个文件描述符 fd,再导入到底层驱动 B,同样的,为该 fd 申请了虚拟内存地址,CPU 也可以从内存空间访问到该底层驱动的 buffer。需要注意的是,dma_buf 仅仅是一种内存映射的机制,并不是一种独立的内存类型或内存池。

假设有一个设备驱动程序 A 和一个用户空间应用程序 B,它们需要在共享数据时使用DMA。使用 dma_buf 可以将驱动程序 A 中的内存区域导出到用户空间,然后在应用程序 B中通过文件描述符(fd)访问该内存区域。

  1. 驱动程序A中,申请一块内存区域用于映射dma_buf
1
2
3
4
5
6
7
8
9
10
struct dma_buf *dbuf; void *buf;
/* 分配一块内存 */
buf = kmalloc(size, GFP_KERNEL);
if (!buf)
return -ENOMEM;
/* 映射为 dma_buf */
dbuf = dma_buf_export(buf, size, DMA_ATTR_NON_CONSISTENT, NULL);
if (IS_ERR(dbuf)) {
kfree(buf); return PTR_ERR(dbuf);
}
  1. 应用程序B中,通过文件按系统调用ioctl()打开该dma_buf并获得文件描述符(fd)
1
2
3
4
5
6
7
int fd; struct dma_buf_sync sync;
/* 打开 dma_buf */
fd = open("/dev/dma_buf", O_RDWR);
if (fd < 0)
return fd;
/* 同步 DMA_BUF */ sync.flags = DMA_BUF_SYNC_READ | DMA_BUF_SYNC_WRITE;
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
  1. 应用程序B,通过fd访问DMA内存
1
2
3
4
5
6
7
void *buf;
/* 将 fd 映射到用户空间 */
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (buf == MAP_FAILED)
return -errno;
/* 在 DMA 内存中进行操作 */ memcpy(buf, src, size);
/* 解除映射 */ munmap(buf, size);

该示例只是在逻辑上完成dma_buf应用,实际上在多进程进行访问同一块区域时需要考虑加锁问题。

SylixOS中DMA相关API

CPU 通过控制指令控制 DMA 使能或禁用。DMA 控制器在使能模式下通过配置指令配置 DMA 搬运的源地址,目的地址,搬运方向和搬运长度。在完成相关配置后,CPU 处理其他事务,DMA 控制器开始进行内存搬运。DMA 搬运成功或者失败会触发相应的中断,CPU 会对中断进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <SylixOS.h>
typedef struct {
UINT8 *DMAT_pucSrcAddress; /* 源端缓冲区地址 */
UINT8 *DMAT_pucDestAddress; /* 目的端缓冲区地址 */ size_t DMAT_stDataBytes; /* 传输的字节数 */
INT DMAT_iSrcAddrCtl; /* 源端地址方向控制 */
INT DMAT_iDestAddrCtl; /* 目的地址方向控制 */
INT DMAT_iHwReqNum; /* 外设请求端编号 */
BOOL DMAT_bHwReqEn; /* 是否为外设启动传输 */
BOOL DMAT_bHwHandshakeEn; /* 是否使用硬件握手 */
INT DMAT_iTransMode; /* 传输模式, 自定义 */
PVOID DMAT_pvTransParam; /* 传输参数, 自定义 */
ULONG DMAT_ulOption; /* 体系结构相关参数 */
PVOID DMAT_pvArgStart; /* 启动回调参数 */
} LW_DMA_TRANSACTION;
typedef LW_DMA_TRANSACTION *PLW_DMA_TRANSACTION;

该LW_DMA_TRANSACTION结构体作为DMA使用到的参数;

因为DMA操作的是物理地址,所以Src和Dest地址均为物理地址。

API_DmaDrvInstall

在用户注册 DMA 设备之前,需要调用该函数安装 DMA 设备库,其功能是用户配置好DMA 控制器后调用此函数使得驱动加入内核中。

在具体的实现中此函数其实就是实现初始化了一个队列用于存储相关驱动节点。

1
2
3
4
#include <SylixOS.h>
INT API_DmaDrvInstall (UINT uiChannel,
PLW_DMA_FUNCS pdmafuncs,
size_t stMaxDataBytes)
  • 函数成功返回 ERROR_NONE,失败返回 PX_ERROR。

  • 参数 uiChannel 是 DMA 通道号。

  • 参数 pdmafuncs 是 DMA 驱动加咱函数。

  • 参数 stMaxDataBytes 是 DMA 驱动最大传输字节数。

API_DmaReset

该函数实现复位指定的 DMA 通道:

1
2
#include <SylixOS.h>
INT API_DmaReset(UINT uiChannel);
  • 成功返回 ERROR_NAONE,失败返回 PX_ERROR。

  • 参数 uiChannel 是 DMA 通道号。

API_DmaJobNodeNum

1
2
3
#include <SylixOS.h>
INT API_DmaJobNodeNum(UINT uiChannel,
INT *piNodeNum);

DmaJobNodeNum 函数可获取当前队列中的节点数,具体实现是赋值出结构体中的 size_t DMAT_stDataBytes成员。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

  • 参数 piNodeNum 是 DMA 驱动函数节点号。

API_DmaMaxNodeNumGet

1
2
3
#include <SylixOS.h>
INT API_DmaMaxNodeNumGet(UINT uiChannel,
INT *piMaxNodeNum);

用于获取最大队列节点数,同样是通过赋值出结构体中的 DMAC_iMaxNode 成员。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

  • 参数 piMaxNodeNum 是最大节点数。

API_DmaMaxNodeNumSet

1
2
3
#include <SylixOS.h>
INT API_DmaMaxNodeNumSet(UINT uiChannel,
INT iMaxNodeNum);

设 置 最 大 队 列 节 点 数 , 具 体 实 现 是 将 传 入 的 参 数 iMaxNodeNum 赋 值 给 成 员DMAC_iMaxNode

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

  • 参数 piMaxNodeNum 是最大节点数。

API_DmaJobAdd

1
2
3
#include <SylixOS.h>
INT API_DmaJobAdd(UINT uiChannel,
PLW_DMA_TRANSACTION pdmatMsg);

添加一个 DMA 传输请求,具体实现是通过加锁进入访问后调用插入队列节点的相关函数再解锁的过程。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

  • 参数 pdmatMsg 是传入数据指针。

API_DmaGetMaxDataBytes

1
2
#include <SylixOS.h>
INT API_DmaGetMaxDataBytes(UINT uiChannel);

获取一次可以传输的最大字节数,通过返回结构体的成员变量实现。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

API_DmaFlush

1
2
#include <SylixOS.h>
INT API_DmaFlush(UINT uiChannel);

删除所有被延迟处理的传输请求,具体实现是通过加锁后访问队列进行逐个删除。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

API_DmaContext

1
2
#include <SylixOS.h>
INT API_DmaContext(UINT uiChannel);

用于使用 DMA 传输完成后的 CPU 中断服务。

  • 函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。

  • 参数 uiChannel 是 DMA 通道号。

参考资料

SylixOS 实时操作系统-翼辉信息 (acoinfo.com)

宋宝华:世上最好的共享内存
宋宝华:那些年你误会的Linux DMA

由 Hexo 驱动 & 主题 Keep