流式DMA相关的接口为dma_map_sg(),dma_unmap_sg(),dma_map_single(),dma_unmap_single()。流式DMA一般用于已经分配好的内存,然后再对其进行DMA操作,而不是提前申请好一块cache一致性的内存给DMA用。例如从协议栈里发下来的一个包,想通过网卡发送出去。但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。
相关例程源码可以通过以下链接免费下载:
https://download.csdn.net/download/slov8/89668736
1、SG DMA
先介绍一下什么是SG(scatter-gather)DMA,翻译过来就是分散聚集DMA,SG-DMA应该算是流式DMA的一种。进行一次DMA数据搬运的一个前提是这段DMA内存的物理地址必须是连续的,DMA搬运完就会产生一次中断(前提是开启中断的情况下),如果有十个不同区域的DMA传输,就需要产生10次中断,这样效率明显不高。SG DMA可以把这十个不同区域的DMA串起来,一次性地把这十个DMA区域都传输完再产生一次中断。直白点说就是把分散的几个DMA区域都聚集到一起,一起打包传输,所以也叫集散DMA。
结合笔记的开发经历,Linux在用户层申请的内存一般都是物理地址不连续,用户层申请的地址传递到内核后,在内核中把地址转换成内存页,页是物理地址的, 然后用这些页去做SG DMA映射,就可以使用户层申请的物理地址不连续的内存做DMA操作了。后面笔者会给出实际例程。笔者认为每写一篇文章都要经过自己的实际操作,然后总结,还要有自己的理解,这样写文章才用意义......扯远了,接着往下看吧。
1.1、重要的结构体
struct scatterlist {
#ifdef CONFIG_DEBUG_SG
unsigned long sg_magic;
#endif
unsigned long page_link;
unsigned int offset;
unsigned int length;
dma_addr_t dma_address;
#ifdef CONFIG_NEED_SG_DMA_LENGTH
unsigned int dma_length;
#endif
};
dma_addrss:DMA设备能操作到的虚拟地址即IOVA;
dma_length:DMA设备通过它可以知道数据区的长度;
page_link:CPU看到的虚拟地址即VA;
length:CPU能看到的数据长度;
offset:数据在页中的偏移;
单个scattherlist对应单个内存页;
单个struct scatterlist和一个页的对应关系如图:
1.2、重要的API函数
sg_alloc_table:用于申请struct scatterlist类型的数组,并初始化;
sg_free_table:释放struct scatterlist类型的数组;
dma_map_sg:对应scatterlist进行dma映射;
dma_unmap_sg:释放dma映射;
get_user_pages_fast:锁定用户层申请的内存,并返回对应的内存页地址,需要通过put_page释放;
get_kernel_pages:锁定内核申请的内存,并返回对应的内存页地址,需要通过put_page释放;
2、流式DMA例程
这里以散集表(scatter-gather)dma为例,并且使用sg dma来实现memcpy的功能,包括以下几种类型:
实际的dma操作一般都是内存到外设,或者外设到内存的传输,此例程使用独立的dma控制器来进行内存到内存的传输,操作流程都是一样的。此例程只能在arm结构上的CPU运行,在X86上无法运行,因为X86平台的CPU没有独立的DMA控制器。笔记在正点原子RK3568开发板上面是可以跑成功的。
2.1、工作流程
以数据传输方向: 用户缓冲区->SG DMA->内核缓冲区 为例:
以下是最核心的传输函数:
static int sg_dma_xfer_submit(struct user_sg_dma_io *src_io, struct user_sg_dma_io *dst_io, struct user_sg_dma_dev *dev, enum SG_DMA_MEMCPY_TYPE memcpy_type)
{
int ret = 0;
int i;
struct scatterlist *src_sg;
struct sg_table *src_sgt;
struct scatterlist *dst_sg;
struct sg_table *dst_sgt;
struct dma_chan *dma_chan;
struct dma_device *dma_dev;
enum dma_ctrl_flags flags;
enum dma_status status;
dma_cookie_t cookie;
dma_addr_t src_addr;
dma_addr_t dst_addr;
unsigned int dst_len;
unsigned int src_len;
struct dma_async_tx_descriptor *tx = NULL;
if (memcpy_type == SG_DMA_MEMCPY_USER2KERNEL || memcpy_type == SG_DMA_MEMCPY_KERNEL2USER ||
memcpy_type == SG_DMA_MEMCPY_KERNEL2KERNEL) {
ret = prepare_kernel_sg_dma_buf(dev);
if (ret) {
return ret;
}
sg_dma_init_kernel_srcs(src_io->kernel_buf, src_io->kernel_buf_len);
sg_dma_init_kernel_dsts(dst_io->kernel_buf, dst_io->kernel_buf_len);
}
// 分配dma通道
ret = request_channels(dev, DMA_MEMCPY);
if (ret) {
return ret;
}
// 申请dma
if (memcpy_type == SG_DMA_MEMCPY_USER2KERNEL || memcpy_type == SG_DMA_MEMCPY_KERNEL2USER ||
memcpy_type == SG_DMA_MEMCPY_USER2USER) {
ret = alloc_user_sg_dma(dev);
if (ret) {
return ret;
}
}
if (memcpy_type == SG_DMA_MEMCPY_USER2KERNEL || memcpy_type == SG_DMA_MEMCPY_KERNEL2USER ||
memcpy_type == SG_DMA_MEMCPY_KERNEL2KERNEL) {
ret = alloc_kernel_sg_dma(dev);
if (ret) {
return ret;
}
}
src_sg = src_io->sgt.sgl;
src_sgt = &src_io->sgt;
dst_sg = dst_io->sgt.sgl;
dst_sgt = &dst_io->sgt;
flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
dma_chan = dev->chan;
dma_dev = dma_chan->device;
for (i = 0; i < src_sgt->nents; i++, src_sg = sg_next(src_sg), dst_sg = sg_next(dst_sg)) {
src_len = sg_dma_len(src_sg);
src_addr = sg_dma_address(src_sg);
dst_len = sg_dma_len(dst_sg);
dst_addr = sg_dma_address(dst_sg);
tx = dma_dev->device_prep_dma_memcpy(dma_chan,
dst_addr, src_addr, dst_len, flags);
if (!tx) {
pr_err("[%s]get dma tx descriptor failed.\n", __func__);
ret = -1;
break;
}
dev->done = false;
tx->callback = dma_memcpy_callback;
tx->callback_param = dev;
cookie = tx->tx_submit(tx);
if (dma_submit_error(cookie)) {
pr_err("[%s]dma submit error.\n", __func__);
ret = -1;
break;
}
dma_async_issue_pending(dma_chan);
wait_event_freezable_timeout(dev->done_wait, dev->done, msecs_to_jiffies(dev->timeout));
status = dma_async_is_tx_complete(dma_chan, cookie, NULL,
NULL);
if (!dev->done) {
pr_err("[%s]dma submit timeout.\n", __func__);
ret = -1;
break;
}
if (status != DMA_COMPLETE) {
pr_err("[%s]dma no complete.\n", __func__);
ret = -1;
break;
}
}
pr_info("[%s]release\n", __func__);
dmaengine_terminate_sync(dma_chan);
char_user_sgdma_release(dev);
char_kernel_sgdma_release(dev);
release_kernel_sg_dma_buf(dev);
dma_release_channel(dma_chan);
// 要释放掉dma映射才能校验通过,是因为cache一致性问题,调用dma_unmap_sg可以使cache和内存重新刷新,使其保持一致
if (!ret) {
// 验证数据 只验证内核缓冲区的数据
if (memcpy_type == SG_DMA_MEMCPY_KERNEL2USER || memcpy_type == SG_DMA_MEMCPY_KERNEL2KERNEL) {
// pr_info("[%s]: verifying source buffer...\n", __func__);
ret = dmatest_verify(src_io->kernel_buf, src_io->kernel_buf_len, PATTERN_SRC | PATTERN_COPY);
}
if (memcpy_type == SG_DMA_MEMCPY_USER2KERNEL || memcpy_type == SG_DMA_MEMCPY_KERNEL2KERNEL) {
// pr_info("[%s]: verifying dest buffer...\n", __func__);
ret = dmatest_verify(dst_io->kernel_buf, dst_io->kernel_buf_len, PATTERN_SRC | PATTERN_COPY);
}
}
return ret;
}
以上代码大部分都是参考了内核驱动kernel/drivers/dma/dmatest.c,以及Xilinx的xdma驱动(dma_ip_drivers-2019.2/XDMA/linux-kernel/xdma)代码,并在此基础上修改而来的。所以说很多轮子都不需要自己造,你直接参考拿过来用就行,前提是你能完全看懂它们hahhh.....,这需要一定的功底。
完整的代码可以通过以下链接免费获取,免费!免费!不需要你的积分或者钱!其中包括内核的驱动代码,用户层的测试代码。
https://download.csdn.net/download/slov8/89668736
2.2、遇到的问题
还是遇到了Cache一致性问题。
现象:在dma传输结束之后,释放dma映射之前,进行数据校验dmatest_verify会失败。
原因:在DMA传输结束后,在调用dma_unmap_sg之前,dcache和内存的数据是不一致的,所以cpu读取的数据是旧的数据。所以一定要在dma_unmap_sg之后,相应的dcache会被置无效,cpu从cache读到数据才是正确的。
解决方法:在调用dma_unmap_sg之后,cpu再去操作数据。
3、总结
流式dma适用于在已经分配好内存的情况下,再进行dma操作,cache的一致性问题由流式dma的API函数保证。
使用流式dma映射保证cache一致性的前提是在dma传输结束之后,还要把dma映射释放掉,cpu再去访问相应的数据缓冲区。
4、参考资料
linux kernel-4.19的内核驱动代码kernel/drivers/dma/dmatest.c
Xilinx的xdma驱动(dma_ip_drivers-2019.2/XDMA/linux-kernel/xdma)代码
《Linux设备驱动开发详解-基于最新的Linux4.0内核》---宋宝华编著