Linux驱动开发基础之DMA

文章目录


前言

提示:这里可以添加本文要记录的大概内容:

本文对于DMA在Linux驱动中的相关运用进行了简单的介绍,并且也提到了一些DMA的相关概念以函数。对于DMA的传输过程进行了详细的介绍,并通过相关的代码进行了实现。

下文所使用的硬件设备是基于ARM Cortex-A9处理器的Exynos-tiny4412开发板所实现的,所使用的内核版本为Linux-3.5


提示:以下是本篇文章正文内容,下面案例可供参考

一、DMA是什么?

1. 什么是DMA
DMA(Direct Memory Access,直接内存访问)
        既然叫 直接内存访问,那么相对应地,应该就有 “间接的内存访问”。间接的内存访问,我的理解是,就是指最常见的,我们利用CPU的指令,去从一个内存地址中读出数据,然后写到另外一个内存地址中,完成对应的赋值操作。此过程,完全都是CPU去操作的,如果是单个这样的数据读取和写入,还没啥,但是如果数据量很大,比如我们用memcpy(addr1,addr2,1024)去从地址addr1地址开始,拷贝1024个字节到内存addr2处,那么CPU这段时间,就不要干别的事情了,就一直这么的给你读取,写入数据吧,另外的还有,常见于驱动中的,尤其是涉及到和外设打交道,我们让CPU从内存一个地址,读取了一个数据,然后写入到某个设备的FIFO或者DATA寄存器中,咋写入之前,常常会等待FIFO不是满的,然后才能写入数据,要从FIFO中读取数据,要等到FIFO不是空,才能读取,这样来来回回,会比较消耗资源。
        鉴于此,才出现了DMA这个硬件设备,专门设计用来处理这些相对用CPU去操作这样的事情,效率很低,换做专门的硬件的DMA来负责数据的读取和写入,释放了CPU这个苦力,可以让,在DMA忙着数据传输的过程中,CPU去忙其他更重要的事情。而专门的DMA硬件负责这样的数据传输,效率也会更高。之所以这样,才叫做内存直接访问的。

2. DMA的一些相关基础概念
        DMA传输,总的来说就是,硬件上,会有对应的控制寄存器ctrl和配置寄存器config,比如你想要从内存一个地址addr传输,N个字word(32bit)的数据到设备dev上,那么你就要先去根据你的请求,去配置config寄存器,首先是传输方向,是DMA_TO_DEVICE,然后是源地址source address是你的内存地址addr,和目标destination address是你的dev的DATA寄存器地址,然后要传输额transfer size是N个,每个位宽是32bit,将源地址,目标地址,位宽,DMA传输方向设置好,整理成一个结构,专有名称叫做LLI(Link List Item),把这个LLi设置到ctrl里面。
        然后去enable DMA,DMA就可以按照你的要求,把数据传输过去了。这样的DMA叫做single DMA传输,LLI中的next lli的域设置为空,表示就一个LLI要传输。如果源地址或目标地址是多个分散的地址,叫做scatter/gather DMA,就要将这些LLI组合一下,即将第一个LLI的next lli那个域,设置成下一个LLI的地址,这样一个个链接起来,最后一个LLI的nex lli的域为空,这样设置好后,将第一个LLI的值写入到ctrl中,DMA就会自动地去执行第一个LLI的数据传输,传完后,发现next lli不为空,就找到next lli的位置,找到对应的配置,开始这个lli的数据传送,直至传完所有的数据为止。

3. DMA种类:
        分为外设DMA和DMA控制器。其中外设DMA实现的为特定的外设与内存之间的数据传输,一般是外设向RAM单向传输数据。而DMA控制器则可以实现任意外设与内存之间的数据传输。此时外设跟CPU控制器之间通过流控制信号来保证传输通道的正常运行。

4. DMA传输的数据宽度不固定.
还可以实现任意长度的burst 操作。burst是DMA控制地址总线自行改变。
(DMA中的 burst 操作是指一次数据传输中可以连续传输多个数据块的操作。在传统的 DMA 方案中,每个数据传输操作只能传输一个数据块到内存或者从内存中读取一个数据块到外设,这会导致数据传输效率较低。)
DMA也支持分散集合模式,即内存中数据并非连续,而是分配在多个块中,块大小也不一样,这时候DMA可以根据Scatter Gather Descriptors来进行DMA数据传输。

