[Previous] [Next]

Starting and Stopping Your Device

Working with the bus driver, the PnP Manager automatically detects hardware and assigns I/O resources in Windows 2000 and Windows 98. Most modern devices have Plug and Play features that allow system software to detect them automatically and to electronically determine which I/O resources they require. In the case of legacy devices that have no electronic means of identifying themselves to the operating system or of expressing their resource requirements, the registry database contains the information needed for the detection and assignment operations.

NOTE
I find it hard to give an abstract definition of the term I/O resource that isn't circular (for example, a resource used for I/O), so I'll give a concrete one instead. The WDM encompasses four standard I/O resource types: I/O ports, memory registers, direct memory access (DMA) channels, and interrupt requests.

When the PnP Manager detects hardware, it consults the registry to learn which filter drivers and function drivers will manage the hardware. As I discussed in Chapter 2, "Basic Structure of a WDM Driver," it loads these drivers (if necessary—one or more of them might already be present, having been called into memory on behalf of some other hardware) and calls their AddDevice functions. The AddDevice functions, in turn, create device objects and link them into a stack. At this point, the stage is set for the PnP Manager, working with all of the device drivers, to assign I/O resources.

The PnP Manager initially creates a list of resource requirements for each device and allows the drivers to filter that list. I'm going to ignore the filtering step for now because not every driver will need to take this step. Given a list of requirements, the PnP Manager can then assign resources so as to harmonize the potentially conflicting requirements of all the hardware present on the system. Figure 6-2 illustrates how the PnP Manager can arbitrate between two different devices that have overlapping requirements for an interrupt request number, for example.

Click to view at full size.

Figure 6-2. Arbitration of conflicting I/O resource requirements.

Once the resource assignments are known, the PnP Manager notifies each device by sending it a PnP request with the minor function code IRP_MN_START_DEVICE. Filter drivers are typically not interested in this IRP, so they usually pass the request down the stack by using the DefaultPnpHandler technique I showed you in "IRP_MJ_PNP Dispatch Function." Function drivers, on the other hand, need to do a great deal of work on the IRP to allocate and configure additional software resources and to prepare the device for operation. This work needs to be done, furthermore, at PASSIVE_LEVEL after the lower layers in the device hierarchy have processed this IRP.

Forwarding and Awaiting the IRP

To regain control of the IRP_MN_START_DEVICE request after passing it down, the dispatch routine needs to wait for a kernel event that will be signalled by the eventual completion of the IRP in the lower layers. In Chapter 4, "Synchronization," I cautioned you not to block an arbitrary thread. PnP IRPs are sent to you in the context of a system thread that you are allowed to block, so that caution is unnecessary. Since forwarding and awaiting an IRP is a potentially useful function in other contexts, I suggest writing a helper routine to perform the mechanics:



1

2
3



4
5
6
NTSTATUS ForwardAndWait(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  KEVENT event;
  KeInitializeEvent(&event, NotificationEvent, FALSE);
  IoCopyCurrentIrpStackLocationToNext(Irp);
  IoSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)
    OnRequestComplete, (PVOID) &event, TRUE, TRUE, TRUE);
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)
    fdo->DeviceExtension;
  IoCallDriver(pdx->LowerDeviceObject, Irp);
  KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
  return Irp->IoStatus.Status;
  }

  1. We create a kernel event object as an automatic variable. KeInitializeEvent must be called at PASSIVE_LEVEL. Luckily, PnP requests are always sent at PASSIVE_LEVEL, so this particular requirement is met. The event object itself must occupy nonpaged memory, too. For most purposes, including this one, you can treat the execution stack as being nonpaged.
  2. We must make a copy of the stack parameters for the next driver because we're going to install a completion routine.
  3. We specify a completion routine so that we'll know when something underneath us completes this IRP. We might wait for the completion to occur, so we must be sure that our completion routine is called. That's why we specify TRUE for the three flag arguments to indicate that we want OnRequestComplete called when the IRP completes normally, completes with an error, or is cancelled. The context argument for the completion routine is the address of our event object.
  4. IoCallDriver calls the next lower driver, which can be a lower filter or the physical device object (PDO) driver itself. The PDO driver will perform some processing and either complete the request immediately or return STATUS_PENDING.
  5. No matter what IoCallDriver returns, we call KeWaitForSingleObject to wait forever on the kernel event we created earlier. Our completion routine will gain control when the IRP completes to signal this event.
  6. Here, we capture the ending status of the IRP and return it to our caller.

Once we call IoCallDriver, we relinquish control of the IRP until something running in some arbitrary thread context calls IoCompleteRequest to signal completion of the IRP. IoCompleteRequest will then call our completion routine. Refer to Figure 6-3 for an illustration of the timing involved. The completion routine is particularly simple:



