Linux 设备驱动程序(三)(下)

简介: Linux 设备驱动程序(三)

Linux 设备驱动程序(三)(中):https://developer.aliyun.com/article/1597488

(4)通用 DMA 层

   DMA 操作最终会分配缓冲区,并将总线地址传递给设备。一个可移植的驱动程序要求对所有体系架构都能安全而正确地执行 DMA 操作,编写这样一个驱动程序的难度超出了一般人的想像。不同的系统对处理缓存一致性上有不同的方法;如果不能正确处理该问题,驱动程序会引起内存冲突。一些系统拥有复杂的总线硬件,使得 DMA 任务或变得简单,或变得困难。并且不是所有的系统都能对全部的内存执行 DMA。幸运的是,内核提供了一个与总线体系架构无关的 DMA 层,它会隐藏大多数问题。强烈建议在编写驱动程序时,为 DMA 操作使用该层。


   下面许多函数都需要一个指向 device 结构的指针。该结构是在 Linux 设备模型中用来表示设备底层的。驱动程序通常不直接使用该结构,但是在使用通用 DMA 层时,需要使用它。该结构内部隐藏了描述设备的总线细节。比如可以在 pci_device 结构或者 usb_device 结构的 dev 成员中发现它。


   使用下列函数的驱动程序都要包含头文件 <linux/dma-mapping.h>。

(a)处理复杂的硬件

    在执行 DMA 之前,第一个必须回答的问题是:是否给定的设备在当前主机上具备执行这些操作的能力。出于很多原因,许多设备受限于它们的寻址范围。默认的情况下,内核假设设备都能在 32 位地址上执行 DMA。如果不是这样,应该调用下面的函数通知内核:

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


    该掩码显示与设备能寻址能力对应的位。比如设备受限于 24 位寻址,则 mask 应该是 0x0FFFFFF 。如果使用指定的 maskDMA 能正常工作,则返回非零值。如果 dma_set_mask 返回 0,则对该设备不能使用 DMA。因此,一个受限于 24DMA 操作的驱动程序初始化代码有如下的形式:

  if (dma_set_mask (dev, 0xffffff))
    card->use_dma = 1;
  else {
    card->use_dma = 0;
    /* 不得不在没有 DMA 情况下操作 */
    printk (KERN_WARN, "mydev: DMA not supported\n");
  }

  再强调一遍,如果设备支持常见的 32DMA 操作,则没有必要调用 dma_set_mask

(b)DMA 映射

  一个 DMA 映射是要分配的 DMA 缓冲区与为该缓冲区生成的、设备可访问地址的组合。我们可以通过对 virt_to_bus 函数的调用获得该地址,但是有许多理由建议不要这么做。第一个理由是具有 IOMMU 的硬件为总线提共了一套映射寄存器。IOMMU 在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来说变成连续的。对 IOMMU 的运用需要使用到通用 DMA 层,而 virt_to_bus 函数不能完成这个任务。


   请注意不是所有的体系架构都有 IOMMU;特别是常见的 x86 平台没有对 IOMMU 的支持。但是,一个正确的驱动程序不需要知道其运行系统上的 I/O 支持硬件。


   在某些情况下,为设备设置可用地址需要建立回弹缓冲区。当驱动程序要试图在外围设备不可访问的地址上执行 DMA 时(比如高端内存),将创建回弹缓冲区。然后,必要时会将数据写入或者读出回弹缓冲区。对回弹缓冲区的使用势必会降低系统性能,但有的时候却没有其他可替代的方法。


   DMA 映射必须解决缓存一致性的问题。现代处理器在内部的快速缓存器中保存了最近访问的内存区域;没有该缓存器,将得不到期望的性能。如果设备改变了主内存中的区域,则任何覆盖该区域的处理器缓存都将无效;否则处理器将使用不正确的主内存映射,从而产生不正确的数据。与此类似,当设备使用 DMA 从主内存中读取数据时,在处理器缓存中的任何改变也必须立刻得到刷新。这些缓存一致性的问题为系统带来诸多不确定因素,如果程序员不细致谨慎的话,这些错误非常难以查找。一些体系架构在硬件中管理缓存的一致性,但是其他一些体系架构则需要软件的支持。通用 DMA 层端尽全力来保证在所有体系架构中都能正常运行,但是必须看到,正确的行为需要一套规则来保障。


   DMA 映射建立了一个新的结构类型 —— dma_addr_t 来表示总线地址。dma_addr_t 类型的变量对驱动程序是不透明的;唯一允许的操作是将它们传递给 DMA 支持例程以及设备本身。作为一个总线地址,如果 CPU 直接使用了 dma_addr_t,将会导致发生不可预期的问题。


   根据 DMA 缓冲区期望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:


一致性 DMA 映射

这种类型的映射存在于驱动程序生命周期中。一致性映射的缓冲区必须可同时被 CPU 和外围设备访问(其他类型的映射,如后面将要讨论的类型,在给定时刻只能被一个设备访问)。因此一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的。

流式 DMA 映射

