How Bus Drivers Work

Posted by David Liao

Abstract

 

A bus driver is designed for controlling and configuring a specific Bus. It also configures and controls hardware on the bus and loads & unloads hardware drivers called client drivers. It also carries out bus request from its client drivers. A bus driver has two basic functions from software perspective. One is serving its client driver. Another is configuring, loading and controlling its client drivers. Microsoft provides a bus driver for most common buses, such as PCI, PCCARD and the “Root” Bus.

What a Bus Driver does

 

A Bus Driver is responsible for Hardware Configuration, Hardware Power Control, Bus address translation, and loading & unloading upper-level client drivers.

Loading & Unloading upper-level client drivers

Most upper-level client drivers are loaded by a bus driver calling ActivateDevice(Ex). ActivateDevice(Ex) loads a client driver according to the contents of a registry entry that is passed in by the caller.

This means you need the registry in order to load the driver. A Bus driver has to either use an existing registry entry or create an instance registry entry according to a template.

For PNP drivers, the bus driver configures the hardware and sets up instance registry entries according to PNP information. Then, it loads a client driver by calling ActivateDevice(Ex) and specifying the new instance registry entries.

For non-PNP drivers and intermediate drivers, usually the registry is set up manually. It could be Microsoft, an OEM, or a third party who provides the bus driver. The bus driver normally still uses the registry to load those drivers.

A Bus driver can also be loaded by a “parent” bus driver, creating a “tree” of buses. The exception is the “Root” Bus Driver, or trunk of the tree. The Root Bus Driver is loaded by Device Manager using a registry path value found at a specific registry location:

[HKEY_LOCAL_MACHINE\Drivers]
"RootKey"="Drivers\BuiltIn"

The default value is set up by Microsoft. However, it can be changed by an OEM to allow the Root Bus Driver to enumerate drivers in some other registry path.

The above example indicates that the Root Bus driver is located at

[HKEY_LOCAL_MACHINE\Drivers\Builtin]

Device Manager loads the Root Bus Driver during initialization.

Important:
Device Manager makes the Root Bus Driver run TWICE in a system with a two-phase boot.

In Boot Phase One, the Device Manager deletes the Drivers “Active” Key holding any stale driver instances (i.e. HKLM\drivers\Active), then loads the Root Bus Driver with the fixed Bus name “BuiltInPhase1”. If this system has a two-phase boot, after the system “Phase 2” Event is signaled, the Device Manager loads the Root Bus Driver again. The second time the Device Manager uses a bus name found in the registry.

Is it possible that a driver can be loaded twice in two phase boot system? The answer is yes.

If a driver is included in HIVE registry and its entries do not include the DWORD “Flags” entry specifying DEVFLAGS_BOOTPHASE_1 (0x00001000), it will be loaded twice. It is important to take this into consideration when specifying the registry entries for a Bus Driver.

In theory, some Bus drivers should be loaded twice.

Bus Driver Access & the CEDDK Bus Functions

A client driver is capable of using its Bus Driver to:

- do bus address translation between a subordinate Bus and a Parent Bus
- do Device Power State changes
- access Configuration Data.

There are many reasons to use a Bus Driver. One important reason is to make a Client Driver become bus-agnostic. This means a driver is capable of moving to user mode, or of working on different bus-architectures with the same binary. Some examples of bus-agnostic drivers are COM16550.dll and NE2000.dll. These can work as client drivers of the PCI Bus, Native Bus and PCMCIA bus with the same binary.

Bus Access Functions

In order to get access to its parent Bus Driver, a client driver has to call the CreateBusAccessHandle() function to get a bus driver access handle. This function is usually called by a client driver during its initialization (XXX_Init) because it requires that the client driver pass the “Active Registry Path” as an argument. CloseBusAccessHandle() is used for closing the access handle returned by calling CreateBusAccessHandle(). Usually the CloseBusAccessHandle() function is called before exiting XXX_Deinit. Please refer to MSDN online documentation for detailed information on these functions.

After a Bus Access Handle is created, a Client can use any function which uses this handle as an argument.

