编写设备驱动程序

第 9 章 直接内存访问 (Direct Memory Access, DMA)

许多设备都可以临时控制总线。这些设备可以执行涉及主内存和其他设备的数据传送。由于设备执行这些操作的过程中无需借助于 CPU,因此该类型的数据传送称为直接内存访问 (direct memory access, DMA)。可以执行的 DMA 传送类型如下:

本章仅介绍设备和内存之间的传送。本章提供有关以下主题的信息:

DMA 模型

Solaris 设备驱动程序接口/驱动程序内核接口 (Device Driver Interface/Driver-Kernel Interface, DDI/DKI) 为 DMA 提供了独立于体系结构的高级别模型。通过此模型,框架(即 DMA 例程)可以隐藏此类体系结构特定的详细信息,例如:

DDI/DKI 中使用了若干个抽象术语来描述 DMA 事务的各个方面:

设备驱动程序不会直接将对象映射到内存中,而是为内存对象分配 DMA 资源。然后,DMA 例程将执行为 DMA 访问设置对象时所需的任何特定于平台的操作。驱动程序将收到一个 DMA 句柄,用于标识为该对象分配的 DMA 资源。此句柄对于设备驱动程序而言是不透明的。驱动程序必须保存句柄并在后续调用中将其传递给 DMA 例程。驱动程序不应以任何方式解释句柄。

针对 DMA 句柄定义的操作可提供以下服务:

设备 DMA 的类型

设备可执行以下三种类型的 DMA:

总线主控器 DMA

在设备用作实际总线主控器的情况下,驱动程序应直接对该设备的 DMA 寄存器进行编程。例如,如果 DMA 引擎驻留在设备板上,则设备便会用作总线主控器。传送地址和计数从 DMA cookie 中获取,并将传递给设备。

第三方 DMA

第三方 DMA 使用驻留在主系统板上的系统 DMA 引擎,该引擎中有若干个可供设备使用的 DMA 通道。设备依赖于系统的 DMA 引擎来执行设备与内存之间的数据传送。驱动程序使用 DMA 引擎例程(请参见 ddi_dmae(9F) 函数)对 DMA 引擎进行初始化和编程。每次进行 DMA 数据传送时,驱动程序都会对 DMA 引擎进行编程,然后会向设备发出命令,以便借助该引擎来启动传送操作。

第一方 DMA

执行第一方 DMA 时,设备使用系统 DMA 引擎中的通道来驱动该设备的 DMA 总线循环。使用 ddi_dmae_1stparty(9F) 函数可在级联模式下对此通道进行配置,以免 DMA 引擎干扰传送。

主机平台 DMA 的类型

设备运行的平台可提供直接内存访问 (direct memory access, DMA) 或直接虚拟内存访问 (direct virtual memory access, DVMA)。

在支持 DMA 的平台上,系统会为设备提供物理地址以执行传送。在此情况下,DMA 对象的传送实际上会包含许多在物理上不连续的传送。例如,当应用程序传送跨越若干连续虚拟页(但这些虚拟页映射到物理上不连续的页)的缓冲区时。要处理不连续的内存,用于这些平台的设备通常需要具有特定种类的分散/集中 DMA 功能。通常,x86 系统会为直接内存传送提供物理地址。

在支持 DVMA 的平台上,系统会为设备提供虚拟地址以执行传送。在此情况下,基础平台提供的内存管理单元 (memory management unit, MMU) 会将对这些虚拟地址的设备访问转换为正确的物理地址。设备会与可映射到不连续物理页的连续虚拟映像之间来回进行传送。在这些平台上运行的设备无需分散/集中 DMA 功能。通常,SPARC 平台会为直接内存传送提供虚拟地址。

DMA 软件组件:句柄、窗口和 Cookie

DMA 句柄是表示对象(通常为内存缓冲区或地址)的不透明指针。设备通过 DMA 句柄可执行 DMA 传送。对 DMA 例程的若干个不同调用可使用句柄来标识为对象分配的 DMA 资源。

DMA 句柄所表示的对象全部包含在一个或多个 DMA cookie 中。DMA cookie 表示 DMA 引擎在数据传送中使用的一段连续内存。系统会根据以下信息将对象划分为多个 cookie:

如果一个对象不满足 DMA 引擎的限制,则必须将该对象分为多个 DMA 窗口。一次只能为一个窗口激活和分配资源。使用 ddi_dma_getwin(9F) 函数可在一个对象内的多个窗口之间切换。每个 DMA 窗口都包含一个或多个 DMA cookie。有关更多信息,请参见DMA 窗口

一些 DMA 引擎可以接受多个 cookie。此类引擎不用借助系统即可执行分散/集中 I/O。如果从一个绑定中返回多个 cookie,则驱动程序应重复调用 ddi_dma_nextcookie(9F) 以检索每个 cookie。然后,必须将这些 cookie 编程到引擎中。随后可对设备进行编程,以传送这些 DMA cookie 聚集所包含的总字节数。

DMA 操作

不同类型的 DMA 之间,DMA 传送的步骤都相似。以下各节提供了执行 DMA 传送的方法。


注 –

在来自文件系统的缓冲区的块驱动程序中,不必确保 DMA 对象是否已在内存中锁定。该文件系统已在内存中锁定了数据。


