dma

dma & cma

dma misc knowledge points

Posted by icecube on March 11, 2025

没有IOMMU的arm64架构嵌入式设备上,跟了下代码执行流程,记录了一些DMA相关知识点。 有点流水账。

  1. 有三种方式分配CMA内存(dts/内核传参/系统默认),CMA预留用的memblock接口,时间点早于DMA初始化时间。
  2. DMA首选从CMA分配,没有的话__get_free_pages动态获取。需要打开配置项CONFIG_DMA_CMA,DMA才可以利用CMA分配内存。
  3. 一致性是通过配置内存页表属性保证的,这个属性值MT_NORMAL_NC,NC表示non-cached,普通内存,不经缓存。
  4. 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以下,一块位于0x40000000016G以上。

驱动申请数据缓冲区时最好还是记得加上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  

参考索引

linux-4.4
DMA Mapping API