The NT Insider

In My Space - Choosing the Correct HAL Function for Device Access
(By: The NT Insider, Vol 4, Issue 2, Mar-Apr 1997 | Published: 15-Apr-97| Modified: 22-Aug-02)

One of the most frequently misunderstood topics about writing NT device drivers seems to be how driver writers choose between using the HAL?s port access functions (such as READ_PORT_UCHAR) and the HAL?s register access functions (such as READ_REGISTER_UCHAR). Judging from the number of times we get this question in our kernel device driver seminars, and the number of times we see this question show up in news groups, there are plenty of people who are confused. Like so many things in NT, this isn?t a difficult or complex topic, it?s just that how to make the decision isn?t well documented. Let?s see if we can help.

When a controller or adapter card is designed, the hardware designer implements a set of registers that will be used to control and access the device. These registers will typically contain things like the device?s interrupt enable and status bit and (depending on the device type) even the registers to or from which data is moved. The address corresponding to each of these registers may reside in either memory space or port I/O space. When designing the board, the hardware designer will decide the address space in which each one of these registers resides.

The way in which this decision is implemented is straight forward. On the PCI bus, for example, if a particular register is to be located in port I/O space, the command type encoded on the PCI bus (on the C/BE#[3::0] signal lines) when accessing the register?s address will indicate that a port I/O space address is being accessed. If a register is to be located in memory space, the address decoding for that register will require the command type indicate a memory space access. The way port addresses are differentiated on the ISA, EISA, and MCA busses is similar.

As you can probably guess, Intel x86 architecture systems issue PCI bus port access commands when performing one of the special port I/O space access instructions (such as IN and OUT). But even on CPUs that do not support a separate port I/O space (such as the Alpha) a device on the PCI bus still require port access commands to access port space addresses. Without special I/O space instructions available on these systems, how do port access commands ever get built? The typical approach is that hardware system designer chooses some set of physical memory addresses that will be used on the system to correspond to port I/O addresses. For example, the hardware designer might choose to map port addresses to the physical address range 0xFFFF0000-0xFFFFFFFF. Thus, whenever the address 0xFFFF0180 is referenced on the system the support hardware on the system translates this reference to address 0x180 on the PCI bus, with a command indicating a port access. Of course, the HAL cooperates in this game. Neat, eh?

So the hardware designer of a particular controller or adapter board decides in which address space the board?s registers reside. Once this decision has been made, so has the driver writer?s decision about which of the HAL?s functions the driver writer will use to reference those registers. The driver writer will always use the HAL function that corresponds to the space in which the register(s) he wants to access resides.

If a device driver writer wants to access a register that a hardware designer has located in Port I/O space, the driver writer will code all accesses to this register from his driver using READ_PORT_xxx and WRITE_PORT_xxx functions. To access registers that the hardware designer has located in memory space, the driver writer will use the HAL?s READ_REGISTER_xxx and WRITE_REGISTER_xxx functions. Note that this is true whether or not the platform on which the driver is running has native support for a separate port I/O space.

That things work this way is the very point of NT having a HAL. The driver writer develops platform independent code, targeted to the HAL?s processor abstraction. The HAL "does the right thing" so that when a READ_PORT_xxx call is coded on an Intel x86 architecture system, the HAL issues an x86 IN instruction. Likewise, when that same call is coded on an Alpha architecture system the HAL issues a move (memory) instruction. In this way, platform independence is preserved ? Driver writers need neither know nor care if the particular CPU on which their code is running has support for port I/O space. The HAL takes care of things for us.

If the rules are so clear then why is there such confusion, even among otherwise proficient driver writers? The problem stems from the HAL?s abstraction of a processor that supports multiple independent buses. An example of the abstraction supported by the HAL appears in Figure 1.

 

Figure 1

Notice in Figure 1 that the system supports multiple data buses, even multiple disconnected buses of the same type. Let?s say that Device A, located on EISA bus 1, has a control and status register located at port I/O space address 0x180. In the HAL?s architecture model the port I/O space address 0x180alone is not sufficient to uniquely identify the device?s register address on the system, because all the buses on the system could have a port I/O address 0x180. As a result, before any device address can be used, it must be translated by the HAL to a logical address that unambiguously identifies one location in the system?s memory scheme.

The function that performs this translation is HalTranslateBusAddress(?) the prototype for which is shown in Figure 2. This function is passed the bus type, bus number, address, and address space of a particular address (which in the case of our example, would be EISA bus 1, port I/O space 0x180), and returns an unambiguous logical address that can be used thereafter to refer to that location.

  BOOLEAN HalTranslateBusAddress(
        IN INTERFACE_TYPE InterfaceType,
        IN ULONG BusNumber,
        IN PHYSICAL_ADDRESS
BusAddress,
       
IN
OUT PULONG AddressSpace,
        OUT PPHYSICAL_ADDRESS TranslatedAddress);

Figure 2

HalTranslateBusAddress(?) returns a physical address, along with an indication of the address space in which the physical address resides on the current system. The reason that AddressSpace is returned is that it is needed to determine if the returned physical address can be used directly. If the physical address returned by the HAL is in port space, then the address can be used directly. However, if AddressSpace indicates that the address is memory space, a driver will need to map the returned physical address into kernel virtual address space using the MmMapIoSpace(?) function. The kernel virtual address returned from MmMapIoSpace(..) can then be used by the driver to access the register.

The confusion arises as a result of the returned AddressSpace value. A number of driver writers I?ve spoken to have guessed that they determine whether to call READ_PORT_xxx or READ_REGISTER_xxx based on the value returned in AddressSpace. Although this will work on all current NT platforms, this is not the way the HAL was designed.

Let?s take a look at one final example, again based on Figure 1. If Device A on EISA bus 1 has a register located at 0x180 on that bus, and the hardware designer of that device has wired that register as a port I/O space address, the driver writer will always use READ_PORT_xxx and WRITE_PORT_xxx to reference that register. This decision is based solely on the space to which the device?s address is wired. Before the address can be used in a READ_PORT_xxx or WRITE_PORT_xxx function call, however, it will have to be translated using the HalTranslateBusAddress(?) function. After this translation, the value that the HAL returns for AddressSpace will indicate whether the translated address is in port I/O space or memory space on the current system. If the address is in memory space, the driver will need to map it into kernel virtual memory. Irrespective of what the HAL returns as its value for AddressSpace, the ultimate device address will be accessed using READ_PORT_xxx or WRITE_PORT_xxx because the registers on the device itself are in port space, and the HAL performs the appropriate adaptation automatically to reference the correct space on this machine.

The code in Figure 3 demonstrates this last example.

So, is deciding which function to use complicated? Nope. Is it difficult? As you can see, not at all. You just need to know the rules of the game.

ULONG AddressSpace;
PHYSICAL_ADDRESS PhysicalAddress;
VOID * AddressToUse;

ULONG ValueRead;

//
// Tell the HAL the address on the device is in port I/O space
//

AddressSpace = 0x01;

if ( HalTranslateBusAddress(Eisa, 0, 0x180, &AddressSpace, &PhysicalAddress) ) {

//
// Check the value the HAL returns for AddressSpace. This indicates
// in which space this address resides on the current system
//

if(AddressSpace == 0x00) {

// The address is in MEMORY space? thus, we need to
// map the physical address returned to us into Kernel Virtual space.
// It?s 4 bytes long, and not cachable

AddressToUse = MmMapIoSpace(PhysicalAddress, 4, FALSE);

} else {

//
// The address is in PORT space?
//

AddressToUse = PhysicalAddress.LowPart;

}

}

ValueRead = READ_PORT_UCHAR(AddressToUse);

Figure 3

This article was printed from OSR Online http://www.osronline.com

Copyright 2017 OSR Open Systems Resources, Inc.