执行总线主控器 DMA 传送

对于总线主控器 DMA,驱动程序应执行以下步骤:

  1. 描述 DMA 特性。通过此步骤,例程可确保设备能够访问缓冲区。

  2. 分配 DMA 句柄。

  3. 确保 DMA 对象已在内存中锁定。请参见 physio(9F)ddi_umem_lock(9F) 手册页。

  4. 为该对象分配 DMA 资源。

  5. 对设备的 DMA 引擎进行编程。

  6. 启动引擎。

  7. 传送完成后,继续执行总线主控器操作。

  8. 执行所需的对象同步。

  9. 释放 DMA 资源。

  10. 释放 DMA 句柄。

执行第一方 DMA 传送

对于第一方 DMA,驱动程序应执行以下步骤:

  1. 分配 DMA 通道。

  2. 使用 ddi_dmae_1stparty(9F) 配置通道。

  3. 确保 DMA 对象已在内存中锁定。请参见 physio(9F)ddi_umem_lock(9F) 手册页。

  4. 为该对象分配 DMA 资源。

  5. 对设备的 DMA 引擎进行编程。

  6. 启动引擎。

  7. 传送完成后,继续执行总线主控器操作。

  8. 执行所需的对象同步。

  9. 释放 DMA 资源。

  10. 取消分配 DMA 通道。

执行第三方 DMA 传送

对于第三方 DMA,驱动程序应执行以下步骤:

  1. 分配 DMA 通道。

  2. 使用 ddi_dmae_getattr(9F) 检索系统的 DMA 引擎特性。

  3. 在内存中锁定 DMA 对象。请参见 physio(9F)ddi_umem_lock(9F) 手册页。

  4. 为该对象分配 DMA 资源。

  5. 使用 ddi_dmae_prog(9F) 对系统 DMA 引擎进行编程,以执行传送。

  6. 执行所需的对象同步。

  7. 使用 ddi_dmae_stop(9F) 停止 DMA 引擎。

  8. 释放 DMA 资源。

  9. 取消分配 DMA 通道。

某些硬件平台会以特定于总线的方式限制 DMA 功能。驱动程序应使用 ddi_slaveonly(9F) 来确定设备是否位于可以执行 DMA 的插槽中。

DMA 特性

DMA 特性描述 DMA 引擎的特性和限制,其中包括:

设备驱动程序必须通过 ddi_dma_attr(9S) 结构向系统通知任何 DMA 引擎限制。此操作可以确保设备的 DMA 引擎可以访问系统分配的 DMA 资源。系统可能对设备特性实施附加限制,但绝不会取消驱动程序实施的任何限制。

ddi_dma_attr 结构

DMA 特性结构包含以下成员:

typedef struct ddi_dma_attr {
    uint_t      dma_attr_version;       /* version number */
    uint64_t    dma_attr_addr_lo;       /* low DMA address range */
    uint64_t    dma_attr_addr_hi;       /* high DMA address range */
    uint64_t    dma_attr_count_max;     /* DMA counter register */
    uint64_t    dma_attr_align;         /* DMA address alignment */
    uint_t      dma_attr_burstsizes;    /* DMA burstsizes */
    uint32_t    dma_attr_minxfer;       /* min effective DMA size */
    uint64_t    dma_attr_maxxfer;       /* max DMA xfer size */
    uint64_t    dma_attr_seg;           /* segment boundary */
    int         dma_attr_sgllen;        /* s/g length */
    uint32_t    dma_attr_granular;      /* granularity of device */
    uint_t      dma_attr_flags;         /* Bus specific DMA flags */
} ddi_dma_attr_t;

其中:

dma_attr_version

特性结构的版本号。dma_attr_version 应设置为 DMA_ATTR_V0。

dma_attr_addr_lo

DMA 引擎可以访问的最低总线地址。

dma_attr_addr_hi

DMA 引擎可以访问的最高总线地址。

dma_attr_count_max

指定 DMA 引擎可在一个 cookie 中处理的最大传送计数。该限制表示为最大计数减 1。此计数用作位掩码,因此计数也必须比 2 的幂小 1。

dma_attr_align

指定通过 ddi_dma_mem_alloc(9F) 分配内存时的对齐要求。例如,以页边界对齐。dma_attr_align 字段仅在分配内存时使用。在绑定操作过程中,将省略此字段。对于绑定操作,驱动程序必须确保缓冲区已正确对齐。

dma_attr_burstsizes

指定设备支持的突发流量大小。突发流量大小是指设备在放弃总线之前可以传输的数据量。此成员是突发流量大小的二进制编码,这些大小是 2 的幂次方。例如,如果设备能够执行 1 字节、2 字节、4 字节和 16 字节突发传输,则应将此字段设置为 0x17。系统还会使用此字段来确定对齐限制。

dma_attr_minxfer

设备可以执行的最小有效传送大小。此大小还会影响对齐和填充的限制。

dma_attr_maxxfer

描述 DMA 引擎在一个 I/O 命令中可以容纳的最大字节数。此限制仅在 dma_attr_maxxfer 小于 (dma_attr_count_max + 1) * dma_attr_sgllen 时才会有意义。

