1.DMA的宏观理论:
DMA,即Direct Memory Access,直接内存访问.主要是考虑到RAM和外设之间拷贝大量数据时提升性能的一种硬件策略.数据交互,需要一个数据的源地址和目的地址,类似memcpy()函数.DMA也不能例外,它也必须提供源、目的寄存器,只不过其内部是靠硬件实现的从而达到更高效的数据交互目的而已.这里可以假想DMA是一种数据传输过程中的"桥梁",DMA的源寄存器存放的就是我们要拷贝的数据的开始位置地址,DMA的目的寄存器存放的我们要拷贝数据的目的地址.这样硬件DMA就帮助我们高效完成了数据的传输.
下面是DMA操作相关的伪代码:
static void DmaCpy(char *src,char *dest)
{
DmaSrcReg = src;
DmaDestReg = dest;
CfgDmaRegs();
}
比如,我们通过DMA来播放音频文档,其中音频数据源存放在数组ArrayWav,我们要实现播放就需要把这音频数组里面的音频数据丢到IISFIFO里面去.如下:
static void PlayWav(char *wav)
{
... ...;
DmaCpy(wav,iisfiforegs);
... ...;
}
由于DMA是硬件实现的,而且不占用CPU资源,因此,其效率是很高的.一般CPU都提供了几路DMA通道,例如S3C2440就提供了四路DMA Channel.
2.内核中DMA相关:
DMA属于系统级别的资源,它只是提供一种设备I/O和RAM的硬件数据交互的手段,这里需求注意的是,它不属于类似字符设备、块设备、网络设备这样的外设.这样的外设一般都有一定的功能暴露给用户空间去操作,而DMA都是在默默无闻在内核空间协助完成外设到RAM的数据交互.它相当于CPU的一个助手,它是因为CPU本身的性能而存在,而不是因为用户的功能而存在的.
也就是说,DMA本身不会有像网卡、串口一样有"常规驱动",它的存在,只是给需要用到DMA传输数据的外设提供了一种更高效的手段而已.当然,具体的外设驱动可以选择不用DMA的.
2-1.DMA和Cache的一致性:
如果驱动编程中需要用到DMA进行数据交互的话,必须注意DMA和Cache的一致性问题.Cache,即CPU内置的缓存,其功能类似外设RAM,但是Cache被CPU访问更快,它缓存的是最近CPU访问过的指令或数据.DMA传输数据的过程中CPU是不知道的,如果DMA的目的地址和Cache有重叠的时候,Cache里面的数据内容发生变化了,而CPU并不知道,它还是把Cache里面的数据当作RAM中的数据.这样一来,同样一个数据,可能即存在于Cache中,也可能存在于主存RAM中.如果二者并不形成同步逻辑的话,将会引发驱动无法使用.
2-2.DMA缓冲:
内存中与外设交互数据的一块区域叫做"DMA缓冲区",即具有DMA能力的内存区域.比如ISA设备,其最前16M的内存区域是具有DMA能力的.DMA缓冲区是内核中实现DMA数据传输有着重要的地位--无论是通过DMA发数据还是通过DMA收数据,DMA缓冲都是必须的"中转环节".
2-2-1.通过DMA往外发数据的流程:
我们需要通过DMA往外传输数据的时候,大体流程如下:
申请DMA缓冲区
-->
把目标数据(可以是用户空间的数据通过mmap映射下来的)存放到DMA缓冲区
-->
把目标DMA缓冲区的地址转换为设备在总线上的地址
-->
把设备的总线地址告诉DMA,并开启DMA控制器使能.DMA控制器便开始自动传输
-->
DMA传输完毕后,发出一个中断告诉CPU.
2-2-2.通过DMA接收数据的流程:
设备到主控的数据通过DMA传输过程(同步):
1. 当一个进程调用 read, 驱动方法分配一个 DMA 缓冲并引导硬件来传输它的数据到那个缓冲. 这个进程被置为睡眠.
2. 硬件写数据到这个 DMA 缓冲并且在它完成时引发一个中断.
3. 中断处理获得输入数据, 确认中断, 并且唤醒进程, 它现在可以读数据了.
此机制只用到了单一的DMA缓冲.
设备到主控的数据通过DMA传输过程(异步):
1. 硬件引发一个中断来宣告新数据已经到达.
2. 中断处理分配一个DMA缓冲并且告知硬件在哪里传输数据.
3. 外设写数据到缓冲并且引发另一个中断当完成时.
4. 处理者分派新数据, 唤醒任何相关的进程, 并且负责杂务.
此机制用到了DMA缓冲队列.
例如网卡常常期望见到一个在内存中和处理器共享的环形缓冲(常常被称为一个 DMA 的缓冲); 每个到来的报文被放置在环中下一个可用的缓冲, 并且发出一个中断. 驱动接着传递网络本文到内核其他部分并且在环中放置一个新 DMA 缓冲.
在所有这些情况中的处理的步骤都强调,有效的 DMA 处理依赖中断报告.虽然可能实现 DMA 使用一个轮询驱动,它不可能有意义,因为一个轮询驱动可能浪费 DMA 提供的性能益处超过更容易的处理器驱动的I/O.
2-3.分配DMA缓存:
当我们在驱动中需要用到DMA来实现DMA数据的传输时,可以通过函数kmalloc()或get_free_pages()并设置GFP_DMA标志来获取一块具有DMA能力的内存缓冲区.更可读更明了的API如下:
static unsigned long dma_mem_alloc(int size)
{
int order = get_order(size);
return __get_dma_pages(GFP_KERNEL, order);
}
[附:]有时候我们获取比较大的内存区域(如几MB)或者比较小的内存区域(如远小于128KB)时,大的容易导致内存获取失败,小的容易产生内存碎片.因此,当内核无法返回请求数量的内存或者当你需要多于 128 KB时,一个替代返回 -ENOMEM 的做法是在启动时分配内存或者保留物理 RAM 的顶部给你的缓冲. 保留 RAM 的顶部是通过在启动时传递一个 mem= 参数给内核实现的. 例如, 如果你有 256 MB, 参数 mem=255M 使内核不使用顶部的1MByte. 你的模块可能后来使用下列代码来获得对这个内存的存取:
dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);
基于DMA的硬件使用的是总线地址.而我们申请的DMA缓存地址是内核需要的虚拟地址.因此,需要借助下面两个API完成两者的转换:
unsigned long virt_to_bus(void *address)
void *bus_to_virt(unsigned long address)
2-4.DMA映射:
我们通过上述可以知道,使用DMA来传输数据包括下面几个方面:考虑Cache的一致性、分配DMA缓冲区、地址转换.其中,这些步骤均可由DAM映射来完成.
DMA映射的的主要工作是:
1).分配一块DMA缓冲;
2).为此DMA缓冲产生设备可访问的地址;
3).保证此DMA缓冲Cache的一致性.
DMA映射方式主要有三种:一致性DMA映射、流DMA映射、SGDMA映射.
2-4-1.一致性DMA映射:
分配一个DMA一致性的内存区域:
void *dma_alloc_coherent(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp);
返回的就是申请到的DMA缓冲区的虚拟地址;参数handle返回的是DMA缓冲区的总线地址.此外,此函数还保证了该DMA缓冲区Cache的一致性.
释放一个DMA一致性的内存区域:
void dma_free_coherent(struct device *dev,size_t size,void *cpu_addr,dma_addr_t handle);
2-4-2.流DMA映射:
流DMA映射,处理对象为"单个已经分配好的缓冲(比如uboot传递的时候,256M内存传递mem = 250MB,保留出最高端的5M通过ioremap来使用这块预留内存缓冲)".使用下面的函数来建立流DMA映射:
dma_addr_t dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction);
如果成功,返回的是总线地址,否则返回NULL.第二个参数即为已经分配好的缓冲,第三个参数为缓冲大小,第四个参数为方向,可以是DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL和DMA_NONE.
解除流DMA映射:
void dma_unmap_single(struct device *dev,dma_addr_t dma_addr,size_t size,enum dma_data_direction direction);
流式DMA映射可能存在竞争的情况,可以通过下面的函数获取DMA缓冲区的拥有权:
void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr,size_t size,enum dma_data_direction direction);
访问完DMA缓冲区后,应该将其返给设备:
void dma_sync_single_for_device(stryct devuce *devmdna_handle_t bus_addr,size_t size,enum dma_data_direction direction);
2-4-3.SGDMA映射:
SGDMA映射属于流式DMA,只不过流式DMA是单个缓冲,而SGDMA是队列缓冲.如果一个设备需要较大的DMA缓冲区并支持SG模式的情况下,申请多个不连续的、相对较小的DMA缓冲区通常是防止申请太大的连续的物理空间的一种有效的策略.
建立SGDMA映射通过下面的API:
int dma_map_sg(struct device *dev,struct scatterlist *sg,int nets,enum dma_data_direction direction);
参数nets为散列表(scatterlist)入口的数量,参数sg为DMA缓冲队列的头,其组织最基本的单元为scatterlist,第4个参数为方向.函数返回值为DMA缓冲区的数据,它可能小于nets.既然有了DMA缓冲区,那么,我们需要的总线地址呢?我们知道,通过上述的API产生的DMA缓冲被组织在sg参数里面,其类似是scatterlist,里面就包含了DMA数据传输相关的所有信息,当然,总线地址也包含其中.如下:
struct scatterlist {
unsigned long page_link;
unsigned int offset; /* buffer offset */
dma_addr_t dma_address; /* dma address */
unsigned int length; /* length */
};
其中,成员dma_address就是总线地址.成员length标记了scatterlist对应的缓冲区的长度.可以通过下面两个内核API返回这两成员:
dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);
释放SGDMA映射:
void dma_unmap_sg(struct device *dev,struct scatterlit *list,int nents,enum dma_data_direction direction);
SGDMA也属于流DMA,存在着竞争的情况,因此在访问之前,需要先获取相应的DMA缓冲区的所有权:
void dma_sync_sg_for_cpu(struct device *dev,struct scatterlist *sg,int nents,enum dma_data_direction direction);
访问结束后,通过下列函数将所有权返回设备:
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);
设备驱动要使用DMA,先向系统申请DMA通道,通过下面API实现:
int request_dma(unsigned int dmanr,const char *device_id);
参数dmanr是DMA通道编号,参数device_id往往传入设备结构体struct device.
使用完DMA通道后,通过下面函数释放该通道:
void free_dma(unsigned int dmanr);
因此,在LINUX设备驱动中设备要使用DMA来传输数据的时候,流程如下:
request_dma()并初始化DMA控制器;
-->
申请DMA缓冲区;
-->
进行DMA传输;
-->
若全能了对应的中断,进行DMA传输后的中断处理;
-->
释放DMA缓冲区
-->
free_dma()
下面以DMA控制器8237为例看DMAC(DMA控制器端)的操作:
/* 8237 DMA 控制器 */
#define IO_DMA1_BASE 0x00 /* 8 位从 DMA,0~3 通道 */
#define IO_DMA2_BASE 0xC0 /* 16 位主 DMA,4~7 通道 */
/* DMA 控制器 1 控制寄存器 */
#define DMA1_CMD_REG 0x08 /* 命令寄存器 (w) */
#define DMA1_STAT_REG 0x08 /* 状态寄存器 (r) */
#define DMA1_REQ_REG 0x09 /* 请求寄存器 (w) */
#define DMA1_MASK_REG 0x0A /* 单通道屏蔽 (w) */
#define DMA1_MODE_REG 0x0B /* 模式寄存器 (w) */
#define DMA1_CLEAR_FF_REG 0x0C /* 清除 flip-flop (w) */
#define DMA1_TEMP_REG 0x0D /* 临时寄存器 (r) */
#define DMA1_RESET_REG 0x0D /* 主清除 (w) */
#define DMA1_CLR_MASK_REG 0x0E /* 清除屏蔽 */
#define DMA1_MASK_ALL_REG 0x0F /* 所有通道屏蔽 */
/* DMA 控制器 2 控制寄存器 */
#define DMA2_CMD_REG 0xD0 /* 命令寄存器 (w) */
... /*类似于 DMA 控制器 1*/
/* DMA0~7 通道地址寄存器 */
#define DMA_ADDR_0 0x00
...
/* DMA0~7 通道计数寄存器 */
#define DMA_CNT_0 0x01
...
/* DMA0~7 通道页寄存器 */
#define DMA_PAGE_0 0x87
...
#define DMA_MODE_READ 0x44
#define DMA_MODE_WRITE 0x48
...
/*使能 DMA 通道*/
static _ _inline_ _ void enable_dma(unsigned int dmanr)
{
if (dmanr <= 3)
dma_outb(dmanr, DMA1_MASK_REG);
else
dma_outb(dmanr &3, DMA2_MASK_REG);
}
/*禁止 DMA 通道*/
static _ _inline_ _ void disable_dma(unsigned int dmanr)
{
if (dmanr <= 3)
dma_outb(dmanr | 4, DMA1_MASK_REG);
else
dma_outb((dmanr &3) | 4, DMA2_MASK_REG);
}
/* 设置传输尺寸(通道 0~3 最大 64KB, 通道 5~7 最大 128KB */
static _ _inline_ _ void set_dma_count(unsigned int dmanr, unsigned int count)
{
count--;
if (dmanr <= 3)
{
dma_outb(count &0xff, ((dmanr &3) << 1) + 1+IO_DMA1_BASE);
dma_outb((count >> 8) &0xff, ((dmanr &3) << 1) + 1+IO_DMA1_BASE);
}
else
{
dma_outb((count >> 1) &0xff, ((dmanr &3) << 2) + 2+IO_DMA2_BASE);
dma_outb((count >> 9) &0xff, ((dmanr &3) << 2) + 2+IO_DMA2_BASE);
}
}
/* 设置传输地址和页位 */
static _ _inline_ _ void set_dma_addr(unsigned int dmanr, unsigned int a)
{
set_dma_page(dmanr, a >> 16);
if (dmanr <= 3)
{
dma_outb(a &0xff, ((dmanr &3) << 1) + IO_DMA1_BASE);
dma_outb((a >> 8) &0xff, ((dmanr &3) << 1) + IO_DMA1_BASE);
}
else
{
dma_outb((a >> 1) &0xff, ((dmanr &3) << 2) + IO_DMA2_BASE);
dma_outb((a >> 9) &0xff, ((dmanr &3) << 2) + IO_DMA2_BASE);
}
}
/* 设置页寄存器位,当已知 DMA 当前地址寄存器的低 16 位时,连续传输 */
static _ _inline_ _ void set_dma_page(unsigned int dmanr, char pagenr)
{
switch (dmanr)
{
case 0:
dma_outb(pagenr, DMA_PAGE_0);
break;
case 1:
dma_outb(pagenr, DMA_PAGE_1);
break;
case 2:
dma_outb(pagenr, DMA_PAGE_2);
break;
case 3:
dma_outb(pagenr, DMA_PAGE_3);
break;
case 5:
dma_outb(pagenr &0xfe, DMA_PAGE_5);
break;
case 6:
dma_outb(pagenr &0xfe, DMA_PAGE_6);
break;
case 7:
dma_outb(pagenr &0xfe, DMA_PAGE_7);
break;
}
}
/*清除 DMA flip-flop*/
static _ _inline_ _ void clear_dma_ff(unsigned int dmanr)
{
if (dmanr <= 3)
dma_outb(0, DMA1_CLEAR_FF_REG);
else
dma_outb(0, DMA2_CLEAR_FF_REG);
}
/*设置某通道的 DMA 模式*/
static _ _inline_ _ void set_dma_mode(unsigned int dmanr, char mode)
{
if (dmanr <= 3)
dma_outb(mode | dmanr, DMA1_MODE_REG);
else
dma_outb(mode | (dmanr &3), DMA2_MODE_REG);
}
可以看到,DMAC端主要是对直接对相碰寄存器的赋值,只不过需要一些特定的内核函数(如dma_outb())完成地址的转换.
5.一个利用DMAC的外设:
一个外设如果要用到DMA来实现数据的传输,无非涉及到两个方面:外设通过DMA到RAM和RAM通过DMA到外设.从硬件上讲,当RAM通过DMA到外设传输数据时,把数据的大小长度赋值(这个值由于是我们要发送给外设的,当然是可知的)给DMAC的一个计数寄存器,当配置DMAC计数寄存器为0时中断的话,即数据传输长度计数为0时触发中断;当外设通过DMA传输数据到RAM时,这时候我们并不知道外设过来的数据长度,此需要第三方策略(具体可google/百度搜索"DMA 接收数据").
假设设备 xxx 使用了 DMA,DMA 相关的信息应该被添加到设备结构体内.在模块加载函数或打开函数中,DMA 通道和中断应该被申请,而 DMA 本身也应被初始化.以设备xxx使用上述的DMAC 8237全例.示例代码如下:
/*xxx 设备结构体*/
typedef struct
{
...
void *dma_buffer; //DMA 缓冲区
/*当前 DMA 的相关信息*/
struct
{
unsigned int direction; //方向
unsigned int length; //尺寸
void *target; //目标
unsigned long start_time; //开始时间
}current_dma;
unsigned char dma;//DMA 通道
}xxx_device;
在这里具体的设备xxx_device需要用到DMA传输数据应该封装一些具有DAM信息成员结构体.
static int xxx_open(...)
{
...
/*安装中断服务程序 */
if ((retval = request_irq(dev->irq, &xxx_interrupt, 0, dev->name,dev)))
{
printk(KERN_ERR "%s: could not allocate IRQ%d\n", dev->name,dev->irq);
return retval;
}
/*申请 DMA*/
if ((retval = request_dma(dev->dma, dev->name)))
{
free_irq(dev->irq, dev);
printk(KERN_ERR "%s: could not allocate DMA%d channel\n", ...);
return retval;
}
/*申请 DMA 缓冲区*/
dev->dma_buffer = (void *) dma_mem_alloc(DMA_BUFFER_SIZE);
if (!dev->dma_buffer)
{
printk(KERN_ERR "%s:could not allocate DMA buffer\n",dev->name);
free_dma(dev->dma);
free_irq(dev->irq, dev);
return -ENOMEM;
}
/*初始化 DMA*/
init_dma();
...
}
在设备xxx_device打开的时候,我们应该申请好DMA缓冲区和硬件DMA通道,并且安装xxx_device中断.
/*内存到外设*/
static int mem_to_xxx(const byte *buf,int len)
{
...
dev->current_dma.direction = 1; /*DMA 方向*/
dev->current_dma.start_time = jiffies; /*记录 DMA 开始时间*/
memcpy(dev->dma_buffer, buf, len); /*复制要发送的数据到 DMA 缓冲区*/
target = isa_virt_to_bus(dev->dma_buffer);/*假设 xxx 挂接在 ISA 总线*/
/*进行一次 DMA 写操作*/
flags=claim_dma_lock();
disable_dma(dev->dma); /*禁止 DMA*/
clear_dma_ff(dev->dma); /*清除 DMA flip-flop*/
set_dma_mode(dev->dma, 0x48);
set_dma_addr(dev->dma, target); /*设置 DMA 地址*/
set_dma_count(dev->dma, len); /*设置 DMA 长度*/
outb_control(dev->x_ctrl | DMAE | TCEN, dev);/*让设备接收 DMA*/
enable_dma(dev->dma);
release_dma_lock(flags);
printk(KERN_DEBUG "%s: DMA transfer started\n", dev->name);
...
}
可以看到,内存到外设的过程中,涉及到了虚拟地址到总线地址的转换,并且内存到外设数据长度是可知的,直接赋值给DMAC的计数寄存器即可,所以,当DMAC的计数寄存器为0时,便触发了DMAC中断.
/*外设到内存*/
static void xxx_to_mem(const char *buf, int len,char * target)
{
...
/*记录 DMA 信息*/
dev->current_dma.target = target;
dev->current_dma.direction = 0;
dev->current_dma.start_time = jiffies;
dev->current_dma.length = len;
/*进行一次 DMA 读操作*/
outb_control(dev->x_ctrl | DIR | TCEN | DMAE, dev);
flags = claim_dma_lock();
disable_dma(dev->dma);
clear_dma_ff(dev->dma);
set_dma_mode(dev->dma, 0x04); /* I/O->mem */
set_dma_addr(dev->dma, isa_virt_to_bus(target));
set_dma_count(dev->dma, len);
enable_dma(dev->dma);
release_dma_lock(flags);
...
}
从外设通过DMA到RAM时,如果外设的数据长度是固定的,我们当然可以直接写进DMAC的计数寄存器,这样当接收到预期的数据长度时,便触发DMA中断.如CCIC外接SENSOR,一般SENSOR以YUV格式输出一帧图像时,大小为640X480X2个字节.笔者曾处理过JZ4775平台的CCIC外接SENSOR,由于某DATA脚虚焊导致了DMAC收不到足够的数据而无法引发中断.如果接收到的数据大小不确定时,就得想第三方策略了.
/*设备中断处理*/
static irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *reg_ptr)
{
...
do
{
/* DMA 传输完成?*/
if (int_type==DMA_DONE)
{
outb_control(dev->x_ctrl &~(DMAE | TCEN | DIR), dev);
if (dev->current_dma.direction)
{
/*内存->I/O*/
...
}
else
/*I/O->内存*/
{
memcpy(dev->current_dma.target, dev->dma_buffer, dev->current_dma.length);
}
}
else if(int_type==RECV_DATA) /*收到数据*/
{
xxx_to_mem(...);/*通过 DMA 读接收到的数据到内存*/
}
...
}
...
}
这里应该说是DAMC中断而不是xxx_device中断更准确一些?