Ports and Registers
Windows XP uses the abstract computer model depicted in Figure 7-4 to provide a unified driver interface in all CPU architectures. In this mode, a CPU 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 a special mechanism such as the x86 IN and OUT instructions.
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 in 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.
Access Width | Functions for Port Access | Functions for Memory Access |
8 bits | READ_PORT_UCHAR WRITE_PORT_UCHAR | READ_REGISTER_UCHAR WRITE_REGISTER_UCHAR |
16 bits | READ_PORT_USHORT WRITE_PORT_USHORT | READ_REGISTER_USHORT WRITE_REGISTER_USHORT |
32 bits | READ_PORT_ULONG WRITE_PORT_ULONG | READ_REGISTER_ULONG WRITE_REGISTER_ULONG |
String of 8-bit bytes | READ_PORT_BUFFER_UCHAR WRITE_PORT_BUFFER_UCHAR | READ_REGISTER_BUFFER_UCHAR WRITE_REGISTER_BUFFER_UCHAR |
String of 16-bit words | READ_PORT_BUFFER_USHORT WRITE_PORT_BUFFER_USHORT | READ_REGISTER_BUFFER_USHORT WRITE_REGISTER_BUFFER_USHORT |
String of 32-bit doublewords | READ_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 1 byte from the designated I/O port. The Microsoft Windows 98/Me 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 ensure that all CPU caches are 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 XP. 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.
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 (pdx->mappedport) { pdx->portbase = (PUCHAR) MmMapIoSpace(portbase, pdx->nports, MmNonCached); if (!pdx->portbase) return STATUS_NO_MEMORY; } else pdx->portbase = (PUCHAR) portbase.QuadPart;
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.
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.
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.
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 will look like the fragment below.
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;
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.
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; }