White paper

 

Hollis Technology Solutions

info@hollistech.com

 

Copyright Ó2000 by Mark Roddy

 


Replacing HalGetBusData In windows 2000

Introduction

PCI device drivers frequently need to examine and or modify their PCI configuration space data.

 

In older versions of the Windows NTÔ operating system (for this article older versions means specifically version 4.0,) a pair of functions HalGetBusData and HalSetBusData allowed a driver to read and write PCI configuration space safely. (Because NT is a preemptive multi-tasking multiprocessor operating system, a serialization mechanism is required to correctly access PCI configuration space.) The NT4.0 Hardware Abstraction Layer (HAL) provided this mechanism while also hiding the platform specific mechanics of how to access PCI configuration space.

 

Windows 2000Ô continues to provide the HAL functions for PCI configuration access. However, the Windows 2000 DDK clearly warns the developer that these functions are obsolete and may not be supported in future versions of the operating system.

 

The Windows 2000 DDK recommends two new mechanisms, both using IO Request Packet (IRP) messages, to obtain bus-specific device configuration information:

 

“Drivers should use the PnP Manager's IRP_MN_QUERY_INTERFACE and IRP_MN_READ_CONFIG requests instead.”

 

Windows 2000 moves bus specific processing out of the HAL and into Bus Device Drivers. For backwards compatibility, Windows 2000 retains some NT 4 interfaces, but in the near future this will change.

 

This article looks at the two recommended alternatives for the soon-to-be-gone HAL functions. First the article examines the IRP_MN_READ_CONFIG/IRP_MN_WRITE_CONFIG message based interface, and provides a code sample for wrapping this interface within a generic procedure. Then the article examines the second method, which uses the new Driver Interface facility in Windows 2000 and provides a procedural mechanism for communicating with a bus driver. Again the article provides a code sample that can be readily adapted as a generic mechanism for your drivers.



IRP_MN_READ_CONFIG and IRP_MN_WRITE_CONFIG

This method requires the client driver (PCI Function Driver for example,) to construct and send an IRP every time it needs configuration information. This is acceptable if configuration access is essentially once per device object lifetime. It is however rather cumbersome to use if your device requires more frequent configuration space access. (Most device do not, consequently this mechanism is acceptable. In my opinion it should however be wrapped inside a procedure rather provided by the DDK rather than exposed in its raw interface.)

 

READ/WRITE_CONFIG have another restriction: this method can only be used at an interrupt level less than DISPATCH_LEVEL. It is this last restriction that may be critical in deciding which mechanism is the correct one to use in your driver.

 

The DDK does not however provide an example of how a driver could use the READ/WRITE_CONFIG interface. The following code segment illustrates how one might use this interface. The code is provided for explanatory purposes only. If you use this code in your programs, you do so with the understanding that it is provided AS IS and WITH ALL DEFECTS. (Blah blah blah… etc. etc. etc.)

 

A further word of warning about our code samples: we only write code using the C++ version rather than the C version of the VisualC compiler. We make use of C++ features. This stuff will not compile if you include it in a source module that does not have a .cpp extension.

 

NTSTATUS

HtsReadWriteConfig(

    PDEVICE_OBJECT DeviceObject,

    ULONG WhichSpace,

    PVOID Buffer,

    ULONG Offset,

    ULONG Length,

    BOOLEAN ReadConfig)

{

    ASSERT(DeviceObject);

    ASSERT(Buffer);

    ASSERT(Length);

 

    PIRP Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);

 

    if (!Irp) {

      

        return STATUS_INSUFFICIENT_RESOURCES;

    }

    Irp->IoStatus.Status = STATUS_NOT_SUPPORTED;

 

    KEVENT Event;

    KeInitializeEvent(&Event, NotificationEvent, FALSE);

 

    IoSetCompletionRoutine(Irp,HtsGenericCompletion,

                           &Event, TRUE, TRUE, TRUE);

 

    PIO_STACK_LOCATION IoStack = IoGetNextIrpStackLocation(Irp);

    IoStack->MajorFunction= IRP_MJ_PNP;

    IoStack->MinorFunction= ReadConfig ? IRP_MN_READ_CONFIG : IRP_MN_WRITE_CONFIG;

    IoStack->Parameters.ReadWriteConfig.WhichSpace = WhichSpace;

    IoStack->Parameters.ReadWriteConfig.Buffer = Buffer;

    IoStack->Parameters.ReadWriteConfig.Offset = Offset;

    IoStack->Parameters.ReadWriteConfig.Length = Length;

 

    if (ReadConfig) {

 

        RtlZeroMemory(Buffer, Length);

    }

 

    NTSTATUS Status = IoCallDriver(DeviceObject, Irp);

    if (Status == STATUS_PENDING) {

       

       KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, NULL);

 

       Status = Irp->IoStatus.Status;

    }

 

    IoFreeIrp(Irp);

 

    return Status;

}

 

