对 DMA 硬件编程
总线主 DMA 设备的 KMDF 驱动程序通常在其 EvtProgramDma 事件回调函数中提供一些功能。 如果驱动程序使用框架的 DMA 支持,驱动程序必须提供此回调。 此信息也适用于具有硬件中断 的系统模式 DMA 设备的 KMDF 驱动程序。
在 IRQL = DISPATCH_LEVEL调用的 EvtProgramDma 回调函数对设备进行编程以启动 DMA 传输。 此回调函数的输入参数提供传输方向 (输入或输出) 以及Scatter/Gather列表。 如果传输包含单个数据包,则Scatter/Gather列表包含单个元素。
EvtProgramDma 回调函数使用驱动程序的 EvtDevicePrepareHardware 回调函数接收的硬件资源对设备进行编程。 如果 EvtProgramDma 回调函数成功对硬件进行编程,则返回 TRUE。
硬件完成 DMA 传输后,硬件通常会发出中断,系统调用驱动程序的 EvtInterruptIsr 回调函数。 驱动程序的 EvtInterruptIsr 回调函数通常:
- 清除硬件中断;
- 如果需要,请保存中断的上下文信息。 回调函数返回且系统降低 IRQL (后,此信息可能会丢失,因为降低 IRQL 允许) 发生其他中断;
- 调用 WdfInterruptQueueDpcForIsr 来计划 EvtInterruptDpc 回调函数;
EvtInterruptDpc 回调函数使用 EvtInterruptIsr 回调函数保存的上下文信息完成 DMA 传输。
如果 EvtProgramDma 回调函数检测到错误,驱动程序可以停止事务。
若要在驱动程序检测到错误时停止事务, EvtProgramDma 回调函数必须:
- 调用 WdfDmaTransactionDmaCompletedFinal;
- 调用 WdfObjectDelete 以删除 DMA 事务对象,或调用 WdfDmaTransactionRelease 以释放并重用 DMA 事务对象;
- 如果事务与框架请求对象关联,请重新排队I/O 请求或完成 I/O 请求。 若要检索请求的句柄,驱动程序可以调用 WdfDmaTransactionGetRequest;
- 返回 FALSE;
下面的代码示例演示了步骤 1 和 4,这些步骤用于读取请求:
// If errors occur in the EvtProgramDma callback,
// release the DMA transaction object and complete the request.
if (errors) {
NTSTATUS status;
//
// Must abort the transaction before deleting.
//
(VOID) WdfDmaTransactionDmaCompletedFinal(Transaction, 0, &status);
ASSERT(NT_SUCCESS(status));
PLxReadRequestComplete( Transaction, STATUS_INVALID_DEVICE_STATE );
TraceEvents(TRACE_LEVEL_ERROR, DBG_READ,
"<-- PLxEvtProgramReadDma: errors ****");
return FALSE;
}
步骤 2 和 3:
VOID
PLxReadRequestComplete(
IN WDFDMATRANSACTION DmaTransaction,
IN NTSTATUS Status
)
/*++
Routine Description:
Arguments:
Return Value:
--*/
{
WDFREQUEST request;
size_t bytesTransferred;
//
// Get the associated request from the transaction.
//
request = WdfDmaTransactionGetRequest(DmaTransaction);
ASSERT(request);
//
// Get the final bytes transferred count.
//
bytesTransferred = WdfDmaTransactionGetBytesTransferred( DmaTransaction );
TraceEvents(TRACE_LEVEL_INFORMATION, DBG_DPC,
"PLxReadRequestComplete: Request %p, Status %!STATUS!, "
"bytes transferred %d\n",
request, Status, (int) bytesTransferred );
WdfDmaTransactionRelease(DmaTransaction);
//
// Complete this Request.
//
WdfRequestCompleteWithInformation( request, Status, bytesTransferred);
}
完成 DMA 传输
通常,驱动程序的 EvtInterruptDpc 回调函数完成每个 DMA 传输的处理。
首先,由于多个 DMA 事务可以同时进行, 因此 EvtInterruptDpc 回调函数必须确定与已完成的传输关联的 DMA 事务。 回调函数可以通过检索驱动程序 在启动 DMA 事务时存储的事务句柄来执行此操作。 为了检索设备扩展, 驱动可能在头文件中定义了一个名为 PLxGetDeviceContext 的函数:
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DEVICE_EXTENSION, PLxGetDeviceContext)
然后,在驱动程序的 EvtInterruptDpc 回调中,它会执行以下操作:
WDFDMATRANSACTION dmaTransaction;
PDEVICE_EXTENSION devExt;
...
devExt = PLxGetDeviceContext(WdfInterruptGetDevice(Interrupt));
...
dmaTransaction = devExt->WriteDmaTransaction;
接下来, EvtInterruptDpc 回调函数必须通过调用以下传输完成方法之一来通知框架传输已完成:
- WdfDmaTransactionDmaCompleted,如果传输成功完成,并且硬件未报告传输的字节计数;
- WdfDmaTransactionDmaCompletedWithLength,如果传输成功完成,并且硬件报告 (传输字节计数或未) 传输的字节计数,或者驱动程序检测到错误并将传输计数指定为零以重试传输。 如果驱动程序将传输计数指定为零,则框架将从剩余字节数中减去零,从而将相同的传输发送到 EvtProgramDma 回调函数。
- WdfDmaTransactionDmaCompletedFinal: 如果硬件报告运行不足或故障条件,那么会调用这个函数;
驱动程序可以调用 WdfDmaTransactionGetCurrentDmaTransferLength 来获取已完成传输的原始长度。 如果设备报告未传输的字节数,此调用非常有用,因为驱动程序可以从原始传输长度中减去未传输的字节数,然后调用 WdfDmaTransactionGetCurrentDmaTransferLength 来报告实际传输大小。
上述每个传输完成方法都会通知框架单个 DMA 传输 (不是整个 DMA 事务) 完成。 驱动程序调用以下方法之一后,驱动程序会检查方法的返回值,以查看事务是否需要更多传输:
如果完成方法的返回值为 FALSE,则框架已确定需要额外的 DMA 传输才能完成 DMA 事务的处理。通常,驱动程序的 EvtInterruptDpc 回调函数仅返回。 框架再次调用驱动程序的 EvtProgramDma 回调函数,回调函数可以对硬件进行编程,以便进行下一次传输。传输完成方法提供状态值,在本例中始终STATUS_MORE_PROCESSING_REQUIRED。
如果返回值为 TRUE,则 DMA 事务不会再发生传输,传输完成方法提供状态值。 如果状态值为STATUS_SUCCESS,则 DMA 事务的所有传输都已完成,驱动程序必须 完成 DMA 事务。 如果状态值为任何其他值,则会发生错误,并且 DMA 事务可能尚未完成。
如果 EvtInterruptDpc 回调函数检测到错误(通常是由于计时器过期或硬件中断指示传输错误),驱动程序可以重启事务的当前传输。
若要重启事务的当前传输,驱动程序的 EvtInterruptDpc 回调函数可以调用 WdfDmaTransactionDmaCompletedWithLength ,并将 TransferedLength 参数设置为零。
完成 DMA 事务
每次驱动程序的设备完成 DMA 传输时,驱动程序都必须调用 WdfDmaTransactionDmaCompleted、WdfDmaTransactionDmaCompletedWithLength 或 WdfDmaTransmaTransactionDmaCompletedFinal,然后检查返回值。
如果返回值为 TRUE,则 DMA 事务不需要更多传输,驱动程序必须完成 DMA 事务。 通常,驱动程序尚未从其 EvtInterruptDpc 回调函数返回。 因此,此回调函数通过以下方式完成 DMA 事务:
- 调用 WdfObjectDelete 以删除事务对象;如果驱动程序重用 DMA 事务对象,则调用 WdfDmaTransactionRelease;
- 如果事务与框架请求对象关联,则调用 WdfRequestComplete 或 WdfRequestCompleteWithInformation;
如果驱动程序调用 WdfRequestCompleteWithInformation,则通常首先调用 WdfDmaTransactionGetBytesTransferred 以获取所有事务传输) 的总长度 (字节数。
下面的代码示例演示了这些步骤:
if (readComplete) {
BOOLEAN transactionComplete;
WDFDMATRANSACTION dmaTransaction;
size_t bytesTransferred;
// Get the current Read DmaTransaction.
dmaTransaction = devExt->CurrentReadDmaTransaction;
// Indicate that this DMA operation has completed:
// This may start the transfer on the next packet if
// there is still data to be transferred.
transactionComplete =
WdfDmaTransactionDmaCompleted( dmaTransaction, &status );
if (transactionComplete) {
// Complete the DmaTransaction and the request.
devExt->CurrentReadDmaTransaction = NULL;
bytesTransferred =
((NT_SUCCESS(status)) ?
WdfDmaTransactionGetBytesTransferred(dmaTransaction): 0 );
WdfDmaTransactionRelease(dmaTransaction);
WdfRequestCompleteWithInformation(request, status, bytesTransferred);
}
}