[Previous] [Next]

Ports and Registers

Windows 2000 models driver access to many devices, as depicted in Figure 7-4. Generally, CPUs can have separate memory and I/O address spaces. To access a memory-mapped device, the CPU employs a memory-type reference such as a load or a store directed to a virtual address. The CPU translates the virtual address to a physical address by using a set of page tables. To access an I/O-mapped device, on the other hand, the CPU invokes some special mechanism such as the x86 IN and OUT instructions.

Click to view at full size.

Figure 7-4. Accessing ports and registers.

Devices have bus-specific ways of decoding memory and I/O addresses. In the case of the PCI bus, a host bridge maps CPU physical memory addresses and I/O addresses to a bus address space that's directly accessible to devices. Flag bits in the device's configuration space determine whether the bridge maps the device's registers to a memory or an I/O address on CPUs that have both address spaces.

As I've said, some CPUs have separate memory and I/O address spaces. Intel architecture CPUs have both, for example. Other CPUs, such as the Alpha, have just a memory address space. If your device is I/O-mapped, the PnP Manager will give you port resources. If your device is memory-mapped, it will give you memory resources instead.

Rather than have you place reams of conditionally compiled code into your driver for all possible platforms, the Windows NT designers invented the hardware abstraction layer (HAL) to which I've alluded a few times in this book. The HAL provides functions that you use to access port and memory resources. See Table 7-3. As the table indicates, you can READ/WRITE either a single UCHAR/USHORT/ULONG or an array of them from or to a PORT/REGISTER. That makes 24 HAL functions in all that are used for device access. Since a WDM driver doesn't directly rely on the HAL for anything else, you might as well think of these 24 functions as being the entire public interface to the HAL.

Table 7-3. HAL functions for accessing ports and memory registers.
Access WidthFunctions for Port AccessFunctions for Memory Access
8 bitsREAD_PORT_UCHAR
WRITE_PORT_UCHAR
READ_REGISTER_UCHAR
WRITE_REGISTER_UCHAR
16 bitsREAD_PORT_USHORT
WRITE_PORT_USHORT
READ_REGISTER_USHORT
WRITE_REGISTER_USHORT
32 bitsREAD_PORT_ULONG
WRITE_PORT_ULONG
READ_REGISTER_ULONG
WRITE_REGISTER_ULONG
string of 8-bit bytesREAD_PORT_BUFFER_UCHAR
WRITE_PORT_BUFFER_UCHAR
READ_REGISTER_BUFFER_UCHAR
WRITE_REGISTER_BUFFER_UCHAR
string of 16-bit wordsREAD_PORT_BUFFER_USHORT
WRITE_PORT_BUFFER_USHORT
READ_REGISTER_BUFFER_USHORT
WRITE_REGISTER_BUFFER_USHORT
string of 32-bit double wordsREAD_PORT_BUFFER_ULONG
WRITE_PORT_BUFFER_ULONG
READ_REGISTER_BUFFER_ULONG
WRITE_REGISTER_BUFFER_ULONG

What goes on inside these access functions is (obviously!) highly dependent on the platform. The Intel x86 version of READ_PORT_CHAR, for example, performs an IN instruction to read one byte from the designated I/O port. The Microsoft Windows 98 implementation goes so far as to overstore the driver's call instruction with an actual IN instruction in some situations. The Alpha version of this routine performs a memory fetch. The Intel x86 version of READ_REGISTER_UCHAR performs a memory fetch also; this function is macro'ed as a direct memory reference on the Alpha. The buffered version of this function (READ_REGISTER_BUFFER_UCHAR), on the other hand, does some extra work in the Intel x86 environment to be sure that all CPU caches get properly flushed when the operation finishes.

The whole point of having the HAL in the first place is so that you don't have to worry about platform differences or about the sometimes arcane requirements for accessing devices in the multitasking, multiprocessor environment of Windows 2000. Your job is quite simple: use a PORT call to access what you think is a port resource, and use a REGISTER call to access what you think is a memory resource.

Port Resources

I/O-mapped devices expose hardware registers that, on some CPU architectures (including Intel x86), are addressed by software using a special I/O address space. On other CPU architectures, no separate I/O address space exists and these registers are addressed using regular memory references. Luckily, you don't need to understand these addressing complexities. If your device requests a port resource, one iteration of your loop over the translated resource descriptors will find a CmResourceTypePort descriptor and you'll save three pieces of information.















1 

2 






3 




4 
typedef struct _DEVICE_EXTENSION {
  ...
  PUCHAR portbase;
  ULONG nports;
  BOOLEAN mappedport;
  ...} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