原文链接:https://blog.csdn.net/qq_21792169/article/details/51277975

二、在Linux驱动开发过程中DMA的基本实现步骤。

  • 总结一下DMA传输的一般步骤;
  • 首先设置对应的标志位。
  • 获取当前的DMA设备通道。
  • 创建设备描述符,设置相关参数。
  • 数据传输完成后调用回调函数(callback)通知CPU传输已经完成,并且对数据进行相关的操作。(在这里是对于源地址和目标地址的数据进行比对,进一步验证DMA传输的正确进行)
  • 判断是否有需要回传给回调函数的参数。在这里设置为NULL表示没有额外的参数传递给回调函数。
  • 判断DMA传输是否提交成功(dma_submit_error(cookie))
  • 最后启动异步传输函数,方便与后续的传输操作,若为单次传输则可跳过相关的步骤。

三、相关代码实现以及运行结果分析。

 1.驱动程序


#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/miscdevice.h>
#include <linux/irq.h>
#include <linux/delay.h>
 
#include <linux/device.h>
#include <linux/string.h>
#include <linux/errno.h>
 
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
 
#include <asm/io.h>
#include <asm/uaccess.h>
 
#include <linux/amba/pl330.h>
#include <mach/dma.h>
#include <plat/dma-ops.h>
 
#define BUF_SIZE        512
#define MEM_CPY_NO_DMA	_IOW('L', 0x1210, int)
#define MEM_CPY_DMA	_IOW('L', 0x1211, int)
 
 
 
char *src = NULL;
char *dst = NULL;
dma_addr_t dma_src;
dma_addr_t dma_dst;
enum dma_ctrl_flags flags;
dma_cookie_t cookie;
static struct dma_chan *chan = NULL;
struct dma_device *dev = NULL;
struct dma_async_tx_descriptor *tx = NULL;
 
 
void dma_callback_func(void)
{
    if(0 == memcmp(src, dst, BUF_SIZE))
		
	/*mcmp函数将src与dst两个两个内存区域指针进行比较,
	BUF_SIZE则是需要比对的字节,当返回值小于0时,表示src小于dst
	当返回值大于0时,表示src大于dst
	返回值为0,则表示两段空间大小相同*/
	
	printk("MEM_CPY_DMA OK\n");
    else
        printk("MEM_CPY_DMA ERROR\n");
}
 
int exynos4_dma_open(struct inode *inode, struct file *filp)
{
    return 0;
}
 
long exynos4_dma_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int i = 0;
 
    memset(src, 0xAA, BUF_SIZE);
    memset(dst, 0xBB, BUF_SIZE);

	/*
	mset是一个用于内存赋值的函数,通常用于
	将一块内存区域全部或部分地赋为某个指定的值。
	参数分别为需要复制的空间指针,具体的赋值,所需要赋值的字节长度
	*/
    switch (cmd)
    {
    // MEM_CPY_NO_DMA,通过标准的库函数进行两个内存空间的数据拷贝
        case MEM_CPY_NO_DMA:
            for(i = 0; i < BUF_SIZE; i++)
                dst[i] = src[i];
 
            if(0 == memcmp(src, dst, BUF_SIZE))
                printk("MEM_CPY_NO_DMA OK\n");
            else
                printk("MEM_CPY_DMA ERROR\n");
 
            break;
			
	//MEM_CPY_DMA,通过DMA通道传输的方式进行数据的拷贝
        case MEM_CPY_DMA:
			
	    flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;

		/*
		进行flags标志位的设置
		DMA_CTRL_ACK表示在DMA传输完成后,需要从DMA控制器处获取一个ACK信号,
		以便确认数据已经正确传输完成;DMA_PREP_INTERRUPT则表示在准备好DMA
		传输时需要产生一个中断信号,以通知CPU开始进行数据传输。
		*/
	    dev = chan->device;
		//获取当前的DMA设备通道
        tx = dev->device_prep_dma_memcpy(chan, dma_dst, dma_src, BUF_SIZE, flags);
			
			/*chan,当前DMA传输通道
			dma_dst,源内存地址
			dma_src,目标内存地址
			BUF_SIZE,传输字节
			flags,传输设置的标志位*/

			if(!tx)
                printk("%s failed to prepare DMA memcpy\n", __func__);
 
            tx->callback = dma_callback_func;
			
			//在DMA传输完成后,为了能够进行处理,需要使用回调函数来通知CPU传输已经完成。
			//tx->callback用于存储回调函数的指针。 
			
            tx->callback_param = NULL;
			
			//将tx->callback_param设置为NULL,表示没有额外的参数需要传递给回调函数。
            cookie = tx->tx_submit(tx);  
            if(dma_submit_error(cookie)) {
				//dma_submit_error是一个用于判断DMA传输是否提交成功的函数。如果返回值为非负数
				//则表示DMA传输成功,反之则表示传输失败。cookie用于存储dma_submit_error函数的返回值
                printk("failed to do DMA tx_submit");
            }
 
            dma_async_issue_pending(chan); //dma_async_issue_pending函数是一个用于启动异步DMA传输的函数。
			//由于异步DMA传输是异步的,也就是说,可以在DMA传输完成之前启动下一次数据传输,
			//这样可以显著减少CPU与DMA引擎之间的空闲时间。
            break;
 
        default:
            break;
    }
 
    return 0;
}
 
