學習目的:
- 熟悉Linux下DMA驅動程序編寫
1、DMA基本概念
DMA,全稱Direct Memory Access,即直接儲存器訪問。
它是一種高速的數據傳送操作,可用於芯片的外設和存儲器或存儲器和存儲器或外設和外設之間數據的傳輸。DMA的數據傳送過程不需要CPU干預,是通過DMA控制器完成的,因此在DMA數據傳送過程中不占用CPU的資源。不過在啟動DMA控制器進行數據傳輸前,需要CPU來告訴DMA控制器數據傳輸的源地址、目的地址、數據傳輸長度等信息。
2、硬件相關配置
DMA控制器的配置依賴於硬件平台,這里使用的是S3C2440芯片,它有一個4通道的DMA控制器,每個通道都可以不受限制地在系統總線和/或外圍總線中的設備之間執行數據移動。
2.1 請求模式設置
S3C2440芯片的DMA數據傳輸支持兩者請求模式,一種是硬件請求模式,另一種是軟件請求模式。請求模式可以通過DMA控制寄存器DCONn的第23位來設置,如果設置成0,該通道的DMA工作在軟件請求模式,如果設置成1,則該通道DMA工作在硬件請求模式。
對於工作在硬件請求模式的DMA,各通道的請求源,如下圖所示:

各通道請求源的選擇可以通過DMA控制寄存器DCONn的[26:24]設置,如想設置通道0的DMA傳輸的請求源為UART0,只要設置DCON0[26:24] = 001,這樣通道0的DMA何時進行數據傳送將由UART0外設決定。
對於工作在軟件請求模式的DMA,可以由軟件來觸發該通道的DMA進行數據的傳輸。設置DMASKTRIGn寄存器的SW_TRIG位為1,請求DMA數據傳輸。
2.2 傳輸源、目的地址設置
DMA數據傳輸的源起始地址、目的起始地址分別是在寄存器DISRCn和寄存器DIDSTn里設置的
如我們想使用DMA通道0將0x00000000為起始地址數據傳輸到0x10000000的地址中去,直接將0x00000000寫入到DISRC0源起始地址寄存器,將0x10000000寫入到DIDST0寄存器,這樣就告訴了DMA控制器數據傳輸的起始位置信息
但是DMA控制器不像我們想的那么智能,只告訴數據傳輸地址信息還是不夠的,還要去指定可以訪問源、目的地址的總線信息,以及傳輸過程地址是否增加。這些信息的設置分別是在DISRCCn和DIDSTCn寄存器進行的
DISRCCn [1]:源地址所在的總線 DISRCCn[0]: 傳輸過程源地址是否增加
DIDSTCn [2]:中斷發生時間 DIDSTCn [1]:目的地址所在的總線 DIDSTCn[0]: 傳輸過程目的地址是否增加
2.3 傳輸長度設置
我們知道了源、目的地址如何設置的,這在傳輸數據過程還是不夠的,還需要知道傳輸數據的長度。傳輸數據的長度是通過DCONn寄存器的TSZ 、DSZ 、TC位一起間接確定的,傳輸數據長度 = TSZ * DSZ * TC
DCONn寄存器的TSZ位[28]用來選擇DMA數據傳輸的方式,0表示單元傳輸,1表示突發傳輸
1)單元傳輸
指傳輸過程中,每執行一次,則讀1次,寫1次
2)突發傳輸
指傳輸過程中,每執行一次,則讀4次,然后寫4次

