3.3.4.2 Common-Buffer System DMA

A driver that uses a system DMA controller’s autoinitialize mode must allocate memory for a buffer into which or from which DMA transfers can be carried out. Such a driver must call HalAllocateCommonBuffer when it initializes in order to provide this buffer. Figure 3.9 illustrates such a driver’s call to HalAllocateCommonBuffer.

Figure 3.9 Allocating a Common Buffer for System DMA

As Figure 3.9 shows, the driver must pass a pointer to the adapter object that was returned by HalGetAdapter, along with the length in bytes requested for its buffer. To use memory economically, the input Length value for such a buffer either should be less than or equal to PAGE_SIZE or should be an integral multiple of PAGE_SIZE.

If the call succeeds, HalAllocateCommonBuffer returns two different types of pointers to the buffer:

If HalAllocateCommonBuffer returns a NULL pointer, the driver should free any system resources it has already claimed, call IoDeleteDevice to release any device objects it has already created, and so forth. The DriverEntry routine also should return STATUS_INSUFFICIENT_RESOURCES, because the driver must not be loaded when it cannot respond to data transfer requests.

Otherwise, the driver must call IoAllocateMdl with the VirtualAddress of the buffer returned by HalAllocateCommonBuffer and the Length of its buffer to allocate an MDL. It should then call MmBuildMdlForNonPagedPool with the pointer returned by IoAllocateMdl to map the virtual address range for its resident buffer to system physical memory.

NT drivers of slave devices that use a common buffer for DMA call the following general sequence of support routines as they process an IRP requesting a DMA transfer:

  1. At the driver writer’s discretion, RtlMoveMemory to copy data from a locked-down user buffer into the driver-allocated common buffer for a transfer to the device

  2. IoAllocateAdapterChannel when the driver is ready to program its device for DMA and needs the system DMA controller

  3. IoMapTransfer with the MDL, describing the driver-allocated common buffer, to set up the system DMA controller for the transfer operation

    Note that such a driver calls IoMapTransfer only once to set up the system DMA controller to use its common buffer. During a transfer, the driver can call HalReadDmaCounter to determine how many bytes remain to be transferred, and if necessary, call RtlMoveMemory to copy more data to or from a user buffer.

  4. IoFlushAdapterBuffers when the driver has completed its DMA transfer to/from the slave device

  5. IoFreeAdapterChannel as soon as all the requested data has been transferred or the driver fails the IRP because of a device I/O error

The AdapterObject pointer returned by HalGetAdapter is a required parameter to each of these support routines except RtlMoveMemory.

Individual NT drivers call this sequence of support routines at different points, depending on how each driver is implemented to service its device. For example, one driver’s StartIo routine might make the call to IoAllocateAdapterChannel, another driver might make this call from a routine that removes IRPs from a driver-created interlocked queue, and still another driver might make this call when its slave DMA device indicates it is ready to transfer data.

3.3.4.2.1 Allocating an Adapter Channel for Common-Buffer System DMA

A driver calls IoAllocateAdapterChannel after its Dispatch routine for IRP_MJ_READ and/or IRP_MJ_WRITE requests, or for any other request that requires a DMA transfer, has already checked the validity of the IRP’s parameters (if necessary), queued one or more IRPs to another driver routine for further processing, and possibly loaded its common buffer with data to be transferred. See Figure 3.7 for an illustration of a call to IoAllocateAdapterChannel.

The driver routine that calls IoAllocateAdapterChannel must be executing at IRQL DISPATCH_LEVEL when this call occurs. For more information about the IRQLs at which NT drivers’ standard routines execute, see Chapters 5 through 15. For more information about support-routine-specific IRQL requirements, see the Kernel-Mode Driver Reference.

IoAllocateAdapterChannel queues the driver’s AdapterControl routine, which executes when the system DMA controller is assigned to this driver and a set of map registers (described in Section 3.3.1) has been allocated for the driver’s DMA operation.

On entry, an AdapterControl routine is given pointers to the device object and context passed in the call to IoAllocateAdapterChannel, as well as a handle for the allocated map register(s). The AdapterControl routine also is given a pointer to the DeviceObject->CurrentIrp if the driver has a StartIo routine. If the driver manages its own queueing of IRPs instead of having a StartIo routine, the driver should include a pointer to the current IRP as part of the context data it passes when it calls IoAllocateAdapterChannel.

The AdapterControl routine usually does the following:

  1. Saves or initializes whatever context the driver maintains about DMA operations, such as saving the MapRegisterBase handle the driver must pass to IoMapTransfer and IoFlushAdapterBuffers and, possibly, the Length of transfer requested from its I/O stack location in the IRP

  2. Sets up the slave device to start the transfer operation

  3. Returns the value KeepObject

For NT drivers that use a system DMA controller’s autoinitialize mode, the AdapterControl routine must return the value KeepObject. This allows the driver to retain “ownership” of the system DMA controller and allocated map register(s) until it has transferred all the data.

Note that an AdapterControl routine cannot wait for the slave device to carry out the DMA operation, so an AdapterControl routine must at least do the following:

  1. Save context information, particularly the MapRegisterBase handle, in the driver’s device extension, controller extension, or other driver-accessible resident storage area (nonpaged pool allocated by the driver).

  2. Return KeepObject.

Another driver routine (probably the DpcForIsr) must call IoFlushAdapterBuffers and IoFreeAdapterChannel when the DMA transfer operation is complete.

3.3.4.2.2 Setting Up the System DMA Controller for Common-Buffer DMA

When IoAllocateAdapterChannel transfers control to a driver’s AdapterControl routine, the driver “owns” the system DMA controller and a set of map registers. Then, the driver must call IoMapTransfer to set up the system DMA controller to use the driver-allocated common buffer before the driver sets up its device for the transfer operation. See Figure 3.8 for an illustration of calls to IoMapTransfer.

The driver supplies the following parameters to IoMapTransfer:

IoMapTransfer returns a logical address, which drivers that use system DMA must ignore. When IoMapTransfer returns control, the driver should set up its device for the DMA operation. Note that such a driver calls IoMapTransfer only once but continues to copy data between its common buffer and a locked-down user buffer until the requested transfer is done.

The driver can call HalReadDmaCounter to determine how many bytes currently remain to be transferred in the common buffer, so such a driver can continue to fill its common buffer with user data or copy data from its common buffer to the user buffer.

When all the requested transfer is complete or the driver must return an error status for the IRP, the driver calls IoFlushAdapterBuffers to ensure that any data cached in the system DMA controller is read into system memory or written out to the device. Then, the driver should call IoFreeAdapterChannel promptly to release the system DMA controller for other drivers and this driver to use.