第11章 内存与IO访问之DMA

11.6 DMA(直接存储器访问

    DMA是一种无须CPU参与就可以让外设和系统内存之间进行双向数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率(bit/s)。DMA通常与硬件体系结构,特别是外设的总线技术密切相关。

    DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后处理。

11.6.1 DMA与Cache一致性

    Cache被用作CPU针对内存的缓存,利用程序的空间局部性和时间局部性原理,达到较高的命中率,从而避免CPU每次都必须要与相对慢速的内存交互数据来提高数据的访问速率。DMA可以作为内存与外设之间传输数据的方式,在这种传输方式下,数据并不需要经过CPU中转。

    假设DMA针对内存的目的地址Cache缓存的对象没有重叠区域(如图11.12所示),DMA和Cache之间将相安无事。如果DMA的目的地址与Cache所缓存的内存地址访问有重叠(如图11.13所示),经过DMA操作,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,它仍认为Cache中的数据就是内存中的数据,在以后访问Cache映射的内存时,它仍使用陈旧的Cache数据。就会发生Cache与内存之间数据“不一致性”的错误。


图11.12 DMA目的地址与Cache对象没有重叠


图11.13 DMA目的地址与Cache对象有重叠

    Cache数据与内存数据的不一致性,指在采用Cache的系统中,同样一个数据可能既存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,数据若不一样则具有不一致性。

   备注: 在发生Cache与内存不一致性错误后,驱动将无法正常运行。Cache的不一致性问题并不是只发生在DMA的情况下,还存在于Cache使能和关闭的时刻。例如,对于带MMU功能的ARM处理器,在开启MMU之前,需先置Cache无效,对于TLB(转换旁路缓存),也是如此。

11.6.2 Linux下的DMA编程

    DMA本身不属于一种等同于字符设备、块设备和网络设备的外设,DMA只是一种外设与内存交互数据的式。

内存中用于与外设交互数据的一块区域称为DMA缓冲区,在设备不支持scatter/gather(分散/聚集)操作的情况下,DMA缓冲区在物理上必须是连续的。

1.DMA区域

对于x86系统的ISA设备来说,DMA操作只能在16MB以下的内存中进行,在使用kmalloc()、__get_free_pages()及其类似函数申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存位DMA区域中,并具备DMA能力。

在内核中定义__get_free_pages()针对DMA的“快捷方式”__get_dma_pages(),它在申请标志中添加了GFP_DMA,如下所示:

<linux/gfp.h>

#define _ _get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA,(order))

    如果不想使用order为参数申请DMA内存,可以使用函数dma_mem_alloc(),其源代码如代码清单11.13所示。

arch/x86/include/asm/floppy.h

static unsigned long dma_mem_alloc(unsigned long size)

{

        int order = get_order(size); /* 大小 -> 指数 */

return __get_dma_pages(GFP_KERNEL|__GFP_NORETRY, order);
}

    对于大多数现代嵌入式处理器来说,DMA操作可以在整个常规内存区域进行,因此DMA区域就直接覆盖了常规内存。

2.虚拟地址、物理地址和总线地址

    基于DMA的硬件使用的是总线地址,总线地址是从设备角度看到的内存地址,物理地址是从CPU、 MMU控制器外围角度看到的内存地址,虚拟地址是从CPU核角度看到的内存地址。虽然在PC上,对于ISA和PCI(外围设备互联总线)来说,总线地址即为物理地址,但并不是每个平台都是如此。因为有时接口总线通过桥接电路连接,桥接电路会将I/O地址映射为不同的物理地址。还有一些系统提供了页面映射机制,它能将任意的页面映射为连续的外设总线地址。

内核提供如下函数以进行简单的虚拟地址/总线地址转换:

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

使用IOMMU或反弹缓冲区的情况下,上述函数一般不会正常工作。这两个函数并不建议使用。如图11.14所示,IOMMU的工作原理与CPU内的MMU非常类似,不过它针对的是外设总线地址和内存地址之间的转化。由于IOMMU可以使得外设DMA引擎看到“虚拟地址”,因此在使用IOMMU的情况下,在修改映射寄存器后,可以使得SG(分散/聚集)中分段的缓冲区地址对外设变得连续。


    图11.14 MMU与IOMMU

3.DMA地址掩码

    设备并不一定能在所有的内存地址上执行DMA操作,这种情况下应该通过下列函数执行DMA地址掩码:

arch/arm64/include/asm/dma-mapping.h

int dma_set_mask(struct device *dev, u64 mask);

例如,对于只能在24位地址上执行DMA操作的设备,就应该调用dma_set_mask(dev,0xffffff)。该API本质上就是修改device结构体中的dma_mask成员,如ARM64平台的定义为:

static inline int dma_set_mask(struct device *dev, u64 mask)
{
        if (!dev->dma_mask || !dma_supported(dev, mask))
                return -EIO;
        *dev->dma_mask = mask;

        return 0;

}

#include <linux/device.h>

struct device {
        struct device           *parent;

        struct device_private   *p;

        struct kobject kobj;
        const char              *init_name; /* initial name of the device */
        const struct device_type *type;

        struct mutex            mutex;  /* mutex to synchronize calls to
                                         * its driver.
                                         */
        struct bus_type *bus;           /* type of bus device is on */
        struct device_driver *driver;   /* which driver has allocated this
                                           device */
        void            *platform_data; /* Platform specific data, device
                                           core doesn't touch it */
        void            *driver_data;   /* Driver data, set and get with
                                           dev_set/get_drvdata */
        struct dev_pm_info      power;
        struct dev_pm_domain    *pm_domain;

#ifdef CONFIG_GENERIC_MSI_IRQ_DOMAIN
        struct irq_domain       *msi_domain;
#endif
#ifdef CONFIG_PINCTRL
        struct dev_pin_info     *pins;
#endif

#ifdef CONFIG_NUMA
        int             numa_node;      /* NUMA node this device is close to */
#endif
        u64             *dma_mask;      /* dma mask (if dma'able device) */
        u64             coherent_dma_mask;/* Like dma_mask, but for
                                             alloc_coherent mappings as
                                             not all hardware supports
                                             64 bit addresses for consistent
                                             allocations such descriptors. */
        unsigned long   dma_pfn_offset;

        struct device_dma_parameters *dma_parms;

        struct list_head        dma_pools;      /* dma pools (if dma'ble) */

        struct dma_coherent_mem *dma_mem; /* internal for coherent mem
                                             override */
#ifdef CONFIG_DMA_CMA
        struct cma *cma_area;           /* contiguous memory area for dma
                                           allocations */
#endif
        struct removed_region *removed_mem;
        /* arch specific additions */
        struct dev_archdata     archdata;

        struct device_node      *of_node; /* associated device tree node */
        struct acpi_dev_node    acpi_node; /* associated ACPI device node */

        dev_t                   devt;   /* dev_t, creates the sysfs "dev" */
        u32                     id;     /* device instance */

        spinlock_t              devres_lock;
        struct list_head        devres_head;

        struct klist_node       knode_class;
        struct class            *class;
        const struct attribute_group **groups;  /* optional groups */

        void    (*release)(struct device *dev);
        struct iommu_group      *iommu_group;

        bool                    offline_disabled:1;
        bool                    offline:1;
};

在device结构体中,dma_mask是设备DMA可以寻址的范围,coherent_dma_mask作用于申请一致性的DMA缓冲区。

4.一致性DMA缓冲区

    DMA映射包括两个方面的工作:分配一片DMA缓冲区;为这片缓冲区产生设备可访问的地址。同时,DMA映射也必须考虑Cache一致性问题。内核中提供如下函数以分配一个DMA一致性的内存区域:

void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle,gfp_t gfp);

该函数的返回值为申请到的DMA缓冲区的虚拟地址,此外,该函数还通过参数handle返回DMA缓冲区的总线地址。handle的类型为dma_addr_t,代表的是总线地址。

dma_alloc_coherent()申请一片DMA缓冲区,以进行地址映射并保证该缓冲区的Cache一致性。与
dma_alloc_coherent()对应的释放函数为:

void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,dma_addr_t handle);

用于分配一个写合并(Writecombining)的DMA缓冲区的函数:

void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);

与dma_alloc_writecombine()对应的释放函数dma_free_writecombine()实际上就是
dma_free_coherent(),它定义为:

#define dma_free_writecombine(dev,size,cpu_addr,handle) \
         dma_free_coherent(dev,size,cpu_addr,handle)

Linux内核还提供了PCI设备申请DMA缓冲区的函数pci_alloc_consistent(),

include/asm-generic/pci-dma-compat.h

其原型为:

void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp);

对应的释放函数为pci_free_consistent(),其原型为:

void pci_free_consistent(struct pci_dev *hwdev, size_t size, void *vaddr, dma_addr_t dma_handle);

备注:

dma_alloc_xxx()函数虽然是以dma_alloc_开头的,但是其申请的区域不一定在DMA区域里面。

在使用ARM等嵌入式Linux系统的时候,GPU、Camera、HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着。


猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/80453673