PHYSICAL_ADDRESS portbase;    // base address of range
...
for (ULONG i = 0; i < nres; ++i, ++resource)
  {
  switch (resource->Type)
    {
  case CmResourceTypePort:
    portbase = resource->u.Port.Start;
    pdx->nports = resource->u.Port.Length;
    pdx->mappedport = (resource->Flags & CM_RESOURCE_PORT_IO) == 0;
    break;
  ...
    }
...
if (mappedport)
  {
  pdx->portbase = (PUCHAR) MmMapIoSpace(portbase, nports, MmNonCached);
  if (!pdx->portbase)
    return STATUS_NO_MEMORY;
  }
else
  pdx->portbase = (PUCHAR) portbase.QuadPart;

  1. The resource descriptor contains a union named u that has substructures for each of the standard resource types. u.Port has information about a port resource. u.Port.Start is the beginning address of a contiguous range of I/O ports, and u.Port.Length is the number of ports in the range. The start address is a 64-bit PHYSICAL_ADDRESS value.
  2. The Flags member of the resource descriptor for a port resource has the CM_RESOURCE_PORT_IO flag set if the CPU architecture has a separate I/O address space to which the given port address belongs.
  3. If the CM_RESOURCE_PORT_IO flag was clear, as it will be on an Alpha and perhaps other RISC platforms, you must call MmMapIoSpace to obtain a kernel-mode virtual address by which the port can be accessed. The access will really employ a memory reference, but you'll still call the PORT flavor of HAL routines (READ_PORT_UCHAR and so on) from your driver.
  4. If the CM_RESOURCE_PORT_IO flag was set, as it will be on an x86 platform, you do not need to map the port address. You'll call the PORT flavor of HAL routines from your driver when you want to access one of your ports. The HAL routines demand a PUCHAR port address argument, which is why we cast the base address to that type. The QuadPart reference, by the way, results in your getting a 32-bit or 64-bit pointer, as appropriate to the platform for which you're compiling.

Whether or not the port address needs to be mapped via MmMapIoSpace, you'll always call the HAL routines that deal with I/O port resources: READ_PORT_UCHAR, WRITE_PORT_UCHAR, and so on. On a CPU that requires you to map a port address, the HAL will be making memory references. On a CPU that doesn't require the mapping, the HAL will be making I/O references; on an x86, this means using one of the IN and OUT instruction family.

Your StopDevice helper routine has a small cleanup task to perform if you happen to have mapped your port resource:

VOID StopDevice(...)
  {
  ...
  if (pdx->portbase && pdx->mappedport)
    MmUnmapIoSpace(pdx->portbase, pdx->nports);
  pdx->portbase = NULL;
  ...
  }

Memory Resources

Memory-mapped devices expose registers that software accesses using load and store instructions. The translated resource value you get from the PnP Manager is a physical address, and you need to reserve virtual addresses to cover the physical memory. Later on, you'll be calling HAL routines that deal with memory registers, such as READ_REGISTER_UCHAR, WRITE_REGISTER_UCHAR, and so on. Your extraction and configuration code would look like this fragment:














1 





2 
typedef struct _DEVICE_EXTENSION {
  ...
  PUCHAR membase;
  ULONG nbytes;
  ...} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

PHYSICAL_ADDRESS membase;     // base address of range
...
for (ULONG i = 0; i < nres; ++i, ++resource)
  {
  switch (resource->Type)
    {
  case CmResourceTypeMemory:
  membase = resource->u.Memory.Start;
    pdx->nbytes = resource->u.Memory.Length;
    break;
  ...
    }
...
pdx->membase = (PUCHAR) MmMapIoSpace(membase, pdx->nbytes,
  MmNonCached);
if (!pdx->membase)
  return STATUS_NO_MEMORY;

  1. Within the resource descriptor, u.Memory has information about a memory resource. u.Memory.Start is the beginning address of a contiguous range of memory locations, and u.Memory.Length is the number of bytes in the range. The start address is a 64-bit PHYSICAL_ADDRESS value. It's not an accident that the u.Port and u.Memory substructures are identical—it's on purpose, and you can rely on it being true if you want to.
  2. You must call MmMapIoSpace to obtain a kernel-mode virtual address by which the memory range can be accessed.

Your StopDevice function unconditionally unmaps your memory resources:

VOID StopDevice(...)
  {
  ...
  if (pdx->membase)
    MmUnmapIoSpace(pdx->membase, pdx->nbytes);
  pdx->membase = NULL;
  ...
  }