1
2
NTSTATUS OnRequestComplete(PDEVICE_OBJECT fdo, PIRP Irp, PKEVENT pev)
  {
  KeSetEvent(pev, 0, FALSE);
  return STATUS_MORE_PROCESSING_REQUIRED;
  }

  1. We set the event on which ForwardAndWait can currently be blocked.
  2. By returning STATUS_MORE_PROCESSING_REQUIRED, we halt the unwinding process through the I/O stack. None of the completion routines installed by upper filter drivers will be called at the present time, and the I/O Manager will cease its work on this IRP. The situation is just as if IoCompleteRequest has not been called at all—except, of course, that some lower-level completion routines might have been called. At this instant, the IRP is in limbo, but our ForwardAndWait routine will presently retake ownership.

Click to view at full size.

Figure 6-3. Timing of ForwardAndWait.

Extracting Resource Assignments

In the preceding section, I showed you how to use the ForwardAndWait helper routine to send an IRP_MN_START_DEVICE request down the device stack and wait for it to complete. You call ForwardAndWait from a subdispatch routine—reached from the DispatchPnp dispatch routine shown earlier—that has the following skeletal form:



1

2

3
4
NTSTATUS HandleStartDevice(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  Irp->IoStatus.Status = STATUS_SUCCESS;
  NTSTATUS status = ForwardAndWait(fdo, Irp);
  if (!NT_SUCCESS(status))
    return CompleteRequest(Irp, status, Irp->IoStatus.Information);
  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
  status = StartDevice(fdo, <additional args>);
  return CompleteRequest(Irp, status, Irp->IoStatus.Information);
  }

  1. The bus driver uses the incoming setting of IoStatus.Status to determine whether upper-level drivers have handled this IRP. The bus driver makes a similar determination for several other minor functions of IRP_MJ_PNP. We therefore need to initialize the Status field of the IRP to STATUS_SUCCESS before passing it down.
  2. ForwardAndWait returns a status code. If it denotes some sort of failure in the lower layers, we propagate it back to our own caller. Because our completion routine returned STATUS_MORE_PROCESSING_REQUIRED, we halted the completion process inside IoCompleteRequest. Therefore, we have to complete the request all over again, as shown here.
  3. Our configuration information is buried inside the stack parameters. I'll show you where a bit further on.
  4. StartDevice is a helper routine you write to handle the details of extracting and dealing with configuration information. In my sample drivers, I've placed it in a separate source module named READWRITE.CPP. I'll explain shortly what arguments you would pass to this routine besides the address of the device object.

You might guess (correctly!) that the IRP_MN_START_DEVICE handler has work to do that concerns the transition from the initial STOPPED state to the WORKING state. I can't explain that yet because I need to first explain the ramifications of other Plug and Play requests on state transitions, IRP queuing, and IRP cancellation. So, I'm going to concentrate for a while on the configuration aspects of the PnP requests.

The I/O stack location's Parameters union has a substructure named StartDevice that contains the configuration information you pass to the StartDevice helper function. See Table 6-2.

Table 6-2. Fields in the Parameters.StartDevice substructure of an IO_STACK_LOCATION.

Field Name Description
AllocatedResources Contains raw resource assignments
AllocatedResourcesTranslated Contains translated resource assignments

Both AllocatedResources and AllocatedResourcesTranslated are instances of the same kind of data structure, called a CM_RESOURCE_LIST. This seems like a very complicated data structure if you judge only by its declaration in WDM.H. As used in a start device IRP, however, all that remains of the complication is a great deal of typing. The "lists" will have just one entry, a CM_PARTIAL_RESOURCE_LIST that describes all of the I/O resources assigned to the device. You could use statements like the following to access the two lists:

PCM_PARTIAL_RESOURCE_LIST raw, translated;
raw = stack->Parameters.StartDevice
  .AllocatedResources->List[0].PartialResourceList;
translated = stack->Parameters.StartDevice
  .AllocatedResourcesTranslated->List[0].PartialResourceList;

The only difference between the last two statements is the reference to either the AllocatedResources or AllocatedResourcesTranslated member of the parameters structure.

The raw and translated resource lists are the logical arguments to send to the StartDevice helper function, by the way:

status = StartDevice(fdo, raw, translated);

There are two different lists of resources because I/O buses and the CPU can address the same physical hardware in different ways. The raw resources contain numbers that are bus-relative, whereas the translated resources contain numbers that are system-relative. Prior to the WDM, a kernel-mode driver might expect to retrieve raw resource values from the registry, the PCI (Peripheral Component Interconnect) configuration space, or some other source, and to translate them by calling routines such as HalTranslateBusAddress or HalGetInterruptVector. See, for example, Art Baker's The Windows NT Device Driver Book: A Guide for Programmers (Prentice Hall, 1997), at pages 122-62. Both the retrieval and translation steps are done by the PnP Manager now, and all a WDM driver needs to do is access the parameters of a start device IRP as I'm now describing.

What you actually do with the resource descriptions inside your StartDevice function is a subject for the next chapter, "Reading and Writing Data."

IRP_MN_STOP_DEVICE

