DMA(Direct Memory Access) 即直接存儲器訪問, DMA 傳輸方式無需 CPU 直接控制傳輸,通過硬體為 RAM 、I/O 設備開闢一條直接傳送數據的通路,能使 CPU 的效率大為提高。 學了這麼多驅動,不難推出DMA的編寫套路: 1)註冊DMA中斷,分配緩衝區 2)註冊字元設 ...
DMA(Direct Memory Access)
即直接存儲器訪問, DMA 傳輸方式無需 CPU 直接控制傳輸,通過硬體為 RAM 、I/O 設備開闢一條直接傳送數據的通路,能使 CPU 的效率大為提高。
學了這麼多驅動,不難推出DMA的編寫套路:
- 1)註冊DMA中斷,分配緩衝區
- 2)註冊字元設備,並提供文件操作集合fops
- -> 2.1)file_operations里設置DMA硬體相關操作,來啟動DMA
由於我們是用字元設備的測試方法測試的,而本例子只是用兩個地址之間的拷貝來演示DMA的作用,所以採用字元設備方式編寫
1.驅動編寫之前,先來講如何分配釋放緩衝區、DMA相關寄存器介紹、使用DMA中斷
1.1在linux中,分配釋放DMA緩衝區,只能使用以下幾個函數
1)
/*該函數只禁止cache緩衝,保持寫緩衝區,也就是對註冊的物理區寫入數據,也會更新到對應的虛擬緩存區上*/ void *dma_alloc_writecombine(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 用來為用戶空間頁來分配記憶體; 它可能睡眠.
2)
/*該函數禁止cache緩存以及禁止寫入緩衝區*/ void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); //分配DMA緩存區,返回值和參數和上面的函數一直
3)
dma_free_writecombine(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle); //釋放DMA緩存,與dma_alloc_writecombine()對應 //size:釋放長度 //cpu_addr:虛擬地址, //handle:物理地址
4)
dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle) //釋放DMA緩存,與dma_alloc_coherent ()對應 //size:釋放長度
//cpu_addr:虛擬地址, //handle:物理地址
(PS: dma_free_writecombine()其實就是dma_free_conherent(),只不過是用了#define重命名而已。)
而我們之前用的記憶體分配kmalloc()函數,是不能用在DMA上,因為分配出來的記憶體可能在物理地址上是不連續的.
1.2 那麼2440開發板如何來啟動DMA,先來看2440的DMA寄存器
(PS:實際這些DMA相關的寄存器,在linux內核中三星已封裝好了,可以直接調用,不過非常麻煩,還不如直接設置寄存器,可以參考: http://blog.csdn.net/mirkerson/article/details/6632273)
1.2.1 2440支持4個通道的DMA控制器
其中4個通道的DMA外設請求源,如下圖所示(通過DCONn寄存器的[26:24]來設置)
(PS:如果請求源是系統匯流排上的,就只需要設置DCONn寄存器的[23]=0即可)
1.2.2 且每個通道都可以處理以下4種情況:
1) 源和目標都在系統匯流排上(比如:兩個物理記憶體地址)
2) 當目標在外設匯流排上時,源在系統匯流排上(外設指:串口,定時器,I2C,I2S等)
3) 當目標在系統匯流排上時,源在外設匯流排上
4) 源和目標都在外設匯流排上
1.2.3 DMA有兩種工作模式(通過DCONn寄存器的[28]來設置)
查詢模式:
當DMA請求XnXDREQ為低電平時,則DMA會一直傳輸數據,直到DMA請求拉高,才停止
握手模式:
當DMA請求XnXDREQ有下降沿觸發時,則DMA會傳輸一次數據
1.2.4 DMA有兩種傳輸模式(通過DCONn寄存器的[31]來設置)
單元傳輸:
指傳輸過程中,每執行一次,則讀1次,寫1次.(如上圖所示)
突發4傳輸:
指傳輸過程中,每執行一次,則讀4次,然後寫4次(如下圖所示)
1.2.5 2440中的DMA寄存器如下圖所示:
共有4個通道的寄存器,且每個通道的寄存器內容都一致,所以我們以DMA通道0為例:
1)DISRC0初始源寄存器
[30:0] : 存放DMA源的基地址
2)DISRCC0初始源控制寄存器
[1] : 源位置選擇,0:源在系統匯流排上, 1:源在外設匯流排上
[0] : 源地址選擇,0:傳輸時源地址自動增加, 1:源地址固定
3)DIDST0初始目標寄存器
[30:0] : 設置DMA目的的基地址
4)DIDSTC0初始目標控制寄存器
[2] : 中斷時間選擇, 0:當DMA傳輸計數=0,立即發生中斷 1:執行完自動載入後再發送中斷(也就是計數為0,然後重新載入計數值)
[1] : 目的位置選擇, 0:目的在系統匯流排上, 1:目的在外設匯流排上
[0] : 目的地址選擇, 0:傳輸時目的地址自動增加, 1:目的地址固定
5)DCON0控制寄存器
[31] : 工作模式選擇, 0:查詢模式 1:握手模式 (當源處於外設時,儘量選擇握手模式)
[30] : 中斷請求(DREQ)/中斷回應(DACK)的同步時鐘選擇, 0:PCLK同步 1:HCLK同步
(PS:如果有設備在HCLK上,該位應當設為1,比如:(SDRAM)記憶體數組, 反之當這些設備在PCLK上,應當設為0,比如:ADC,IIS,I2C,UART)
[29] : DMA傳輸計數中斷使能/禁止 0:禁止中斷 1:當傳輸完成後,產生中斷
[28] : 傳輸模式選擇, 0:單元傳輸 1:突發4傳輸
[27] : 傳輸服務模式
0:單服務模式,比如:有2個DMA請求,它們會被順序執行一次(單元傳輸/突發4傳輸)後停止,然後直到有下一次DMA請求,再重新開始另一次迴圈。
1:全服務模式,指該DMA若有請求,則會占用DMA匯流排,一直傳輸,期間若有其它DMA請求,只有等待傳輸計數TC為0,才會執行其它DMA請求
[26:24] : DMA外設請求源選擇
[23] : 軟體/硬體請求源選擇 0:軟體請求 1:硬體請求(還需要設置[26:24]來選擇外設源)
[22] : 重新載入開關選項 為0即可
[21:20] : 傳輸數據大小 為00(8位)即可
[19:0] : 設置DMA傳輸的計數TC
6)DSTAT0狀態寄存器
[21:20] : DMA狀態 00:空閑 01:忙
[19:0] : 傳輸計數當前值CURR_TC 為0表示傳輸結束
7)DCSRC0當前源寄存器
[30:0] : 存放DMA當前的源基地址
8)DCDST0當前目標寄存器
[30:0] : 存放DMA當前的目的基地址
9)DMASKTRIG0觸發屏蔽寄存器
[2] : 停止STOP 該位寫1,立刻停止DMA當前的傳輸
[1] : DMA通道使能 0:關閉DMA的通道0(禁止DMA請求) 1:開啟DMA的通道0(開啟DMA請求)
[0] : 軟體請求觸發 1:表示啟動一次軟體請求DMA,只有DCONn[23]=0和DMASKTRIGn[1]=1才有效,DMA傳輸時,該位自動清0
1.3接下來就開始講linux註冊DMA中斷
首先,DMA的每個通道只能有一個源- >目的,所以輸入命令 cat /proc/interrupts ,找到DMA3中斷未被使用
所以在linux中使用:
request_irq(IRQ_DMA3, s3c_dma_irq, NULL, "s3c_dma", 1);// s3c_dma_irq:中斷服務函數,這裡註冊DMA3中斷服務函數 //NULL:中斷產生類型, 不需要,所以填NULL //1:表示中斷時,傳入中斷函數的參數,本節不需要所以填1,切記不能填0,否則註冊失敗
2.接下來,我們便來寫一個DMA的字元設備驅動
步驟如下:
- 1) 註冊DMA中斷,分配兩個DMA緩衝區(源、目的)
- 2) 註冊字元設備,並提供文件操作集合fops
- -> 2.1) 通過ioctl的cmd來判斷是使用DMA啟動兩個地址之間的拷貝,還是直接兩個地址之間的拷貝
- -> 2.2)若是DMA啟動,則設置DMA的相關硬體,並啟動DMA傳輸
2.1 所以,驅動代碼如下所示:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/irq.h> #include <asm/irq.h> #include <asm/arch/regs-gpio.h> #include <asm/hardware.h> #include <asm/uaccess.h> #include <asm/io.h> #include <linux/dma-mapping.h> #define S3C_DMA_SIZE 512*1024 //DMA傳輸長度 512KB #define NORMAL_COPY 0 //兩個地址之間的正常拷貝 #define DMA_COPY 1 //兩個地址之間的DMA拷貝 /*函數聲明*/ static DECLARE_WAIT_QUEUE_HEAD(s3c_dma_queue); //聲明等待隊列 static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags); /* * 定義中斷事件標誌 * 0:進入等待隊列 1:退出等待隊列 */ static int s3c_dma_even=0; static unsigned char *source_virt; //源虛擬地址 static unsigned int source_phys; //源物理地址 static unsigned char *dest_virt; //目的虛擬地址 static unsigned int dest_phys; //目的虛擬地址 /*DMA3寄存器*/ struct S3c_dma3_regs{ unsigned int disrc3 ; //0x4b0000c0 unsigned int disrcc3 ; unsigned int didst3 ; unsigned int didstc3 ; unsigned int dcon3 ; unsigned int dstat3 ; unsigned int dcsrc3 ; unsigned int dcdst3 ; unsigned int dmasktrig3; //0x4b0000e0 };
static volatile struct S3c_dma3_regs *s3c_dma3_regs; /*字元設備操作*/ static struct file_operations s3c_dma_fops={ .owner = THIS_MODULE, .ioctl = s3c_dma_ioctl, }; /*中斷服務函數*/ static irqreturn_t s3c_dma_irq (int irq, void *dev_id) { s3c_dma_even=1; //退出等待隊列 wake_up_interruptible(&s3c_dma_queue); //喚醒 中斷 return IRQ_HANDLED; } /*ioctl函數*/ static int s3c_dma_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long flags) { int i; memset(source_virt, 0xAA, S3C_DMA_SIZE); memset(dest_virt, 0x55, S3C_DMA_SIZE); switch(cmd) { case NORMAL_COPY: //正常拷貝 for(i=0;i<S3C_DMA_SIZE;i++) dest_virt[i] = source_virt[i]; if(memcmp(dest_virt, source_virt, S3C_DMA_SIZE)==0) { printk("NORMAL_COPY OK\n"); return 0; } else { printk("NORMAL_COPY ERROR\n"); return -EAGAIN; } case DMA_COPY: //DMA拷貝 s3c_dma_even=0; //進入等待隊列 /*設置DMA寄存器,啟動一次DMA傳輸 */ /* 源的物理地址 */ s3c_dma3_regs->disrc3 = source_phys; /* 源位於AHB匯流排, 源地址遞增 */ s3c_dma3_regs->disrcc3 = (0<<1) | (0<<0); /* 目的的物理地址 */ s3c_dma3_regs->didst3 = dest_phys; /* 目的位於AHB匯流排, 目的地址遞增 */ s3c_dma3_regs->didstc3 = (0<<2) | (0<<1) | (0<<0); /* 使能中斷,單個傳輸,軟體觸發, */ s3c_dma3_regs->dcon3=(1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(S3C_DMA_SIZE<<0); //啟動一次DMA傳輸 s3c_dma3_regs->dmasktrig3 = (1<<1) | (1<<0); wait_event_interruptible(s3c_dma_queue, s3c_dma_even); //進入睡眠,等待DMA傳輸中斷到來才退出 if(memcmp(dest_virt, source_virt, S3C_DMA_SIZE)==0) { printk("DMA_COPY OK\n"); return 0; } else { printk("DMA_COPY ERROR\n"); return -EAGAIN; } break; } return 0; } static unsigned int major; static struct class *cls; static int s3c_dma_init(void) { /*1.1 註冊DMA3 中斷 */ if(request_irq(IRQ_DMA3, s3c_dma_irq,NULL, "s3c_dma",1)) { printk("Can't request_irq \"IRQ_DMA3\"!!!\n "); return -EBUSY; } /*1.2 分配兩個DMA緩衝區(源、目的)*/ source_virt=dma_alloc_writecombine(NULL,S3C_DMA_SIZE, &source_phys, GFP_KERNEL); if(source_virt==NULL) { printk("Can't dma_alloc \n "); return -ENOMEM; } dest_virt=dma_alloc_writecombine(NULL,S3C_DMA_SIZE, &dest_phys, GFP_KERNEL); if(dest_virt==NULL) { printk("Can't dma_alloc \n "); return -ENOMEM; } /*2.註冊字元設備,並提供文件操作集合fops*/ major=register_chrdev(0, "s3c_dma",&s3c_dma_fops); cls= class_create(THIS_MODULE, "s3c_dma"); class_device_create(cls, NULL,MKDEV(major,0), NULL, "s3c_dma"); s3c_dma3_regs=ioremap(0x4b0000c0, sizeof(struct S3c_dma3_regs)); return 0; } static void s3c_dma_exit(void) { iounmap(s3c_dma3_regs); class_device_destroy(cls, MKDEV(major,0)); class_destroy(cls); dma_free_writecombine(NULL, S3C_DMA_SIZE, dest_virt, dest_phys); dma_free_writecombine(NULL, S3C_DMA_SIZE, source_virt, source_phys); free_irq(IRQ_DMA3, 1); } module_init(s3c_dma_init); module_exit(s3c_dma_exit); MODULE_LICENSE("GPL");
2.2 應用測試程式如下所示:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <string.h> /* ./dma_test NORMAL * ./dma_test DMA */ #define NORMAL_COPY 0 //兩個地址之間的正常拷貝 #define DMA_COPY 1 //兩個地址之間的DMA拷貝 void print_usage(char *name) { printf("Usage:\n"); printf("%s <NORMAL | DMA>\n", name); } int main(int argc, char **argv) { int fd,i=30; if (argc != 2) { print_usage(argv[0]); return -1; } fd = open("/dev/s3c_dma", O_RDWR); if (fd < 0) { printf("can't open /dev/s3c_dma\n"); return -1; } if (strcmp(argv[1], "NORMAL") == 0) { while (i--) //調用驅動的ioctl(),30次 { ioctl(fd, NORMAL_COPY); } } else if (strcmp(argv[1], "DMA") == 0) { while (i--) //調用驅動的ioctl(),30次 { ioctl(fd, DMA_COPY); } } else { print_usage(argv[0]); return -1; } return 0; }
3.測試運行
輸入 ./dma_test NORMAL & ,使用CPU正常拷貝,可以發現占用了大部分資源,輸入 ls 無反應:
輸入./dma_test DMA & ,使用DMA拷貝,輸入 ls 立馬有反應,從而釋放了CPU的壓力: