Choosing the Correct HAL Function for Device Access

                  Ó 1997 OSR Open Systems Resources, Inc.

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.



                                  [Image]



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);

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 the final figure 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);



Return to Previously Published Articles

Return to OSR's Home Page

  [Image]consulting& developing/ kits / seminars / NT insider/ resources /
                          client area / about OSR