CEDDK Functions for Bus Driver operations are provided as wrappers – they pack the corresponding function parameters and call the Bus Driver with an operation-specific IO Control Code (IOCTL). Microsoft strongly recommends that driver writers use the CEDDK functions instead of calling the Bus Driver directly with the IO Control Code. The parameters for the Bus IO Control Code may change from release to release, but the CEDDK functions (as formal APIs) signatures will not.

Listed below are the CEDDK functions and their corresponding IO Control Codes:

CreateBusAccessHandle

 

Creates an access handle for other CEDDK Bus functions. This results in the Parent bus’ Open() entry being called.

CloseBusAccessHandle

 

Closes the handle which was created by using CreateBusAccessHandle(). This results in the Parent bus’ Close() entry called.

SetDevicePowerState

IOCTL_BUS_SET_POWER_STATE

GetDevicePowerState

IOCTL_BUS_GET_POWER_STATE

TranslateBusAddr

IOCTL_BUS_TRANSLATE_BUS_ADDRESS

TranslateSystemAddr

IOCTL_BUS_TRANSLATE_SYSTEM_ADDRESS

SetDeviceConfigurationData

IOCTL_BUS_SET_CONFIGURE_DATA

GetDeviceConfigurationData

IOCTL_BUS_GET_CONFIGURE_DATA

GetChildDeviceRemoveState

IOCTL_BUS_IS_CHILD_REMOVED

 

 

 

SetDevicePowerState() and GetDevicePowerState() are used by a client driver to request that its parent Bus Driver put the client into a certain power state. Although some client drivers do the actual power state change by themselves, but it is the Bus driver’s responsibility for putting its client drivers to their effective power states based upon the requests. Before a Bus Driver loads a client driver, it should put client driver’s hardware into Power State D0. After the Bus driver unloads the client drivers, it should put the corresponding hardware into Power State D4.

TranslateBusAddr() and TranslateSystemAddr() are used to translate between physical CPU addresses and subordinate Bus addresses. To do so, TranslateBusAddr() should translate the target address from a Subordinate Bus to a Parent Bus. These calls should propagate up all the way to the Root Bus driver. Then Root Bus driver then calls the OAL function HalTranslateBusAddress() to acquire the final CPU-relative physical address . TranslateSystemAddr() works similarly, but in the other direction. The OAL uses HalTranslateSystemAddress() to translate from a CPU address to a Bus address. So, multi-layer bus address translation only can be resolved by TranslateBusAddr() and TranslateSystemAddr() .

Before doing a virtual or static system memory mapping function for a driver, the BusTransBusAddrToVirtual() and BusTransBusAddrToStatic() functions use TranslateBusAddr() to translate a Bus address to a CPU(system) address.

 

IO Control Codes that are not supported by CEDDK

CEDDK provides the BusIoControl() and BusChildIoControl() functions to client drivers. Client drivers can use these two functions to issue a Bus IO Control call directly. The difference with these function is that BusChildIoControl() is used to issue an IO Control call related to client driver which is making the call.

The IOCTL_BUS_ACTIVATE_CHILD and IOCTL_BUS_DEACTIVATE_CHILD control codes are standard IO Control codes for which no wrapper function is implemented in the CEDDK. These IO Control codes can be called by any application if it opens a handle to the bus driver. The two Io Control Codes are used to activate or deactivate a client driver. Note: For legacy reasons not all client drivers can be deactivated.

How a driver’s bus name is assigned

 

A Client Driver’s bus name is assigned by its parent Bus Driver.

The Bus Name that Bus Driver assigns follows a simple four-part pattern:

<busname> _ <bus#> _ <device#> _ <function#>

Usually, “busname” is assigned to a Bus Driver according to the value of its “BusName” registry entry under the Bus Driver device’s registry key. This is value is pre-assigned either by Microsoft or by the OEM that is using the Bus Driver.

The “bus#”, “device#” and “function#” are set to the “BusNumber”, “DeviceNumber” and “FunctionNumber” registry entry values under the device key for the Client Driver. These registry value are created by the PnP Bus Driver. For non-PnP Bus drivers (for example, the Root Bus Driver), the “bus#” is assigned by the Bus Driver’s device registry key. The “device#” and “function#” are then assigned automatically by the Bus driver.