通常为单独的操作建立流式映射。当使用流式映射时,一些体系架构可以最大程度地优化性能,但是这些映射也要服从一组更加严格的访问规则。内核开发者建议尽量使用流式映射,然后再考虑一致性映射。这么做有两个原因。第一个原因是在支持映射寄存器的系统中,每个 DMA 映射使用总线上的一个或者多个映射寄存器。一致性映射具有很长的生命周期,因此会在相当长的时间内占用这些寄存器,甚至在不使用它们的时候也不释放所有权。第二个原因是在一些硬件中,流式映射可以被优化,但优化的方法对一致性映射无效。

必须用不同的方法操作这两种映射,下面详细描述操作细节。

(c)建立一致性 DMA 映射

    驱动程序可调用 pci_alloc_consistent 函数建立一致性映射:

void *dma_alloc_coherent(struct device *dev, size_t size,
            dma_addr_t *dma_handle, int flag);

   该函数处理了缓冲区的分配和映射。前两个参数是 device 结构和所需缓冲区的大小。函数在两处返回 DMA 映射的结果。函数的返回值是缓冲区的内核虚拟地址,可以被驱动程序使用;而与其相关的总线地址,返回时保存在 dma_handle 中。该函数对分配的缓冲区做了一些处理,从而缓冲区可用于 DMA;通常只是通过 get_free_pages 函数分配内存(请注意 size 是以字节为单位的,而不是幕次的值)。flag 参数通常是描述如何分配内存的 GFP_ 值;通常是 GFP_KERNEL 或者是 GFP_ATOMIC(在原子上下文中运行时)。


   当不再需要缓冲区时(通常在模块卸载的时候),调用 dma_free_coherent 向系统返回缓冲区:

void dma_free_coherent(struct device *dev, size_t size,
            void *vaddr, dma_addr_t dma_handle);

    请注意该函数与其他通用 DMA 函数一样,需要提供缓冲区大小、CPU 地址、总线地址等参数。

(d)DMA 池

   DMA 池是一个生成小型、一致性 DMA 映射的机制。调用 dma_alloc_coherent 函数获得的映射,可能其最小大小为单个页。如果设备需要的 DMA 区域比这还小,就要使用 DMA 池了。在对内嵌于某个大结构中的小型区域执行 DMA 时,也可以使用 DMA 池。一些不容易察觉的驱动程序一致性缓存错误,往往存在于结构中与小型 DMA 区域相邻的成员中。为了避免这一问题的出现,应该总是显式地为 DMA 操作分配区域,而与其他非 DMA 数据结构的操作分开。


   在 <linux/dmapool.h> 中定义了 DMA 池的函数。


   DMA 池必须在使用前,调用下面的函数创建:

struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                  size_t size, size_t align,
                  size_t allocation);

   这里,name 是 DMA 池的名字,dev 是 device 结构,size 是从该池中分配的缓冲区的大小,align 是该池分配操作所必须遵守的硬件对齐原则(用字节表示),如果 allocation 不为零、表示内存边界不能超越 allocation。比如传入的 allocation 是 4096,从该池中分配的缓冲区不能跨越 4KB 的界限。

当使用完 DMA 池后,调用下面的函数释放:

void dma_pool_destroy(struct dma_pool *pool);

  在销毁前,必须向 DMA 池返回所有分配的内存。

使用 dma_pool_alloc 函数处理分配问题:

void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);

    在这个函数中,mem_flags 通常设置为GFP_ 分配标志。如果一切正常,将分配并返回内存区域(拥有创建 DMA 池时指定的大小)。像 dma_alloc_coherent 函数一样,返回的 DMA 缓冲区的地址是内核虚拟地址,并作为总线地址保存在 handle 中。

使用下面的通数返回不需要的缓冲区:

void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
(e)建立流式 DMA 映射

   由于多种原因,流式映射具有比一致性映射更为复杂的接口。这些映射希望能与已经由驱动程序分配的缓冲区协同工作,因而不得不处理那些不是它们选择的地址。在某些体系架构中,流式映射也能够拥有多个不连续的页和多个 “分散 / 聚集” 缓冲区。出于上面这些原因的考虑,流式映射拥有自己的设置函数。


   当建立流式映射时,必须告诉内核数据流动的方向。为此定义了一些符号(dma_data_direction 枚举类型):


DMA_TO_DEVICE

DMA_FROM_DEVICE

这两个符号的作用很明显。如果数据被发送到设备(可能使用 write 系统调用作为响应),应使 用 DMA_TO_DEVICE ;而如果数据被发送到 CPU ,则使用 DMA_FROM_DEVICE。

DMA_BIDIRECTIONAL

如果数据可双向移动,则使用 DMA_BIDIRECTIONAL。

DMA_NONE

提供该符号只是出于调试目的。如果要使用设置了该符号的缓冲区,会导致内核错误。

   可能有的读者认为任何时候都使用 DMA_BIDIRECTIONAL 就可以了,但是驱动程序作者不能这么做。在一些体系架构中,可能会为这个选择付出很大的性能代价。


   当只有一个缓冲区要被传输的时候,使用 dma_map_single 函数映射它:

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size,
              enum dma_data_direction direction);

    返回值是总线地址,可以把它传递给设备;如果执行遇到错误,则返回 NULL

当传输完毕后,使用 dma_unmap_single 函数删除映射:

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,
            enum dma_data_direction direction);

   在该函数中,size 和 direction 参数必须与映射缓冲区的参数相匹配。有几条非常重要的原则用于流式 DMA 映射:


缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向值。

一旦缓冲区被映射,它将属于设备,而不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只有当 dma_unmap_single 函数被调用后,驱动程序才能安全访问缓冲区中的内容(还存在一个例外,不久就会看到)。尤其要说明的是,这条规则意味着:在包含了所有要写入的数据之前,不能映射要写入设备的缓冲区。

在 DMA 处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。

   读者可能提出疑问:为什么在缓冲区被映射后,驱动程序不能访问它? 有两个原因来解释这条规则。第一,当一个缓冲区建立 DMA 映射时,内核必须保证在该缓冲区内的全部数据都被写入了内存。当调用 dma_unmap_single 函数时,很可能有一些数据还在处理器的缓存中,因此必须被显式刷新。在刷新动作后,处理器写入缓冲区的数据对设备是不可见的。


   第二,如果要映射的缓冲区位于设备不能访问的内存区段时,该怎么办? 一些体系架构会只产生一个错误,但是其他一些体系架构将创建一个回弹缓冲区。回弹缓冲区是内存中的独立区域,它可被设备访问。如果使用 DMA_TO_DEVICE 方向标志映射缓冲区,并且需要使用回弹缓冲区,则在最初缓冲区中的内容作为映射操作的一部分被拷贝。很明显 , 在拷贝操作后,最初缓冲区内容的改变对设备也是不可见的。同样 DMA_FROM_DEVICE 回弹缓冲区被 dma_unmap_single 函数拷贝回最初的缓冲区中,也就是说,直到拷贝操作完成,来自设备的数据才可用。


   顺便说一下,为什么获得正确的传输方向是一个重要的问题,回弹缓冲区就是一个解释。DMA_BIDIRECTIONAL 回弹缓冲区在操作前后都要拷贝数据,这通常会浪费不必要的 CPU 指令周期。


   有时候,驱动程序需要不经过撤销映射就访问流式 DMA 缓冲区的内容,为此内核提供了如下调用:

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,
                size_t size, enum dma_data_direction direction);

    应该在处理器访问流式 DMA 缓冲区前调用该函数。一旦调用了该函数,处理器将 “拥有” DMA 缓冲区,并可根据需要对它进行访问。然而在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备:

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,
                size_t size, enum dma_data_direction diredtion);

    再次强调,处理器在调用该通数后,不能再访问 DMA 缓冲区了。

(f)单页流式映射

    有时候,要为 page 结构指针指向的缓冲区建立映射;这种情况是有可能发生的,比如使用 get_user_pages 映射用户空间缓冲区。使用下面的函数,建立和撤销使用 page 结构指针的流式映射:

dma_addr_t dma_map_page(struct device *dev, struct page *page,
            unsigmed long offset, size_t size,
            enum dma_data_direction direction);
void dma_unmap_page(struct device *dev, dma_addr_t dma_address,
          size_t size, enum dma_data_direction direction);

    offsetsize 参数用于映射一页中的一部分。建议尽量避免映射部分内存页,除非明了其中的原理。如果分配的页是缓存流水线的一部分,则映射部分页会引起一致性问题,比如内存冲突,以及产生非常难以调试的代码缺陷等。

(g)分散 / 聚集映射

   分散/集聚集映射是一种特殊类型的流式 DMA 映射。假设有几个缓冲区,它们需要与设备双向传输数据。有几种方式能产生这种情形,包括从 readv 或者 writev 系统调用产生,从集群的磁盘 I/O 请求产生,或者从映射的内核 I/O 缓冲区中的页面链表产生。可以简单地依次映射每一个缓冲区并且执行请求的操作,但是一次映射整个缓冲区表还是很有利的。


   许多设备都能接受一个指针数组的分散表,以及它的长度,然后在一次 DMA 操作中把它们全部传输走。比如将所有的数据包放在多个数据单元中,“零拷贝” 网络非常容易实现。把分散表作为一个整体的另外一个原因是,充分利用那些在总线硬件中含有映射寄存器系统的优点。在这些系统中,从设备角度上看、物理上不连续的内存页,可以被组装成一个连续数组。这种技术只能用在分散表中的项在长度上等于页面大小的时候(除了第一个和最后一个之外),但是在其工作时,它能够将多个操作转化成单个 DMA 操作,因而能够加速处理工作。


   最后,如果必须用到回弹缓冲区,将整个表接合成一个单个缓冲区是很有意义的(因为无论如何它也会被复制)。


   所以现在可以确信在某些情况下分散表的映射是值得做的。映射分散表的第一步是建立并填充一个描述被传送缓冲区的 scatterlist 结构的数组。该结构是与体系架构相关的,并且在头文件 <linux/scatterlist.h> 中描述。然而,该结构会始终包含两个成员:


