没有IOMMU的arm64架构嵌入式设备上,跟了下代码执行流程,记录了一些DMA相关知识点。 有点流水账。
- 有三种方式分配CMA内存(dts/内核传参/系统默认),CMA预留用的memblock接口,时间点早于DMA初始化时间。
- DMA首选从CMA分配,没有的话__get_free_pages动态获取。需要打开配置项CONFIG_DMA_CMA,DMA才可以利用CMA分配内存。
- 一致性是通过配置内存页表属性保证的,这个属性值MT_NORMAL_NC,NC表示non-cached,普通内存,不经缓存。
- swiotlb的作用在于,使得寻址能力较低,无法直接寻址到内核所分配的DMA buffer的那些设备,也能够进程DMA操作。
dma_mask
dev->dma_mask 定义了设备可访问的最大物理地址
默认值全FF,代表所有物理地址都有dma能力,具体寻址能力驱动根据实际情况调整。
dev->dma_mask = 0xFFFFFFFFFFFFFFFF;
如果提供的虚拟地址转换后拿到的物理地址phys超过了dma_mask,
那么走bounce buffer,map_single 返回的内存地址就不固定
static inline bool dma_capable(struct device *dev, dma_addr_t addr, size_t size)
{
if (!dev->dma_mask)
return false;
return addr + size - 1 <= *dev->dma_mask;
}
754 dma_addr_t swiotlb_map_page(struct device *dev, struct page *page,
755 unsigned long offset, size_t size,
756 enum dma_data_direction dir,
757 struct dma_attrs *attrs)
758 {
759 phys_addr_t map, phys = page_to_phys(page) + offset;
760 dma_addr_t dev_addr = phys_to_dma(dev, phys);
761
762 BUG_ON(dir == DMA_NONE);
763 /*
764 * If the address happens to be in the device's DMA window,
765 * we can safely return the device addr and not worry about bounce
766 * buffering it.
767 */
768 if (dma_capable(dev, dev_addr, size) && !swiotlb_force)
769 return dev_addr; // 走这里的话,返回的物理地址就是固定的
770
771 trace_swiotlb_bounced(dev, dev_addr, size, swiotlb_force);
772
773 /* Oh well, have to allocate and map a bounce buffer. */
// 返回的物理地址不固定,走的是bounce buffer,就从有dma寻址能力的地址空间找一块可用的区域
774 map = map_single(dev, phys, size, dir);
DMA初始化流程
系统启动阶段预分配atomic dma pool,不允许休眠的分配场合会从这里拿。
arch_initcall(arm64_dma_init);
arm64_dma_init
atomic_pool_init
if (dev_get_cma_area(NULL))
page = dma_alloc_from_contiguous(NULL, nr_pages, pool_size_order);
else
page = alloc_pages(GFP_DMA, pool_size_order); // 如果没有预留cma,走这里
DMA一致性内存分配流程
dma_alloc_coherent
dma_alloc_attrs
a. dma_alloc_from_coherent // 如果存在一致性内存区域,则从这里分配;否则走swiotlb
b. ops->alloc(dev, size, dma_handle, flag, attrs); // __dma_alloc swiotlb
dma_alloc_from_coherent
mem = dev->dma_mem // 这块内存是设备驱动初始化probe的时候提前申请好的(通过dma_declare_coherent_memory预留)
if (!dev || !dev->dma_mem) // 直接返回0,由其它流程分配
bitmap_find_free_region(mem->bitmap, mem->size, order); // 从驱动指定的区域分配
__dma_alloc // 现网问题设备走swiotlb
bool coherent = is_device_dma_coherent(dev); // 驱动负责初始化
pgprot_t prot = __get_dma_pgprot(attrs, PAGE_KERNEL, false); // 页表属性保护位
pgprot_writecombine(prot);
if (!coherent && !gfpflags_allow_blocking(flags)) // 驱动没有提供,并且不允许阻塞,从dma pool中分配,实测未走这里
__alloc_from_pool(size, &page, flags);
__dma_alloc_coherent
if (dev_get_cma_area(dev) && gfpflags_allow_blocking(flags))
dma_alloc_from_contiguous
cma_alloc(dev_get_cma_area(dev), count, align); // cma区域分配
else
swiotlb_alloc_coherent(dev, size, dma_handle, flags); // 实测设备走这里
__get_free_pages(flags, order); // 启动阶段分配的内存大概率位于低数值物理地址
__dma_flush_range(ptr, ptr + size); // 刷cache
dma_common_contiguous_remap(page, size, VM_USERMAP, prot, NULL); // 建立映射,同时设置PTE保护位
397 #define pgprot_writecombine(prot) \
398 __pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN) // non-cached不经缓存
CMA初始化流程
1 drivers/base/dma-contiguous.c|138| <<dma_contiguous_reserve>> dma_contiguous_reserve_area(selected_size, selected_base,
2 include/linux/dma-contiguous.h|107| <<dma_declare_contiguous>> ret = dma_contiguous_reserve_area(size, base, limit, &cma, true);
CONFIG_DMA_CMA配置项目不开,dma_contiguous_default_area为空,驱动分配的时候只能__get_free_pages动态获取
arm64_memblock_init
early_init_fdt_scan_reserved_mem(); // 1. dts里指定cma保留区域
dma_contiguous_reserve(arm64_dma_phys_limit);
dma_contiguous_reserve_area // 必须开启 CONFIG_DMA_CMA 配置项才能预留cma,从而后续允许驱动使用dma接口从cma区域分配
cma_declare_contiguous // 实际分配内存
memblock_alloc_range
65 static inline struct cma *dev_get_cma_area(struct device *dev)
66 {
67 if (dev && dev->cma_area)
68 return dev->cma_area; // 从设备驱动指定的cma区域分配
69 return dma_contiguous_default_area; // 从系统预留的cma区域分配
70 }
指定CMA内存块的三种方式
void __init dma_contiguous_reserve(phys_addr_t limit)
{
phys_addr_t selected_size = 0;
phys_addr_t selected_base = 0;
phys_addr_t selected_limit = limit;
bool fixed = false;
pr_err("%s(limit %08lx)\n", __func__, (unsigned long)limit);
if (size_cmdline != -1) { // 2. 内核传参cma指定大小和起始地址
selected_size = size_cmdline;
selected_base = base_cmdline;
selected_limit = min_not_zero(limit_cmdline, limit);
if (base_cmdline + size_cmdline == limit_cmdline)
fixed = true;
} else { // 不走这里
#ifdef CONFIG_CMA_SIZE_SEL_MBYTES
selected_size = size_bytes;
#elif defined(CONFIG_CMA_SIZE_SEL_PERCENTAGE)
selected_size = cma_early_percent_memory();
#elif defined(CONFIG_CMA_SIZE_SEL_MIN)
selected_size = min(size_bytes, cma_early_percent_memory());
#elif defined(CONFIG_CMA_SIZE_SEL_MAX)
selected_size = max(size_bytes, cma_early_percent_memory());
#endif
}
// 3. 没有给内核传递 cma=size@start_addr的参数,并且dts里也没有linux,cma节点,那么内核自己选择一块连续内存
if (selected_size && !dma_contiguous_default_area) {
dma_contiguous_reserve_area(selected_size, selected_base,
selected_limit,
&dma_contiguous_default_area,
fixed);
}
}
swiotlb dma map
嵌入式设备4G内存,实际上分成两块2G DDR,物理地址空间并不连续
一块0xFFFFFFFF
4G以下,一块位于0x400000000
16G以上。
驱动申请数据缓冲区时最好还是记得加上DMA标记,直接从有DMA寻址能力的低物理地址处分配内存
这样就跳过swiotlb的处理逻辑,省掉了memcpy
否则有可能从高物理地址分配,而该地址空间位于设备DMA寻址能力范围之外,还会走swiotlb流程。
dma_map_single
__swiotlb_map_page
swiotlb_map_page
map_single
swiotlb_tbl_map_single
swiotlb_bounce(orig_addr, tlb_addr, size, DMA_TO_DEVICE);
} else if (dir == DMA_TO_DEVICE) {
// 从非DMA内存区域(驱动__get_free_page区域,高物理地址)拷贝到DMA内存区域(低物理地址)
memcpy(vaddr, phys_to_virt(orig_addr), size);
} else { // DMA_FROM_DEVICE
// 从DMA内存区域(低物理地址)拷贝到非DMA内存区域(驱动__get_free_page区域,高物理地址)
memcpy(phys_to_virt(orig_addr), vaddr, size);
}
__dma_map_area // flush cache
dma_unmap_single
__dma_unmap_area // 刷cache
__swiotlb_unmap_page
swiotlb_unmap_page
unmap_single
swiotlb_tbl_unmap_single
swiotlb_bounce // 根据入参方向direction,非DMA内存和DMA内存之间拷贝
dma_sync_single_for_cpu
__swiotlb_sync_single_for_cpu
__dma_unmap_area // flush cache
swiotlb_sync_single_for_cpu
swiotlb_sync_single
swiotlb_bounce // 根据入参方向direction,非DMA内存和DMA内存之间拷贝
dma_sync_single_for_device
__swiotlb_sync_single_for_device
swiotlb_sync_single_for_device
swiotlb_sync_single
swiotlb_bounce // 根据入参方向direction,非DMA内存和DMA内存之间拷贝
__dma_unmap_area // flush cache