In the previous article, we introduced the driver code generated by the VS KMDF template, providing a preliminary understanding of the driver code. It should be evident that this driver is quite simple, essentially lacking any real functionality. After installing the driver we wrote, this PCIe device remains in the D0 state (if you are not familiar with the power states of PCIe devices, please refer to Introduction to PCIe Configuration Space: Power Management Capability). The normal behavior should be that after installing the driver, if the device is in an idle state, it should automatically enter the D3 state. In this article, we will improve this PCI driver so that the device can automatically enter the D3 state after the driver is installed.
To allow the device to enter the D3 state when it is idle after the driver is installed, the driver must implement a series of PnP callbacks, which are encapsulated in a structure called WDF_PNPPOWER_EVENT_CALLBACKS.WDF_PNPPOWER_EVENT_CALLBACKS is the core structure in WDF used to manage Plug and Play (PnP) and power events for devices. It defines a series of callback functions that the driver implements to respond to changes in device status (such as hardware resource allocation, power state transitions, device removal, etc.). The Windows kernel will call these callbacks at appropriate times, and the driver needs to perform corresponding operations within these callbacks. This structure contains many callbacks, with the important ones highlighted in red.
EvtDevicePrepareHardwareCall Timing: During the process of loading the driver onto the device, after the system allocates resources for the device (such as PCI BAR resources).Main Function: Enumerates and parses various resources of the device, mainly including I/O Port resources, MMIO resources, and interrupt resources, records these resources in the device context maintained by the driver, maps memory resources (MMIO), registers interrupts, etc.EvtDeviceReleaseHardwareCall Timing: Typically when the device is being removed, the system calls this callback to prepare to release the resources allocated to the device.Main Function: Performs operations opposite to those in EvtDevicePrepareHardware, such as unmapping resources, disabling interrupts, etc.EvtDeviceD0EntryCall Timing: When the device enters the D0 state.Main Function: Executes all operations required to bring the device into full operational status, such as loading firmware, enabling features that were disabled in low power states, or restoring the device state saved from the last low power state.EvtDeviceD0ExitCall Timing: When the device exits the D0 state and enters a low power state.Main Function: Executes all operations required before the device enters the specified low power state, such as saving information needed by the driver to restore the device to the D0 power state (e.g., hardware register values or device context).EvtDeviceQueryRemoveCall Timing: When the system wants to remove the device, if the driver provides this callback, the system will call it to ask the driver if the device can be removed.Main Function: Determines whether the device can currently be removed, such as checking if there are ongoing I/O operations on the device or if any applications are holding a handle to the device. The system will decide whether to remove the device based on the return value of this callback.If the driver does not provide this callback, the system assumes that the device can always be removed.
EvtDeviceSurpriseRemoval
Call Timing: When the device is unexpectedly removed, such as when the user unplugs the device. Another situation that triggers this callback is when the device fails to enter the D0 state from a low power idle state.Main Function: Performs some emergency cleanup tasks.
To register these callbacks, the driver needs to call the WDF API function WdfDeviceInitSetPnpPowerEventCallbacks before creating the device object, setting the initialized WDF_PNPPOWER_EVENT_CALLBACKS structure to the WDFDEVICE_INIT structure, and then calling the function WdfDeviceCreate using the WDFDEVICE_INIT structure to create the device object. Below, we will modify the previously generated driver code based on the KMDF template to include the registration of PnP callbacks. The function we need to modify is PciTestDriverCreateDevice, where we will add the code highlighted in red at the beginning of this function. Currently, we have only implemented EvtDevicePrepareHardware, EvtDeviceReleaseHardware, EvtDeviceD0Entry, and EvtDeviceD0Exit callbacks.

Next, let’s look at the implementation of EvtDevicePrepareHardware. Currently, this function’s implementation is quite simple; it merely enumerates all resources of the device and prints out the information about these resources. We use the WDF API function WdfCmResourceListGetCount to get the number of resources, and then use the WDF API function WdfCmResourceListGetDescriptor to retrieve each resource descriptor one by one and parse them according to their resource types. It should be noted that PCI BAR resources are sorted according to the order of the corresponding BARs in the PCI configuration space. The resources we parse include I/O Port resources, Memory resources, and Interrupt resources.I/O Port and Memory resources correspond to the physical base address information configured on the PCI BAR registers for the device. In fact, the PCIe device we experimented with previously did not have I/O Port resources; this implementation is included here for demonstration purposes.

The implementation of EvtDeviceReleaseHardware is very simple; it only prints a log and exits, as we have not mapped or allocated resources in EvtDevicePrepareHardware.

The implementations of EvtDeviceD0Entry and EvtDeviceD0Exit are also quite simple. EvtDeviceD0Entry only prints which low power state it is entering from, while EvtDeviceD0Exit only prints which low power state it is entering from D0 state.

After completing the implementations of these callbacks, we still cannot make our test PCIe device enter the D3 state after the driver is installed. We also need to report to the system (WDF Framework) when the device can enter a low power state. WDF provides an API called WdfDeviceAssignS0IdleSettings to accomplish this. The driver needs to first initialize a WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS structure and then use this function to set this structure to the device. When the system is in the S0 state but the device is idle, the conditions set by this function will trigger the device to enter a low power state (such as D1, D2, D3), reducing power consumption.

The WDF_DEVICE_POWER_POLICY_IDLE_SETTINGS structure has two members that we need to pay attention to: IdleTimeout indicates the idle timeout for the device, measured in milliseconds. We set it to 1 millisecond; when the device has been idle for longer than this set time, the system will allow the device to enter a low power state. IdleTimeoutType specifies how the system uses the idle timeout. We specify the type as SystemManagedIdleTimeoutWithHint, indicating that the system will use the time specified by IdleTimeout as the idle timeout for the device, and the idle time before the device enters a low power state may be greater than the timeout we specified.
The complete modified PciTestDriverCreateDevice function is shown below, where we added some error logging for easier debugging.

After recompiling the driver, signing it, copying it to the test machine, and installing the driver, the results are as follows.

This PCIe device can finally enter the D3 state. Checking the installation log of the driver (we will introduce how to obtain the WPP log of the driver later), we can see that the two MMIO base addresses we output in the log are consistent with the resource information of the device viewed in the device manager, and we can also see that the last log output during driver installation is the log printed when EvtDeviceD0Exit is called, indicating that the device entered state 4, corresponding to WdfPowerDeviceD3, which indicates that we have achieved our goal.