dma_attr_seg

DMA 引擎的地址寄存器的上限。当地址寄存器的高 8 位为包含段号的锁存器时,通常会使用 dma_attr_seg 。低 24 位用于寻址段。在此情况下,dma_attr_seg 会设置为 0xFFFFFF,这可以防止系统在为对象分配资源时跨越 24 位段边界。

dma_attr_sgllen

指定分散/集中列表中的最大项数。dma_attr_sgllen 是 DMA 引擎在对设备的一个 I/O 请求中可以使用的 cookie 数。如果 DMA 引擎不包含任何分散/集中列表,则此字段应设置为 1。

dma_attr_granular

此字段用于提供设备 DMA 传送能力的粒度(以字节为单位)。指定海量存储设备的扇区大小即是关于如何使用该值的一个例子。如果绑定操作需要部分映射,则可使用此字段确保 DMA 窗口中的 cookie 大小之和为粒度的整数倍。但是,如果设备没有分散/集中功能,则 DDI 无法确保粒度。对于此情况,dma_attr_granular 字段的值应为 1。

dma_attr_flags

此字段可以设置为 DDI_DMA_FORCE_PHYSICAL,这表示如果系统同时支持物理 I/O 地址和虚拟 I/O 地址,则系统应返回物理 I/O 地址而非虚拟 I/O 地址。如果系统不支持物理 DMA,则 ddi_dma_alloc_handle(9F) 的返回值为 DDI_DMA_BADATTR。在此情况下,驱动程序必须清除 DDI_DMA_FORCE_PHYSICAL 并重试该操作。

S 总线示例

在 SPARC 计算机中,S 总线上的 DMA 引擎具有以下特性:

在 SPARC 计算机中,S 总线上的 DMA 引擎具有以下特性结构:

static ddi_dma_attr_t attributes = {
    DMA_ATTR_V0,   /* Version number */
    0xFF000000,    /* low address */
    0xFFFFFFFF,    /* high address */
    0xFFFFFFFF,    /* counter register max */
    1,             /* byte alignment */
    0x7,           /* burst sizes: 0x1 | 0x2 | 0x4 */
    0x1,           /* minimum transfer size */
    0xFFFFFFFF,    /* max transfer size */
    0xFFFFFFFF,    /* address register max */
    1,             /* no scatter-gather */
    512,           /* device operates on sectors */
    0,             /* attr flag: set to 0 */
};

ISA 总线示例

在 x86 计算机中,ISA 总线上的 DMA 引擎具有以下特性:

在 x86 计算机中,ISA 总线上的 DMA 引擎具有以下特性结构:

static ddi_dma_attr_t attributes = {
    DMA_ATTR_V0,   /* Version number */
    0x00000000,    /* low address */
    0x00FFFFFF,    /* high address */
    0xFFFF,        /* counter register max */
    1,             /* byte alignment */
    0x7,           /* burst sizes */
    0x1,           /* minimum transfer size */
    0xFFFFFFFF,    /* max transfer size */
    0x000FFFFF,    /* address register max */
    17,            /* scatter-gather */
    512,           /* device operates on sectors */
    0,             /* attr flag: set to 0 */
};

管理 DMA 资源

本节介绍如何管理 DMA 资源。

对象锁定

为内存对象分配 DMA 资源之前,必须防止该对象移动。否则,在设备尝试向该对象进行写入时,系统会从内存中删除该对象。缺少对象会导致数据传送失败,并且可能损坏系统。防止内存对象在 DMA 传送过程中移动的过程称为锁定对象

以下对象类型不要求显式锁定:

对于其他对象(如用户空间中的缓冲区),必须使用 physio(9F)ddi_umem_lock(9F) 来锁定对象。使用这些函数来锁定对象通常在字符设备驱动程序的 read(9E)write(9E) 例程中执行。有关示例,请参见数据传输方法

分配 DMA 句柄

DMA 句柄是一个不透明的对象,用作对后续分配的 DMA 资源的引用。DMA 句柄通常在驱动程序的使用 ddi_dma_alloc_handle(9F)attach() 入口点中分配。ddi_dma_alloc_handle() 函数采用 dip 引用的设备信息以及 ddi_dma_attr(9S) 结构描述的设备的 DMA 特性作为参数。ddi_dma_alloc_handle() 函数的语法如下所示:

int ddi_dma_alloc_handle(dev_info_t *dip,
    ddi_dma_attr_t *attr, int (*callback)(caddr_t),
    caddr_t arg, ddi_dma_handle_t *handlep);

其中:

dip

指向设备的 dev_info 结构的指针。

attr

指向 ddi_dma_attr(9S) 结构的指针,如DMA 特性中所述。

callback

用于处理资源分配故障的回调函数的地址。

arg

要传递给回调函数的参数。

handlep

指向 DMA 句柄的指针,用于存储返回的句柄。

分配 DMA 资源

以下两个接口用于分配 DMA 资源:

如果存在驱动程序的 xxstart() 例程,则 DMA 资源通常在 xxstart() 例程中分配。有关 xxstart() 的讨论,请参见异步数据传输(块驱动程序)。这两个接口的语法如下:

int ddi_dma_addr_bind_handle(ddi_dma_handle_t handle,
    struct as *as, caddr_t addr,
    size_t len, uint_t flags, int (*callback)(caddr_t),
    caddr_t arg, ddi_dma_cookie_t *cookiep, uint_t *ccountp);

int ddi_dma_buf_bind_handle(ddi_dma_handle_t handle,
    struct buf *bp, uint_t flags,
    int (*callback)(caddr_t), caddr_t arg,
    ddi_dma_cookie_t *cookiep, uint_t *ccountp);

以下参数对于 ddi_dma_addr_bind_handle(9F)ddi_dma_buf_bind_handle(9F) 是通用的:

handle

DMA 句柄和用于分配资源的对象。

flags

表示传送方向和其他特性的标志集。DDI_DMA_READ 表示从设备向内存传送数据。DDI_DMA_WRITE 表示从内存向设备传送数据。有关可用标志的完整讨论,请参见 ddi_dma_addr_bind_handle(9F)ddi_dma_buf_bind_handle(9F) 手册页。

callback

用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。

arg

要传递给回调函数的参数。

cookiep

指向此对象的第一个 DMA cookie 的指针。

ccountp

指向此对象的 DMA cookie 数的指针。

对于 ddi_dma_addr_bind_handle(9F),对象通过包含以下参数的地址范围进行描述:

as

指向地址空间结构的指针。as 的值必须为 NULL

addr

对象的基本内核地址。

len

对象长度(以字节为单位)。

对于 ddi_dma_buf_bind_handle(9F),对象通过 bp 所指向的 buf(9S) 结构进行描述。

设备寄存器结构

对于具有 DMA 功能的设备,要使用的寄存器比前面示例中所用寄存器多。

设备寄存器结构中使用以下字段来支持具有 DMA 功能但不支持分散/集中的设备:

uint32_t      dma_addr;      /* starting address for DMA */
uint32_t      dma_size;      /* amount of data to transfer */

设备寄存器结构中使用以下字段来支持具有 DMA 功能并支持分散/集中的设备:

struct sglentry {
    uint32_t    dma_addr;
    uint32_t    dma_size;
} sglist[SGLLEN];

caddr_t       iopb_addr;     /* When written, informs the device of the next */
                             /* command's parameter block address. */
                             /* When read after an interrupt, contains */
                             /* the address of the completed command. */

DMA 回调示例

示例 9–1 中,xxstart() 用作回调函数。特定设备状态结构用作 xxstart() 的参数。xxstart() 函数将尝试启动命令。如果由于资源不可用而无法启动该命令,则会安排以后在资源可用时调用 xxstart()

由于 xxstart() 用作 DMA 回调,因此 xxstart() 必须遵守以下规则,DMA 回调中将强制执行这些规则:


示例 9–1 DMA 回调示例

static int
xxstart(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    struct device_reg *regp;
    int flags;
    mutex_enter(&xsp->mu);
    if (xsp->busy) {
        /* transfer in progress */
        mutex_exit(&xsp->mu);
        return (DDI_DMA_CALLBACK_RUNOUT);
    }
    xsp->busy = 1;
    regp = xsp->regp;
    if ( /* transfer is a read */ ) {
        flags = DDI_DMA_READ;
    } else {
        flags = DDI_DMA_WRITE;
    }
    mutex_exit(&xsp->mu);
    if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp,flags, xxstart,
        (caddr_t)xsp, &cookie, &ccount) != DDI_DMA_MAPPED) {
        /* really should check all return values in a switch */
        mutex_enter(&xsp->mu);
        xsp->busy=0;
        mutex_exit(&xsp->mu);
        return (DDI_DMA_CALLBACK_RUNOUT);
    }
    /* Program the DMA engine. */
    return (DDI_DMA_CALLBACK_DONE);
}

确定最大突发流量大小

驱动程序在 ddi_dma_attr(9S) 结构的 dma_attr_burstsizes 字段中指定其设备支持的 DMA 突发流量大小。此字段是所支持的突发流量大小的位图。但是,在分配 DMA 资源时,系统可能会对设备实际使用的突发流量大小施加更多限制。ddi_dma_burstsizes(9F) 例程可用来获取允许的突发流量大小。此例程将为设备返回适当的突发流量大小位图。分配 DMA 资源时,驱动程序可向系统请求用于其 DMA 引擎的适当突发流量大小。


示例 9–2 确定突发流量大小

#define BEST_BURST_SIZE 0x20 /* 32 bytes */

    if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp, flags, xxstart,
        (caddr_t)xsp, &cookie, &ccount) != DDI_DMA_MAPPED) {
        /* error handling */
    }
    burst = ddi_dma_burstsizes(xsp->handle);
    /* check which bit is set and choose one burstsize to */
    /* program the DMA engine */
    if (burst & BEST_BURST_SIZE) {
        /* program DMA engine to use this burst size */
    } else {
        /* other cases */
    }

分配专用 DMA 缓冲区

一些设备驱动程序除了执行用户线程和内核请求的传送外,可能还需要为 DMA 传送分配内存。分配专用 DMA 缓冲区的一些示例包括设置用于与设备之间进行通信的共享内存以及分配中间传送缓冲区。使用 ddi_dma_mem_alloc(9F) 可为 DMA 传送分配内存。

