Bootstrap

Linux DMA Engine 基础

1 DMA基础信息查看 /sys/class/dma

root:~# ls /sys/class/dma/
dma0chan0   dma1chan10  dma1chan27  dma2chan14  dma2chan30  dma2chan47  dma2chan63  dma3chan21  dma3chan38  dma3chan54
dma0chan1   dma1chan11  dma1chan28  dma2chan15  dma2chan31  dma2chan48  dma2chan7   dma3chan22  dma3chan39  dma3chan55
......

还可以查看特定通道信息:

root:~# ls /sys/class/dma/dma2chan1/
bytes_transferred  device  in_use  memcpy_count  power  slave  subsystem  uevent

该命令查看有哪些可用的dma channel, 每个dma有多少个channel. 还可以,
查看特定channel是否在使用,下面这个就是在使用:

cat /sys/class/dma/dma2chan1/in_use
1

2 调试 DMA Engine /sys/kernel/debug/dmaengine

它通过 debugfs 文件系统提供了对 DMA 控制器、通道、传输状态等底层信息的访问,是开发者排查 DMA 相关问题时的重要工具。

2.1 目录结构与核心文件

root:~# ls /sys/kernel/debug/dmaengine/
42000000.dma-controller  42df0000.dma-controller  44000000.dma-controller  4b110000.epxp  summary

虽然网上说的这个目录的调试用法天花乱坠,但是我发现只有summary有用:

root:~# cat /sys/kernel/debug/dmaengine/summary
dma0 (4b110000.epxp): number of channels: 16

dma1 (44000000.dma-controller): number of channels: 32
 dma1chan0    | in-use
 dma1chan20   | 44380000.serial:tx
 dma1chan21   | 44380000.serial:rx

dma2 (42000000.dma-controller): number of channels: 64
 dma2chan0    | 42530000.i2c:tx (给出了具体分给哪个节点了)
 dma2chan1    | 42530000.i2c:rx

dma3 (42df0000.dma-controller): number of channels: 64
 dma3chan0    | 42540000.i2c:tx
 dma3chan1    | 42540000.i2c:rx
 dma3chan2    | 426c0000.i2c:tx
 dma3chan3    | 426c0000.i2c:rx

可以发现,得到的信息包括:控制器名称,如44000000.dma-controller,总通道数 32、64,已经分配的通道分给哪个具体的节点了,还给出了通道对应的外设偏移地址 等。

3 看手册学习

linux-kernel$ ls Documentation/driver-api/dmaengine/
client.rst  dmatest.rst  index.rst  provider.rst  pxa_dma.rst

index.rst 就是个简单的摘要。
dmatest.rst 如何编译和使用dmatest工具来测试指定dma channel。
client.rst 如何使用DMAEngine的Slave-DMA API(仅适用slave DMA)。
provider.rst

3.1 dmatest.rst

文档说明dmatest工具怎么编译,如何使用来测试dma, 该工具只能测试memory to memory。

如何使用edma进行内存到内存测试

echo 2000 > /sys/module/dmatest/parameters/timeout  单位是毫秒
echo 1 > /sys/module/dmatest/parameters/iterations
echo dma1chan0 > /sys/module/dmatest/parameters/channel //可以让多个通道同时测试
echo dma1chan1 > /sys/module/dmatest/parameters/channel
echo dma1chan2 > /sys/module/dmatest/parameters/channel
【注】此时通过 cat /sys/kernel/debug/dmaengine/summary 可以看哪些通道正在使用。
echo 1 > /sys/module/dmatest/parameters/run 为1时会启动传输,写0会释放通道
【注】可以通过cat /proc/interrupts | grep dma 查看对应的DMA channel是否发生中断。

检查手段
ls /sys/class/dma/ 可以得到哪些cannel可用
echo "" > /sys/module/dmatest/parameters/channel 这样会将所有不忙的通道加入测试,我日了。
cat /sys/module/dmatest/parameters/run 查看run的状态
cat /sys/module/dmatest/parameters/channel 可用查看成功添加的channel
cat /sys/module/dmatest/parameters/test_list 会打印pending的test
echo 0 > /sys/module/dmatest/parameters/run 会释放通道。
dmesg | tail -n 1 查看位于kernel log的测试结果,会显示错误数量