int exynos4_dma_release(struct inode *inode, struct file *filp)
{
    return 0;
}
 
const struct file_operations exynos4_dma_fops = {
    .open   = exynos4_dma_open,
    .unlocked_ioctl = exynos4_dma_ioctl,
    .release = exynos4_dma_release,
};
 
struct miscdevice dma_misc = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "dma_m2m_test",
    .fops   = &exynos4_dma_fops,
};
 
static void __devexit
exynos4_dma_exit(void)
{
    dma_release_channel(chan);
    dma_free_coherent(NULL, BUF_SIZE, src, &dma_src);
    dma_free_coherent(NULL, BUF_SIZE, dst, &dma_dst);
    misc_deregister(&dma_misc);
}
 
static int __devinit
exynos4_dma_init(void)
{
    int ret = misc_register(&dma_misc);
	//misc 设备是一种没有特定驱动程序的设备,通常用于一些简单的、与内核
	//无关的操作,如读取/写入文件、执行简单的控制操作等。
    if(ret < 0){
        printk("%s misc register failed !\n", __func__);
        return -EINVAL;
    }
 	//	给src,dst分配空间
    src = dma_alloc_coherent(NULL, BUF_SIZE, &dma_src, GFP_KERNEL);
    printk("src = 0x%x, dma_src = 0x%x\n", src, dma_src);
 
    dst = dma_alloc_coherent(NULL, BUF_SIZE, &dma_dst, GFP_KERNEL);
    printk("dst = 0x%x, dma_dst = 0x%x\n", dst, dma_dst);
 
    dma_cap_mask_t mask;
	/*
	dma_cap_mask_t是Linux内核中用于描述DMA控制器支持的特性的数据类型。
	它是一个位掩码(bitmask)类型的数据结构,表示DMA控制器所支持的各种
	功能和选项
	*/

	
    dma_cap_zero(mask);
	//dma_cap_zero(mask)是一个宏,用于将dma_cap_mask_t结构体中的位掩码值全部清零。  

    dma_cap_set(DMA_SLAVE, mask);                         
    //dma_cap_set(type, mask)是另一个宏,在dma_cap_mask_t结构体中设置指定的位掩码值,以表示DMA控制器支持指定的DMA功能。


	chan = dma_request_channel(mask, pl330_filter, NULL);  
	
	/*
	请求DMA通道
		mask:为使用DMA的设备的结构体指针。
		pl330_filter:是一个回调函数指针,可以对 DMA 控制器进行过滤和匹配。
					  如果传入 NULL,则默认使用平台提供的 DMA 控制器。
		第三个参数用来传递附加参数,NULL表示没有附加参数
	*/
		
    if(NULL == chan){
	msleep(100);
	chan = dma_request_channel(mask, NULL, NULL);  
    }
	//返回值为NULL则表示请求失败,重新进行申请
 
    if(NULL == chan)
	printk("chan request failed !\n");
    else
	printk("chan OK!\n");
 
    return 0;
}
 