struct page *page;

与在 scatter/gather 操作中用到缓冲区相对应的 page 结构指针。

unsigned int length;

unsigned int offset;

在页内缓神区的长度和偏移量。

   为了映射一个分散/聚集 DMA 操作,驱动程序应当为传输的每个缓冲区在 scatterlist 结构对应入口项上设置 page、offset 和 length 成员。然后调用:

int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
        enum dma_data_direction direction)

   这里的 nents 是传入的分散表入口的数量。返回值是要传送的 DMA 缓冲区数;它可能会小于 nents。

   对在输入分散表中的每一个缓冲区,dma_map_sg 函数返回了指定设备的正确的总线地址。作为任务的一部分,它还把内存中相邻的缓冲区接合起来。如果运行驱动程序的系统拥有一个 I/O 内存管理单元,dma_map_sg 函数会对该单元的映射寄存器编程,如果没有发生什么错误,则从设备角度上看,其能够传输一块连续的缓冲区。然而在调用之前是无法知道传输结果的。


   驱动程序应该传输由 dma_map_sg 函数返回的每个缓冲区。总线地址和每个缓冲区的长度被保存在 scatterlist 结构中,但是它们在结构中的位置会随体系架构的不同而不同。使用已经定义的两个宏,可用来编写可移植代码:

dma_addr_t sg_dma_address(struct scatterlist *sg);

从该分散表的入口项中返回总线(DMA)地址。

unsigned int sg_dma_len(struct scatterlist *sg);

返回缓冲区的长度。

    再次强调,被传输缓冲区的地址和长度与传递给 dma_map_sg 函数的值是不同的。一旦传输完毕,使用 dma_uпmap_sg 函数解除分散 / 聚集映射:

void dma_unmap_sg(struct device *dev, struct scatterlist *list,
          int nents, enum dma_data_direction direction);

    请注意,nents 一定是先前传递给 dma_map_sg 函数的入口项的数量,而不是函数返回的 DMA 缓冲区的数量。


    分散/聚集映射是流式 DMA 映射,因此适用于流式映射的规则也适用于该种映射。如果必须访问映射的分散 / 聚集列表,必须首先对其进行同步:

void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,
            int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,
              int nents, enum dma_data_direction direction);
(h)PCI 双重地址周期映射

   通常 DMA 支持层使用 32 位总线地址,其为设备的 DMA 掩码所约束。然而 PCI 总线还支持 64 位地址模式 ,既双重地址周期(DAC)。出于多种原因,通用 DMA 层并不支持。该模式,首先这是 PCI 独有的特性。其次,许多 DAC 的实现都是有缺陷的,而且 DAC 也比常用的 32 位 DMA 要慢,会增加性能开销。虽然如此,还是有一些应用程序能正确使用 DAC;如果设备需要使用放在高端内存的大块缓冲区,可以考虑实现 DAC 支持。这种支持只有对 PCI 总线有效,因此必须使用与 PCI 总线相关的例程。

 为了使用 DAC,驱动程序必须包含头文件 <linux/pci.h>,还必须设置一个单独的 DMA 掩码:

int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);

只有该函数返回 0 时,才能使用 DAC 地址。

    在 DAC 映射中使用了一个特殊类型(dma64_addr_t)。调 pci_dac_page_to_dma 函数建立一个这样的映射:

dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page,
                unsigned long offset, int direction);

   读者会注意到,可以只使用 page 结构指针(毕竟它们应当保存在高端内存中,否则是毫无意义的)来建立 DAC 映射,而且必须以一次一页的方式创建它们。direction 参数与在通用 DMA 层中使用的 dma_data_direction 枚举类型等价,因此可以取 PCI_DMA_TODEVICE、PCI_DMA_FROMDEVICE 或者 PCI_DMA_BIDIRECTIONAL。


   DAC 映射不需要其他另外的资源,因此在使用过后,不需要显式释放它。然而像对待其他流式映射一样对待它是必要的,关于缓冲区所有权的规则也适用于它。有一套用于同步 DMA 缓冲区的函数,其形式如下:

void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev,
                  dma64_addr_t dma_addr,
                  size_t len,
                  int direction);
void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev,
                    dma64_addr_t dma_addr,
                    size_t len,
                    int direction);
(i)一个简单的 PCI DMA 例子

   这里提供了一个 PCI 设备的 DMA 例子源代码,以说明如何使用 DMA 映射。实际 PCI 总线上的 DMA 操作形式,与它所驱动的设备密切相关,因此这个例子不能应用于任何真实设备。但它是一个假定的叫 dad(DMA Acquisition Device,DMA 获取设备)驱动程序的一部分。该设备的驱动动程序要用类似下面的代码定义传输函数:

int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count) {
  dma_addr_t bus_addr;
  
  /* 映射 DMA 需要的缓冲区 */
  dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
  dev->dma_size = count;
  bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count,
                dev->dma_dir);
  dev->dma_addr = bus_addr;
  
  /* 设置设备 */
  writeb(dev->registers.command, DAD_CMD_DISABLEDMA);
  writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);
  writel(dev->registers.addr, cpu_to_le32(bus_addr));
  writel(dev->registers.len, cpu_to_le32(count));
  
  /* 开始操作 */
  writeb(dev->registers.command, DAD_CMD_ENABLEDMA);
  return 0;
}

    该函数映射了准备进行传输的缓冲区并且启动设备操作。另一半工作必须在中断服务例程中完成,它看起来类似下面这样:

void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs) {
  struct dad_dev *dev = (struct dad_dev *) dev_id;
  
  /* 确定该中断确实是从对应的设备发来的 */
  /* 释放对 DMA 缓冲区的映射 */
  dma_unmap_single(dev->pci_dev->dev, dev->dma_addr,
            dev->dma_size, dev->dma_dir);
  /* 只有到现在这个时候,对缓冲区的访问才是安全的,把它拷贝给用户。 */
  // ...
}

    显而易见,这个例子忽略了大量细节,包括用来阻止同时开始多个 DMA 操作的必要步骤。

(5)ISA 设备的 DMA

   ISA 总线允许两种 DMA 传输:本地(native) DMA 和 ISA 总线控制 (bus-master) DMA。本地 DMA 使用主板上的标准 DMA 控制器电路来驱动 ISA 总线上的信号线。另一方面, ISA 总线控制 DMA 完全由外围设备控制。后一种 DMA 类型很少被使用,并且也不需要在这里讨论,因为至少从设备角度上看,它与 PCI 设备的 DMA 非常类似。一个 ISA 总线控制 DMA 的例子是 1542 SCSI 控制器,它的驱动程序在内核代码 drivers/scsi/ahal542.c 中。


   至于这里所关心的本地 DMA,有三种实体涉及到 ISA 总线上的 DMA 数据传输:


8237 DMA 控制器(DMAC)

控制器保存了有关 DMA 传输的信息,比如方向、内存地址、传输数据量大小等。它还包含了一个跟踪传送状态的计数器。当控制器接收到一个 DMA 请求信号时,它将获得总线控制权并驱动信号线,这样设备就能读写数据了。

外围设备

当设备准备传送数据时,必须激活 DMA 请求信号。DMAC 负责管理实际的传输工作;当控制器选通设备后,硬件设备就可以顺序地读写总线上的数据。当传输结束时,设备通常会产生一个中断。

设备驱动程序

需要驱动程序完成的工作很少,它只是负责提供 DMA 控制器的方向、总线地址、传输量的大小等等。它还与外围设备通信、做好传输数据的准备,当 DMA 传输完毕后,响应中断。

   在 PC 中使用的早期 DMA 控制器能够管理四个 “通道” ,每个通道都与一套 DMA 寄存器相关联。四个设备可以同时在控制器中保存它们的 DMA 信息。现在的 PC 包含了两个与 DMAC 等价的设备(注 6):第二控制器(主控制器)连接系统的处理器,第一控制

器(从控制器)与第二控制器的 0 通道相连(注7)。


   注 6: 这种电路现在是主板芯片组的一部分,但在几年前,它们是两个独立的 8237 芯片。


   注 7: 最初的 PC 只有两个控制器,第二个控制器出现在 286 平台中。但是,第二个控制器被连接为主控制器,这是因为它可以处理 16 位传输,而第一个控制器每次只能传输八位,因此仅用于向后兼容。


   通道编号从 0 到 7: 通道 4 在内部用来将从属控制器级联到主控制器上、因此对 ISA 外围设备来说,通道 4 不可用。所以可用的通道是从属控制器(8 位通道)上的 0 ~ 3 和主控制器(16 位通道)上的 5 ~ 7。每次 DMA 传输的大小保存在控制器中,它是一个 16 位的数,表示传递所需要的总线周期数。因此最大传输大小对从属控制器来说是 64KB(因为它在一个周期内传输 8 位),对主控制器来说是 128KB(它使用 16 位传输)。


   因为 DMA 控制器是系统资源、因此内核协助处理这一资源。内核使用 DMA 注册表为 DMA 通道提供了请求/释放机制,并且提供了一组函数在 DMA 控制器中配置通道信息。

(a)注册 DMA

    读者应该对内核注册表很熱悉了,在 I/O 端口和中断号部分就接触过它。DMA 通道注册表与此非常类似。在包含了头文件 <asm/dma.h> 之后,用下面的函数可以获得和释放对 DMA 通道的所有权:

int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);

   channel 参数是 0 到 7 的整数,或者更精确地说,是一个小于 MAX_DMA_CHANNELS 的正整数。在 PC 中,MAX_DMA_CHANNELS 定义为与硬件匹配的 8。name 参数是标识设备的字符串,指定的名字出现在文件 /proc/dma 中,用户程序可以读取该文件。


   request_dma 函数返回 0 表示执行成功,返回 -EINVAL 或者 - EBUSY 表示失败。前一个错误码表示所请求的通道超出了范围,后面一个错误码表示通道正为另外一个设备所占用。


   建议读者像对待 I/O 端口和中断信号线一样地小心处理 DMA 通道;在 open 操作时请求通道,比在模块初始化函数中请求通道更好一些。延迟请求允许在驱动程序间共享信息;比如声卡和类似的 I/O 接口如果不在同一时间内使用 DMA 通道时,就可以共享同一个 DMA 通道。


   这里还建议读者在请求中断信号线之后请求 DMA 通道,并且在中断之前释放通道。这是在请求两种资源时常用的请求顺序;按照这个顺序可以避免可能的死锁。请注意使用 DMA 的每个设备还需要一根 IRQ 线,否则它将无法通知数据已经传输完毕。


   在典型应用中,open 函数的代码有类似以下的形式,这段代码使用了假想的 dad 模块。


   dad 模块不支持共享 IRQ 信号线,它使用了一个快速中断处理例程。

int dad_open(struct inode *inode, struct file *filp) {
  struct dad_device *my_device;
  /* ... */
  if ( (error = request_irq(my_device->irq, dad_interrupt,
              SA_INTERRUPT, "dad", NULL)) )
    return error;   /* 或者实现阻塞的 open 操作 */
  if ( (error = request_dma(my_device->dma, "dad")) ) {
    free_irq(my_device->irq, NULL);
    return error;   /* 或者实现阻塞的 open 操作 */
  }
  /* ... */
  return 0;
}

  与 open 函数相匹配的 close 函数的实现如下:

void dad_close(struct inode *inode, struct file *filp) {
  struct dad_device *my_device;
  
  /* ... */
  free_dma(my_device->dma);
  free_irq(my_device->irq, NULL);
  /* ... */
}

下面是一个安装了声卡系统的 /proc/dma 文件:

    请注意默认的声卡驱动程序在系统启动时获得了DMA 通道,并且不会释放它。cascade 入口是一个占位符,表示通道 4 对驱动程序不可用,其原因在前面讲述过了。

(b)与 DMA 控制器通信

  注册之后,驱动程序的主要任务包括为适当的操作配置 DMA 控制器。该任务不是可有可无的,但幸运的是,内核导出了驱动程序所需要的所有函数。


   当 read 或者 write 函数被调用时,或者准备异步传输时,驱动程序都要对 DMA 控制器进行配置。根据驱动程序和其实现配置的策略,这一工作可以在调用 open 函数时,或者响应 ioctl 命令时被执行。这里的代码是被 read 和 write 设备方法调用的典型代码。


   这一小节提供了 DMA 控制器内部的概貌,这样,读者就能理解这里所给出的代码。如果想要了解更多信息,可阅读 <asm/dma.h> 文件以及描述 PC 体系架构的硬件手册。特别是在这里不处理与 16 位数据传输相对的 8 位传输问题。如果要为 ISA 设备板卡编写驱动程序,应该仔细阅读该设备的硬件手册,以查找相关信息。


   DMA 通道是一个可共享的资源,如果多于一个的处理器要同时对其进行编程,则会产生冲突。因此有一个叫作 dma_spin_lock 的自旋锁保护控制器。驱动程序不能直接处理该自旋锁;但是有两个函数能够对它进行操作:

unsigned long claim_dma_lock();

  获得 DMA 自旋锁。该函数也阻塞本地处理器上的中断,因此返回值是描述先前中断状态的一系列标志。在重新开中断时,返回值必须传递给下面的函数以恢复中断状态。

void release_dma_lock(unsigned long flags);

返回 DMA 自旋锁并恢复先前的中断状态。


   当使用下面描述的函数时,应该拥有自旋锁。但是在实际的 I/O 中却不应该拥有自旋锁。当拥有自旋锁时,驱动程序不能处于休眠状态。


   必须被装入控制器的信息包含三个部分: RAM 的地址、必须被传输的原子项个数(以字节或字为单位)以及传输的方向。为达到这个目的,<asm/dma.h> 定义了下面的函数:

void set_dma_mode(unsigned int channel, char mode);

  表明是从设备读入通道(DMA_MODE_READ)还是向设备写入数据(DMA_MODE_WRITE)。还有第三个模式 —— DMA_MODE_CASCADE,它用来释放对总线的控制。级联是将第一控制器连接到第二控制器顶端的方法,但是也可以用于真正的 ISA 总线控制设备。在这里不讨论总线控制。

void set_dma_addr(unsigned int channel, unsigned int addr);

DMA 缓冲区分配地址。该函数将 addr 的最低 24 位存储到控制器中。addr 参数必须是总线地址(请参看本章前面的 “总线地址” 部分)。

void set_dma_count(unsigned int channel, unsigned int count);

为传输的字节量赋值。count 参数也可以表示 16 位通道的字节数,此时字节数必须是偶数。


    除了这些函数外,当处理 DMA 设备时,还有许多用于管理设备的函数:

void disable_dma(unsigned int channel);

    控制器内的 DMA 通道可以被禁用。应该在配置控制器前禁用通道,以防止不正确的操作(否则由于控制器是针对 8 位数据传输编程的,可能会引起冲突,因此前面所介绍的函数都不能原子地执行)。

