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.
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 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 double words | 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 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.
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; |
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-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; |
Your StopDevice function unconditionally unmaps your memory resources:
VOID StopDevice(...) { ... if (pdx->membase) MmUnmapIoSpace(pdx->membase, pdx->nbytes); pdx->membase = NULL; ... } |