NTSTATUS

HtsGenericCompletion (

    IN PDEVICE_OBJECT   DeviceObject,

    IN PIRP             Irp,

    IN PVOID            Context

    )

 

{

    UNREFERENCED_PARAMETER (DeviceObject);   

    KeSetEvent ((PKEVENT) Context, IO_NO_INCREMENT, FALSE);

    return STATUS_MORE_PROCESSING_REQUIRED; // Keep this IRP

}

 

 

Lets examine what this function does. As this is an IRP based interface, HtsReadWriteConfig has to allocate and initialize an IRP. It does that using IoAllocateIrp, the lowest-level structured interface for IRP allocation. It then proceeds to set the parameters in the IO_STACK_LOCATION up for the appropriate operation (ReadConfig or WriteConfig) based on the values provided by the caller. An event is initialized and a completion handler is configured to complete the IRP initialization. The IRP is then simply sent to the target device object using IoCallDriver.

 

At this point either the target driver completes the IRP immediately or it queues it for deferred processing and returns STATUS_PENDING. If STATUS_PENDING is returned then HtsReadWriteConfig waits for the event it initialized and provided as its completion handler Context parameter to be signaled. Once the event is signaled, HtsReadWriteConfig fetches the result of the operation from the IRP.

 

In either the immediate or deferred completion case HtsReadWriteConfig finishes by returning the status of the IRP to the caller.

 

HtsReadWriteConfig is a generic bus configuration routine. It does not know or care which bus your device is attached to. The caller is responsible for providing the correct values for WhichSpace, Offset, and Length, and for providing a buffer of the appropriate size. If the caller is writing bus configuration space it is the callers responsibility to correctly format the contents of the data buffer.

 

Suppose for example that you wished to read the standard PCI device configuration space. The call to HtsReadWriteConfig would look like this:

 

Status = HtsReadWriteConfig(pdoDevice, PCI_WHICHSPACE_CONFIG, &pciCOnfig,

                            0, sizeof(PCI_COMMON_CONFIG), TRUE);

 

Note that this interface does not provide a mechanism to specify either the target bus number, slot number, or function number. The bus driver derives this information from the PDO the request arrives on.

 

Where do the input values come from? The PCI_COMMON_CONFIG structure and the value PCI_WHICHSPACE_CONFIG are defined in ntddk.h. The pdoDevice value is provided to you by the system when you are called at AddDevice. You must save this value for later use, this is yet another case where it comes in handy. Typically you would save the pdo in your device extension.

 


The DRIVER INTERFACE (IRP_MN_QUERY_INTERFACE) method

The second method provided by the windows 2000 DDK is a driver interface and therefore a procedural mechanism. The user pays a one time cost to construct (and destruct) a procedural interface to the target bus driver. Once the interface is established, the interface is simply called like any other function. The QueryInterface method is less restrictive that the Read/WriteConfiguration method in another important quality as well. It may be called at <= DISPATCH_LEVEL, and consequently it may be used to directly replace HalGet/SetBusData in drivers that require this functionality.

 

What is a driver interface?  It is a procedural mechanism for communicating between device drivers without using IRP messages. It is a formalization of one of the two traditional mechanisms for creating procedural interfaces between device drivers in the NT operating system. Sending an IRP_MN_QUERY_INTERFACE IRP to a target device object creates a driver interface. If the IRP completes successfully the sender gets back a data structure initialized with appropriate addresses for functions provided by the target device object’s driver.

 

The formalization includes the following major features:

1)        Microsoft has published standard driver interfaces, one of which, the BUS_INTERFACE_STANDARD, we are going to examine in some detail here, supported by specific classes of drivers. (Bus drivers, for example, must support the BUS_INTERFACE_STANDARD driver interface.)

2)        The driver interface mechanism performs reference counting on target device objects. Reference counting prevents the target device object from being deleted until all references to it are released, including a driver interface reference. Correct use prevents a user of a driver interface from calling a function that uses an address of a code segment in a driver that has been unloaded from the system.

 

A word of warning.

There is a very similar sounding mechanism in the Windows 2000 operating system called a device interface.

 