DCONn寄存器的DSZ位[21:20]決定的是每次傳輸的數據位寬 00=Byte 01=Half Word 10=Word
DCONn寄存器的TC位[19:0]表示傳輸次數
假設我們要傳輸1024字節數據,我們多種選擇方式,如選擇單元傳輸,傳輸數據位寬是1個字節,那么傳輸次數TC就是1024,也可以選單元傳輸,傳輸數組位寬是4字節,那么傳輸次數TC就是256。
我們只需保證TSZ * DSZ * TC求出結果等於我們要傳輸數據長度即可
3、內核DMA緩沖區分配函數
對於DMA傳輸使用的緩沖區,需要使用物理地址連續的內存,而且對物理地址和虛擬地址的操作都應該直接改變緩沖區的內容。所以如果DMA使用cache,那么一定要考慮cache的一致性。解決DMA導致的一致性的方法最簡單的是禁止DMA目標地址范圍內的cache功能,因此我們不能直接使用kzmalloc直接分配
(注:經過DMA操作,cache緩存對應的內存數據已經被修改了,而CPU本身不知道(DMA傳輸是不通過CPU的),它仍然認為cache中的數 據就是內存中的數據,以后訪問Cache映射的內存時,它仍然使用舊的Cache數據。這樣就發生Cache與內存的數據“不一致性”錯誤。)
內核中提供了一些專門用於DMA使用內存的分配和釋放的函數,在這里做個簡單介紹
1)DMA緩沖區分配
void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
dma_alloc_writecombine函數分配的內存禁止了C(cache)域,不會使用緩存,但會使用寫緩沖區,函數參數如下:
- dev:平台初始化里指定,主要用到dma_mask之類參數,不使用可設置成NULL
- size:申請分配內存大小
- handle:返回申請到的物理內存起始地址
- gfp:分配出內存的參數
函數返回值是分配內存起始地址的虛擬地址
2)DMA緩沖區釋放
#define dma_free_writecombine(dev,size,cpu_addr,handle) \ dma_free_coherent(dev,size,cpu_addr,handle)
dma_free_writecombine函數用來釋放分配的DAM內存,函數參數如下:
- dev:平台初始化里指定,主要用到dma_mask之類參數,不使用可設置成NULL
- size:釋放內存大小
- cpu_addr:釋放內存虛擬地址
- handle:釋放內存物理地址
4、DMA驅動編寫
編寫一個字符驅動程序,程序中分配兩個固定大小的緩沖區,分別支持普通數據傳輸和DMA數據傳輸。然后,編寫應用程序分別使用這兩種傳輸方式進行數據傳輸,來進一步感受DMA數據傳輸的優勢
注冊字符設備的file_operations結構體中實現了.open、.unlocked_ioctl、.release成員
static struct file_operations fops = { .owner = THIS_MODULE, .open = dma_drv_open, .unlocked_ioctl = dma_drv_ioctl, .release = dma_drv_close, };
dma_drv_open函數
static int dma_drv_open(struct inode *inode, struct file *file) { if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", NULL))-------------->s3c_dam_irq是DMA數據傳輸完成后的中斷處理函數 { printk("can't request_irq for DMA\n"); return -EBUSY; } src_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &src_phy_addr, GFP_KERNEL); if(src_addr == NULL) { free_irq(IRQ_DMA3, NULL); printk("can't alloc buffer for src\n"); return -ENOMEM; } dst_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &dst_phy_addr, GFP_KERNEL); if(dst_addr == NULL) { free_irq(IRQ_DMA3, 1); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); printk("can't alloc buffer for dst\n"); return -ENOMEM; } dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs)); return 0; }
dma_drv_open函數在使用open打開設備節點時被調用,open函數中分配了用於DMA傳輸的內存,注冊了DMA數據傳輸完成后中斷處理函數,完成了S3C2440的DMA控制器的寄存器物理地址到虛擬地址映射
dma_drv_ioctl函數
static long dma_drv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { unsigned int n; memset(src_addr, 0x55, COPY_BUFF_SIZE); memset(dst_addr, 0, COPY_BUFF_SIZE); switch(cmd) { case COPY_NO_DMA: for(n = 0; n < COPY_BUFF_SIZE; n++) dst_addr[n] = src_addr[n]; if(memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_NO_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; case COPY_BY_DMA: trans_ok = 0; /* 把源,目的,長度告訴DMA */ dma_regs->disrc = src_phy_addr; /* 源的物理地址 */ dma_regs->disrcc = (0<<1) | (0<<0); /* 源位於AHB總線, 源地址遞增 */ dma_regs->didst = dst_phy_addr; /* 目的的物理地址 */ dma_regs->didstc = (0<<2) | (0<<1) | (0<<0); /* 目的位於AHB總線, 目的地址遞增 */ dma_regs->dcon = (1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(COPY_BUFF_SIZE<<0); /* 使能中斷,單個傳輸,軟件觸發, */ /* 啟動DMA */ dma_regs->dmasktrig = (1<<1) | (1<<0); wait_event_interruptible(dma_waitq, trans_ok); if (memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; default: break; } return 0; }
dma_drc_ioctl函數在應用程序使用ioctl操作打開設備文件描述符時被調用,這個函數實現了驅動程序的核心功能。該函數支持應用程序傳入兩種類型命令,一種是使用普通傳輸模式進行數據傳送,另一種是使用DMA傳輸模式進行數據傳送
cmd參數:
COPY_NO_DMA:普通數據傳輸方式
COPY_BY_DMA:使用DMA控制器進行數據傳送
對於DMA傳輸模式在啟動DMA控制器進行傳輸后,應用程序進入休眠狀態,直到DMA控制器將數據傳輸完畢后觸發相關中斷程序,在中斷程序中被喚醒。喚醒之后來比較源地址和目的地址內容,如果內容一致傳輸成功,打印提示語句
dma_drv_close函數
static int dma_drv_close(struct inode *inode, struct file *file) { printk("dma_drv_close...\n"); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); dma_free_writecombine(NULL, COPY_BUFF_SIZE, dst_addr, dst_phy_addr); free_irq(IRQ_DMA3, NULL); iounmap(dma_regs); return 0; }
dma_drv_close在close文件描述符時被調用,函數的功能和dma_drv_open函數相反,釋放申請的源、目的緩存區,釋放中斷,取消DMA寄存器物理地址到虛擬地址的映射
5、小結
編寫了一個簡單的DMA驅動程序,同時也對S3C2440的DMA控制器的設置做了簡單介紹。通過對驅動程序進行測試,可以體會到DMA控制器在數據傳送過程中的優勢所在。另外,對於DMA硬件相關的操作,在linux內核中三星已封裝好了,在編寫實際的驅動程序中可以嘗試着區直接使用
完成驅動程序
#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/uaccess.h> #include <asm/irq.h> #include <asm/io.h> #include <plat/gpio-fns.h> #include <mach/gpio-nrs.h> #include <linux/interrupt.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/device.h> #include <linux/gpio.h> #include <linux/dma-mapping.h> #define COPY_NO_DMA 0 #define COPY_BY_DMA 1 #define COPY_BUFF_SIZE (1024*512) #define DMA0_BASE_ADDR 0x4B000000 #define DMA1_BASE_ADDR 0x4B000040 #define DMA2_BASE_ADDR 0x4B000080 #define DMA3_BASE_ADDR 0x4B0000C0 struct s3c_dma_regs { unsigned long disrc; unsigned long disrcc; unsigned long didst; unsigned long didstc; unsigned long dcon; unsigned long dstat; unsigned long dcsrc; unsigned long dcdst; unsigned long dmasktrig; }; static int major = 0; static int trans_ok = 0; static char *src_addr; static dma_addr_t *src_phy_addr; static char *dst_addr; static dma_addr_t *dst_phy_addr; static volatile struct s3c_dma_regs *dma_regs; static DECLARE_WAIT_QUEUE_HEAD(dma_waitq); static struct class *cls; static struct class_device *cls_dev; static irqreturn_t s3c_dma_irq(int irq, void *dev_id) { trans_ok = 1; wake_up_interruptible(&dma_waitq); return IRQ_HANDLED; } static int dma_drv_open(struct inode *inode, struct file *file) { if (request_irq(IRQ_DMA3, s3c_dma_irq, 0, "s3c_dma", NULL)) { printk("can't request_irq for DMA\n"); return -EBUSY; } src_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &src_phy_addr, GFP_KERNEL); if(src_addr == NULL) { free_irq(IRQ_DMA3, NULL); printk("can't alloc buffer for src\n"); return -ENOMEM; } dst_addr = (char *)dma_alloc_writecombine(NULL, COPY_BUFF_SIZE, &dst_phy_addr, GFP_KERNEL); if(dst_addr == NULL) { free_irq(IRQ_DMA3, 1); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); printk("can't alloc buffer for dst\n"); return -ENOMEM; } dma_regs = ioremap(DMA3_BASE_ADDR, sizeof(struct s3c_dma_regs)); return 0; } static long dma_drv_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { unsigned int n; memset(src_addr, 0x55, COPY_BUFF_SIZE); memset(dst_addr, 0, COPY_BUFF_SIZE); switch(cmd) { case COPY_NO_DMA: for(n = 0; n < COPY_BUFF_SIZE; n++) dst_addr[n] = src_addr[n]; if(memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_NO_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; case COPY_BY_DMA: trans_ok = 0; /* 把源,目的,長度告訴DMA */ dma_regs->disrc = src_phy_addr; /* 源的物理地址 */ dma_regs->disrcc = (0<<1) | (0<<0); /* 源位於AHB總線, 源地址遞增 */ dma_regs->didst = dst_phy_addr; /* 目的的物理地址 */ dma_regs->didstc = (0<<2) | (0<<1) | (0<<0); /* 目的位於AHB總線, 目的地址遞增 */ dma_regs->dcon = (1<<30)|(1<<29)|(0<<28)|(1<<27)|(0<<23)|(0<<20)|(COPY_BUFF_SIZE<<0); /* 使能中斷,單個傳輸,軟件觸發, */ /* 啟動DMA */ dma_regs->dmasktrig = (1<<1) | (1<<0); wait_event_interruptible(dma_waitq, trans_ok); if (memcmp(src_addr, dst_addr, COPY_BUFF_SIZE) == 0) { printk("MEM_CPY_DMA OK\n"); } else { printk("MEM_CPY_DMA ERROR\n"); } break; default: break; } return 0; } static int dma_drv_close(struct inode *inode, struct file *file) { printk("dma_drv_close...\n"); dma_free_writecombine(NULL, COPY_BUFF_SIZE, src_addr, src_phy_addr); dma_free_writecombine(NULL, COPY_BUFF_SIZE, dst_addr, dst_phy_addr); free_irq(IRQ_DMA3, NULL); iounmap(dma_regs); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = dma_drv_open, .unlocked_ioctl = dma_drv_ioctl, .release = dma_drv_close, }; static int __init dma_drv_init(void) { major = register_chrdev(0, "dma_dev", &fops); cls = class_create(THIS_MODULE, "dma_cls"); cls_dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "dma_drv"); return 0; } static void __exit dma_drv_exit(void) { unregister_chrdev(major, "dma_dev"); device_unregister(cls_dev); class_destroy(cls); } module_init(dma_drv_init); module_exit(dma_drv_exit); MODULE_LICENSE("GPL");
完整測試程序
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #define COPY_NO_DMA 0 #define COPY_BY_DMA 1 #define IOCTL_CNT (100*10000) void print_usage(char *name) { printf("Usage: %s <no_dma | dma>\n", name); } int main(int argc, char **argv) { int fd; int n, cmd; if(argc != 2) { print_usage(argv[0]); return -1; } fd = open("/dev/dma_drv", O_RDWR); if(fd < 0) { printf("can't open\n"); return -1; } if(strcmp(argv[1], "no_dma") == 0) { cmd = COPY_NO_DMA; } else if(strcmp(argv[1], "dma") == 0) { cmd = COPY_BY_DMA; } else { print_usage(argv[0]); return -1; } while(n < IOCTL_CNT) { ioctl(fd, cmd); n++; } close(fd); return 0; }