Because the “busname” has to be unique in the system, an appropriate name must be chosen when the system has multiple bus drivers. The following is a list of well-known bus driver names:

1) Root Bus Driver: “BuiltIn”

2) PCI Bus Driver: “PCI”

3) PC Card Bus Driver: “PCCARD”

Power Manageable Drivers and Bus Drivers Power IO Control Codes

 

There are two different types of Power Manageable Drivers. The first type is a driver which is under the control of the system Power Manager (PM). The second type is a driver which manages its own power. Drivers which are under the control of the system Power Manager have to support the following IO Control Codes:

IOCTL_POWER_CAPABILITIES: The Power Manager uses this IO Control Code to query a driver’s supported device power states. After a driver returns from this call with the Device Power States that it supports (D0->D4), and IOCTL_POWER_SET commands (see below) that specify a supported Device Power State should succeed. If the device does not accept the command to change power state, the PM will consider the refusal as a severe error.

IOCTL_POWER_QUERY: (Optional) This function simply returns a driver’s current power state.

IOCTL_POWER_SET: The Power Manager uses this IO Control Code to change the driver’s power state. Power Manager only specifies valid power states reported by a driver from IOCTL_POWER_CAPABILITIES.

Role the Bus Driver plays in Power Management of a Client Driver

As we described previously, the Client’s Bus Driver is responsible for changing the hardware power state. Therefore, it would seem that the best solution would be for the Power Manager to call the Bus Driver directly to have it manage power. In fact, this does not happen. There are two reasons why not:

1) A Client Driver need to know the current power state in order to perform its function correctly. For example, the Client Driver has to make sure there is no ongoing processing on the hardware or a blocking future function request (if requests are queued) before calling the Bus Driver to turn off the hardware.

2) Driver before Windows CE 5.0 performed Power Control by itself. To make processing backwards compatible, the Power Manager needs to continue to call the Client Driver directly.

1) The Power Control commands from the System Power Manager are sent to the client driver. They are propagated to the client’s Bus Driver via an Bus IO Control Code. The process is:PM calls DeviceIoControl (IOCTL_POWER_SET) on the client Driver.

2) The Client Driver calls the SetDevicePowerState() CEDDK function which translates IOCTL_BUS_SET_POWER_STATE to the bus driver.

3) The Bus driver determines what to do base on the call from the client driver.

There can be confusion and fear resulting from the driver being simultaneously commanded by I/O functions (IoControl/Read/Write), and the Power Manager IO Control Code. When simultaneous requests happen, something must be done so that the Client driver still performs its functions correct.y. Use of a few CEDDK functions can help resolve the situation by following a general pattern:

Init()

{

hPwrHandle = DDKPwr_Initialize(__in PFN_SETPOWERLEVEL pSetPowerLevelFn,

                               __in DWORD dwContext,

                               __in BOOL fAbortOnPMRequests,

                               __in DWORD dwTimeout );

};

Deinit

{

DDKPwr_Deinitialize(hPwrHandle);

}

Function_Request()

{

       HANDLE hLevelHandle = DDKPwr_RequestLevel(hPwrHandle,

                                                  __in CEDEVICE_POWER_STATE dx );

       … // Do some work.

       DDKPwr_ReleaseLevel(hPwrHandle, hLevelHandle);

}

PowerMgr_Request(Dx)

{ // From Power IO Control

DDKPwr_SetDeviceLevel(hPwrHandle ,Dx , __in PFN_SETPOWERLEVELCALLBACK pCallbackFn );

}

This pseudocode gives a general reference for how this is done. The released example for how to use the set of CEDDK function can be found public\common\oak\drivers\serial\serpddcm\cserpdd.cpp .

Bus Driver Library

Microsoft provides a Bus Driver Library to allow a user to write a Bus Driver. CEDDK functions pass Bus Io Control Codes to a Bus Driver. There are two different IO Control Codes. The first is targeted at the Bus Driver, the second is targeted at one client driver to control Configuration, Power State and Bus Address translation:

Class DefaultBusDriver {

Public:

      // Constructor

      // Client Driver Specific function.

      // Bus Driver Function.

Protected:

      // Container for all Folders of Child Client Driver.

      // Child Folder Class Manufacture.

};

 

