GitHub: DMA

Email: BuddyZhang1 buddy.zhang@aliyun.com

目录

  1. DMA 原理

  2. Kernel 中使用 DMA

  3. 用户空间使用 DMA

  4. DMA 性能测试

  5. 附录


DMA 原理

Menuconfig1

DMA 的原意为 direct memory access,也就是直接内存访问(可以理解为读写)。DMA 传 输实际上是 DMA 控制器将数据从一个设备拷贝到另一个设备的过程,DMA 控制器的初始 化需要 cpu 参与,但是数据传输过程是不需要 cpu 参与的。实际上 DMA 不只适用于有 内存参与下的数据传输,下表是 DMA 适用的数据传输场景

  1. 内存 to 内存

  2. 内存 to 外围设备

  3. 外围设备 to 内存

  4. 外围设备 to 外围设备

数据传输过程

Menuconfig1

有两种方式可以引发 DMA 数据传输:软件对数据的请求、硬件异步地将数据传递给系统。

软件对数据请求传输过程

  1. 当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个 缓冲区中,进程进入睡眠状态

  2. 硬件将数据写入到 DMA 缓冲区,写入完成后产生一个中断(由 DMAC 产生)

  3. 中断处理程序获取到输入的数据,然后唤醒进程,进程可以读取数据

硬件异步数据传输过程

  1. 硬件产生中断,宣告数据的到来

  2. 中断处理程序分配一个缓冲区,并且告诉硬件向哪里传输数据

  3. 外围设备将数据写入缓冲区,完成后产生另外一个中断

  4. 处理程序分发新数据,唤醒相关进程,执行相应操作

网卡数据传输就是使用硬件异步数据传输方式,在下载、访问的过程中,kernel 并不确 定数据到来的时间。网络数据最先到达的是硬件网卡,网卡接收到一定量的网络数据时, 产生中断,通过kernel处理,进入硬件异步数据传输过程。

数据一致性问题

CPU 写内存的时候有两种方式:

  1. write through: CPU 直接写内存,不经过 cache。

  2. 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:

  1. 一致性DMA缓存(Coherent DMA buffers)

  2. 流式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 的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到 外设:

  1. 从内存到外设:CPU 会做 cache 的 flush 操作,将 cache 中新的数据刷到内存。

  2. 从外设到内存: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 性能测试如下:

Menuconfig1

CPU 性能如下:

Menuconfig1


附录

Linux 下的DMA浅析

Linux 内存管理 – DMA 和一致性缓存

BiscuitOS Home

BiscuitOS Driver

BiscuitOS Kernel Build

Linux Kernel

Bootlin: Elixir Cross Referencer

赞赏一下吧 🙂

MMU