int ddi_dma_mem_alloc(ddi_dma_handle_t handle, size_t length,
    ddi_device_acc_attr_t *accattrp, uint_t flags,
    int (*waitfp)(caddr_t), caddr_t arg, caddr_t *kaddrp,
    size_t *real_length, ddi_acc_handle_t *handlep);

其中:

handle

DMA 句柄

length

所需分配的长度(以字节为单位)

accattrp

指向设备访问特性结构的指针

flags

数据传送模式标志。可能的值包括 DDI_DMA_CONSISTENTDDI_DMA_STREAMING

waitfp

用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。

arg

要传递给回调函数的参数

kaddrp

成功返回时包含已分配存储空间的地址的指针

real_length

分配的长度(以字节为单位)

handlep

指向数据访问句柄的指针

如果设备以不连续的方式进行访问,则应将 flags 参数设置为 DDI_DMA_CONSISTENT。由于会频繁应用于小型对象,因此使用 ddi_dma_sync(9F) 的同步步骤应尽可能为轻量步骤。这种访问类型通常称为一致访问。一致访问对用于设备与驱动程序之间通信的 I/O 参数块特别有用。

在 x86 平台上,物理上连续的 DMA 内存的分配有以下要求:

以下示例说明如何分配 IOPB 内存以及访问此内存必需的 DMA 资源。仍然必须分配 DMA 资源,并且必须将 DDI_DMA_CONSISTENT 标志传递给分配函数。


示例 9–3 使用 ddi_dma_mem_alloc(9F)

if (ddi_dma_mem_alloc(xsp->iopb_handle, size, &accattr,
    DDI_DMA_CONSISTENT, DDI_DMA_SLEEP, NULL, &xsp->iopb_array,
    &real_length, &xsp->acchandle) != DDI_SUCCESS) {
    /* error handling */
    goto failure;
}
if (ddi_dma_addr_bind_handle(xsp->iopb_handle, NULL,
    xsp->iopb_array, real_length,
    DDI_DMA_READ | DDI_DMA_CONSISTENT, DDI_DMA_SLEEP,
    NULL, &cookie, &count) != DDI_DMA_MAPPED) {
    /* error handling */
    ddi_dma_mem_free(&xsp->acchandle);
    goto failure;
}

对于顺序、单向、块大小和按块对齐的内存传送,flags 参数应设置为 DDI_DMA_STREAMING。这种访问类型通常称为访问。

在某些情况下,使用 I/O 高速缓存可以加快 I/O 传送。I/O 高速缓存最少传送一个高速缓存行。ddi_dma_mem_alloc(9F) 例程会将 size 舍入为高速缓存行的倍数,以避免数据损坏。

ddi_dma_mem_alloc(9F) 函数将返回已分配的内存对象的实际大小。由于存在填充和对齐要求,实际大小可能会大于所请求的大小。ddi_dma_addr_bind_handle(9F) 函数要求使用实际长度。

使用 ddi_dma_mem_free(9F) 函数可以释放 ddi_dma_mem_alloc(9F) 分配的内存。


注 –

驱动程序必须确保缓冲区适当对齐。要求下限 DMA 缓冲区对齐的设备的驱动程序可能需要将数据复制到满足该要求的驱动程序中间缓冲区,然后将该中间缓冲区绑定到 DMA 的 DMA 句柄。使用 ddi_dma_mem_alloc(9F) 可分配驱动程序中间缓冲区。请务必使用 ddi_dma_mem_alloc(9F) 而非 kmem_alloc(9F) 来为要进行访问的设备分配内存。


处理资源分配故障

资源分配例程在处理分配故障时可为驱动程序提供若干选项。waitfp 参数用于指明分配例程是阻塞、立即返回还是安排回调,如下表所示。

表 9–1 资源分配处理

waitfp

表示的操作 

DDI_DMA_DONTWAIT

驱动程序不想等到资源可用 

DDI_DMA_SLEEP

驱动程序愿意无限期地等到资源可用 

其他值 

当资源可能可用时要调用的函数的地址 

对 DMA 引擎进行编程

如果资源已成功分配,则必须对设备进行编程。尽管对 DMA 引擎进行编程是特定于设备的,但所有 DMA 引擎都需要一个起始地址和一个传送计数。设备驱动程序将从 ddi_dma_addr_bind_handle(9F)ddi_dma_buf_bind_handle(9F)ddi_dma_getwin(9F) 的成功调用所返回的 DMA cookie 中检索这两个值。这些函数都会返回第一个 DMA cookie 以及指示 DMA 对象是否包含多个 cookie 的 cookie 计数。如果 cookie 计数 N 大于 1,则必须对 ddi_dma_nextcookie(9F) 调用 N-1 次,以检索其余所有 cookie。

DMA cookie 的类型为 ddi_dma_cookie(9S)。这一类型的 cookie 包含以下字段:

uint64_t    _dmac_ll;       /* 64-bit DMA address */
uint32_t    _dmac_la[2];    /* 2 x 32-bit address */
size_t      dmac_size;      /* DMA cookie size */
uint_t      dmac_type;      /* bus specific type bits */