The stop device request tells you to shut your device down so that the PnP Manager can reassign I/O resources. At the hardware level, shutting down involves pausing or halting current activity and preventing further interrupts. At the software level, it involves releasing the I/O resources you configured at start device time. Within the framework of the dispatch/subdispatch architecture I've been illustrating, you might have a subdispatch function like this one:



1
2

3
NTSTATUS HandleStopDevice(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  <complicated stuff>
   StopDevice(fdo, oktouch);
   Irp->IoStatus.Status = STATUS_SUCCESS;
   return DefaultPnpHandler(fdo, Irp);
  }

  1. Right about here, you need to insert some more or less complicated code that concerns IRP queuing and cancellation. I'll show you the code that belongs in this spot further on in this chapter in "While the Device Is Stopped."
  2. In contrast to the start device case, in which we passed the request down and then did device-dependent work, here we do our device-dependent stuff first and then pass the request down. The idea is that our hardware will be quiescent by the time the lower layers see this request. I wrote a helper function named StopDevice to do the shutdown work. The second argument indicates whether it will be okay for StopDevice to touch the hardware if it needs to. Refer to the sidebar "Touching the Hardware When Stopping the Device" for an explanation of how to set this argument.
  3. We always pass PnP requests down the stack. In this case, we don't care what the lower layers do with the request, so we can simply use the DefaultPnpHandler code to perform the mechanics.

The StopDevice helper function called in the preceding example is code you write that essentially reverses the configuration steps you took in StartDevice. I'll show you that function in the next chapter. One important fact about the function is that you should code it in such a way that it can be called more than once for a single call to StartDevice. It's not always easy for a PnP IRP handler to know whether you've already called StopDevice, but it is easy to make StopDevice proof against duplicative calls.

IRP_MN_REMOVE_DEVICE

Recall that the PnP Manager calls the AddDevice function in your driver to notify you about an instance of the hardware you manage and to give you an opportunity to create a device object. Instead of calling a function to do the complementary operation, however, the PnP Manager sends you a Plug and Play IRP with the minor function code IRP_MN_REMOVE_DEVICE. In response to that, you'll do the same things you did for IRP_MN_STOP_DEVICE to shut down your device, and then you'll delete the device object:

NTSTATUS HandleRemoveDevice(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  <complicated stuff>
  DeregisterAllInterfaces(pdx);
  StopDevice(fdo, oktouch);
  Irp->IoStatus.Status = STATUS_SUCCESS;
  NTSTATUS status = DefaultPnpHandler(fdo, Irp);
  RemoveDevice(fdo);
  return status;
  }

This fragment looks very similar to HandleStopDevice, with a couple of additions. DeregisterAllInterfaces will disable any device interfaces you registered (probably in AddDevice) and enabled (probably in StartDevice), and it will release the memory occupied by their symbolic link names. RemoveDevice will undo all the work you did inside AddDevice. For example:




1
2
VOID RemoveDevice(PDEVICE_OBJECT fdo)
  {
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  IoDetachDevice(pdx->LowerDeviceObject);
  IoDeleteDevice(fdo);
  }

  1. This call to IoDetachDevice balances the call AddDevice made to IoAttachDeviceToDeviceStack.
  2. This call to IoDeleteDevice balances the call AddDevice made to IoCreateDevice. Once this function returns, the device object will no longer exist. If your driver isn't managing any other devices, your driver will shortly be unloaded from memory, too.

You might be troubled by the fact that you call IoDeleteDevice at a time when the lower levels of the device hierarchy might still be processing the IRP_MN_REMOVE_DEVICE request. No harm can come from that, however, because the Object Manager maintains a reference count on your device object to prevent it from disappearing while anything has an active pointer to it.

Note, by the way, that you don't get a stop device request followed by a remove device request. The remove device request implies a shutdown, so you do both pieces of work in reply.

IRP_MN_SURPRISE_REMOVAL

Sometimes the end user has the physical ability to remove a device without going through any user interface elements first. If the system detects that such a surprise removal has occurred, it sends the driver a PnP request with the minor function code IRP_MN_SURPRISE_REMOVAL. It will later send an IRP_MN_REMOVE_DEVICE. Unless you previously set the SurpriseRemovalOK flag while processing IRP_MN_QUERY_CAPABILITIES (as I'll discuss in Chapter 8, "Power Management"), the system also posts a dialog box to inform the user that it's potentially dangerous to yank hardware out of the computer.

In response to the surprise removal request, a device driver should disable any registered interfaces. This will give applications a chance to close handles to your device if they're on the lookout for the notifications I discuss later in "PnP Notifications." Then the driver should release I/O resources and pass the request down:

NTSTATUS HandleSurpriseRemoval(PDEVICE_OBJECT fdo, PIRP Irp)
  {
  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
  <complicated stuff>
  EnableAllInterfaces(pdx, FALSE);
  StopDevice(fdo, oktouch);
  Irp->IoStatus.Status = STATUS_SUCCESS;
  return DefaultPnpHandler(fdo, Irp);
  }