一般情况下,对外设的操作包括轮训方式、中断方式,对于数据量很大的情况会用到DMA操作。本文介绍一种在用户空间实现DMA操作的方法来获取AXI总线上的数据,FPGA部分暂时不详细说明,之后会有专题来介绍。
首先要明白几个Linux的机制:
1、UIO机制,该机制可以在用户空间操作内核空间的IO设备,这里用来实现中断信号的上报。
2、/dev/mem ,该设备能够直接映射Linux物理内存,然后将物理地址映射到用户空间的虚拟地址上。在用户空间完毕对设备寄存器的操作
3、mmap函数,实现2中的内存映射。
下面通过实际代码来说明这种机制:
typedef struct {
int axidma; /* Axi dma descriptor */
void *access_address;
unsigned int mapSzie;
int mem; /* memory descriptor */
void *ddr_address;
unsigned int ddr_mapSize;
}Axidma_t, *AxidmaHandle_t;
在接口文件中定义了Axidma_t, 其中
axidma 打开的设备描述符
access_address 外设地址
mapSzie 映射内存的大小 (外设被映射到内存空间)
mem 打开内存设备的描述符
ddr_address 内存地址
ddr_address 内存地址
ddr_mapSize 映射的内存大小
首先来分析初始化函数dw_initAxiDma_s2mm:
1、AxidmaHandle_t axidma = (AxidmaHandle_t)malloc(sizeof(Axidma_t));//为结构体分配内存空间
2、axidma->mapSzie = DMA_REG_MAP_SIZE; //操作结构体成员,设置 映射内存的大小
3、axidma->axidma = open(UIO_DEVICE, O_RDWR); //打开DMA设备,
#define UIO_DEVICE "/dev/user-s2mm"
4、axidma->mem = open("/dev/mem", O_RDWR | O_SYNC); //打开内存设备
注意"/dev/mem"这个设备!!!!/dev/mem是物理内存的全映像,可以用来访问物理内存,用mmap来访问物理内存以及外设的IO资源,是实现用户空间驱动的一种方法。
5、axidma->access_address = mmap(NULL, axidma->mapSzie, PROT_READ | PROT_WRITE, MAP_SHARED, axidma->axidma
, 0);
映射设备的寄存器地址从内核空间到用户空间,
这里学习一下mmap函数,在linux平台下,一般两种形式来操作文件。
第一是open read write 。内核将文件的内容从磁盘上读取到内核页高速缓冲,再从内核高速缓冲读取到用户进程的地址空间。这么做需要在内核和用户空间之间做四次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。
第二种是使用内存映射的方式,每个进程都将文件内容在内核中的页高速缓冲映射到自己的地址空间。当第一个进程访问内核中的页缓冲时,进程的机器指令会触发一个缺页中断。内核将文件的这一页数据读入到页高速缓冲,并更新进程的页表,使页表指向内核缓冲中的这个页。之后有其他进程再次访问同一页时,该页已经在内存中,内核只需要将进程的页表登记并指向内核中的页高速缓冲即可。
mmap函数是unix/linux下的系统调用,函数原型如下:
#include <sys/mman.h> void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *start, size_t length);
start: 映射区开始的地址
length: 映射区的长度
prot: 期望的内存保护标志, 不能与文件的打开模式冲突 :
- PROT_EXEC: 页内容可以被执行
- PROT_READ: 页内容可以被读取
- PROT_WRITE: 页内容可以被写入
- PROT_NONE: 页不可访问
flags: 指定 映射对象的类型 ,映射选项和映射页是否可以共享。它的值可以使一个或者多个以下位的组合:
- MAX_FIXD: 使用指定的映射起始地址,如果由start和len参数指定的内存重叠与现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页边界上。
- MAX_SHARED: 与其他所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
- MAP_PRIVATE: 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
- MAP_NORESERVE: 不要为这个映射保留交换空间。当交换空间被保留,对映射区的修改可能会得到保证。当交换空间不被保留,同时内存不足时,对映射区的修改会引起段异常信号。
- MAP_LOCKED: 锁定映射区的页面,从而防止页面被交换出内存。一般是要求实时性的应用程序和一些对安全要求较高的程序使用(禁止将RAM中的数据换出到磁盘文件上)。
- MAP_GROWNSDOWN: 用于堆栈,告诉内核VM系统,映射区可以向下扩展。
- MAP_ANONMOUS: 匿名映射,映射区不与任何文件关联。
- MAP_POPULATE: 问文件映射通过预读的方式准备好页表,随后对映射区的访问不会被缺页异常阻塞。
- MAP_NONBLOCK: 仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
- MAP_HUGETLB (since Linux 2.6.32): 申请HugePage,HugePage能减少页表项的使用,进而较少TLB失效的可能,并且在RAM中不会被换入换出。
fd: 有效的文件描述符。如果指定了MAP_ANONYMOUS,为了兼容问题,应该设置fd为-1。
offset: 应映射对象内容的起点。
有了这些东西,来分析下
axidma->access_address = mmap(NULL, axidma->mapSzie, PROT_READ | PROT_WRITE, MAP_SHARED, axidma->axidma
, 0);吧。
第一个参数是指定地址在linux man中这样介绍
If addr is NULL, then the kernel chooses the address at which to create the mapping; this is the most portable method of creating a new mapping. If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the mapping will be created at a nearby page boundary. The address of the new mapping is returned as the result of the call.
大致是说,如果这个地址设置为空,那么内核会自动设置一个。如果不为空,就会放在一个与设置相近,且为页比边的位置。(翻译不好,linux内存管理也不是很懂,暂且这样)。
第二个参数指定了映射了内存空间的大小,这里是在驱动程序中提供了一段外设的寄存器地址,第二个参数是映射的这块地址的大小。
第三个参数和第四个参数读写权限和共享的问题。
第五个参数是打开的文件的设备描述符。
最后一个参数是映射内容的起点,这里外设寄存器正好是从零点开始的,因此直接映射即可。
该函数的返回之是映射完成以后,内核空间数据在进程虚拟空间中的起始地址。有了这个地址,就能够在用户空间对内核空间映射的寄存器地址进行操作了。
6、axidma->mem = open("/dev/mem", O_RDWR | O_SYNC);由于该类目的是实现外设和内存之间的DMA数据传输,因此,需要在内核中分配一段DMA内存,这个函数返回这段DMA内存的文件描述。
7、axidma->ddr_mapSize = DDR_DST_MAP_SIZE; //DMA对应的DMA缓冲区大小设置
8、axidma->ddr_address = mmap(0, axidma->ddr_mapSize, PROT_WRITE, MAP_SHARED, axidma->mem, DDR_DST_BASE_ADDR); 、、通过mmap函数把dma缓冲区映射回到用户空间,注意最后一个参数。DDR_DST_BASE_ADDR,这里不是从零开始的,因为DMA缓冲去设备是一段很大的DMA内存区,该设备只是映射到了其中的一部分。
9、regAddr_t S2MM_DMACR_REG = (regAddr_t)(axidma->access_address + 0x30);
//typedef unsigned int * regAddr_t; //将dma控制器的寄存器地址取出。
/* step 1: Reset */
*S2MM_DMACR_REG = *S2MM_DMACR_REG | S2MM_DMACR_RESET_MASK;
while (*S2MM_DMACR_REG & S2MM_DMACR_RESET_MASK);
复位DMA控制器
/* step 2: Running */
*S2MM_DMACR_REG = *S2MM_DMACR_REG | S2MM_DMACR_RS_MASK;
HAL_setTimeout_us(1000);
do {
if (HAL_hasTimeoutExpired() != 0) {
fprintf(stderr, "Fail to run dma!\n");
munmap(axidma->access_address, axidma->mapSzie);
munmap(axidma->ddr_address, axidma->ddr_mapSize);
close(axidma->axidma);
close(axidma->mem);
free(axidma); /* Release malloc memory */
return BAD_DMA_HANDLE;
}
}while (*(S2MM_DMACR_REG + S2MM_DMASR_OFFSETS2MM_DMASR_HALTED_MASK);
打开DMA控制器
/* step 3: Enable interrupt */
*S2MM_DMACR_REG = *S2MM_DMACR_REG |S2MM_DMACR_IOC_IRQ_ENABLE_MASK;
*S2MM_DMACR_REG = *S2MM_DMACR_REG | S2MM_DMACR_ERR_IRQ_ENABLE_MASK;
打来DMA中断
/* step 4: Setting source address */
*(S2MM_DMACR_REG + S2MM_DA_LSB_OFFSET) = (unsigned int)DDR_DST_BASE_ADDR;
#if __WORDSIZE == 64
*(S2MM_DMACR_REG + S2MM_DA_MSB_OFFSET) = DDR_DST_BASE_ADDR >> 32;
#endif
看到这里是不是好像是在写单片机程序一样简单呢?其实linux驱动就是在用其框架组织了单片机的硬件相关的代码。如此简单不过。但是你会注意到,为什么没有注册中断服务函数呢?这个问题接下来详细的说明。
初始化告一段落,下面分析DMA传输函数。
1、regAddr_t S2MM_DMACR_REG = (regAddr_t)(axidma->access_address + 0x30); //找到DMA控制寄存器
/* Enable UIO interrupt */
2、write(axidma->axidma, &irq_on, sizeof(irq_on));//打开中断线,这里的硬件是soc,前面的使能DMA中断只是DMA控制器完成一次数据传输以后,发出一个中断信号,这里的使能中断是使能CUP中断,让CPU具有接受MDA中断信号的能力。
3、*(S2MM_DMACR_REG + S2MM_LENGTH_OFFSET) = length;//设置数据长度
4、err = read(axidma->axidma, &icount, 4);
5、*rcvBytes = *(S2MM_DMACR_REG + S2MM_LENGTH_OFFSET);//读取数据长度
6、memcpy(dstData, axidma->ddr_address, *rcvBytes);//复制数据
最终完成了数据的传输。