1394 Node-Targeted Asynchronous Transfers
August 5, 20031
Bill McKenzie
Copyright © 2003 by Bill McKenzie. All rights reserved
In the last issue of WD3 I presented an article on 1394 address range allocation for Windows drivers. I covered 1394 address range allocation as my first topic hoping to lay the groundwork for helping readers to understand the 1394 asynchronous communication model and open the door for more discussions in this area. Going forward, I would like to fill in yet more pieces of the Windows 1394 asynchronous communication puzzle. In this article, I am going to cover what I call node-targeted asynchronous transfers. To be clear, all asynchronous transfers on the 1394 bus are initiated from a single source node to some destination node(s). However, by default the Windows 1394 bus driver makes assumptions as to where a driver is sending its asynchronous data. Knowing how to send asynchronous data to a particular node proves useful in many circumstances This article will show the developer how to communicate, using asynchronous transfers, from a single WDM driver to any or all nodes on a given 1394 bus. The following discussion assumes a good bit of familiarity with 1394, Windows WDM device drivers, and with my last article in WD3 which you can find here.
Overview:
To understand Windows 1394 asynchronous transfer operation, one must understand the different types of addressing modes that the Windows 1394 bus driver uses. Addressing modes determine how the bus driver interprets destination address information that is passed along with asynchronous transfer requests. For Windows 2000 and earlier platforms, there were two modes of addressing when using asynchronous transfers normal-mode and raw-mode. What these modes are and how they work is explained later. For XP and later platforms, there is a new addressing mode called virtual-mode addressing. Virtual-mode addressing is used for 1394 virtual devices. The virtual device mechanism for 1394 was introduced with the Windows XP 1394 bus driver stack. In order to fully understand 1394 asynchronous communication on Windows platforms, it is helpful to understand what 1394 virtual devices are and how they are used. In a moment, I will take a step to the side and cover virtual devices in some detail. As for asynchronous transfer addressing modes, none of these were documented in the Windows Device Driver Development Kit (DDK) help until about a year ago. Even now the documentation does not explain how one goes about implementing some of these modes. The following paragraphs will attempt to explain asynchronous addressing modes, and show some code that demonstrates how to go about implementing the different modes in a WDM device driver. First, let us look at 1394 virtual devices. If you do not care about 1394 virtual devices or if you are already familiar with them, you can just skip down to the section titled Asynchronous Transfers.
1394 Virtual Devices:
As I mentioned in my last article, 1394 is peer-to-peer technology. 1394 devices can communicate with one another without the intervention of a PC hosted processor. In addition, each device connected to the 1394 bus represents at least one node. A 1394 node is an addressable entity on the 1394 bus. Even host controllers are nodes on the bus. It is possible to have multiple host controllers communicate with one another directly. These host controllers can reside in the same or separate PCs or not in a PC at all. Back when Windows 2000 was a new operating system is when I first ran into 1394. The first thing I wanted to do to try out this cool technology was to transfer some data from one PC to another. With each host controller being a node, it should be simple, right? The first problem to solve was to load a driver that could communicate on the 1394 bus. Normally a WDM driver is loaded by the PnP manager when a hardware device is enumerated. The PnP manager receives a device ID from the bus driver which enumerates the device. The PnP manager then searches INF files for a matching device ID. If a matching device ID is found, the PnP manager loads the driver specified by the INF file. For 1394, the 1394 bus driver reads the configuration ROM of a 1394 hardware device to get the device ID. A normal 1394 device, like a video camera, sits across a 1394 cable from where its driver runs. For my PC-to-PC setup I needed to load a driver for the host controller sitting in each PC. As with the case for the camera, the driver I would load would sit across the 1394 cable from the host controller. On Windows 2000 and earlier platforms, there is no way to fake the bus into enumerating a device where no remotely connected hardware exists. Fortunately, the 1394 bus enumerates each remotely connected host controller with the device ID 1394\Microsoft&1394_PC as shown in Figure 1.
Host controllers always enumerate with the 1394\Microsoft&1394_PC ID. This ID might appear to be a fine solution to load a set of drivers for a host controller-to-host controller transfer, and indeed this setup does work. However, using 1394\Microsoft&1394_PC in one's INF file is not a practical solution. Multiple drivers might wish to use this same ID, yet only one driver is loaded per host controller. Despite such a powerful peer-to-peer potential, the 1394 bus is severely hobbled on 2000 and earlier Windows platforms. Fortunately, someone at Microsoft realized the limitations being forced on the 1394 bus. The Windows XP and later versions of the 1394 bus driver allow a user to enumerate devices through the 1394 bus using custom device IDs. These devices do not have to correlate to any real hardware. They are called 1394 virtual devices.
Now, you can tell the 1394 bus that you want a virtual device
named XYZ, and the bus enumerates a device with device ID V1394\XYZ. This value
shows up right under the registry's ENUM key just like any other PnP enumerated
device. V1394 is the name of the bus for virtual 1394 devices. If one uses
this ID in an INF file, then the driver specified in the INF file will get
loaded anytime the XYZ virtual device is enumerated on the V1394 bus. With this
mechanism, multiple virtual devices can be enumerated for a single host
controller/1394 bus driver stack instance. This allows multiple manufacturers
to load their own type of driver for transfers over a single host
controller-to-host controller connection. But wait, there is more. The host
controller does not have to be connected to another host controller or to any
other hardware in order to enumerate a virtual device. In addition to solving
the host controller peer-to-peer problem, virtual devices allow even greater
flexibility than was ever possible on 2000 and earlier Windows platforms.
Virtual devices have full access to the 1394 bus through the 1394 bus driver.
Virtual device can perform asynchronous transfers to other nodes, isochronous
transfers, and they can allocate address ranges in the host controller's memory
space. Figure 2 shows how the setup from Figure 1 changes using virtual devices
Instead of enumerating a device across a cable, the 1394 bus driver is now
enumerating a device as if it were across a cable (whether there is a
cable or not). Because these virtual devices have full access to the bus, they
can communicate with one another as well. As Figure 2 shows, remotely connected
host controllers still enumerate with the
1394\Microsoft&1394_PC ID on Windows XP and later platforms. These
platforms have a native driver called enum1394.sys that loads using this device
ID. This ID is thus no longer available for general use on XP and later Windows
platforms. The change in the bus driver stack from Windows 2000 to Windows XP
is confusing; at least it was for me. The benefit of the new more powerful
virtual device mechanism makes the paradigm shift quite worthwhile. I just wish
they had back ported this new 1394 bus driver stack to Windows 2000!
1394 Virtual Device Instantiation:
So, just how does one request to create a 1394 virtual device? Creating a virtual device from user-mode code is demonstrated in the 1394api.dll DDK sample, and I show a similar method in the code listing below.
/////////////////////////////////////////////////////////////////////////////////////////
// AddRemoveV1394Device
// Virtual 1394 device creation/deletion routine
//
// Arguments:
// IN BusInstance
// Bus driver instance to create virtual device for. Also used
// in virtual device's instance ID so that the ID is unique for
// each bus driver instance.
// IN bAddingDevice
// Flag to indicate device addition or removal
//
// Return Value:
// TRUE if successful, FALSE otherwise.
//
BOOL AddRemoveV1394Device(ULONG BusInstance, BOOLEAN bAddDevice)
{
HANDLE hDevice = INVALID_HANDLE_VALUE;
CHAR busName[MAX_STR_LEN];
PIEEE1394_API_REQUEST p1394ApiReq;
PIEEE1394_VDEV_PNP_REQUEST pDevPnpReq;
CHAR deviceId[] = "MY_VIRTUAL_DEVICE";
ULONG length;
BOOLEAN bRetVal = TRUE;
// Use do/while for convenient resource cleanup, not looping
do {
sprintf(busName, "\\\\.\\1394BUS%d", BusInstance);
// Open handle to 1394 bus driver instance
hDevice = CreateFile(
busName,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("Failed to open handle to 1394 bus device, (%d)\n",
GetLastError());
bRetVal = FALSE;
break;
}
p1394ApiReq =
(PIEEE1394_API_REQUEST)LocalAlloc(
LPTR,
sizeof(IEEE1394_API_REQUEST) +
strlen((PCHAR)deviceId));
if (p1394ApiReq == NULL) {
printf("Failed to allocate IEEE1394_API_REQUEST struct\n");
bRetVal = FALSE;
break;
}
if (bAddDevice) {
printf("Creating 1394 Virtual Device on bus %d\n", BusInstance);
// Add the virtual device
p1394ApiReq->RequestNumber = IEEE1394_API_ADD_VIRTUAL_DEVICE;
// For device to be persistent across reboots
p1394ApiReq->Flags = IEEE1394_REQUEST_FLAG_PERSISTENT;
// For the virtual device to use the host controller's unique
// instance ID you can use the following commented out flag.
// If used here it must be used for removal of the virtual device
// as well.
//p1394ApiReq->Flags |= IEEE1394_REQUEST_FLAG_USE_LOCAL_HOST_EUI;
pDevPnpReq = &p1394ApiReq->u.AddVirtualDevice;
} else {
printf("Removing 1394 Virtual Device from bus %d\n", BusInstance);
// Remove the virtual device
p1394ApiReq->RequestNumber = IEEE1394_API_REMOVE_VIRTUAL_DEVICE;
// Use this flag if it was used when adding the device
//p1394ApiReq->Flags |= IEEE1394_REQUEST_FLAG_USE_LOCAL_HOST_EUI;
pDevPnpReq = &p1394ApiReq->u.RemoveVirtualDevice;
}
// Setup the
pDevPnpReq->fulFlags = 0;
pDevPnpReq->Reserved = 0;
pDevPnpReq->InstanceId.LowPart = BusInstance;
pDevPnpReq->InstanceId.HighPart = 0;
strncpy(
(PCHAR)&pDevPnpReq->DeviceId,
(PCHAR)deviceId,
strlen((PCHAR)deviceId));
if (!DeviceIoControl(
hDevice,
IOCTL_IEEE1394_API_REQUEST,
p1394ApiReq,
sizeof(IEEE1394_API_REQUEST) + (DWORD)strlen((PCHAR)deviceId),
NULL,
0,
&length,
NULL)) {
printf("IOCTL_IEEE1394_API_REQUEST failed: %d\n", GetLastError());
bRetVal = FALSE;
break;
}
} while(FALSE);
// Free our allocated resources
if (hDevice != INVALID_HANDLE_VALUE) {
CloseHandle(hDevice);
}
if (p1394ApiReq != NULL) {
LocalFree(p1394ApiReq);
}
return bRetVal;
}
This code is not very complex. The value of BusInstance passed into this function will determine which instance of the 1394 bus driver will create the virtual device. This is only useful if there is more than one 1394 host controller plugged into the same system. If there is only one 1394 host controller in a system, the bus value will always be zero. The other parameter bAddingDevice determines whether to add or remove the virtual device. From there, the code opens a handle to the selected instance of the 1394 bus driver using the CreateFile Win32 API. If the CreateFile call succeeds, the code sets up an IEEE1394_API_REQUEST structure for the request. The RequestNumber member is set equal to IEEE1394_API_ADD_VIRTUAL_DEVICE if adding a device or IEEE1394_API_REMOVE_VIRTUAL_DEVICE if removing a device. The request is then sent down to the 1394 bus driver, using the DeviceIoControl Win32 API. The request uses an IOCTL control code of IOCTL_IEEE1394_API_REQUEST. The IEEE1394_API_REQUEST structure is sent as the IN buffer in the DeviceIoControl call. The only slightly tricky thing to notice in this code is the allocation size of the IEEE1394_API_REQUEST structure. You will notice that the device ID string size is added to the allocation size of this structure. If you look at the internals of this structure from ntdd1394.h in the DDK, you will see that the last member of this structure is a union. The union consists of a couple of different types of the IEEE1394_VDEV_PNP_REQUEST structure. The IEEE1394_VDEV_PNP_REQUEST structure, also in ntdd1394.h, has a variable of type UCHAR as its last member. This member, called DeviceID, is actually intended to be an array of UCHARs representing a null-terminated string. The device ID string has no allocation space inherent in these structures. However, the device ID string is guaranteed to be the last member of these structures. The caller has to allocate space after the end of the IEEE1394_API_REQUEST structure to allow for the device ID string storage. The device ID itself needs to be a NULL terminated string of your choosing. The string needs to be unique among 1394 virtual devices on the system, so choose something you do not think will conflict (GUID maybe?). Also important to note, the instance ID put in the IEEE1394_VDEV_PNP_REQUEST structure needs to be unique per bus instance (per host controller). That is why the code above used the BusInstance parameter as the low part of the instance ID. There really is not much involved in creating a virtual device. By the way, it is possible to create a virtual device from a driver as well. A driver uses the same IOCTL code IOCTL_IEEE1394_API_REQUEST and sends the IOCTL down with an IRP of major type IRP_MJ_INTERNAL_DEVICE_CONTROL.
Hardware Emulation Drivers:
If you are not sick of virtual device talk yet, I should take a moment here and attempt to cover one more rather important topic related to 1394 virtual devices. That topic is hardware emulation drivers. While virtual devices are really nice for some things, they do not really allow a developer to develop a driver for non-existent hardware by themselves. To understand why this is so, consider again how a real 1394 device is connected to a system. A 1394 device is connected to a host controller via 1394 cable, and that device is then enumerated by the 1394 bus driver running on the system where the host controller resides. A driver loaded for this device will also reside on the same system where the host controller resides. The device under control does not reside on the system with the host controller or the device driver. All communication from device driver to device goes across a 1394 cable via the 1394 bus driver stack. Conversely, virtual devices drivers exist on the same platform where their drivers run. There is no hardware sitting across a cable for a virtual device. Emulating data transfers is not practical with virtual devices. What would be nice is if the virtual device could modify the configuration ROM of the host controller on which it resides. The virtual device could make the host controller look like an emulated device to any other 1394 host controller on the bus. This would allow enumeration of an emulated device on a remotely connected system. Turns out, this is exactly the mechanism suggested for emulating 1394 hardware on Windows platforms. The 1394 DDI REQUEST_SET_LOCAL_HOST_PROPERTIES can be used by virtual drivers with the u.SetLocalHostProperties.nLevel member of the IEEE 1394 Request Block (IRB) set to SET_LOCAL_HOST_PROPERTIES_MODIFY_CROM. This request allows the driver to add unit directory entries to the host controller's configuration ROM. The structure of unit directories or configuration ROMs is beyond the scope of this document. For more information on these structures see the IEEE Standard 1394-1995 and ANSI/IEEE Standard 1212, 1994 Edition specifications.
With the ability to add unit directories to the configuration ROM, the virtual device driver, can now act as the emulated hardware itself. The virtual device driver now doubles as a hardware emulation driver. Figure 3 shows an example of how this setup is actually used on XP and later platforms for the 1394 Virtual Net Adapter.
The virtual device driver NIC1394.sys on platform A modifies the configuration
ROM of host controller A and adds unit directory information including the
vendor ID Microsoft and model ID NIC1394. This causes the bus driver stack on
host controller B to see what it thinks is a real 1394 device with these vendor
and model IDs. NIC1394.sys now emulates the virtual NIC device. Keep in mind
that this figure has been simplified. The host controllers, and any other
connected 1394 devices, would still get enumerated on the platforms for the
setup in Figure 3.
With the ability to allocate address ranges, and otherwise fully access the 1394 bus, the virtual device driver is practically unlimited in its flexibility to emulate most any 1394 device. I cannot stress enough what a powerful mechanism this is. There is no other hardware bus today on Windows platforms which has this level of hardware emulation capabilities. Now that we have covered 1394 virtual devices, let us get back to the discussion of node-targeted asynchronous transfers.
As I discussed in my last article, 1394 asynchronous transfers
consist of two phases, a request phase and a response phase. All asynchronous
transfers originate from a 'requestor' node targeted at the 1394 address space
of some 'responder' node. The 'responder' node accepts data from and/or
returns data back to the 'requestor' node. The 'responder' node is also
referred to as the destination node for the request. Any node on a given 1394
bus can communicate with any other node on the same bus using asynchronous
transfers. 1394 WDM device drivers on Windows platforms initiate asynchronous
transfers by constructing IEEE Request Blocks (IRBs) with asynchronous transfer
function types of either REQUEST_ASYNC_WRITE,
REQUEST_ASYNC_READ, or
REQUEST_ASYNC_LOCK. An IRB is sent
down, via IRP, to the 1394 bus driver. The 1394 bus driver stack executes
asynchronous transfer on behalf of the driver from the host controller node to
some destination node.
Asynchronous Addressing Modes:
The following listing shows the relevant IRB members for an asynchronous write request.
typedef struct _IRB { ULONG FunctionNumber; . . . union { struct { IO_ADDRESS DestinationAddress; ULONG nNumberOfBytesToWrite; ULONG nBlockSize; ULONG fulFlags; PMDL Mdl; ULONG ulGeneration; UCHAR chPriority; UCHAR nSpeed; UCHAR tCode; ULONG Reserved; } AsyncWrite; . . . } u; } IRB;
I am not going to cover the utility or meaning of all of the members of this IRB
here. I encourage the reader to read the DDK documentation related to all
asynchronous transfer types for more information. One member I will talk about
is DestinationAddress. As its name
suggests, DestinationAddress has a lot
to do with which node will be the destination node for the transfer request.
This value also determines where in the destination node's address space the
access will occur for this request.
DestinationAddress is of type
IO_ADDRESS, whose structure is shown below.
typedef struct _IO_ADDRESS { NODE_ADDRESS IA_Destination_ID; ADDRESS_OFFSET IA_Destination_Offset; } IO_ADDRESS, *PIO_ADDRESS;
Notice there are two members in this structure IA_Destination_ID and IA_Destination_Offset. IA_Destination_Offset, which has type ADDRESS_OFFSET, holds the 48-bit offset value used to access the destination node's address space for this request. The other member in the IO_ADDRESS structure IA_Destination_ID, of type NODE_ADDRESS, stores the node address of the destination node for this request. The latest DDK documentation that I have says the following about these members: "The driver only needs to fill in the IA_Destination_Offset member of u.AsyncXxx.DestinationAddress; the bus driver fills in the IA_Destination_ID member". Note that this only states what the driver needs to do, not what the driver is forced to do. As it turns out, it is true that the bus driver fills in the IA_DESTINATION_ID for asynchronous transfers, usually. What is not clear from this documentation is that there are cases when the bus driver does not fill in the IA_DESTINATION_ID. It also is not clear that there are times when it is not desirable to have the bus driver fill in the destination information. The asynchronous addressing modes mentioned at the top of the article are what determine whether or not the bus driver fills in this information and how. Let us look more closely at these addressing modes.
Normal-Mode:
Asynchronous addressing modes are nothing more than a convention used by the bus driver to determine how and when to fill in the IA_DESTINATION_ID member of the DestinationAddress structure for asynchronous transfers. The 1394 bus driver makes the following assumption (described in my own words):
Most 1394 device drivers are probably going to control a single real 1394 hardware device. These drivers, if they use asynchronous transfers, will typically only attempt to communicate with that single real 1394 hardware device.
I am not going to comment on what I think of this assumption or assumptions in bus drivers in general. Rather, I will make the observation that the above description is what is expected as the normal asynchronous communication model for Windows device drivers. The designers of the Windows 1394 bus driver stack apparently decided to help us out with this normal case. If a device driver controls some real 1394 hardware device, then the bus driver fills in the destination information for asynchronous transfers automagically. The bus driver uses the node information of the device under control of the device driver. The bus determines if the device driver controls real hardware by storing hardware information in the Physical Device Objects (PDOs) at enumeration time. This mode of asynchronous addressing, where the bus fills in the destination information, is called normal-mode addressing.
Raw-Mode:
What if the device driver needs to execute an asynchronous transfer to some other node on the bus? Good question. I know, why not just fill in the IA_DESTINATION_ID member of the DestinationAddress structure for the asynchronous request and send the request wherever it needs to go? Unfortunately, if the bus driver detects that the device driver is controlling real hardware, it will always attempt to use normal-mode addressing. Thus, the bus driver will always send the request to the device node the driver controls. The value written into IA_DESTINATION_ID by the device driver is ignored. Fortunately, there is a solution to this problem. Until recently, there was no documentation to suggest that a solution existed. Even now, the DDK documentation is very sketchy when explaining the details of how to go about solving this problem.
To understand the solution, one must understand a little of the structure of the 1394 bus driver stack. The structure is not complex it just consists of a bus driver sitting atop a port driver. A 1394 device driver typically sends asynchronous transfer requests to the bus driver. The bus driver determines what needs to be done with the request. If necessary, the bus driver forwards the request to the port driver. The port driver controls the 1394 host controller hardware and executes the actual asynchronous transfers. What does this have to do with sending asynchronous transfers where we want them to go? Well, the 1394 bus driver is stuck in normal-mode addressing for devices that control real hardware. There is apparently no way to shake it. The port driver is obviously capable of handling asynchronous requests. So, it looks like the device driver needs to circumvent the bus driver, and send requests straight to the port driver. This technique actually works, it is legal, and this method is demonstrated in the DDK 1394Diag.sys sample. This technique is called raw-mode addressing. In this mode the driver fills in the IA_DESTINATION_ID member of the DestinationAddress structure for the asynchronous transfer request. The request is then sent straight to the port driver who executes the transfer to the proper node.
Take a look at the following code snippet.
PNODE_DEVICE_EXTENSION nodeDeviceExtension;// Get the bus driver's device extension
nodeDeviceExtension =
(PNODE_DEVICE_EXTENSION)
DeviceExtension->LowerDeviceObject->DeviceExtension;
// Save the port device object in our device extension
DeviceExtension->PortDeviceObject =
nodeDeviceExtension->PortDeviceObject;
This code would typically live in a device driver's start device handler or similar device initialization code. DeviceExtension is the device extension pointer in this device driver. DeviceExtension->LowerDeviceObject is a pointer to the PDO the device driver received it attached to the device stack. DeviceExtension->PortDeviceObject is just a pointer used to store the port driver's device object for later use. The structure type NODE_DEVICE_EXTENSION is defined in 1394.h in the DDK headers. This structure provides the layout used for the device extension of all PDOs created by the 1394 bus driver. PDOs created by the 1394 bus driver store a pointer to the port device object in their device extensions in a member called PortDeviceObject.
The following code shows how the above information can be used
in implementation of the asynchronous transfer request code.
/////////////////////////////////////////////////////////////////////////////////////////
// SampAsyncTransfer
// Sends or receives data on the 1394 bus using asynchronous read or
// write transfers
//
// Arguments:
// IN DeviceExtension
// Our device extension
// IN NumberOfBytes
// Number of data bytes to transfer
// IN OffsetHigh
// Destination address high part
// IN OffsetLow
// Destination address low part
// IN Mdl
// Transfer data buffer
// IN bWrite
// Flag to indicate transfer direction
//
// IN bRawMode
// Flag used to determine whether to send this request
// to the bus driver or to the port driver.
//
// IN Node
// Node number to read/write from/to, only used if bRawMode is TRUE
//
// Return Value:
// NT status code
//
NTSTATUS SampAsyncTransfer(
IN PSAMP_DEVICE_EXTENSION DeviceExtension,
IN ULONG NumberOfBytes,
IN USHORT OffsetHigh,
IN ULONG OffsetLow,
IN PMDL Mdl,
IN BOOLEAN bWrite,
IN USHORT Node)
{
PIRB irb;
NTSTATUS status = STATUS_INSUFFICIENT_RESOURCES;
DbgPrint(__FUNCTION__"++\n");
// Allocate an IRB
irb = (PIRB)ExAllocateFromNPagedLookasideList(
&DeviceExtension->IrbList);
if (irb == NULL)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
// Setup the IRB
RtlZeroMemory(irb, sizeof(IRB));
if (bWrite)
{
irb->FunctionNumber = REQUEST_ASYNC_WRITE;
irb->u.AsyncWrite.DestinationAddress.IA_Destination_Offset.Off_High =
OffsetHigh;
irb->u.AsyncWrite.DestinationAddress.IA_Destination_Offset.Off_Low =
OffsetLow;
irb->u.AsyncWrite.nNumberOfBytesToWrite = NumberOfBytes;
irb->u.AsyncWrite.Mdl = Mdl;
irb->u.AsyncWrite.ulGeneration = DeviceExtension->GenerationCount;
if (Node != MAX_LOCAL_NODES) {
irb->u.AsyncWrite.DestinationAddress.IA_Destination_ID.NA_Bus_Number =
LOCAL_BUS;
irb->u.AsyncWrite.DestinationAddress.IA_Destination_ID.NA_Node_Number =
Node;
}
} else {
irb->FunctionNumber = REQUEST_ASYNC_READ;
irb->u.AsyncRead.DestinationAddress.IA_Destination_Offset.Off_High =
OffsetHigh;
irb->u.AsyncRead.DestinationAddress.IA_Destination_Offset.Off_Low =
OffsetLow;
irb->u.AsyncRead.nNumberOfBytesToRead = NumberOfBytes;
irb->u.AsyncRead.Mdl = Mdl;
irb->u.AsyncRead.ulGeneration = DeviceExtension->GenerationCount;
if (Node != MAX_LOCAL_NODES) {
irb->u.AsyncRead.DestinationAddress.IA_Destination_ID.NA_Bus_Number =
LOCAL_BUS;
irb->u.AsyncRead.DestinationAddress.IA_Destination_ID.NA_Node_Number =
Node;
}
}
// Send it down
if (bRawMode) {
status = SampSubmitIrbSynch(
DeviceExtension->PortDeviceObject,
irb);
} else {
status = SampSubmitIrbSynch(
DeviceExtension->LowerDeviceObject,
irb);
}
// Free our allocated IRB memory
ExFreeToNPagedLookasideList(
&DeviceExtension->IrbList,
irb);
DbgPrint(__FUNCTION__"--. STATUS %x\n", status);
return status;
}
I am not going to walk through all of this code, but a couple of
items are worth noting. First, notice the function handles both asynchronous
reads and asynchronous writes depending on the value of bWrite. The code for
these two operations is obviously very similar. Next, notice that the function
fills in the node address information in the IRB if the
Node parameter does not have the value
MAX_LOCAL_NODES.
MAX_LOCAL_NODES is defined in the DDK
header 1394.h and represents the maximum number of nodes allowed on the bus.
This value is never a valid node value. This value is used as a check to
indicate whether or not normal-mode addressing should be used. If normal-mode
is used, the driver does not need to fill in the destination node information as
the bus driver will take care of it. If normal-mode is not used, the driver
fills in the destination node address values. The bus number portion of the
node address is set to LOCAL_BUS, which
is also defined in 1394.h in the DDK. There is effectively no way a single 1394
bus driver stack instance can communicate with more than one 1394 bus at a time,
so the bus number does not change. Lastly, notice that bRawMode is checked to
determine where to send the request down to. If raw-mode is used, the driver
sends the request straight to the 1394 port driver. Otherwise, the request is
forwarded to the bus driver normally. For completeness, the code for
SampSubmitIrbSynch is provided below.
Important Notice: In the SampAsyncTransfer function above notice that the IRB allocation is from a non-paged lookaside list. Lookaside lists are lists of fixed-size chunks of memory. Lookaside lists can prevent frequent small-sized memory allocations from fragmenting the operating system's memory pools. For IRBs lookaside lists work out quite well as IRBs are fixed in size. There was a point in my career when I thought all frequent small-sized memory allocations should be made with lookaside lists in order to prevent pool fragmentation. I have seen similar reasoning in the DDK documentation, some even related to IRPs. For IRPs, the operating system already maintains multiple pre-allocated lists of IRPs with different stack sizes. A lookaside list for IRPs would be redundant. Also, I learned from authors Solomon and Russinovich that there already exists a small allocation lookaside list that the operating system maintains. According to "Inside Windows 2000" Third Edition, Windows 2000 maintains a list with a chunk size of 256 bytes. Presumably, allocations larger than 256 bytes are allocated directly from pool. If this is true, then frequent IRB allocations do have the potential to fragment the system pools. IRBs are 344 bytes in size at last count. It is probably a good idea to use a lookaside list if your driver will use IRBs frequently. |
/////////////////////////////////////////////////////////////////////////////////////////
// SampSubmitIrbSynch
// Method to synchronously submit an IRB to the bus driver.
//
// Arguments:
// IN DeviceObject
// Target device object
// IN Irb
// IRB to submit
//
// Return Value:
// NT status code.
//
NTSTATUS SampSubmitIrbSynch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRB Irb
)
{
NTSTATUS status;
IO_STATUS_BLOCK ioStatus;
KEVENT event;
PIO_STACK_LOCATION irpStack;
PIRP irp;
DbgPrint(__FUNCTION__"++\n");
KeInitializeEvent(&event, NotificationEvent, FALSE);
irp = IoBuildDeviceIoControlRequest(
IOCTL_1394_CLASS,
DeviceObject,
NULL,
0,
NULL,
0,
TRUE,
&event,
&ioStatus);
if (irp != NULL) {
// Get our IRP stack location
irpStack = IoGetNextIrpStackLocation(irp);
// Set up the parameters for the IRP
irpStack->Parameters.Others.Argument1 = Irb;
// Use internal device control even though
// some DDK documentation may state otherwise
irpStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL;
// send it down
status = IoCallDriver(DeviceObject, irp);
if (status == STATUS_PENDING) {
KeWaitForSingleObject(
&event,
Executive,
KernelMode,
FALSE,
NULL);
status = ioStatus.Status;
}
} else {
status = STATUS_INSUFFICIENT_RESOURCES;
}
DbgPrint(__FUNCTION__"--. STATUS %x\n", status);
return status;
}
Virtual-Mode:
There had to be a reason for that long discussion of virtual
devices at the start of this article, right? There is one more asynchronous
addressing mode that needs to be covered. If the 1394 bus driver determines
that a 1394 device driver is not controlling real hardware, which again the PDO
determines at enumeration time, then the 1394 bus does not automatically fill in
the node address for asynchronous requests made by that driver. Drivers for
1394 virtual devices fit this category. Virtual device drivers are required to
fill in the node address information, just like what would be done for raw-mode
transfers. Since the bus driver does not have any of its own node address
information to use, asynchronous transfer requests from virtual device drivers
can be sent directly to the bus driver. The virtual device driver does not ever
have to worry about sending requests to the port driver. This addressing mode
is called virtual-mode addressing. Named virtual-mode, obviously, as it
applies strictly to virtual device drivers. Because Windows 2000 and earlier
Windows platform did not have a virtual device mechanism built into the bus
driver stack, this addressing mode is not available on those platforms.
Summary:
Being able to target asynchronous transfers to any node on the bus allows the full peer-to-peer functionality of 1394 to be harnessed in the WDM driver. It has been and still is my goal to help those coming into the 1394 driver development world get past the hurdles that have plagued me in my journeys through this space. Hopefully, I have been able to clarify some aspects of the asynchronous communication picture a bit in these first two 1394 articles. In this article especially, I hope that the reader will understand what virtual 1394 devices are and what they are good for. In addition, I hope node-targeted asynchronous transfers now make sense and are approachable by those reading here. Many aspects of Windows 1394 device driver development are not as difficult as they are ill explained. If my articles have helped explain some of the lesser obvious aspects of this technology or if they have not, I would really like to hear about it. Send me some feedback. Thanks for reading!
Bill McKenzie
bill.mckenzie@compuware.com has been developing system level software for
over six years, including over four years experience developing device drivers
for Windows platforms. His primary background is in the development of software
products targeted for Windows device driver developers. He is currently working
on driver development tools and related software products for Compuware
Corporation.
1 Some changes to this article have been made from it's first introduction on July 15, 2003. In the original article I stated that the device ID Microsoft&1394_PC is the diagnostic ID used to enumerate all devices when compliant host controllers are put into diagnostic mode. This is incorrect. The diagnostic ID is 1394\Microsoft&1394_DIAGNOSTIC_DEVICE or 1394\031887&040892.