在 Windows 7 及更早版本上,Kernel-Mode Driver Framework (KMDF) 仅支持 (DMA) 设备的总线-主直接内存访问。 此类设备包含其自己的 DMA 控制器。
在片上系统 (SoC) 上运行Windows 8及更高版本的平台上,该框架还支持系统模式 DMA,其中多个设备共享单个多通道 DMA 控制器。
框架的 DMA 支持包括:
- 一组框架 DMA 对象和方法,驱动程序使用这些对象和方法将 I/O 请求转换为 DMA 操作;
- 一组驱动程序提供的事件回调函数,这些函数在发生不同事件时配置设备的 DMA 行为;
框架支持单数据包和 scatter/gather DMA 传输。 它还支持使用通用缓冲区。
在运行 Windows 8 及更高版本的基于 SoC 的平台上,框架支持单数据包系统模式 DMA 传输。
为了在基于框架的驱动程序中处理总线主机和系统模式 DMA 操作,框架提供了三个对象:
- DMA 启用程序对象:框架的 DMA 启用程序对象使驱动程序能够对特定设备使用框架的 DMA 支持。 驱动程序必须为支持 DMA 操作的每个设备创建 DMA 启用程序对象;
- DMA 任务对象(事务对象):框架的 DMA 任务对象表示单个 DMA I/O 操作。 如果设备使用 DMA 执行请求的操作,基于框架的驱动程序通常会为其接收的每个 I/O 请求创建 DMA 事务对象;
- 公共缓冲区对象:框架的公共缓冲区对象表示计算机内存的一个区域,该区域映射供驱动程序和设备同时访问。 某些驱动程序在为 DMA 设备设置 I/O 操作时 使用通用缓冲区 ;
下面是一些术语的解释:
DMA 任务(事务):DMA 任务是一个完整的 I/O 操作,例如来自应用程序的单个读取或写入请求;
DMA 传输:DMA 传输是单个硬件操作,用于将数据从计算机内存传输到设备或从设备传输到计算机内存;
单个 DMA 任务始终包含至少一个 DMA 传输,但一个任务可以包含多个传输。
当基于框架的驱动程序收到 I/O 请求时,驱动程序通常会创建一个 DMA 任务对象来表示请求。 当框架开始为任务提供服务时,它会确定设备是否可以在单个传输中处理整个任务。 如果任务太大,框架会将任务分解为多个传输。
在适用于总线主控 DMA 设备的 KMDF 驱动程序中处理 I/O 请求
处理总线主 DMA 设备的 KMDF 驱动程序中的 I/O 请求需要多个驱动程序的事件回调函数中的代码,如下图所示:
如上所示,与 DMA 相关的处理分四个阶段进行:
- 驱动程序的 EvtDriverDeviceAdd 或 EvtDevicePrepareHardware 回调函数必须为设备 启用 DMA 事务 ,以便驱动程序可以使用框架的 DMA 功能。 如果设备和驱动程序需要访问共享内存缓冲区,则相同的回调函数还必须 创建一个公共 缓冲区;
- 当驱动程序收到需要设备执行 DMA 操作的 I/O 请求时,驱动程序 的请求处理程序 之一必须 创建并初始化新的 DMA 事务。 请注意,如果驱动程序 重用 DMA 事务对象,驱动程序的 EvtDriverDeviceAdd 回调函数可以创建事务对象。然后,请求处理程序必须 启动 DMA 事务 ,以便框架可以根据需要开始将事务分解为较小的 DMA 传输,并调用驱动程序的 EvtProgramDma 回调函数;
- 驱动程序的 EvtProgramDma 回调函数针对单个 DMA 传输对 DMA 硬件进行编程 ,并启用设备中断;
- 设备中断时,框架会调用驱动程序的 EvtInterruptIsr 回调函数,这将保存易失设备信息并计划驱动程序的 EvtInterruptDpc 回调函数的执行。驱动程序的 EvtInterruptDpc 回调函数在硬件 完成处理后完成每个 DMA 传输 。 DMA 事务的最终传输完成后, EvtInterruptDpc 回调函数 完成 DMA 事务;
驱动程序可能会 重复使用其 DMA 事务对象 ,以确保它们在内存资源不足时可以运行。
驱动程序可以提供一组回调函数,用于处理 特定于 DMA 的电源管理操作。
某些驱动程序使用设备和驱动程序都可以访问的通用缓冲区 。
启用 DMA 事务
如果基于框架的驱动程序处理 DMA 设备的 I/O 操作,则驱动程序必须为每个 DMA 设备启用框架的 DMA 功能。 若要启用这些功能,驱动程序的 EvtDriverDeviceAdd 或 EvtDevicePrepareHardware 回调函数必须:
调用 WdfDeviceSetAlignmentRequirement 以指定设备的缓冲区对齐要求。
调用 WdfDmaEnablerCreate 以指定 DMA 操作的类型 (单个数据包或散点/收集) 以及设备支持的最大传输大小。 从 KMDF 版本 1.11 开始,该框架支持在操作系统的 Windows 8 或更高版本上运行的基于芯片 (SoC) 的系统上的系统模式 DMA。
调用 WdfDmaEnablerSetMaximumScatterGatherElements 以指定设备可在scatter/gather列表中支持的最大元素数,如果设备支持scatter/gather操作。
以下代码示例演示了如何启用框架的 DMA 功能:
WDF_DMA_ENABLER_CONFIG dmaConfig;
WdfDeviceSetAlignmentRequirement( DevExt->Device, PCI9656_DTE_ALIGNMENT_16 );
WDF_DMA_ENABLER_CONFIG_INIT( &dmaConfig,
WdfDmaProfileScatterGather64Duplex,
DevExt->MaximumTransferLength );
status = WdfDmaEnablerCreate( DevExt->Device,
&dmaConfig,
WDF_NO_OBJECT_ATTRIBUTES,
&DevExt->DmaEnabler );
如果驱动程序需要通用缓冲区,驱动程序的 EvtDriverDeviceAdd 回调函数通常会设置它们。
驱动程序调用 WdfDmaEnablerCreate 后,它可以调用 WdfDmaEnablerWdmGetDmaAdapter 以获取指向框架为设备的输入和输出方向创建的 WDM DMA_ADAPTER 结构的指针。 但是,大多数基于框架的驱动程序不需要访问这些结构。
创建并初始化 DMA 事务
在驱动程序可以将 I/O 请求发送到 DMA 设备之前,驱动程序必须:
- 调用 WdfDmaTransactionCreate 为请求创建 DMA 事务对象;
- 调用 WdfDmaTransactionInitializeUsingRequest、 WdfDmaTransactionInitialize 或 WdfDmaTransactionInitializeUsingOffset 来初始化事务对象;
通常,驱动程序会创建 DMA 事务 ,因为 请求处理程序 已收到 框架请求对象 ,并且必须将请求传递给硬件。 在这种情况下,驱动程序应调用 WdfDmaTransactionInitializeUsingRequest,后者接受请求对象句柄作为输入,并从请求对象中提取请求的地址参数。
如果驱动程序必须创建 不 基于驱动程序收到的框架请求对象的 DMA 事务,则驱动程序可以调用 WdfDmaTransactionInitialize 或 WdfDmaTransactionInitializeUsingOffset。 这两种方法都接受驱动程序提供的地址参数。
所有三种初始化方法都需要 EvtProgramDma 事件回调函数的地址作为输入参数。 此回调函数对设备进行程序,并且每次 DMA 传输 可用时,框架都会调用回调函数。
当驱动程序调用 WdfDmaEnablerCreate 来创建 DMA 启用程序对象时,驱动程序将提供包含设备最大传输长度的 WDF_DMA_ENABLER_CONFIG 结构。 框架使用此值作为所有 DMA 传输的默认最大长度。
对于某些类型的 DMA 事务,可能需要指定与设备的默认最大长度不同的最大传输长度。 可以使用 WdfDmaTransactionSetMaximumLength 设置单个事务的最大传输长度。 框架仅在处理指定的事务时使用指定的最大传输长度。
请注意,最大传输长度受操作系统提供给 DMA 启用程序对象的 映射寄存器 数的限制。 若要确定可用的最大传输长度,驱动程序可以调用 WdfDmaEnablerGetFragmentLength。 如果 WdfDmaEnablerGetFragmentLength 返回的值小于驱动程序提供给 WdfDmaEnablerCreate 的最大传输长度,则框架使用较小的值。
在驱动程序创建并初始化 DMA 事务后,驱动程序必须 启动该事务。
启动 DMA 事务
在驱动程序 创建并初始化 DMA 事务后,驱动程序可以调用 WdfDmaTransactionExecute 方法来启动该事务。 此方法为与事务关联的第一个 DMA 传输 生成散点/收集列表。 接下来, 方法调用驱动程序为事务注册的 EvtProgramDma 回调函数。 回调函数 对 DMA 硬件进行程序 以启动传输。
在驱动程序调用 WdfDmaTransactionExecute 之前,驱动程序必须存储 DMA 事务句柄,以便在驱动程序完成与事务关联的每个 DMA 传输时可以检索该句柄。 存储事务句柄的一个好位置是在框架对象的上下文内存中,通常是设备的框架设备对象。
以下代码演示如何初始化并执行 DMA 事务:
VOID PLxEvtIoRead(
IN WDFQUEUE Queue,
IN WDFREQUEST Request,
IN size_t Length
)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
PDEVICE_EXTENSION devExt;
// Get the DevExt from the queue handle
devExt = PLxGetDeviceContext(WdfIoQueueGetDevice(Queue));
do {
// Validate the Length parameter.
if (Length > PCI9656_SRAM_SIZE) {
status = STATUS_INVALID_BUFFER_SIZE;
break;
}
// Initialize the DmaTransaction.
status =
WdfDmaTransactionInitializeUsingRequest(
devExt->ReadDmaTransaction,
Request,
PLxEvtProgramReadDma,
WdfDmaDirectionReadFromDevice
);
if(!NT_SUCCESS(status)) {
. . . //Error-handling code omitted
break;
}
// Execute this DmaTransaction.
status = WdfDmaTransactionExecute( devExt->ReadDmaTransaction,
WDF_NO_CONTEXT);
if(!NT_SUCCESS(status)) {
. . . //Error-handling code omitted
break;
}
// Indicate that the DMA transaction started successfully.
// The DPC routine will complete the request when the DMA
// transaction is complete.
status = STATUS_SUCCESS;
} while (0);
// If there are errors, clean up and complete the request.
if (!NT_SUCCESS(status )) {
WdfDmaTransactionRelease(devExt->ReadDmaTransaction);
WdfRequestComplete(Request, status);
}
return;
}