dmac_laddress 指定适用于对设备的 DMA 引擎进行编程的 64 位 I/O 地址。如果设备具有 64 位 DMA 地址寄存器,则驱动程序应使用此字段对 DMA 引擎进行编程。dmac_address 字段指定应该用于具有 32 位 DMA 地址寄存器的设备的 32 位 I/O 地址。dmac_size 字段包含传送计数。根据总线体系结构,驱动程序可能需要 cookie 中的 dmac_type 字段。驱动程序不应对 cookie 执行任何处理,如逻辑或算术处理。


示例 9–4 ddi_dma_cookie(9S) 示例

ddi_dma_cookie_t            cookie;

     if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp, flags, xxstart,
     (caddr_t)xsp, &cookie, &xsp->ccount) != DDI_DMA_MAPPED) {
         /* error handling */
      }
     sglp = regp->sglist;
     for (cnt = 1; cnt <= SGLLEN; cnt++, sglp++) {
     /* store the cookie parms into the S/G list */
     ddi_put32(xsp->access_hdl, &sglp->dma_size,
         (uint32_t)cookie.dmac_size);
     ddi_put32(xsp->access_hdl, &sglp->dma_addr,
         cookie.dmac_address);
     /* Check for end of cookie list */
     if (cnt == xsp->ccount)
         break;
     /* Get next DMA cookie */
     (void) ddi_dma_nextcookie(xsp->handle, &cookie);
     }
    /* start DMA transfer */
     ddi_put8(xsp->access_hdl, &regp->csr,
     ENABLE_INTERRUPTS | START_TRANSFER);

释放 DMA 资源

DMA 传送完成后(通常在中断例程中),驱动程序可以通过调用 ddi_dma_unbind_handle(9F) 来释放 DMA 资源。

同步内存对象中所述,ddi_dma_unbind_handle(9F) 可调用 ddi_dma_sync(9F),从而无需进行任何显式同步。调用 ddi_dma_unbind_handle(9F) 之后,DMA 资源将无效,并且对资源的进一步引用会产生无法预料的结果。以下示例说明如何使用 ddi_dma_unbind_handle(9F)


示例 9–5 释放 DMA 资源

static uint_t
xxintr(caddr_t arg)
{
     struct xxstate *xsp = (struct xxstate *)arg;
     uint8_t    status;
    volatile   uint8_t   temp;
     mutex_enter(&xsp->mu);
     /* read status */
     status = ddi_get8(xsp->access_hdl, &xsp->regp->csr);
     if (!(status & INTERRUPTING)) {
        mutex_exit(&xsp->mu);
        return (DDI_INTR_UNCLAIMED);
     }
     ddi_put8(xsp->access_hdl, &xsp->regp->csr, CLEAR_INTERRUPT);
      /* for store buffers */
     temp = ddi_get8(xsp->access_hdl, &xsp->regp->csr);
     ddi_dma_unbind_handle(xsp->handle);
     /* Check for errors. */
     xsp->busy = 0;
     mutex_exit(&xsp->mu);
     if ( /* pending transfers */ ) {
        (void) xxstart((caddr_t)xsp);
     }
     return (DDI_INTR_CLAIMED);
}

应释放 DMA 资源。如果要在下一传送中使用不同对象,则应重新分配 DMA 资源。但是,如果始终使用同一个对象,则分配一次资源即可。只要保持对 ddi_dma_sync(9F) 的介入调用,随后便可重用资源。

释放 DMA 句柄

分离驱动程序时,必须释放 DMA 句柄。ddi_dma_free_handle(9F) 函数可销毁 DMA 句柄以及系统在该句柄上高速缓存的任何剩余资源。如果再对 DMA 句柄进行任何引用,将会产生无法预料的结果。

取消 DMA 回调

DMA 回调不能取消。取消 DMA 回调需要在驱动程序的 detach(9E) 入口点中附加一些代码。如果存在任何未完成的回调,则 detach() 例程一定不会返回 DDI_SUCCESS。请参见示例 9–6。发生 DMA 回调时,detach() 例程必须等待回调运行。回调完成时,detach() 必须防止回调自行重新安排。通过状态结构中的附加字段可以防止重新安排回调,如以下示例所示。


示例 9–6 取消 DMA 回调

static int
xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
     /* ... */
     mutex_enter(&xsp->callback_mutex);
     xsp->cancel_callbacks = 1;
     while (xsp->callback_count > 0) {
        cv_wait(&xsp->callback_cv, &xsp->callback_mutex);
     }
     mutex_exit(&xsp->callback_mutex);
     /* ... */
 }

static int
xxstrategy(struct buf *bp)
{
     /* ... */
     mutex_enter(&xsp->callback_mutex);
       xsp->bp = bp;
     error = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp, flags,
         xxdmacallback, (caddr_t)xsp, &cookie, &ccount);
     if (error == DDI_DMA_NORESOURCES)
       xsp->callback_count++;
     mutex_exit(&xsp->callback_mutex);
     /* ... */
}