3.2 provider.rst

对dmaengine框架做了介绍,DMAEngine APIs小节讲解内容比较重要。 Device operations 介绍了注册到dma_device structure的操作函数都是干嘛的,什么时候用。

3.3 client.rst

slave DMA 的使用包含的步骤:申请DMA slave channel, 设置参数,得到传输描述符,提交传输,等待回调。这篇文章讲解的很详细。

4 初识 DMA Engine

4.1 DMA controller 了解

DMA Engine是开发DMA控制器驱动程序的通用内核框架。DMA的主要目的是在复制内存的时候减轻CPU的负担。使用通道将事务(I/O数据传输)委托给DMA Engine,DMA Engine通过其驱动程序API提供一组可供其他设备(从设备)使用的通道.

Linux为了方便基于DMA的memcpy、memset等操作,在dma engine之上,封装了一层更为简洁的API,这些API就是Async TX API. 【参考博客
因为 memory 到 memory 有了简洁的API,没必要直接使用 dma engine 提供的API了,所以把 dma engine 提供的API特指为 Slave-DMA API (slave指的是参与DMA传输的设备,比如IIC, UART等)

这篇博客介绍了如何依据DMA engine框架实现 dma controller driver 。

dma controller driver的核心实现如下:

  1. 定义一个struct dma_device变量,并根据实际的硬件情况,填充其中的关键字段(如device_alloc_chan_resources,device_free_chan_resources接口等)。
  2. 调用dma_async_device_register函数,将dma_device注册到dmaengine框架中。

4.2 如何使用 DMA engine 框架传输

对设备驱动的编写者来说,要基于dma engine提供的Slave-DMA API进行DMA传输的话,需要如下的操作步骤【参考博客】:
1)申请一个DMA channel。
2)根据设备(slave)的特性,配置DMA channel的参数。
3)要进行DMA传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。
4)将本次传输(transaction)提交给dma engine并启动传输。
5)等待传输(transaction)结束。

4.3 代码示例

关键代码

struct dmatest_dev {
	struct cdev cdev;		/* 字符设备结构 */
};
static struct dmatest_dev *dmatest_dev_all;
static dev_t devt;           /* 用于存储设备号 */

static int dmatest_major = DMATEST_MAJOR;
static int dmatest_minor = DMATEST_MINOR;
static int dmatest_nr_devs = 1; /* 设备数量 */

//bus address 给DMA用
dma_addr_t dma_src;
dma_addr_t dma_dst;

//virtual address 给驱动程序用
char *src = NULL;
char *dst = NULL ;

struct dma_device *dev; //dma设备结构体
struct dma_chan *chan; //channel 结构体

/*
* 申请主设备号,但需要自己用mknod的方式依据主设备号注册设备节点。
*/
static int dmatest_register_chrdev(void)
{
	int result;	

	if (dmatest_major) {
		devt = MKDEV(dmatest_major, dmatest_minor);
		result = register_chrdev_region(devt, dmatest_nr_devs, DEVICE_NAME); //向内核注册设备号, 设备名称示在 /proc/devices
	} else {
		result = alloc_chrdev_region(&devt, dmatest_minor, dmatest_nr_devs, DEVICE_NAME);//向内核注册设备号, 设备名称示在 /proc/devices
		dmatest_major = MAJOR(devt);
	}
	if (result < 0) {

		printk(KERN_WARNING "dmatest: can't get major %d\n", dmatest_major);
	}
	return result;
}