void enable_dma(unsigned int channel);

该函数告诉控制器,DMA 通道中包含了合法的数据。

int get_dma_residue(unsigned int channel);

  驱动程序有时需要知道 DMA 传输是否已经结束。该函数返回还未传输的字节数。如果传输成功,返回值是 0,当控制器正在工作时,返回值并不确定(但不会是 0)。如果使用两个 8 位输入操作来获得一个 16 位的余量,则返回值是不可预测的。

void clear_dma_ff(unsigned int channel)

   该函数清除了 DMA 的触发器(flip-flop)。触发器用于控制对 16 位寄存器的访问。我们可以通过两个连续的 8 位操作访问该寄存器,而触发器被用来选择低字节(当其清零时)还是高字节(当其被置位)。当传输完 8 位后,触发器自动反转;程序员必须在访问 DMA 寄存器前清除触发器(将其设置为可知状态)。



   使用这些函数,驱动程序可以实现如下函数,为 DMA 传输做准备:

int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count) {
  unsigned long flags;
  
  flags = claim_dma_lock();
  disable_dma(channel);
  clear_dma_ff(channel);
  set_dma_mode(channel, mode);
  set_dma_addr(channel, virt_to_bus(buf));
  set_dma_count (channel, count);
  enable_dma(channel);
  release_dma_lock(flags);
  return 0;
}

接着,下面的代码用来检查是否成功完成 DMA

int dad_dma_isdone(int channel) {
  int residue;
  unsigned long flags = claim_dma_lock();
  residue = get_dma_residue(channel);
  release_dma_lock(flags);
  return (residue == 0);
}

   剩下需要做的唯一事情是配置设备板卡。这个与设备相关的任务通常包括读写一些 I/O 端口。不同设备实现的方法差异很大。比如一些设备希望程序员告诉硬件 DMA 缓冲区的大小,而有些时候驱动程序不得不读出固化在设备中的数据。为了完成对板卡的配置,硬件手册是程序员唯一的朋友。


5、快速参考

    本章介绍了下面这些与内存操作相关的符号。

(1)介绍材料

#include <linux/mm.h>
#include <asm/page.h>
// 在这些头文件中定义了大部分与内存管理相关的函数和结构,并给出了原型。

void *__va(unsigned long physaddr);
unsigned long __pa(void *kaddr);
// 在内核逻辑地址和物理地址之间进行转换的宏。

PAGE_SIZE

PAGE_SHIFT

    前者以字节为单位给出在特定硬件中每页的大小,后者表示为获得物理地址,页帧

    号需要移动的位数。

struct page
// 在系统内存映射中,表示硬件页的结构。

struct page *virt_to_page(void *kaddr);
void *page_address(struct page *page);
struct page *pfn_to_page(int pfn);
// 负责在内核逻辑地址和与其相关的内存映射入口之间进行转换的宏。page_address
// 只能对已经显式映射的低端内存或者高端内存进行操作。pfn_to_page 将页帧号转
// 换为与其相关的 page 结构指针。

unsigned long kmap(struct page *page);
void kunmap(struct page *page);
// kmap 返回映射到指定页的内核虚拟地址,如果需要的话,还创建映射。kunmap 为
// 指定页删除映射。

#include <linux/highmem.h>
#include <asm/kmap_types.h>
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enun km_type type);
// kmap 的高性能版本;只有原子代码才能拥有映射结果。对驱动程序来说,type 可
// 以是 KM_USERO、KM_USER1、KM_IRQ0 或者是 KM_IRQ1。

struct vm_area_struct;
// 描述 VMA 的结构。

(2)mmap 的实现

int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_add,
          unsigned long pfn, unsigned long size, pgprot_t prot);
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_add,
          unsigned long phys_add, unsigned long size, pgprot_t prot);
// mmap 的核心函数。它们映射了物理地址中从 pfn 表示的页号开始的 size 个字节,
// 到虚拟地址 virt_add 上。相关虚拟地址的保护位在 prot 中指定。如果目标地址
// 是在 I/O 地址空间的话,使用 io_remap_page_range 函数。

struct page *vmalloc_to_page(void *vmaddr);
// 将从 vmalloc 函数返回的内核虚拟地址转化为相对应的 page 结构指针。

(3)直接 I/O 的实现

int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned
          long start, int len, int write, int force, struct page **pages, 
          struct vm_area_struct **vmas);
// 该函数将用户空间缓冲区锁进内存,并返回相应的 page 结构指针。调用者必须拥
// 有 mm->mmap_sem。

SetPageDirty(struct page *page);
// 将指定内存页标记为 "已经改动过" ,并在释放该页前将其写入后备存储器的宏。

void page_cache_release(struct page *page);
// 从页缓存中释放指定的页。

int is_sync_kiocb(struct kiocb *iocb);
// 如果指定的 IOCB 需要同步执行,该宏返回非零值。

int aio_complete(struct kiocb *iocb, long res, long res2);
// 表明异步 I/O 操作完成的函数。