module_init(exynos4_dma_init);
module_exit(exynos4_dma_exit);
 
MODULE_LICENSE("GPL");


2.Makefile交叉编译程序

#General Purpose Makefile for Linux Kernel module by guoqingbo  
  
KERN_DIR = /home/mylinux/linux-3.5
#/home/mylinux/linux-3.5为虚拟机中内核存放的文件夹
  
all:  
	make -C $(KERN_DIR) M=$(shell pwd) modules     
  
clean:                                    
	make -C $(KERN_DIR) M=$(shell pwd) modules clean  
	rm -rf modules.order  
  
obj-m +=  mydma.o

3.测试程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
 
 
#define MEM_CPY_NO_DMA	_IOW('L', 0x1210, int)
#define MEM_CPY_DMA	    _IOW('L', 0x1211, int)
 
 
int main(void)
{
    int fd = -1,
        ret = -1;
 
    fd = open("/dev/dma_m2m_test", O_RDWR);
    if(fd < 0){
        printf("open fail !\n");
        exit(1);
    }
 
    ioctl(fd, MEM_CPY_NO_DMA, 0);
    sleep(1);
 
    ioctl(fd, MEM_CPY_DMA, 0);
    sleep(1);
 
    return 0;
}
 

 4.运行结果如下图:

运行结果分析:

        上文的程序通过DMA传输将srt以及dst两个空间的数据进行了传输,并且通过调用memcpy函数进行了验证。运行结果一切正常。


总结

以上就是今天要讲的内容,本文仅仅简单介绍了DMA的相关理论知识,以及DMA在Linux驱动开发过程中的实际运用,并且对于其中的大多数相关函数进行了简要的分析,希望对你有所帮助。

本文章参考了以下的两篇文章,并且对其进行了完善和测试。

原文链接:

⑱tiny4412 Linux驱动开发之DMA子系统驱动程序_device_prep_dma_memcpy___毛豆的博客-CSDN博客

详解ARM的AMBA设备中的DMA设备(Linux驱动之DMA)_source burst size_HeroKern的博客-CSDN博客

  • 3
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Linux驱动开发是一个涉及硬件与软件交互的重要领域,它要求开发者具备深入的操作系统知识、硬件理解能力以及编程技能。以下是学习Linux驱动开发的一些建议: 1. **基础知识**:您需要对Linux操作系统有深入的了解,包括内核架构、进程管理、文件系统等基本概念。这是理解驱动如何与操作系统交互的基础。 2. **硬件知识**:了解硬件的基本工作原理,包括设备寄存器、中断处理、DMA通信等,这些都是驱动程序直接与硬件打交道时必须掌握的内容。 3. **编程技能**:熟练掌握C语言,因为Linux内核和大多数设备驱动都是用C语言编写的。同时,熟悉数据结构和算法也是必不可少的。 4. **实践经验**:通过实际操作来加深理解。可以尝试阅读和修改现有的设备驱动程序,或者尝试自己编写简单的驱动程序。实战操作可以帮助您更好地理解理论知识。 5. **参考资料**:选择合适的书籍和在线资源进行学习。《Linux设备驱动程序》是一本经典的参考书,它详细介绍了Linux驱动开发的原理和实践技巧。 6. **社区交流**:加入Linux驱动开发的社区,如LWN.net、Kernel Newbies等,与其他开发者交流,可以帮助您解决遇到的问题并保持最新的行业动态。*** 学习Linux驱动开发前,应该具备哪些基础知识? 2. 如何通过实际操作来提高Linux驱动开发的技能? 3. 有哪些推荐的书籍或在线资源可以帮助学习Linux驱动开发? 综上所述,Linux驱动开发是一个复杂且具有挑战性的领域,但通过系统的学习和实践,您可以逐步掌握所需的技能和知识。希望以上建议能够帮助您顺利开始Linux驱动开发的学习之旅。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值