Email: BuddyZhang1 buddy.zhang@aliyun.com
目录
DMA 原理
DMA 的原意为 direct memory access,也就是直接内存访问(可以理解为读写)。DMA 传 输实际上是 DMA 控制器将数据从一个设备拷贝到另一个设备的过程,DMA 控制器的初始 化需要 cpu 参与,但是数据传输过程是不需要 cpu 参与的。实际上 DMA 不只适用于有 内存参与下的数据传输,下表是 DMA 适用的数据传输场景
内存 to 内存
内存 to 外围设备
外围设备 to 内存
外围设备 to 外围设备
数据传输过程
有两种方式可以引发 DMA 数据传输:软件对数据的请求、硬件异步地将数据传递给系统。
软件对数据请求传输过程
当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个 缓冲区中,进程进入睡眠状态
硬件将数据写入到 DMA 缓冲区,写入完成后产生一个中断(由 DMAC 产生)
中断处理程序获取到输入的数据,然后唤醒进程,进程可以读取数据
硬件异步数据传输过程
硬件产生中断,宣告数据的到来
中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据
外围设备将数据写入缓冲区,完成后产生另外一个中断
处理程序分发新数据,唤醒相关进程,执行相应操作
网卡数据传输就是使用硬件异步数据传输方式,在下载、访问的过程中,kernel 并不确 定数据到来的时间。网络数据最先到达的是硬件网卡,网卡接收到一定量的网络数据时, 产生中断,通过kernel处理,进入硬件异步数据传输过程。
数据一致性问题
CPU 写内存的时候有两种方式:
write through: CPU 直接写内存,不经过 cache。
write back: CPU 只写到 cache 中。cache 的硬件使用 LRU 算法将 cache 里面的 内容替换到内存。
DMA 可以完成从内存到外设直接进行数据搬移。但 DMA 不能访问 CPU 的 cache,CPU 在 读内存的时候,如果 cache 命中则只是在cache 去读,而不是从内存读,写内存的时候, 也可能实际上没有写到内存,而只是直接写到了 cache。这样一来,如果 DMA 从将数据 从外设写到内存,CPU 中 cache 中的数据(如果有的话)就是旧数据了,这时 CPU 在读 内存的时候命中 cache 了,就是读到了旧数据;CPU 写数据到内存时,如果只是先写到 了 cache,则内存里的数据就是旧数据了。这两种情况(两个方向)都存在 cache 一致 性问题。例如,网卡发包的时候,CPU 将数据写到 cache,而网卡的 DMA 从内存里去读 数据,就发送了错误的数据。
如何解决一致性问题
主要靠两类 APIs:
一致性DMA缓存(Coherent DMA buffers)
流式DMA映射(DMA Streaming Mapping)
一致性DMA缓存(Coherent DMA buffers)
DMA 需要的内存由内核去申请,内核可能需要对这段内存重新做一遍映射,特点是映射的 时候标记这些页是不带 cache 的,这个特性也是存放在页表里面的。上面说“可能”需要 重新做映射,如果内核在 highmem 映射区申请内存并将这个地址通过 vmap 映射到 vmalloc 区域,则需要修改相应页表项并将页面设置为非 cache 的,而如果内核从 lowmem 申请内存,我们知道这部分是已经线性映射好了,因此不需要修改页表,只需修 改相应页表项为非 cache 即可。相关的接口就是
dma_alloc_coherent()
dma_free_coherent()
dma_cache_sync()
dma_alloc_coherent() 会传一个 device 结构体指明给哪个设备申请一致性 DMA 内存, 它会产生两个地址,一个是给 CPU 看的,一个是给 DMA 看的。CPU 需要通过返回的虚拟 地址来访问这段内存,才是非 cache 的。至于 dma_alloc_coherent() 的内部实现可以 不关注,它是和体系结构如何实现非 cache(如mips的kseg1)相关,也可能与硬件特 性(如是否支持 CMA)相关。
还有一个接口 dma_cache_sync(),可以手动去做 cache 同步,上面说 dma_alloc_coherent() 分配的是 uncached 内存,但有时给 DMA用的内存是其他模块已 经分配好的,例如协议栈发包时,最终要把 skb 的地址和长度交给 DMA,除了将 skb 地 址转换为物理地址外,还要将 CPU cache 写回(因为 cache 里可能是新的,内存里是旧 的)。
调用这个函数的时刻就是上面描述的情况:因为内存是可 cache 的,因此在 DMA 读内 存(内存到设备方向)时,由于cache 中可能有新的数据,因此要先将 cache 中的数据 写回到内存;在 DMA 写内存(设备到内存方向)时,cache 中可能还有数据没有写回, 为了防止cache 数据覆盖 DMA 要写的内容,要先将 cache 无效。注意这个函数的 vaddr 参数接收的是虚拟地址。例如在发包时将协议栈的 skb 放进 ring buffer 之前, 要做一次 DMA_TO_DEVICE 的 flush。对应的,在收包后为 ring buffer 中已被使用的 skb 数据 buffer 重新分配内存后,要做一次 DMA_FROM_DEVICE的flush(invalidate 的时候要注意cache align)。
流式DMA映射(DMA Streaming Mapping)
相关接口为
dma_map_sg(), dma_unmap_sg()
dma_map_single(),dma_unmap_single()
一致性缓存的方式是内核专门申请好一块内存给 DMA 用。而有时驱动并没这样做,而是 让 DMA 引擎直接在上层传下来的内存里做事情。例如从协议栈里发下来的一个包,想通 过网卡发送出去。但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有 特殊对待,这个包所在的内存通常都是可以 cache 的。这时,内存在给 DMA 使用之前, 就要调用一次 dma_map_sg() 或 dma_map_single(),取决于你的 DMA 引擎是否支持聚集 散列(DMA scatter-gather),支持就用 dma_map_sg(),不支持就用 dma_map_single()。DMA 用完之后要调用对应的 unmap 接口。
由于协议栈下来的包的数据有可能还在 cache 里面,调用 dma_map_single() 后,CPU 就会做一次 cache 的 flush,将 cache 的数据刷到内存,这样 DMA 去读内存就读到新 的数据了。
注意,在 map 的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到 外设:
从内存到外设:CPU 会做 cache 的 flush 操作,将 cache 中新的数据刷到内存。
从外设到内存:CPU 将 cache 置无效,这样 CPU 读的时候不命中,就会从内存去 读新的数据。
还要注意,这几个接口都是一次性的,每次操作数据都要调用一次 map 和 unmap。并且 在 map 期间,CPU 不能去操作这段内存,因此如果 CPU 去写,就又不一致了。同样的, dma_map_sg() 和 dma_map_single() 的后端实现也都是和硬件特性相关。
其他方式
上面说的是常规 DMA,有些 SoC 可以用硬件做 CPU 和外设的 cache coherence,例如在 SoC 中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到让 DMA 踏到 CPU 的 cache 或者帮忙做 cache 的刷新。这样的话,dma_alloc_coherent() 申请的内 存就没必要是非cache的了。
Kernel 中使用 DMA
项目开发中需要使用 DMA 进行内存到内存的拷贝或者内存与外设之间的数据拷贝,这时 需要使用 DMA 进行数据的拷贝。下面提供了 DMA 使用的驱动,开发者可以根据实际开发 需求进行修改.
/*
* Copyright (C) 2017 buddy.zhang@aliyun.com
*
* dma device driver demo
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/
#include <linux/types.h>
#include <linux/string.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/uaccess.h>
#include <linux/dmaengine.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#define DRIVER_NAME "axidma"
#define AXIDMA_IOC_MAGIC 'A'
#define AXIDMA_IOCGETCHN _IO(AXIDMA_IOC_MAGIC, 0)
#define AXIDMA_IOCCFGANDSTART _IO(AXIDMA_IOC_MAGIC, 1)
#define AXIDMA_IOCGETSTATUS _IO(AXIDMA_IOC_MAGIC, 2)
#define AXIDMA_IOCRELEASECHN _IO(AXIDMA_IOC_MAGIC, 3)
#define AXI_DMA_MAX_CHANS 8
#define DMA_CHN_UNUSED 0
#define DMA_CHN_USED 1
struct axidma_chncfg {
unsigned int src_addr;
unsigned int dst_addr;
unsigned int len;
unsigned char chn_num;
unsigned char status;
unsigned char reserve[2];
unsigned int reserve2;
};
struct axidma_chns {
struct dma_chan *dma_chan;
unsigned char used;
#define DMA_STATUS_UNFINISHED 0
#define DMA_STATUS_FINISHED 1
unsigned char status;
unsigned char reserve[2];
};
struct axidma_chns channels[AXI_DMA_MAX_CHANS];
static int axidma_open(struct inode *inode, struct file *file)
{
printk("Open: do nothing\n");
return 0;
}
static int axidma_release(struct inode *inode, struct file *file)
{
printk("Release: do nothing\n");
return 0;
}
static ssize_t axidma_write(struct file *file, const char __user *data, size_t len, loff_t *ppos)
{
printk("Write: do nothing\n");
return 0;
}
static void dma_complete_func(void *status)
{
*(char *)status = DMA_STATUS_FINISHED;
printk("dma_complete!\n");
}
static long axidma_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct dma_device *dma_dev;
struct dma_async_tx_descriptor *tx = NULL;
dma_cap_mask_t mask;
dma_cookie_t cookie;
enum dma_ctrl_flags flags;
struct axidma_chncfg chncfg;
int ret = -1;
int i;
memset(&chncfg, 0, sizeof(struct axidma_chncfg));
switch(cmd) {
case AXIDMA_IOCGETCHN:
for(i = 0; i < AXI_DMA_MAX_CHANS; i++) {
if(DMA_CHN_UNUSED == channels[i].used)
break;
}
if (AXI_DMA_MAX_CHANS == i) {
printk("Get dma chn failed, because no idle channel\n");
goto error;
} else {
channels[i].used = DMA_CHN_USED;
channels[i].status = DMA_STATUS_UNFINISHED;
chncfg.chn_num = i;
chncfg.status = DMA_STATUS_UNFINISHED;
}
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);
channels[i].dma_chan = dma_request_channel(mask, NULL, NULL);
if(!channels[i].dma_chan) {
printk("dma request channel failed\n");
channels[i].used = DMA_CHN_UNUSED;
goto error;
}
ret = copy_to_user((void __user *)arg, &chncfg,
sizeof(struct axidma_chncfg));
if(ret) {
printk("Copy to user failed\n");
goto error;
}
break;
case AXIDMA_IOCCFGANDSTART:
ret = copy_from_user(&chncfg, (void __user *)arg,
sizeof(struct axidma_chncfg));
if(ret) {
printk("Copy from user failed\n");
goto error;
}
if((chncfg.chn_num >= AXI_DMA_MAX_CHANS) ||
(!channels[chncfg.chn_num].dma_chan)) {
printk("chn_num[%d] is invalid\n", chncfg.chn_num);
goto error;
}
dma_dev = channels[chncfg.chn_num].dma_chan->device;
flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
tx = dma_dev->device_prep_dma_memcpy(channels[chncfg.chn_num].dma_chan,
chncfg.dst_addr, chncfg.src_addr, chncfg.len, flags);
if(!tx) {
printk("Failed to prepare DMA memcpy\n");
goto error;
}
tx->callback = dma_complete_func;
channels[chncfg.chn_num].status = DMA_STATUS_UNFINISHED;
tx->callback_param = &channels[chncfg.chn_num].status;
cookie = tx->tx_submit(tx);
if(dma_submit_error(cookie)) {
printk("Failed to dma tx_submit\n");
goto error;
}
dma_async_issue_pending(channels[chncfg.chn_num].dma_chan);
break;
case AXIDMA_IOCGETSTATUS:
ret = copy_from_user(&chncfg, (void __user *)arg,
sizeof(struct axidma_chncfg));
if(ret) {
printk("Copy from user failed\n");
goto error;
}
if(chncfg.chn_num >= AXI_DMA_MAX_CHANS) {
printk("chn_num[%d] is invalid\n", chncfg.chn_num);
goto error;
}
chncfg.status = channels[chncfg.chn_num].status;
ret = copy_to_user((void __user *)arg, &chncfg,
sizeof(struct axidma_chncfg));
if(ret) {
printk("Copy to user failed\n");
goto error;
}
break;
case AXIDMA_IOCRELEASECHN:
ret = copy_from_user(&chncfg, (void __user *)arg,
sizeof(struct axidma_chncfg));
if(ret) {
printk("Copy from user failed\n");
goto error;
}
if((chncfg.chn_num >= AXI_DMA_MAX_CHANS) ||
(!channels[chncfg.chn_num].dma_chan)) {
printk("chn_num[%d] is invalid\n", chncfg.chn_num);
goto error;
}
dma_release_channel(channels[chncfg.chn_num].dma_chan);
channels[chncfg.chn_num].used = DMA_CHN_UNUSED;
channels[chncfg.chn_num].status = DMA_STATUS_UNFINISHED;
break;
default:
printk("Don't support cmd [%d]\n", cmd);
break;
}
return 0;
error:
return -EFAULT;
}
/*
* Kernel Interfaces
*/
static struct file_operations axidma_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.write = axidma_write,
.unlocked_ioctl = axidma_unlocked_ioctl,
.open = axidma_open,
.release = axidma_release,
};
static struct miscdevice axidma_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DRIVER_NAME,
.fops = &axidma_fops,
};
static int __init axidma_init(void)
{
int ret = 0;
ret = misc_register(&axidma_miscdev);
if(ret) {
printk (KERN_ERR "cannot register miscdev (err=%d)\n", ret);
return ret;
}
memset(&channels, 0, sizeof(channels));
return 0;
}
device_initcall(axidma_init);
Makefile
obj-m += dma.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
ROOT := $(dir $(M))
DEMOINCLUDE := -I$(ROOT)../include -I$(ROOT)/include
GCCVERSION = $(shell gcc -dumpversion | sed -e 's/\.\([0-9][0-9]\)/\1/g' -e 's/\.\([0-9]\)/0\1/g' -e 's/^[0-9]\{3,4\}$$/&00/')
GCC49 := $(shell expr $(GCCVERSION) \>= 40900)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
install: all
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
depmod -a
clean:
rm -rf *.o *.o.d *~ core .depend .*.cmd *.ko *.ko.unsigned *.mod.c .tmp_versions *.symvers \
.cache.mk *.save *.bak Modules.* modules.order Module.markers *.bin
CFLAGS_dma.o := -Wall $(DEMOINCLUDE)
ifeq ($(GCC49),1)
CFLAGS_dma.o += -Wno-error=date-time
endif
CFLAGS_dma.o := $(DEMOINCLUDE)
准备好源码之后,开发者可以将驱动加到内核源码树进行编译,也可以在外部编译,外部 编译使用如下命令:
make
sudo insmod dma.ko
当驱动编译到源码或与模块方式加载到内核之后,在 /dev 目录下将生成一个名为 axidma 的设备节点,至此,驱动加载成功。接下来请参考用户空间 DMA 使用一节内容对 DMA 进行访问。
用户空间使用 DMA
有的项目开发中需要在用户空间使用 DMA,为此可以参考本节进行用户空间 DMA 的使用
github 源码位置: https://github.com/BiscuitOS/HardStack/tree/master/bus/DMA/user
源码如下:
/*
* DMA application
*
* (C) 2018.11.29 BiscuitOS <buddy.zhang@aliyun.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/mman.h>
#define DRIVER_NAME "/dev/axidma"
#define AXIDMA_IOC_MAGIC 'A'
#define AXIDMA_IOCGETCHN _IO(AXIDMA_IOC_MAGIC, 0)
#define AXIDMA_IOCCFGANDSTART _IO(AXIDMA_IOC_MAGIC, 1)
#define AXIDMA_IOCGETSTATUS _IO(AXIDMA_IOC_MAGIC, 2)
#define AXIDMA_IOCRELEASECHN _IO(AXIDMA_IOC_MAGIC, 3)
#define DMA_STATUS_UNFINISHED 0
#define DMA_STATUS_FINISHED 1
struct axidma_chncfg {
unsigned int src_addr;
unsigned int dst_addr;
unsigned int len;
unsigned char chn_num;
unsigned char status;
unsigned char reserve[2];
unsigned int reserve2;
};
#define SRC_ADDR 0x60000000
#define DST_ADDR 0x70000000
#define DMA_MEMCPY_LEN 0x300000
int main(void)
{
struct axidma_chncfg chncfg;
int fd = -1;
int ret;
printf("AXI dma test, only support mem to mem: copy %#lx to %#lx,"
" size:3M\n", SRC_ADDR, DST_ADDR);
/* open dev */
fd = open(DRIVER_NAME, O_RDWR);
if(fd < 0) {
printf("open %s failed\n", DRIVER_NAME);
return -1;
}
/* get channel */
ret = ioctl(fd, AXIDMA_IOCGETCHN, &chncfg);
if(ret){
printf("ioctl: get channel failed\n");
goto error;
}
printf("channel: %d\n", chncfg.chn_num);
/* config addr */
chncfg.src_addr = SRC_ADDR;
chncfg.dst_addr = DST_ADDR;
chncfg.len = DMA_MEMCPY_LEN;
ret = ioctl(fd, AXIDMA_IOCCFGANDSTART, &chncfg);
if(ret) {
printf("ioctl: config and start dma failed\n");
goto error;
}
/* wait finish */
while(1) {
ret = ioctl(fd, AXIDMA_IOCGETSTATUS, &chncfg);
if(ret) {
printf("ioctl: get status failed\n");
goto error;
}
if (DMA_STATUS_FINISHED == chncfg.status) {
break;
}
printf("status:%d\n", chncfg.status);
sleep(1);
}
/* release channel */
ret = ioctl(fd, AXIDMA_IOCRELEASECHN, &chncfg);
if(ret) {
printf("ioctl: release channel failed\n");
goto error;
}
close(fd);
return 0;
error:
close(fd);
return -1;
}
用户空间使用 DMA 的核心是 /dev/axidma 设备节点,如果不存在这个节点,请参考上一 节 kernel 中使用 DMA 的源码。通过上面的代码,用户空间程序就可以便捷使用 DMA。
用户空间使用 DMA 性能测试如下:
CPU 性能如下: