Linux设备驱动程序之内存与I/O访问

“小王,今天咱们开始讲有关内存和I/O访问的内容,心里先要有点低,这部分内容还是有点烦,有点难的哦”说着话,我心里都没底,怕吓着小王,不瞒你说,当时看这部分,我可是没少费劲。

“哦,那咋办,不能不学是不?没事,有小涛哥在,俺就不怕”小王信心十足的说。

“哦!看不出来,还让你来安慰我了”看着小王这充满信心的样子,我也没啥顾虑了。好了,深吸一口气,开始今天的课程。

   我们知道,在X86中,有I/O空间的概念,I/O空间是相对于内存空间的概念,它通过特定的指令in,out来访问。端口号标识了外设的寄存器地址。而巧的是Arm等嵌入式控制器中并不提供I/O空间,所以我们就不考虑了,我们重点放在内存空间。

   内存空间可以通过地址,指针来访问,在C语言中的表现就是通过指针来操作,如在186处理器中:

   unsigned char *p = (unsigned char *)0xF000FF00;

   *P=11;

   这段代码的意思就是:在绝对地址0xF000+0xFF00(186处理器使用16位段地址和16位偏移地址)写入11。在ARM, PPC等未采用段地址的处理器中,P指向的内存

空间就是0xF000FF00,而*p=11就是在该地址写入11。

   说完内存空间,就不得不说说MMU(内存管理单元),它辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射,内存访问权限保护和Cache缓存控制等硬件支持,操作系统内核借助MMU,可以让用户感觉好像程序可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中物理内存的实际容量。

   为了理解基本的MMU操作原理,先介绍几个概念:

   1)TLB:Translation Lookaside Buffer,即转换旁路缓存。也称快表,是转换表的cache,缓存少量的虚拟地址与物理地址的对应关系。

   2)TTW:Translation Table walk,即转换表漫游。当TLB中没有缓冲对应的地址转换关系时,需要通过对内存中转换表的访问来获得虚拟地址和物理地址的对应关系。TTW成功后,结果应写入TLB.

   为了说明MMU在访问内存中的使用,我特意画了一个流程图。如下:

   

   说了MMU,现在就来和linux联系一下,对于包含MMU的处理而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB,这4GB是分为2个部

分---用户空间和内核空间,前者一般分布为0~3GB(即PAGE_OFFSET,在0x86中等于0xC0000000),剩下的3~4GB是内核空间。通常情况下,用户进程只有通过系统调用等方式才可以访问到内核空间。

   小王你学过C语言,知道在用户空间动态申请内存用malloc()函数,释放用free,这在各种操作系统上的使用是一致的。这方面的内容,比如内存泄漏啦,具体使用等就不细讲了,我们的重点放在内核空间,内核空间怎么做呢?

   前边有个知识点没说,后面要用到,就提一下:linux内核空间3~4GB是还可以在分的,从低到高依次是:物理内存映射区->隔离带->vmalloc虚拟内存分配器->隔

离带->高端内存映射区->专用页面影视区->保留区

   在Linux内核空间中申请内存涉及的函数主要包括kmalloc(),__get_free_pages()【这两个申请的内存位于物理内存映射区在物理内存上也是连续的,和物理内存有简单的转换关系】和vmalloc【它是在虚拟内存空间给出一块连续的内存区,在物理内存中不一定是联系的,和物理内存也没有简单的换算关系】等。有关这三个函数的使用网上一大堆,我就我细说了,小王,你的我都给你打印好了,你直接看就行了。

  我们说虚拟地址和物理地址是有一定的转换关系,具体是使用virt_to_phys()可以实现虚拟地址转换为物理地址,代码清单如下:

  #define _ _pa(x)  ((unsigned long)(x)-PAGE_OFFSET)  extern inline unsigned long virt_to_phys(volatile void * address)  {    return _ _pa(address);  }

   与之对应的函数是phys_to_virt(),用于将物理地址转化为虚拟地址。具体代码如下:

   #define  _ _pa(x)      ((unsigned long)(x)+PAGE_OFFSET)

   extern inline unsigned long virt_to_phys(volatile void * address)

   {

        return _ _pa (address);

   }  

   值得说明的是上述方法仅适用与常规内存,高端内存的虚拟地址与物理地址之间不存在这样简单的换算关系。

“小王不知道,你听的怎么样,我说的时候心里都没底啊,这一章确实很难,挺那个的..”

“小涛哥,没事,你放心讲吧,经过那么多,我觉得自己应该有一定能力了,理解起来应该能跟上,如果跟不上再找你哈”

“小涛哥,咱们说Linux设备驱动程序说了那么久,怎么从来不说实际设备呢,顶多就说了下内存,总感觉驱动程序是和设备分离的,怎么关联起来..”小王思索着。

“不错,这也正是这次讲课的内容,设备I/O端口与I/O内存的访问”我啊,禁不住拍拍她的头。

  对于一块实际的设备而言,通常会提供一组寄存器来用于控制设备,读写设备和获取设备状态,也就是我们常说的控制寄存器,数据寄存器和状态寄存器。这些寄存器可能位于I/O空间(这时叫做I/O端口),也可能位于内存空间(对应的内存空间被成为I/O内存)。在Linux中提供了一系列的I/O端口和I/O内存操作的接口如下:

  1)I/O端口操作:在Linux设备驱动中,应使用Linux内核提供的函数来访问定位于I/O空间的端口,包括一下几种:

   *读写字节端口(8位宽)

     unsigned inb(unsigned port);【读】          voi outb(unsigned char byte, unsigned port);【写】

   *读写字端口(16位宽)

     unsigned inw(unsigned port);【读】          voi outw(unsigned short word, unsigned port);【写】

   *读写长字端口(32位宽)

     unsigned inl(unsigned port);【读】          voi outl(unsigned longword, unsigned port);【写】

   *读写一串字节

     unsigned insb(unsigned port, void *addr, unsigned long count);【读】      voi outsb(unsigned port, void *addr, unsigned long count);【写】

   *读写一串字

     unsigned insw(unsigned port, void *addr,unsigned long count);【读】      voi outsb(unsigned port, void *addr, unsigned long count);【写】

   *读写一串长字

     unsigned insl(unsigned port, void * addr, unsigned long count);【读】      voi outsb(unsigned port, void *addr, unsigned long count);【写】

    说明:上述各函数中I/O端口port的类型长度依赖与具体的硬件平台,所以只是写出了unsigned

  2)I/O内存:在内核中访问I/O内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址。

   *ioremap()原型:void *ioremap(unsigned long offset, unsigned long size);

     它返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围。用它返回的虚拟地址应该使用iounmap()函数释放。

   *iounmap()原型:void iounmap(void *addr);

   现在,有了物理地址锁映射出来的虚拟地址后,我们就可以通过c指针来直接访问这些地址,但Linux内核也提供了一组函数来完成这中虚拟地址的读写。如下

   *读IO内存

     unsigned int ioread8(void *addr);      unsigned int ioread16(void *addr);     unsigned int ioread32(void *addr);  与之对应的较早版本是:

     unsigned readb(address);                unsigned readw(address);                 unsigned readl(address);  这些在2.6的内核中依然可以使用。

   *写IO内存

     void iowrite8(u8 value,void *addr);  void iowrite16(u16 value, void *addr);   void iowrite32(u32 value, void *addr);  与之对应的较早版本是:

     void writeb(unsigned value, address); void writew(unsigned value,address);  void writel(unsigned value,address);  2.6的内核中依然可以使用。

   *读一串IO内存                                                                          *写一串IO内存

     void ioread8_rep(void *addr, void *buf, unsigned long count);       void iowrite8_rep(void *addr, void *buf, unsigned long count);

     void ioread16_rep(void *addr, void *buf, unsigned long count);     void iowrite8_rep(void *addr, void *buf, unsigned long count);

     void ioread32_rep(void *addr, void *buf, unsigned long count);     void iowrite8_rep(void *addr, void *buf, unsigned long count);

   *复制IO内存

     void memcpy_fromio(void *dest, void *source, unsigned int count);

     void memcpy_toio(void *dest, void *source, unsigned int count);

   *设置IO内存

     void *ioport_map(unsigned long port, unsigned int count);

  3)把IO端口映射到内存空间

     void *ioport_map(unsigned long port, unsigned int count);   通过这个函数,可以把port开始的count个连续的IO端口重映射为一段“内存空间”。然后

                                                                                     就可以在其返回的地址上向访问IO内存一样访问这些端口,当不再需要这种映射时,调用:

     void ioport_unmap(void *addr);                                      来撤销这种映射

  4)IO端口申请

     struct resource *request_region(unsigned long first, unsigned long n, const char *name);

     这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名称,成功返回非NULL.一旦申请端口使用完成后,应当使用:

     void release_region(unsigned long start, unsigned long n);

  5)IO内存申请

    struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

    这个函数向内核申请n个内存,这些地址从first开始,name为设备的名称,成功返回非NULL,一旦申请的内存使用完成后,应当使用:

    void release_mem_region() ;          来释放归回给系统。需要说明的是这两个函数也不是必须的,但建议使用。  

6)通过以上的基础,我们就可以归纳出设备驱动访问IO端口和IO内存的步骤。

   一种方法是:直接使用IO端口操作函数:在设备打开或驱动模块被加载时申请IO端口区域,之后使用inb(),outb()等进行端口访问,最后在设备关闭或驱动被卸载时释放IO端口范围。流程如下:

   

   另外一种途径是:将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射,流程如下:

   

  上边是IO端口的访问方法,至于IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址,之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源。

流程如下:

   

 

“小王,感觉怎么样呢, 我是尽最大努力了哈”我说。

“可以,可以,感觉挺好,我觉得哈,你要是多画图,我就没问题,就像上边三个图一样”小王调皮的说。

“小王,不瞒你说,我现在是悲喜交加啊,悲的是:这最后一章,我讲的是胆颤心惊(以前自己都没学好,现在也算还账了),喜的是每讲一张,我知道离结束就近了一点,赶快把这个东西过掉,进入下一环节,那又是我牛皮吹破天的时代了”看着小王期盼和怀疑的眼神,我,昔日的风采也不见了。

“没事的,小涛哥,其实说真的,不是我安慰你哈,从开始我什么都不懂,到现在我也算个入门级的高手了,都是你一手带过来的,我已经对你推崇备至了,你就放心吧,会的你尽情教,不会的,你慢慢说,我都听你的”小王善解人意的说。

“嗯,你真好,看来我没看错你,那好,继续课程”。。。

  一般情况下,用户空间是不可能也不应该直接访问设备的,但是设备驱动程序可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。实际上,mmap()S实现了这样的一个映射过程,它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

  mmp()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE

_SIZE的倍数大小进行映射。驱动中mmp()函数原型如下:int (*mmp)(struct file *, struct vm_area_struct *);它实现的机制是建立页表,并填充VMA结构体中

vm_operations_struct指针,vm_area_struct用于描述一个虚拟内存区域。结构体如下:

struct vm_area_struct {    struct mm_struct * vm_mm; //所处的地址空间    unsigned long vm_start;//开始虚拟地址    unsigned long vm_end;//结束虚拟地址    struct vm_area_struct *vm_next;    pgprot_t vm_page_prot;  //访问权限    unsigned long vm_flags; //标志     ...    struct vm_operations_struct * vm_ops;  //操作VMA的函数集指针    unsigned long vm_pgoff; //偏移(页帧号)    struct file * vm_file;    void * vm_private_data;    ...};

其中vm_ops成员指向这个VMA的操作集,结构体定义如下:

struct vm_operations_struct {   void (*open)(struct vm_area_struct * area);   void (*close)(struct vm_area_struct * area);   struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type);   int (*populate)(struct vm_area_struct * area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);    ...};在内核生成一个VMA后,它就会调用该VMA的open()函数。
  而驱动中的mmap()函数将在用户进行mmap()系统调用时最终被调用,当用户调用mmap()时候内核会进行如下处理:
  1)在进程的虚拟空间查找一块VMA.
  2)将这块VMA进行映射。
  3)如果设备驱动程序或文件系统的file_operations定义了mmap()操作,则调用它。
  4)将这个VMA插入到进程的VMA链表中。
file_operations中mmap()函数的第一个参数就是步骤1中找的VMA.由mmap()系统调用映射的内存可由munmap()解除映射。这个函数原型如下:
int munmap(caddr_t addr, size_t len);
  但是,需要注意的是:当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就已经产生,内核却不会调用VMA的open
函数,通常需要在驱动的mmap()函数中先上调用vma->vm_ops->open().为了说明问题,给出一个vm_operations_struct操作范例:
static int xxx_mmp(struct file *filp, struct vm_area_struct *vma){    if(remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->start, vma->vm_page_prot))   //建立页表     return - EAGAIN;    vma->vm_ops = &xxx_remap_vm_ops;    xxx_vma_open(vma);    return 0;}void xxx_vma_open(struct vm_area_struct *vma) //VMA打开函数{  ...  printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %1x\n",vma->vm_start, vma->vm_pgoff 《PAGE_SHIFT);}void xxx_vma_close(struct vm_area_struct *vma)  //VMA关闭函数{  ...  printk(KERN_NOTICE "xxx VMA close. \n");}static struct vm_operation_struct xxx_remap_vm_ops = //VM操作结构体{   .open=xxx_vma_open,   .close=xxx_vma_close,   ...}
在这段代码中调用的remap_pfn_range()创建页表。我们前边说过在内核空间用kmalloc申请内存,这部分内存如果要映射到用户空间可以通过mem_map
_reserve()调用设置为保留后进行,具体怎么操作,咱们下集继续。
 
      
        
        
        
        

时间:晚上7点

地点:寝室中..

“小王,今天就不多话了,接着昨天没讲完的,不然连不起来了,都..”我催促着。

  上节讲到kmalloc()申请的内存若要被映射到用户空间可以通过mem_map_reserve()设置为保留后进行。具体怎么操作呢,给你一个模版吧:

// 内核模块加载函数int __init kmalloc_map_init(void){    ../申请设备号,添加cedv结构体  buffer = kmalloc(BUF_SIZE, GFP_KERNEL); //申请buffer  for(page = virt_to_page(buffer); page< virt_to_page(buffer+BUF_SIZE); page++)  {     mem_map_reserve(page);  //置业为保留  }}//mmap()函数static int kmalloc_map_mmap(struct file *filp, struct vm_area_struct *vma){    unsigned long page, pos;    unsigned long start = (unsigned long)vma->start;    unsigned long size = (unsigned long)(vma->end - vma->start);    printk(KERN_INFO, "mmaptest_mmap called\n");    if(size > BUF_SIZE)  //用户要映射的区域太大        return - EINVAL;    pos = (unsigned long)buffer;    while(size > 0)   //映射buffer中的所有页    {        page = virt_to_phys((void *)pos);        if(remap_page_range(start, page, PAGE_SIZE, PAGE_SHARRED))            return -EAGAIN;        start += PAGE_SIZE;        pos +=PAGE_SIZE;        size -= PAGE_SIZE;    }    return 0;}

另外通常,IO内存被映射时需要是nocache的,这个时候应该对vma->vm_page_prot设置nocache标志。如下:

static int xxx_nocache_mmap(struct file *filp, struct vm_area_struct *vma){  vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);   //赋nocache标志  vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);  if(rempa_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot));     return - EAGGIN;  return 0;}

这段代码中的pgprot_noncached()是一个宏,它实际上禁止了相关页的cache和写缓冲(write buffer),另外一个稍微少的一些限制的宏是:

#define pgprot_writecombine(prot)  __pgprot(pgprot_val (prot) & –L_PTE_CACHEABLE);    它则没有禁止写缓冲

而除了rempa_pfn_range()外,在驱动程序中实现VMA的nopage()函数通常可以为设备提供更加灵活的内存映射途径。当发生缺页时,nopage()会被内核自动调用,。这是因为,当发生缺页异常时,系统会经过如下处理过程:

1)找到缺页的虚拟地址所在的VMA             2)如果必要,分配中间页目录表和页表              

3)如果页表项对应的物理页表不存在,则调用这个VMA的nopage()方法,它返回物理页面的页描述符。

4)将物理页面的地址填充到页表中。

实现nopage后,用户空间可以通过mremap()系统调用重新绑定映射区所绑定的地址,下面给出一个在设备驱动中使用nopage()的典型范例:

static int xxx_mmap(struct file *filp, struct vm_area_struct *vma);{     unsigned long offset = vma->vm_pgoff << PAGE_OFFSET;     if(offset >= _ _pa(high_memory) || (filp->flags &O_SYNC))             vma->vm_flags |=VM_IO;     vma->vm_ops = &xxx_nopage_vm_ops;     xxx_vma_open(vma);     return 0;}struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type){   struct page *pageptr;   unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;   unsigned long physaddr = address - vma->vm_start + offset;   //物理内存   unsigned long pageframe = physaddr >> PAGE_SHIFT;  //页帧号   if(!pfn_valid(pageframe))   //页帧号有效      return NOPAGE_SIGBUS;   pageptr = pfn_to_page(pageframe);    //页帧号->页描述符   get_page(pageptr);   //获得页,增加页的使用计数   if(type)      *type = VM_FAULT_MINOR;   return pageptr;    //返回页描述符
}

上述函数对常规内存进行映射,返回一个页描述符,可用于扩大或缩小映射的内存区域,由此可见,nopage()和remap_pfn_range()一个较大的区别在于remap_pfn

_range()一般用于设备内存映射,而nopage()还可以用于RAM映射。

 

小王,这节和前边一节是在一起看的,我也可以喘口气歇歇了,你慢慢看吧,就不烦你了,晚上吃饭叫上我哈..
小王,告诉你一个好消息,最难理解的部分不知不觉中已经讲完了,今天的课程就简单多了,而且最重要的是咱们的Linux设备驱动核心理论课也差不多了…”

“最难的部分?已经讲完了?我咋没感觉呢..你讲的真是太好了,太通俗易懂了,太..”小王调皮的说。

“切,就你嘴甜,我还不知道你啊,小脑筋..”我白了小王一样。

   那么今天呢?今天就讲讲IO内存静态映射。在将Linux移植到目标电路板中,通常会建立外设IO内存物理地址到虚拟地址的静态映射,这个映射通过在电路板对应的

map_desc结构体数组中添加新的成员来完成,map_desc结构体的定义如下:

struct map_desc{   unsigned long virtual;  //虚拟地址   unsigned long pfn;  //__phys_to_pfn(phy_addr)   unsigned long length;   //大小   unsigned int type;  //类型 }

   将Linux操作系统移植到特定平台上,MACHINE_START到MACHINE_EDN宏之间的定义针对特定电路板而设计,其中的map_io()成员函数完成IO内存的静态映射,最后通过调用cpu—>map_io()建立map_desc数组中物理内存和虚拟内存的静态映射关系。

   在一个已经移植好的OS的内核中,驱动工程师完全可以对非常规内存区域的IO内存(外设控制器寄存器,MCU内部集成的外设控制寄存器等)依照电路板的资源使用情况添加到map_desc数组中。下边给出SMDK2440内存情况定义的map_desc:

SMDK2440内存资源情况定义的map_desc

 

“小王,今天要讲的另外一个主题是DMA”我补充道:

  DMA是一种无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用DMA可以是系统CPU从实际的IO数据传输过程中摆脱出来,从而大大提

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

  说到DMA,就会想到Cache,两者本身似乎是好不相关的事物。的确,假设DMA针对内存的目的地址和Cache缓存的对象没有重叠区域,DMA和Cache之间就相安无事,但是,如果有重叠呢,经过DMA操作,Cache缓存对应的内存的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据仍然还是内存中的数据,以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据,这就会发生Cache与内存之间数据“不一致性”的错误。一旦出现这样的情况,没有处理好,驱动就将无

法正常运行。那么怎样解决呢?最简单的方法是直接禁止DMA目标地址范围内内存的Cache功能,当然这是牺牲性能的,但却高可靠。不是吗,所以这两者之间究竟怎么平衡,还真不好解决。

  其实啊,Cache不一致的情况在其他地方也是可能发生的,比如,对于带MMU功能的ARM处理器,在开启MMU之前,需要设置Cache无效,TLB也是如此

  说了,那么多DMA理论的东西,剩下的来点Linux下DMA编程的东西,当然,这里也是点一下,具体怎么操作,我可要卖个关子到下节了。

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

  对于ISA设备而言,其DMA操作只能在16MB以下的内存进行,因此,在使用kmalloc()和__get_free_pages()及其类似函数申请DMA缓冲区时应使用GFP_DMA标

志,这样能保证获得的内存是具备DMA能力的。

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

  #define __get_dma__pages(gfp_mask, order)  __get_free_pages((gfp_mask) | GFP_DMA, (order))
  "我不想使用order为参数的申请DMA内存,感觉就是怪怪的,那咋办?"小王抱怨道.
  那?这样吧,你就用另外一个函数dma_mem_alloc()源代码如下:
static unsigned long dma_mem_alloc(int size){   int order = get_order(size);    //大小->指数   return __get_dma_pages(GFP_KERNEL, order);}
 
"小王,感觉怎样,要不咱们下节继续?看你挺多不懂的地方?"我看到小王那充满疑惑的眼神。
"好,行,我正有这种打算呢"小王又露出了让人陶醉的笑容。

 

“小王,再告诉你一个好消息,今天是咱们设备驱动程序核心基础理论的最后一节课了,战斗就已经到了最后一刻了,开心不”我眉飞色舞的对小王说。

“嗯,开心,我挣扎许久了,终于结束了,只是..”小王伤感的说“只是我觉得怎么能一下就没了呢, 心里空荡荡的”.

“没关系的…”看着小王噘着嘴调皮而又可爱的样子,我也心软了”核心的理论是讲完了,但你不是没动过手吗,还有很多路要走呢..我还舍…”我一把蒙住自己的嘴.

  嘿嘿,心里咋想咋们都明白,是不…别伤感了,继续咱们上节的东西:

  上节我们说到了dma_mem_alloc()函数,需要说明的是DMA的硬件使用总线地址而非物理地址,总线地址是从设备角度上看到的内存地址,物理地址是从CPU角度上看到的未经转换的内存地址(经过转换的那叫虚拟地址)。在PC上,对于ISA和PCI而言,总线即为物理地址,但并非每个平台都是如此。由于有时候接口总线是通过桥接电路被连接,桥接电路会将IO地址映射为不同的物理地址。例如,在PRep(PowerPC Reference Platform)系统中,物理地址0在设备端看起来是0X80000000,而0通常又被映射为虚拟地址0xC0000000,所以同一地址就具备了三重身份:物理地址0,总线地址0x80000000及虚拟地址0xC0000000,还有一些系统提供了页面映射机制,它能将任意的页面映射为连续的外设总线地址。内核提供了如下函数用于进行简单的虚拟地址/总线地址转换:

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

在使用IOMMU或反弹缓冲区的情况下,上述函数一般不会正常工作。而且,这两个函数并不建议使用。

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

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

比如,对于只能在24位地址上执行DMA操作的设备而言,就应该调用dma_set_mask(dev, 0xffffffff).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缓冲区的总线地址。与之对应的释放函数为:

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

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

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

与之对应的是释放函数:dma_free_writecombine(),它其实就是dma_free_conherent,只不过是用了#define重命名而已。

此外,Linux内核还提供了PCI设备申请DMA缓冲区的函数pci_alloc_consistent(),原型为:

void *pci_alloc_consistent(struct pci_dev *dev, size_t size, dma_addr_t *dma_addrp);  对应的释放函数为:
void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr, dma_addr_t dma_addr);
相对于一致性DMA映射而言,流式DMA映射的接口较为复杂。对于单个已经分配的缓冲区而言,使用dma_map_single()可实现流式DMA映射:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);  如果映射成功,返回的是总线地址,否则返回NULL.最后一个参数DMA的方向,可能取DMA_TO_DEVICE, DMA_FORM_DEVICE, DMA_BIDIRECTIONAL和DMA_NONE;
与之对应的反函数是:
void dma_unmap_single(struct device *dev,dma_addr_t *dma_addrp,size_t size,enum dma_data_direction direction);
通常情况下,设备驱动不应该访问unmap()的流式DMA缓冲区,如果你说我就愿意这么做,我又说写什么呢,选择了权利,就选择了责任,对吧。这时可先使用如下函数获得DMA缓冲区的拥有权:
void dma_sync_single_for_cpu(struct device *dev,dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
在驱动访问完DMA缓冲区后,应该将其所有权还给设备,通过下面的函数:
void dma_sync_single_for_device(struct device *dev,dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);如果设备要求较大的DMA缓冲区,在其支持SG模式的情况下,申请多个不连续的,相对较小的DMA缓冲区通常是防止申请太大的连续物理空间的方法,在Linux内核中,使用如下函数映射SG:
int dma_map_sg(struct device *dev,struct scatterlist *sg, int nents,enum dma_data_direction direction); 其中nents是散列表入口的数量,该函数的返回值是DMA缓冲区的数量,可能小于nents。对于scatterlist中的每个项目,dma_map_sg()为设备产生恰当的总线地址,它会合并物理上临近的内存区域。下面在给出scatterlist结构:
struct scatterlist{   struct page *page;       unsigned int offset;   //偏移量   dma_addr_t dma_address;   //总线地址      unsigned int length;   //缓冲区长度}
执行dma_map_sg()后,通过sg_dma_address()后可返回scatterlist对应缓冲区的总线结构,sg_dma_len()可返回scatterlist对应的缓冲区的长度,这两个函数的原型是:
dma_addr_t sg_dma_address(struct scatterlist *sg);       unsigned int sg_dma_len(struct scatterlist *sg);
在DMA传输结束后,可通过dma_map_sg()的反函数dma_unmap_sg()去除DMA映射:
void dma_map_sg( struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);   SG映射属于流式DMA映射,与单一缓冲区情况下流式DMA映射类似,如果设备驱动一定要访问映射情况下的SG缓冲区,应该先调用如下函数:
int dma_sync_sg_for_cpu( struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
访问完后,通过下列函数将所有权返回给设备:
int dma_map_device( struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
Linux系统中可以有一个相对简单的方法预先分配缓冲区,那就是同步“mem=”参数预留内存。例如,对于内存为64MB的系统,通过给其传递mem=62MB命令行参数可以使得顶部的2MB内存被预留出来作为IO内存使用,这2MB内存可以被静态映射,也可以执行ioremap().
相应的函数都介绍完了: 说真的,好费劲啊,我都想放弃了,可为了小王,我继续哈..在linux设备驱动中如何操作呢:
像使用中断一样,在使用DMA之前,设备驱动程序需要首先向系统申请DMA通道,申请DMA通道的函数如下
int request_dma(unsigned int dmanr, const char * device_id);  同样的,设备结构体指针可作为传入device_id的最佳参数。
使用完DMA通道后,应该使用如下函数释放该通道:void free_dma(unsinged int dmanr);
作为本篇的最后,也作为Linux设备驱动核心理论的结束篇,小王,我给你总结一下在Linux设备驱动中DMA相关代码的流程。如下所示:
“小王,我要讲的讲完了,我轻松了,你可要忙碌了,把这一个月来的知识点都好好串串,为了你,我随叫随到..”我说“课余,我也准备一些后面实际的操作内容,帮你串串”我发现自己越来越喜欢小王了..
"嗯,我会的,放心吧.."那美丽的而迷人的微笑眼神又出现了..

你可能感兴趣的:(嵌入式,linux内核,驱动开发)