對linux用戶空間DMA的分析(和單片機一樣簡單)

一般情況下,對外設的操作包括輪訓方式、中斷方式,對於數據量很大的情況會用到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);//複製數據

最終完成了數據的傳輸。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章