static int
xxdmacallback(caddr_t callbackarg)
{
     struct xxstate *xsp = (struct xxstate *)callbackarg;
     /* ... */
     mutex_enter(&xsp->callback_mutex);
     if (xsp->cancel_callbacks) {
        /* do not reschedule, in process of detaching */
        xsp->callback_count--;
        if (xsp->callback_count == 0)
           cv_signal(&xsp->callback_cv);
        mutex_exit(&xsp->callback_mutex);
        return (DDI_DMA_CALLBACK_DONE);    /* don't reschedule it */
     }
     /*
      * Presumably at this point the device is still active
      * and will not be detached until the DMA has completed.
      * A return of 0 means try again later
      */
     error = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp, flags,
         DDI_DMA_DONTWAIT, NULL, &cookie, &ccount);
     if (error == DDI_DMA_MAPPED) {
        /* Program the DMA engine. */
        xsp->callback_count--;
        mutex_exit(&xsp->callback_mutex);
        return (DDI_DMA_CALLBACK_DONE);
     }
     if (error != DDI_DMA_NORESOURCES) {
        xsp->callback_count--;
        mutex_exit(&xsp->callback_mutex);
        return (DDI_DMA_CALLBACK_DONE);
     }
     mutex_exit(&xsp->callback_mutex);
     return (DDI_DMA_CALLBACK_RUNOUT);
}

同步内存对象

在访问内存对象的过程中,驱动程序可能需要同步与各种高速缓存有关的内存对象。本节提供了有关何时以及如何同步内存对象的准则。

高速缓存

CPU 高速缓存是位于 CPU 和系统的主内存之间的极高速内存。I/O 高速缓存位于设备和系统的主内存之间,如下图所示。

图 9–1 CPU 和系统 I/O 高速缓存

图中显示如何使用高速缓存加快涉及设备的数据传送。

尝试从主内存读取数据时,关联的高速缓存会对请求的数据进行检查。如果数据可用,高速缓存可快速提供这些数据。如果高速缓存中没有数据,则该高速缓存将从主内存中检索数据。然后,高速缓存会将数据传递给请求者并保存数据,以备在后续请求中使用。

类似地,在写循环中,数据会快速存储在高速缓存中。CPU 或设备可以继续执行,即传送数据。将数据存储在高速缓存中所需的时间比等待将数据写入内存所需的时间少得多。

采用此模型,在设备传送完成后,数据仍可位于 I/O 高速缓存中,而主内存中没有数据。如果 CPU 访问内存,CPU 可能会从 CPU 高速缓存中读取错误数据。驱动程序必须调用同步例程,以刷新 I/O 高速缓存中的数据,并使用新数据更新 CPU 高速缓存。此操作可确保内存的情况对于 CPU 而言保持一致。类似地,如果设备要对 CPU 修改的数据进行访问,则需要采用同步步骤。

可在设备和内存之间创建附加的高速缓存和缓冲区,如总线延伸架和桥。使用 ddi_dma_sync(9F) 可以同步所有适用的高速缓存。

ddi_dma_sync() 函数

一个内存对象可能有多个映射,如通过 DMA 句柄用于 CPU 和用于设备的映射。如果使用任何映射来修改内存对象,则具有多个映射的驱动程序需要调用 ddi_dma_sync(9F)。调用 ddi_dma_sync() 可以确保对内存对象的修改在通过不同映射访问该对象之前完成。如果对对象的任何高速缓存引用现在已过时,ddi_dma_sync() 函数还可以通知该对象的其他映射。此外,ddi_dma_sync() 还会根据需要刷新过时的高速缓存引用或使其无效。

通常,当 DMA 传送完成时,驱动程序必须调用 ddi_dma_sync()。此规则的例外情况是如果使用 ddi_dma_unbind_handle(9F) 取消分配 DMA 资源,则会代表驱动程序隐式执行 ddi_dma_sync()ddi_dma_sync() 的语法如下:

int ddi_dma_sync(ddi_dma_handle_t handle, off_t off,
size_t length, uint_t type);

如果设备的 DMA 引擎要读取对象,则必须通过将 type 设置为 DDI_DMA_SYNC_FORDEV 来同步该设备看到的对象信息。如果设备的 DMA 引擎已写入内存对象并且 CPU 将读取该对象,则必须通过将 type 设置为 DDI_DMA_SYNC_FORCPU 来同步该 CPU 看到的对象信息。

以下示例说明如何为 CPU 同步 DMA 对象:

if (ddi_dma_sync(xsp->handle, 0, length, DDI_DMA_SYNC_FORCPU)
    == DDI_SUCCESS) {
    /* the CPU can now access the transferred data */
    /* ... */
} else {
    /* error handling */
}

如果唯一的映射是用于内核的,请使用标志 DDI_DMA_SYNC_FORKERNEL,类似于 ddi_dma_mem_alloc(9F) 所分配的内存中的情况。系统会尝试以比同步 CPU 看到的信息更快的速度来同步内核看到的信息。如果系统无法更快地同步内核看到的信息,则系统将按照如同已设置 DDI_DMA_SYNC_FORCPU 标志的情况执行相应的操作。

DMA 窗口

如果对象不满足 DMA 引擎的限制,则必须将传送分为一系列较小的传送。驱动程序本身即可对传送进行拆分。或者,驱动程序也可以允许系统仅为对象的一部分分配资源,从而创建一系列 DMA 窗口。允许系统分配资源是首选解决方案,因为系统管理资源的效率比驱动程序高。