class DeviceFolder {

public:

      // Constructor

      // Device Power Function

      // Device Configuration Function

      // Device Bus Address Translation Function

      // Device Driver Load & Unload Function.

};

The DefaultBusDriver and DeviceFolder classes are abstract templates for implementation of a Bus Driver. A Bus Driver should inherit from these two basic classes and modify their virtual functions according to specific needs.

The Bus Library does not provide instantiable DefaultBusDriver and DeviceFolder classes. You must subclass these abstract bases and fill in implementation details. There are some default methods in the DefaultBusDriver implementation to manage a DeviceFolder-based class.

There are a few functions that the Bus Library provides:

DefaultBusDriver Class

o Forwards device-specific requests to a folder

o Manages DeviceFolder containers

o Performs Default Bus Translations for the IOCTL_BUS_TRANSLATE_SYSTEM_ADDRESS and IOCTL_BUS_TRANSLATE_BUS_ADDRESS IO Control Codes. It calls its parent bus driver to translate addresses or calls HalTranslateBusAddress or HalTranslateSystemAddress if it is a Root Bus Driver.

o Handles the basic IOCTL_BUS_IS_CHILD_REMOVED, IOCTL_BUS_NAME_PREFIX and other IO Control codes.

o Has a dummy function for PostInit(), which

Dummy Function:

o PostInit function which is called by IOCTL_BUS_POSTINIT

DeviceFolder

o Includes logic for creating a default bus name for use in driver loading and unloading.

o Perform Default Configuration Functions forwarded by the DefaultBusDriver class through IOCTL_BUS_GET_CONFIGURE_DATA and IOCTL_BUS_SET_CONFIGURE_DATA. It calls HalGetBusDataByOffset or HalSetBusDataByOffset when a request is from PCI Client Device Driver.

o Has a dummy function for SetPowerState which is called by the DefaultBusDriver through IOCTL_BUS_SET_POWER_STATE. The provided implementation of the PCI Bus Driver shows a good example of how to do SetPowerState for specific hardware.

PCI Bus Driver

The PCI Bus Driver is required to Configure the PCI Bus, Assign Resources, Search for Drivers according to a template, and set Instant Device Loading Registry keys. The details of this implementation are not described here, but the functions used to support Bus IoControl calls from external clients are detailed.

In order to support a Bus IO Control call from a Client Driver the PCI Bus Driver implements the following subclasses of the ones defined in the Bus Driver Library:

class PciDeviceFolder : public DeviceFolder{

    virtual BOOL PostInit() ;

};

 

class PciBusEnum : public DefaultBusDriver {

};

 

These classes override their bases in the following ways:

PciBusEnum

Implements the PostInit() function to overwrite the dummy implementation in the parent class. This new implementation calls two sub functions:

1) Calls AssignChildDriver() which creates one PciDeviceFolder for each Device Instance. This also inserts the folder into a Device Folder container.

2) Calls ActivateAllChildDriver() which calls DeviceFolder::LoadDevice() to load each client driver in order.

PciDeviceFolder

Implements the SetPowerState() to overwrite the dummy implementation in the parent class. The new method implements its actions according to the PCI PM 1.1 specification.

Root Bus Driver

The default Root Bus Driver is called “BusEnum”. This default only implements a simple BusEnum class which inherits its behavior from the DefaultBusDriver class in the Bus Driver library. It does not modify the default DeviceFolder implementation. Therefore, it does not support the SetPowerState() call to a device folder (default implementation is just a dummy).

class BusEnum : public DefaultBusDriver {

virtual BOOL PostInit() ;

};

BusEnum

Implements the PostInit() function to overwrite the dummy implementation in the parent class. This new implementation calls two sub functions:

o Calls AssignChildDriver() which creates one DeviceFolder for each Device Instance referenced by a sub registry key. It then inserts the folder into a Device Folder container.

o Calls ActivateAllChildDriver() which calls DeviceFolder::LoadDevice() to load drivers in order.

For the Root Bus to support Power State Setting for Client Driver requests, the SetPowerState() Driver Folder function has to be implemented. The F-Sample (OMAP850) has an example of a platform-specific Root Bus Driver to show it how it can be done.