第 1 部分针对 Oracle Solaris 平台设计设备驱动程序
9. 直接内存访问 (Direct Memory Access, DMA)
本节介绍如何管理 DMA 资源。
为内存对象分配 DMA 资源之前,必须防止该对象移动。否则,在设备尝试向该对象进行写入时,系统会从内存中删除该对象。缺少对象会导致数据传送失败,并且可能损坏系统。防止内存对象在 DMA 传送过程中移动的过程称为锁定对象。
以下对象类型不要求显式锁定:
通过执行 strategy(9E) 获得的来自文件系统的缓冲区。这些缓冲区已由文件系统锁定。
设备驱动程序中分配的内核内存,如 ddi_dma_mem_alloc(9F) 分配的内核内存。
对于其他对象(如用户空间中的缓冲区),必须使用 physio(9F) 或 ddi_umem_lock(9F) 来锁定对象。使用这些函数来锁定对象通常在字符设备驱动程序的 read(9E) 或 write(9E) 例程中执行。有关示例,请参见数据传输方法。
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);
其中:
指向设备的 dev_info 结构的指针。
指向 ddi_dma_attr(9S) 结构的指针,如DMA 特性中所述。
用于处理资源分配故障的回调函数的地址。
要传递给回调函数的参数。
指向 DMA 句柄的指针,用于存储返回的句柄。
以下两个接口用于分配 DMA 资源:
ddi_dma_buf_bind_handle(9F)-与 buf(9S) 结构结合使用
ddi_dma_addr_bind_handle(9F)-与虚拟地址结合使用
如果存在驱动程序的 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) 是通用的:
DMA 句柄和用于分配资源的对象。
表示传送方向和其他特性的标志集。DDI_DMA_READ 表示从设备向内存传送数据。DDI_DMA_WRITE 表示从内存向设备传送数据。有关可用标志的完整讨论,请参见 ddi_dma_addr_bind_handle(9F) 或 ddi_dma_buf_bind_handle(9F) 手册页。
用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。
要传递给回调函数的参数。
指向此对象的第一个 DMA cookie 的指针。
指向此对象的 DMA cookie 数的指针。
对于 ddi_dma_addr_bind_handle(9F),对象通过包含以下参数的地址范围进行描述:
指向地址空间结构的指针。as 的值必须是 NULL。
对象的基本内核地址。
对象长度(以字节为单位)。
对于 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. */
在示例 9-1 中,xxstart() 用作回调函数。特定设备状态结构用作 xxstart() 的参数。xxstart() 函数将尝试启动命令。如果由于资源不可用而无法启动该命令,则会安排以后在资源可用时调用 xxstart()。
由于 xxstart() 用作 DMA 回调,因此 xxstart() 必须遵守以下规则,DMA 回调上施加了这些规则:
不能假定资源可用。回调必须尝试再次分配资源。
回调必须向系统指明分配是否成功。如果回调未能分配资源,则应返回 DDI_DMA_CALLBACK_RUNOUT,在此情况下需要以后再次调用 xxstart()。DDI_DMA_CALLBACK_DONE 表示回调成功,因此不需要再进行回调。
示例 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 缓冲区的一些示例包括设置用于与设备之间进行通信的共享内存以及分配中间传送缓冲区。使用 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);
其中:
DMA 句柄
所需分配的长度(以字节为单位)
指向设备访问特性结构的指针
数据传送模式标志。可能的值包括 DDI_DMA_CONSISTENT 和 DDI_DMA_STREAMING。
用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。
要传递给回调函数的参数
成功返回时包含已分配存储空间的地址的指针
分配的长度(以字节为单位)
指向数据访问句柄的指针
如果设备以不连续的方式进行访问,则应将 flags 参数设置为 DDI_DMA_CONSISTENT。由于会频繁应用于小型对象,因此使用 ddi_dma_sync(9F) 的同步步骤应尽可能为轻量级步骤。这种访问类型通常称为一致访问。一致访问对用于设备与驱动程序之间通信的 I/O 参数块特别有用。
在 x86 平台上,物理上连续的 DMA 内存的分配有以下要求:
ddi_dma_attr(9S) 结构中分散/集中列表 dma_attr_sgllen 的长度必须设置为 1。
请勿指定 DDI_DMA_PARTIAL。DDI_DMA_PARTIAL 表示允许进行部分资源分配。
以下示例说明如何分配 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 资源分配处理
|
如果资源已成功分配,则必须对设备进行编程。尽管对 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, ®p->csr, ENABLE_INTERRUPTS | START_TRANSFER);
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 句柄。ddi_dma_free_handle(9F) 函数可销毁 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 高速缓存位于设备和系统的主内存之间,如下图所示。
尝试从主内存读取数据时,关联的高速缓存会对请求的数据进行检查。如果数据可用,高速缓存可快速提供这些数据。如果高速缓存中没有数据,则该高速缓存将从主内存中检索数据。然后,高速缓存会将数据传递给请求者并保存数据,以备在后续请求中使用。
类似地,在写循环中,数据会快速存储在高速缓存中。CPU 或设备可以继续执行,即传送数据。将数据存储在高速缓存中所需的时间比等待将数据写入内存所需的时间少得多。
采用此模型,在设备传送完成后,数据仍可位于 I/O 高速缓存中,而主内存中没有数据。如果 CPU 访问内存,CPU 可能会从 CPU 高速缓存中读取错误数据。驱动程序必须调用同步例程,以刷新 I/O 高速缓存中的数据,并使用新数据更新 CPU 高速缓存。此操作可确保内存的情况对于 CPU 而言保持一致。类似地,如果设备要对 CPU 修改的数据进行访问,则需要采用同步步骤。
可在设备和内存之间创建附加的高速缓存和缓冲区,如总线延伸架和桥。使用 ddi_dma_sync(9F) 可以同步所有适用的高速缓存。
一个内存对象可能有多个映射,如通过 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 标志的情况执行相应的操作。