Arm linux dma mapping
由于处理器存在cache,cache和内存中数据可能不一致,所以驱动在使用dma在内存和device之间搬移数据前后需要cpu对cache和内存中数据进行同步。有些dma寻址能力有限,比如只能寻址内存低128m,但数据在内存的1G地址处,这时需要进行数据转移。
cpu读取数据时先查询l1 cache,如果没有再向l2 cache
查询,如果没有在向内存查询,然后把数据从ddr 内存搬移到l2 cache和l1
cache。Cpu写数据,改变了l1 cache中内容,这时l1 cache可能把数据写会到ddr
内存,也可能过段时间再写。两个l1 cache和l2
cache之间数据同步,arm中按照moesi协议同步,这里不进行介绍。
基本概念:
PoU:在单核中在某一存储层级上,指令cache,数据cache和TLB在某一点上能够看到一致内容或相同copy,称为Pou;或者在an
Inner Shareable shareability
domain中,指令cache,数据cache和TLB在某一点上能够看到一致内容或相同copy,称为Pou;
PoC:系统中所有agents(系统中master,或者observer,例如,cpu,dma)看到的memory
一致点, 称为PoC.
例如,图中CPU core0和CPU core1为一组cluster0,CPU core2和CPU
core3为一组cluster1,
L2 cache为PoU,DDR memory为PoC。
Clean:把cache中指定地址内容写会到PoU或PoC。
Invalidate:
使cache中指定地址内容无效,如果cache中修改没有写回内存,那么这次修改会丢失。
Clean and invalidate:clean指令后执行invalidate指令。
写回(write back):修改了cache中内容后,并不把更新立即写到内存,把修改的cache
line标记为dirty。但执行clean操作时,把cache修改内容写的内存,或者,替换cache
line时,把cache修改内容写的内存。
写直通(write through):修改了cache中内容后,立即写到内存。
流式 DMA 和一致性 DMA
在一致性 DMA 映射中,它采用的是系统预留的一段 DMA 内存用于 DMA 操作,这一段内核在系统启动阶段就已经预留完毕,比如 arm64 平台会在 dts 文件中写明系统预留的 DMA 内存段位于何处,并且会被标志为用于 dma 一致性内存申请,如果你有关注 DMA 的一致性映射操作 API 就会发现,一致性 DMA 不会去使用别的地方申请的内存,它都是通过dma_alloc_coherent自我申请内存,然后驱动自己填充数据最后被提交给 DMA 控制器。
流式 DMA 中,它可以是随意的内存交给 DMA 进行处理,不需要从系统预留的 DMA 位置进行内存申请,任何普通的 kmalloc 申请的内存都能交给 DMA 控制器进行操作。
二者是如何做到缓存一致性的:
一致性 DMA ,在 DMA 内存申请的过程中,首先进行一个 ioremap_nocache 的映射,然后调用函数 dma_cache_wback_inv 保证缓存已经刷新到位之后,后面使用这一段内存时不存在一二级缓存;
流式 DMA ,不能直接禁止缓存,因为流式 DMA 可以使用系统中的任意地址范围的地址,CPU 总不能将系统所有的地址空间都禁止缓存,这不科学,那么为了实现缓存一致性,流式 DMA 需要不断的对缓存进行失效操作,告诉 CPU 这一段缓存是不可信的,必须从内存中重新获取。一致性 DMA 就是直接将缓存禁止,而流式 DMA 则是将缓存失效刷新。
cpu执行clean指令,把cache中写到内存
cpu配置dma,dma搬移内存中数据到device
dma搬移device数据到内存
cpu执行 invalidate指令,使cache中内容无效;
cpu处理数据,由于之前设置了cache无效,所以会从内存读取数据到cache
swio技术,当dma寻址能力小于cpu分配的物理地址时,swio技术会从低端地址,比如0-64m中分配一块物理地址,建立低端物理地址和cpu分配的物理地址映射,并且后续dma使用低端dma地址进行数据搬移,如果是dma从物理内存搬移数据到设备
(DMA TO DEVICE
),那么cpu先把数据从cpu分配物理地址复制到低端物理地址,后续dma从低端物理地址搬移到设备。如果是dma从设备搬移数据到物理内存(DMA
FROM DEVICE
),那么dma从设备搬移到低端物理地址,然后通知cpu,cpu从低端物理地址复制数据到原高端物理地址,
但如果dma寻址能力够,就不需要这一步。
\arch\arm64\mm\dma-mapping.c 中设置dma_ops
static int \__init arm64_dma_init(void)
{
int ret = 0;
ret \|= swiotlb_late_init();
ret \|= atomic_pool_init();
return ret;
}
arch_initcall(arm64_dma_init);
static int \__init swiotlb_late_init(void)
{
size_t swiotlb_size = min(SZ_64M, MAX_ORDER_NR_PAGES \<\< PAGE_SHIFT);
/\*
\* These must be registered before of_platform_populate().
\*/
bus_register_notifier(&platform_bus_type, \&platform_bus_nb);
注册notifier,当有bus注册到系统时,会回调这个bus_nb函数
bus_register_notifier(&amba_bustype, \&amba_bus_nb);
dma_ops = \&noncoherent_swiotlb_dma_ops; 设置了noncoherent ops
return swiotlb_late_init_with_default_size(swiotlb_size);
}
static int dma_bus_notifier(struct notifier_block \*nb,
unsigned long event, void \*_dev)
{
struct device \*dev = \_dev;
if (event != BUS_NOTIFY_ADD_DEVICE)
return NOTIFY_DONE;
如果dts中指定了bus dma是dma-coherent,那么设置ops为coherent_swiotlb_dma_ops。
if (of_property_read_bool(dev-\>of_node, "dma-coherent"))
set_dma_ops(dev, \&coherent_swiotlb_dma_ops);
return NOTIFY_OK;
}
看一下noncoherent dma ops
struct dma_map_ops noncoherent_swiotlb_dma_ops = {
.alloc = \__dma_alloc_noncoherent,
.free = \__dma_free_noncoherent,
.mmap = \__swiotlb_mmap_noncoherent,
.map_page = \__swiotlb_map_page,
.unmap_page = \__swiotlb_unmap_page,
.map_sg = \__swiotlb_map_sg_attrs,
.unmap_sg = \__swiotlb_unmap_sg_attrs,
.sync_single_for_cpu = \__swiotlb_sync_single_for_cpu,
.sync_single_for_device = \__swiotlb_sync_single_for_device,
.sync_sg_for_cpu = \__swiotlb_sync_sg_for_cpu,
.sync_sg_for_device = \__swiotlb_sync_sg_for_device,
.dma_supported = swiotlb_dma_supported,
.mapping_error = swiotlb_dma_mapping_error,
};
EXPORT_SYMBOL(noncoherent_swiotlb_dma_ops);
Cpu会调用dma_map_single
例如:usb驱动收发数据
Cpu会调用dma_map_single进行clean或invalid
int usb_gadget_map_request(struct usb_gadget \*gadget,
struct usb_request \*req, int is_in)
{
req-\>dma = dma_map_single(&gadget-\>dev, req-\>buf, req-\>length,
is_in ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
DMA_TO_DEVICE从内存搬移数据到device,DMA_FROM_DEVICE从device搬移数据到内存
return 0;
}
dma_map_single—》dma_map_single_attrs—》dma_map_single—》ops-\>map_page-》__swiotlb_map_page
static dma_addr_t \__swiotlb_map_page(struct device \*dev, struct page \*page,
unsigned long offset, size_t size,
enum dma_data_direction dir,
struct dma_attrs \*attrs)
{
dma_addr_t dev_addr;
dev_addr = swiotlb_map_page(dev, page, offset, size, dir, attrs);
swio技术,这里不再介绍,认为dma寻址能力足够;
这里先进行了dma地址和cpu内存物理地址,cpu虚拟地址转换
这里dma地址和cpu内存物理地址相同。这个由soc总线设计决定。
Cpu看到的内存地址和dma看到的内存地址可能不同。
\__dma_map_area(phys_to_virt(dma_to_phys(dev, dev_addr)), size, dir);
return dev_addr;
}
\\arch\\arm64\\mm\\cache.S
/\*
\* \__dma_map_area(start, size, dir)
\* - start - kernel virtual start address
\* - size - size of region
\* - dir - DMA direction
\*/
ENTRY(__dma_map_area)
add x1, x1, x0
cmp w2, \#DMA_FROM_DEVICE 来自设备,就是从设备搬移数据到ap ddr中。
b.eq \__dma_inv_range
走这里,所以要先使cache中数据无效,后续dma搬移后,从ddr中直接读取到cache中。
b \__dma_clean_range 接着把cache中内容刷到内存中,
clean。确保之前cache修改内容,写回内存。
实际这里有没有必要执行,可以根据场景进行优化。
\__dma_inv_range: invalid就是使cache中内容无效,下次使用时需要从内存中重新读取
dcache_line_size x2, x3
sub x3, x2, \#1
tst x1, x3 // end cache line aligned?
bic x1, x1, x3
b.eq 1f
dc civac, x1 // clean & invalidate D / U line这里检查结束地址是否cache line
aligned,如果不对齐,需要把cacheline数据写到内存。因为cache
是按line处理,否则其它(非本次dma搬移地址)地址数据也变为无效,导致数据无法写会内存。
1: tst x0, x3 // start cache line aligned?
bic x0, x0, x3
b.eq 2f
dc civac, x0 // clean & invalidate D / U line 同上,起时地址
b 3f
2: dc ivac, x0 // invalidate D / U line 使无效
3: add x0, x0, x2
cmp x0, x1
b.lo 2b
dsb sy
ret
ENDPROC(__inval_cache_range)
ENDPROC(__dma_inv_range)
/\*
\* \__dma_clean_range(start, end)
\* - start - virtual start address of region
\* - end - virtual end address of region
\*/
\__dma_clean_range:
dcache_line_size x2, x3
sub x3, x2, \#1
bic x0, x0, x3
1: dc cvac, x0 // clean D / U line
add x0, x0, x2
cmp x0, x1
b.lo 1b
dsb sy
ret
ENDPROC(__dma_clean_range)
上面用到的invalidate指令,都是到PoC的。civac ,ivac
DC CIVAC, Xt Clean and invalidate by virtual address to Point of Coherency
DC IVAC, Xt Invalidate by virtual address to Point of Coherency
void usb_gadget_unmap_request(struct usb_gadget \*gadget,
struct usb_request \*req, int is_in)
{
if (req-\>length == 0)
return;
if (req-\>num_mapped_sgs) {
dma_unmap_sg(&gadget-\>dev, req-\>sg, req-\>num_mapped_sgs,
is_in ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
req-\>num_mapped_sgs = 0;
} else {
dma_unmap_single(&gadget-\>dev, req-\>dma, req-\>length,
is_in ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
}
}
上面map_page,然后dma搬移,现在进行unmap_page
static void \__swiotlb_unmap_page(struct device \*dev, dma_addr_t dev_addr,
size_t size, enum dma_data_direction dir,
struct dma_attrs \*attrs)
{
\__dma_unmap_area(phys_to_virt(dma_to_phys(dev, dev_addr)), size, dir);
先进行cache操作
swiotlb_unmap_page(dev, dev_addr, size, dir, attrs); swio技术
}
/\*
\* \__dma_unmap_area(start, size, dir)
\* - start - kernel virtual start address
\* - size - size of region
\* - dir - DMA direction
\*/
ENTRY(__dma_unmap_area)
add x1, x1, x0
cmp w2, \#DMA_TO_DEVICE 如果是写数据到device,这里不需要再操作,
b.ne \__dma_inv_range 从device搬移到内存走这里,再次invalid
cache,防止dma搬移期间,cpu操作过这段地址或相邻地址,会导致这块地址被读到cache中。 特别是dma搬移的地址不是cache
line对齐场景。
ret
ENDPROC(__dma_unmap_area)