Kernel drivers can provide procedural driver interfaces through the IRP_MN_QUERY_INTERFACE mechanism described here. The PnP Manager subsystem provides another mechanism called a device interface, for communicating the names of devices to both kernel and user mode components. In the kernel a driver can use the IoGetDeviceInterfaces() API to obtain a list of symbolic links to device objects that have registered as members of a specific class of device interface, while user space processes can use SetupDiEnumDeviceInterfaces() and related functions to access device names by their interface class.

 

Lets look at how QueryInterface works. The following code segment, unlike the ReadConfig example, is taken directly from a functioning PCI driver. The driver was written using our Generic WDM C++ Class Library, however the portions abstracted here ought to be readily re-usable in a driver using standard (and very obsolete) C language constructs. Note that because these are class methods, they reference class private data through an implied this pointer.

 

NTSTATUS pciFdo::createBusInterface()

{

 

    if (busInterface) {

        //

        // we have already started?

        //

        return STATUS_SUCCESS;

    }

    //

    // get the bus interface for our pci bus driver

    // oddly we can’t use PCI_BUS_INTERFACE_STANDARD

    //

    busInterface = new('IBFG', NonPagedPool) BUS_INTERFACE_STANDARD;

 

    if (!busInterface) {

 

         return STATUS_INSUFFICIENT_RESOURCES;

 

    }

    //

    // Initialize an event to block on

    //

    KEVENT event;

    KeInitializeEvent( &event, SynchronizationEvent, FALSE );

    //

    // Build an irp

    //

    IO_STATUS_BLOCK     ioStatus;

 

    PIRP irp = IoBuildSynchronousFsdRequest(

        IRP_MJ_PNP,

        NextLowerDevice,

        NULL,

        0,

        NULL,

        &event,

        &ioStatus

        );

 

    if (!irp) {

 

         delete busInterface;

 

         busInterface = NULL;

 

         return STATUS_INSUFFICIENT_RESOURCES;

    }

 

    irp->IoStatus.Status = STATUS_NOT_SUPPORTED;

    irp->IoStatus.Information = 0;

 

    //

    // Get the irp stack location

    //

    PIO_STACK_LOCATION irpSp = IoGetNextIrpStackLocation( irp );

 

    //

    // Use QUERY_INTERFACE to get the address of the direct-call

    // ACPI interfaces.

    //

    irpSp->MajorFunction = IRP_MJ_PNP;

    irpSp->MinorFunction = IRP_MN_QUERY_INTERFACE;

    irpSp->Parameters.QueryInterface.InterfaceType =

    (LPGUID) &GUID_BUS_INTERFACE_STANDARD;

    irpSp->Parameters.QueryInterface.Version  1;

    irpSp->Parameters.QueryInterface.Size = sizeof (BUS_INTERFACE_STANDARD);

    irpSp->Parameters.QueryInterface.Interface  = (PINTERFACE) busInterface;

    irpSp->Parameters.QueryInterface.InterfaceSpecificData  = NULL;

 

    //

    // send the request down

    //

    NTSTATUS status = IoCallDriver( NextLowerDevice, irp );

 

    if (status == STATUS_PENDING) {

 

        KeWaitForSingleObject( &event, Executive, KernelMode, FALSE, NULL );

        status = ioStatus.Status;

 

    }

 

    if (!NT_SUCCESS(status)) {

 

        delete busInterface;

 

        busInterface = NULL;

 

        return status;

    }

 

    //

    // stash the pci congfig registers for our slot

    //

    length = readConfig(&configRegs, 0, sizeof(configRegs));

 

    if (length == sizeof(configRegs)) {

 

         configValid = TRUE;

 

    } else {

 

        (*busInterface->InterfaceDereference)(busInterface->Context);

 

        delete busInterface;

 

        busInterface = NULL;

 

        status = STATUS_UNSUCCESSFUL;

    }

 

    //

    // Done

    //

    return status;

 

}

 

The method createBusInterface is called as part of the generic start device processing in our pciFdo class of objects. It sets up the procedural interface for direct access to the bus driver. A second method readConfig provides read access similar to HalGetBusData and of course there is an equivalent writeConfig that provices write access similar to HalSetBusData. Finally as we have obtained a driver interface, our STOP_DEVICE processing must delete this interface.

 

Lets look at the details of our initialization function first before we look at how readConfig and writeConfig work.

 

BusInterface is private data of our pciFdo object, initialized to NULL by the pciFod constructor. Obviously if you were to port this function to a C language driver BusInterface would have to be a member of your device extension and createBusInterface would need a DeviceExtension parameter as input.

 

But what is BusInterface anyhow? Well it is defined as a BUS_INTERFACE_STANDARD pointer. What is a BUS_INTERFACE_STANDARD? It is defined in ntddk.h as follows:

 

