内核中与驱动相关的内存操作之十七(DMA)

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);

3.设备使用DMA传输数据的流程:

    设备驱动要使用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()
  
4.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中断更准确一些?


你可能感兴趣的:(Ldd)