(4)直接内存访问

#include <asm/io.h>
unsigned long virt_to_bus(volatile void * address);
void * bus_to_virt(unsigned long address);
// 用来在内核、虚拟地址、总线地址之间进行转换的老版本函数。与外围设备通信时,
// 必须使用总线地址。

#include <linux/dma-mapping.h>
// 用来定义通用 DMA 函数的头文件。

int dma_set_mask(struct device *dev, u64 mask);
// 对于那些不能在全部 32 位寻址的外围设备来说,该函数通知内核可寻址的范围,如
// 果 DMA 可行则返回非零值。

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t
            *bus_addr, int flag)
void dma_free_coherent(struct device *dev, size_t size, void *cpuaddr,
            dma_handle_t bus_addr);
// 为缓冲区分配和释放一致性 DMA 映射,该缓冲区的生命周期与驱动程序相同。

#include <linux/dmapool.h>
struct dma_pool *dma_pool_create(const char *name, struct device *dev,
                size_t size, size_t align, size_t allocation);
void dma_pool_destroy(struct dma_pool *pool);
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t
            *handle);
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t handle);
// 用来创建、销毁和使用 DMA 池的函数,用来管理小型的 DMA 区域。

enum dma_data_direction;

   

    DMA_TO_DEVICE

    DMA_FROM_DEVICE

    DMA_BIDIRECTIONAL

    DMA_NONE

   

    用来告诉流式映射函数的符号,表明数据传输的方向。

dma_addr_t_dma_map_single(struct device *dev, void *buffer, size_t size, 
            enum dma_data_direction direction);
void dma_unmap_single(struct device *dev, dma_addr_t bus_addr, size_t size,
            enum dma_data_direction direction);
// 创建和销毁单个流式 DMA 映射。

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, 
              size_t size, enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,
              size_t size, enum dma_data_direction direction);
// 同步拥有流式映射的缓冲区。在使用流式映射时,如果处理器要访问缓冲区,则必
// 须使用这些函数。

#include <asm/scatterlist.h>
struct scatterlist { /* */ };
dma_addr_t_sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);
// scatterlist 结构描述了包含多个缓冲区的 I/O 操作。当实现了分散/聚集操作时,
// 宏 sg_dma_address 和 sg_dma_len 用来获得总线地址和缓冲区长度,并将它们传递
// 给设备。

dma_map_sg(struct device *dev, struct scatterlist *list, int nents,
      enum dma_data_direction direction);
dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, 
      enum dma_data_direction direction);
void dma_sync_sg_for_cpu(struct_device *dev, struct scatterlist *sg, 
            int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, 
              int nents, enum dma_data_direction direction);
// dma_map_sg 函数映射了分散 / 聚集操作,dma_unmap_sg 函数负责解除映射。当
// 映射处于活动状态而又必须访问缓冲区时,需要使用 dma_sync_sg_* 函数进行同
// 步。

/proc/dma

    包含 DMA 控制器中被分配通道信息快照的文本文件。其中不会显示基于 PCI 的。

    DMA,因为它们独立工作,不需要在 DMA 控制器中分配一个通道。

#include <asm/dma.h>
// 定义与 DMA 相关的函数和宏的头文件,其中给出了函数原型。如果要使用下面的
// 符号,必须包含该文件。

int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);
// 访问 DMA 注册表。 在使用 ISA DMA 通道前必须执行注册。

unsigned long claim_dma_lock();
void release_dma_lock(unsigned long flags);
// 获得和释放 DMA 自旋锁,在调用 ISA DMA 函数前必须拥有自旋锁。它们也能禁
// 止和重新打开本地处理器的中断。

void set_dma_mode(unsigned int channel, char mode);
void set_dma_addr(unsigned int channel, unsigned int addr);
void set_dma_count(unsigned int channel, unsigned int count);
// 在 DMA 控制器内对 DMA 信息编程。addr 是总线地址。

void disable_dma(unsigned int. channel);
void enable_dma(unsigned int channel);
// 在配置时,DMA 通道必须被禁止。这些函数用来改变 DMA 通道状态。

int get_dma_residue(unsigned int channel);
// 如果驱动程序需要知道 DMA 传输的进展情况,可以调用这个函数,它将返回未被
// 传输数据的数量。在成功完成 DMA 后,该函数返回 0;当数据正在被传输时,返
// 回值是不可预测的。

void clear_dma_ff(unsigned int channel)
// DMA 控制器使用触发器并通过两个 8 位操作传输一个 16 位的值。在把数据传递给
// 控制器前,必须清除触发器。

目录
相关文章
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
18天前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
22 6
|
18天前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
23 5
|
18天前
|
存储 缓存 Unix
Linux 设备驱动程序(三)(上)
Linux 设备驱动程序(三)
18 3
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
16 3
|
18天前
|
安全 数据管理 Linux
Linux 设备驱动程序(一)(中)
Linux 设备驱动程序(一)
17 2
|
18天前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
10 1
|
18天前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
13 1
|
18天前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
15 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1