/* 初始化module */
static int dmatest_init(void)
{
	int i, result;
	dma_cap_mask_t mask;
    
    result = dmatest_register_chrdev();//向内核申请主设备号

	printk(KERN_NOTICE "%s : get device major number %d\n", __func__, dmatest_major);

	dmatest_dev_all = kmalloc(dmatest_nr_devs * sizeof(struct dmatest_dev), GFP_KERNEL);


	for (i = 0; i < dmatest_nr_devs; i++)
		dmatest_setup_cdev(&dmatest_dev_all[i], i); //调用cdev_init,cdev_add实现字符设备注册

    dma_cap_zero(mask);
	dma_cap_set(DMA_MEMCPY, mask); //direction:memory to memory
	chan = dma_request_channel(mask, 0, NULL); //request a dma channel

	flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
	dev = chan->device;
	//alloc 512B src memory and dst memory
	src = dma_alloc_coherent(dev->dev, 512, &dma_src, GFP_KERNEL); //一致性dma映射,参数3为总线地址会给DMA用,返回值为内核虚拟地址驱动程序可用
	pr_info("src = %px, dma_src = %#llx\n",src, dma_src);
	
	dst = dma_alloc_coherent(dev->dev, 512, &dma_dst, GFP_KERNEL); //一致性dma映射,参数3为总线地址,给DMA用,返回值为内核虚拟地址驱动程序可用
	pr_info("dst = %px, dma_dst = %#llx\n",dst, dma_dst);
		
	for (i = 0; i < 512; i++){
		*(src + i) = (unsigned char)(i % 256); //启动传输前将源地址初始化
	}
...
}

/* 卸载退出module */
static void dmatest_exit(void)
{
	int i;
	if (dmatest_dev_all) {
		for (i = 0; i < dmatest_nr_devs; i++) {
			cdev_del(&dmatest_dev_all[i].cdev);
		}
		kfree(dmatest_dev_all);
	}

	unregister_chrdev_region(devt, dmatest_nr_devs);

	
	//free memory and dma channel
	dma_free_coherent(dev->dev, 512, src, dma_src);
	dma_free_coherent(dev->dev, 512, dst, dma_dst);
	
	dma_release_channel(chan);
}

注册字符设备

//When dma transfer finished,this function will be called.
static void dma_callback_func(void * arg)
{
	int i;
	printk("Debug in %s() \n", __func__);
	for (i = 0; i < 10; i++){
		pr_info("%c",dst[i]); //打印EDMA传输结束后目的地址内容
	}
}

static int dmatest_open(struct inode *inode, struct file *filp)
{
	return 0;
}
static int dmatest_release(struct inode *inode, struct file *filp)
{
	return 0;
}
static ssize_t dmatest_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
	int ret = 0;
	//alloc a desc,and set dst_addr,src_addr,data_size.
	tx = dev->device_prep_dma_memcpy(chan, dma_dst, dma_src, 512, flags);
	if (!tx){
		pr_info("Failed to prepare DMA memcpy \n");
	}
	
    /* 设置回调函数 */
	tx->callback = dma_callback_func;
	tx->callback_param = NULL;
	cookie = tx->tx_submit(tx); //submit the desc
	if (dma_submit_error(cookie)){
		pr_info("Failed to do DMA tx_submit \n");
	}
	/* begin dma transfer */
	dma_async_issue_pending(chan);
	
	return ret;
}
 
static ssize_t dmatest_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
	int ret = 0;
	return ret;
}
 
static const struct file_operations dmatest_fops = {
	.owner = THIS_MODULE,
	.read = dmatest_read,
	.write = dmatest_write,
	.open = dmatest_open,
	.release = dmatest_release,
};

/*
 * dmatest charter device registration
 */
static void dmatest_setup_cdev(struct dmatest_dev *devt, int index)
{
	int err, devno = MKDEV(dmatest_major, dmatest_minor + index);
	
	cdev_init(&devt->cdev, &dmatest_fops);
	devt->cdev.owner = THIS_MODULE;
	devt->cdev.ops = &dmatest_fops;
	err = cdev_add(&devt->cdev, devno, 1);
	if (err)
		printk(KERN_NOTICE "Error %d adding dmatest%d", err, index);
}

编译成module后运行 mknod_edma.sh 依据设备号注册设备节点, 文件内容如下。

#!/bin/sh
module='edma_test_lyrix'
device='edma_test_lyrix'
mode='664'

# 使用传入该脚本的i所有参数调用insmod,同时使用路径名来指定模块位置
/sbin/insmod ./$module.ko $* || exit 1

# 删除原有节点
rm -f /dev/${device}[0-4]

major=$(awk "\$2==\"$module\"{print \$1}" /proc/devices)

mknod /dev/${device}0 c $major 0

最后在文件系统中使用 cat /dev/edma_test_lyrix0 触发DMA拷贝操作,然后可以看到模块打印,验证其目的地址内容与源地址内容相同了。

;