typedef struct _BUS_INTERFACE_STANDARD {

    //

    // generic interface header

    //

    USHORT Size;

    USHORT Version;

    PVOID Context;

    PINTERFACE_REFERENCE InterfaceReference;

    PINTERFACE_DEREFERENCE InterfaceDereference;

    //

    // standard bus interfaces

    //

    PTRANSLATE_BUS_ADDRESS TranslateBusAddress;

    PGET_DMA_ADAPTER GetDmaAdapter;

    PGET_SET_DEVICE_DATA SetBusData;

    PGET_SET_DEVICE_DATA GetBusData;

 

} BUS_INTERFACE_STANDARD, *PBUS_INTERFACE_STANDARD;

 

This structure defines a private interface, that is a directly callable driver interface, provided by Windows 2000 bus drivers. Once you have a valid pointer to a bus drivers BUS_INTERFACE you may call any of the interface procedures defined by the BUS_INTERFACE. The ones we are interested in here are SetBusData and GetBusData.

Lets look at how a PGET_SET_DEVICE_DATA object is defined:

 

typedef ULONG (*PGET_SET_DEVICE_DATA)(

    IN PVOID Context,

    IN ULONG DataType,

    IN PVOID Buffer,

    IN ULONG Offset,

    IN ULONG Length

    );

 

This of course is a function pointer definition. And its parameter list looks a lot like the parameters we had to supply for IRP_MN_READ_CONFIG or for HalGetBusData.

 

After we allocate our busInterface object we need to ask the bus driver to intialize it. We do that using IoBuildSynchronousFsdRequest, a somewhat higher level IRP method provided by the IO Manager. We initialize the IRP and send it to the TOP of the device stack below us, not directly to the bus driver. This allows any filter drivers above the bus driver to participate in the driver interface mechanism. Note that we do not need a completion handler for this operation as the IoBuildSynchronousFsdRequest method takes care of signaling us when the IRP is complete, and also disposes of the IRP for us. DO NOT REFERENCE THE IRP POINTER AFTER CALLING IOCALLDRIVER!

 

If our request for a driver interface succeeds then we now have a usable bus interface object. Note that this bus interface object represents an open reference on device objects below our device, and must be dereferenced correctly on STOP_DEVICE (or SURPRISE_REMOVE.)

 

Another point to note is that we must supply a GUID as a parameter. The correct GUID to supply is GUID_BUS_INTERFACE_STANDARD, defined in wdmguid.h. You might be tempted to use GUID_PCI_BUS_INTERFACE_STANDARD, but this will not work, even if you know for a fact that the pci bus driver is beneath you on the stack.

 

Having obtained the bus interface, we call our method for reading the interface in order to cache a copy of our device configuration data. The read configuration method is quite simple, and there is of course an equivalent write operation:

 

ULONG pciFdo::readConfig(PVOID Buffer,

                         ULONG Offset,

                         ULONG Length)

{

    ULONG bytesRead  = 0;

 

    if (busInterface) {

 

        bytesRead =  (*busInterface->GetBusData)(

                            busInterface->Contex,

                            PCI_WHICHSPACE_CONFIG,

                            Buffer, Offset, Length);

    }

 

    return bytesRead;

}

 

Because a driver interface keeps a reference on the related device object, you must release the interface on REMOVE or STOP PnP states for your device stack. (If you are using this mechanism to access an unrelated device stack you have to register for device change notification in order to release your reference at the appropriate time.)

 

Releasing an interface is quite simple, as the following code segment demonstrates:

 

pciFdo::deleteBusInterface()

{

 

    if (busInterface) {

 

        (*busInterface->InterfaceDereference)(busInterface->Context);

 

        delete busInterface;

 

        busInterface = NULL;

 

    }

 

}

 


Conclusion

This article briefly examines the two methods provided by Windows 2000 to access bus configuration information. If you are developing a new driver for Windows 2000 or are porting an NT 4.0 driver to Windows 2000, rather than using the obsolete HAL functions, you should replace them with either of these methods.

 

The choice of which method to use is up to you. The set-up cost for the driver interface method is more complicated, and you are responsible for deconstructing the interface as well. However the IRP-based ReadConfig method is limited to operations at less than DISPATCH_LEVEL, and can suffer from resource allocation failures. (Well actually if you can’t allocate an IRP the system isn’t going to be surviving much longer anyhow, so this last concern is rather minor.)

 

In both cases, as the code samples indicate, complexity of use can be contained within generic wrapper functions that you need to write only once. Replacing the HAL bus configuration functions in Windows2000 is a relatively simple procedure.