DMA 窗口有两个特性。offset 特性是从对象的开头度量的。length 特性是要分配的内存的字节数。在进行部分分配后,只有一系列在 offset 开始的 length 字节分配了资源。

请求 DMA 窗口的方法是将 DDI_DMA_PARTIAL 标志指定为 ddi_dma_buf_bind_handle(9F)ddi_dma_addr_bind_handle(9F) 的参数。如果可以建立窗口,则这两个函数都将返回 DDI_DMA_PARTIAL_MAP。但是,系统可能会为整个对象分配资源,此时将返回 DDI_DMA_MAPPED。驱动程序应检查返回值,以确定 DMA 窗口是否正在使用。请参见以下示例。


示例 9–7 设置 DMA 窗口

static int
xxstart (caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    struct device_reg *regp = xsp->reg;
    ddi_dma_cookie_t cookie;
    int status;
    mutex_enter(&xsp->mu);
    if (xsp->busy) {
        /* transfer in progress */
        mutex_exit(&xsp->mu);
        return (DDI_DMA_CALLBACK_RUNOUT);
    }
    xsp->busy = 1;
    mutex_exit(&xsp->mu);
    if ( /* transfer is a read */) {
        flags = DDI_DMA_READ;
    } else {
        flags = DDI_DMA_WRITE;
    }
    flags |= DDI_DMA_PARTIAL;
    status = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp,
        flags, xxstart, (caddr_t)xsp, &cookie, &ccount);
    if (status != DDI_DMA_MAPPED &&
        status != DDI_DMA_PARTIAL_MAP)
        return (DDI_DMA_CALLBACK_RUNOUT);
    if (status == DDI_DMA_PARTIAL_MAP) {
        ddi_dma_numwin(xsp->handle, &xsp->nwin);
        xsp->partial = 1;
        xsp->windex = 0;
    } else {
        xsp->partial = 0;
    }
    /* Program the DMA engine. */
    return (DDI_DMA_CALLBACK_DONE);
}

有两个函数可对 DMA 窗口执行操作。第一个函数 ddi_dma_numwin(9F) 可为特定的 DMA 对象返回 DMA 窗口数。另一个函数 ddi_dma_getwin(9F) 允许在对象内重新定位,即重新分配系统资源。ddi_dma_getwin () 函数用于从当前窗口切换到对象中的新窗口。由于 ddi_dma_getwin() 会将系统资源重新分配给新窗口,因此前面的窗口将变为无效。


注意 – 注意 –

在向当前窗口的传送完成之前,请勿通过调用 ddi_dma_getwin() 来移动 DMA 窗口。请一直等待,直至向当前窗口的传送完成为止,即出现中断的时候。然后,调用 ddi_dma_getwin() 以避免数据损坏。


通常从中断例程中调用 ddi_dma_getwin() 函数,如示例 9–8 中所示。调用驱动程序会导致启动第一个 DMA 传送。后续传送将从中断例程中启动。

中断例程会检查设备的状态,以确定设备是否已成功完成传送。如果未成功完成传送,则会进行正常错误恢复。如果传送成功,例程必须确定逻辑传送是否已完成。完整的传送包括 buf(9S) 结构所指定的整个对象。在部分传送中,仅会移动一个 DMA 窗口。在部分传送中,中断例程将使用 ddi_dma_getwin(9F) 移动窗口、检索新 cookie 并启动其他 DMA 传送。

如果逻辑请求已完成,则中断例程将检查待处理的请求。如有必要,中断例程会启动传送。否则,例程将返回,而不调用其他 DMA 传送。以下示例说明了常见的流程控制。


示例 9–8 使用 DMA 窗口中断处理程序

static uint_t
xxintr(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    uint8_t    status;
    volatile   uint8_t   temp;
    mutex_enter(&xsp->mu);
    /* read status */
    status = ddi_get8(xsp->access_hdl, &xsp->regp->csr);
    if (!(status & INTERRUPTING)) {
        mutex_exit(&xsp->mu);
        return (DDI_INTR_UNCLAIMED);
    }
    ddi_put8(xsp->access_hdl,&xsp->regp->csr, CLEAR_INTERRUPT);
    /* for store buffers */
    temp = ddi_get8(xsp->access_hdl, &xsp->regp->csr);
    if ( /* an error occurred during transfer */ ) {
        bioerror(xsp->bp, EIO);
        xsp->partial = 0;
    } else {
        xsp->bp->b_resid -= /* amount transferred */ ;
    }

    if (xsp->partial && (++xsp->windex < xsp->nwin)) {
        /* device still marked busy to protect state */
        mutex_exit(&xsp->mu);
        (void) ddi_dma_getwin(xsp->handle, xsp->windex,
            &offset, &len, &cookie, &ccount);
        /* Program the DMA engine with the new cookie(s). */
        return (DDI_INTR_CLAIMED);
    }
    ddi_dma_unbind_handle(xsp->handle);
    biodone(xsp->bp);
    xsp->busy = 0;
    xsp->partial = 0;
    mutex_exit(&xsp->mu);
    if ( /* pending transfers */ ) {
        (void) xxstart((caddr_t)xsp);
    }
    return (DDI_INTR_CLAIMED);
}