Features


Portable I/O Drivers

Jan Kristoffersen

If you've ever tried to move a low-level I/O driver between chips, or even between compilers for the same chip, you'll really appreciate this latest addition to the revised C Standard. And you don't have to wait for the Standard to be approved before you can take advantage of it.


Introduction

Writing I/O drivers in C or C++ always requires a bit of "magic" to perform the low-level input/output operations. It is generally accepted among programmers of embedded systems that this magic code is heavily dependent on both the target microprocessor and the compiler used to generate the executable code. Huge investments in software development and maintenance can be saved if I/O drivers can be moved from one embedded processor platform to another, and compiled with C or C++ compilers from different vendors, without any modifications in the source code.

It sounds like magic of yet another kind, but this is exactly what the Stimuli-Gateway (SG) method enables you to do. All it requires is a little programming discipline and a drop of know-how. Like most ingenious ideas, the idea behind the SG method is very simple. The most important part is different way of thinking.

Let me illustrate it in this way: If you ask programmers of embedded microprocessors what they're doing at the moment, you'll typically get an answer something like, "Well, right now I'm writing a driver program for processor XXX using a compiler from vendor YYY."

A programmer using the SG method would instead more likely say, "I'm writing a driver program for peripheral chip ZZZ." You would get this answer simply because by using the SG method it does not matter which C/C++ compiler or CPU family are used. These parts can even be selected or changed at a later stage. The source code will look the same.

With the SG method, the I/O registers are all that matters. If an I/O hardware chip (or I/O cell) can be used with more than one CPU family, then the driver modules for that chip can be reused as well. The source code can be recompiled without modifications by C compilers for the different CPU families.

All the programmer needs to do in order to assure portability is to use a few well defined and "standardized" low-level I/O functions for basic I/O operations on hardware — the Stimuli-Gateway functions. As the name implies, the SG functions create a standardized virtual gateway for exchange of stimuli between an I/O register in the hardware and a line of source code. And best of all, this standardization can be achieved today, without any overhead in machine code.

This article presents the Stimuli-Gateway design method, which provides a practical solution to I/O driver portability problems. It is an efficient, easy-to-use, and backward-compatible method for making I/O operations portable in embedded C/C++ source code. As proof of its effectiveness, C source code that performs direct hardware operations has been compiled with more than 20 different C compilers for more than 15 different embedded processors, without any modification in the C source code and without any overhead of machine code.

Background

With the standardization of programming language C, the world for the first time got a single programming language that can be used with most of the computers, microprocessors, and microcontrollers used today. The explanations for why C, and later C++, became so widely used are legion. My favorite explanation is that C was the first standardized programming language that allowed the programmer to operate on a relatively high abstraction level but at the same time did not prevent the programmer from doing things efficiently when working close to the physical level.

But no rose without a thorn. The C/C++ standards of today do not standardize the syntax for basic low-level I/O operations on hardware. Because of this, and because handling of hardware is a must in embedded programs, the compiler vendors have had to invent their own syntax for performing such low-level I/O operations. The consequence of this lack of standardization is non-portability of driver functions between compilers. If you take ten compilers from ten different vendors you will probably find ten different ways of handling basic I/O operations at the source-code level.

The consequences of this missing I/O standardization, considered from a company level or a market level, can be summarized as:

To achieve the full economical potential that already exists with C/C++ today, the syntax for making basic low-level I/O operations on hardware within these languages must thus be standardized as well.

Official standardization is carried out by the existing ISO/ANSI standardization committees. The C Committees WG14 (ISO) and J11 (ANSI) are in fact working on implementing a standardized syntax for basic I/O operations right now. For all practical purposes, the syntax being standardized is identical to the Stimuli-Gateway method described in this article.

It takes many years before an ISO or ANSI standard becomes official. The next revision of the C Standard is expected to be finalized in 1999. Even then, it may take more time before your compiler vendor has implemented the new requirements of Standard C. The best way to achieve full I/O driver portability now is to use the Stimuli-Gateway method as outlined in this article. Fortunately the SG method is backward compatible. The standardized I/O function syntax can, with a few limitations, be implemented with existing C/C++ compilers on the market.

Important Objectives

It is important to keep in mind that standardized I/O does not means standardized hardware. The goal is to standardize the syntax for I/O operations, not the platform functionality. As a fact of life we must accept that the processor architectures are different. Different platforms use different bus architectures. Some processors have multiple addressing ranges (memory mapped, I/O mapped, etc.). I/O access may require special instructions on some microprocessors (as with the 80x86 CPU family). I/O access may be restricted and limited to a few operation types (e.g. read and write).

Most important is the fact that I/O registers do not normally behave like memory cells. I/O registers have special individual characteristics. They can be:

Note that individual bits in an I/O register may have individual characteristics. Only true read-modify-write registers behave like conventional memory.

Normally, arithmetic operations on I/O registers cannot be performed, or they have have no logical meaning. Often read-modify-write operations on I/O registers are prohibited by the actual hardware. Operators that modify stored values, such as:

+= -= *= /= >>= <<= ++ --

are only meaningful where the I/O register and the bus architecture both allow read-modify-write operations.

Experiments with different processor architectures and compilers have shown that the best way to encapsulate differences in allowed I/O access methods, and at the same time to create a uniform C/C++ syntax for I/O access, is by use of a few standardized I/O functions. Processor architectures and hardware platforms are different, so standardization must also provide a method to separate the description of the hardware differences from the source code. The standardization method should encapsulate descriptions of hardware differences in a separate header file.

The Standardization Method

The idea behind the Stimuli-Gateway standardization method is to define a few basic C functions which must be used with all direct hardware operations. It involves three components:

1. a few basic hardware I/O functions for the most common I/O register sizes (1, 8, 16, and 32 bits)
2. a new conceptual type, access_type
3. a standardized header file, <iohw.h>

For instance, functions for operations on byte (eight-bit) registers have the following syntax:

// Read
unsigned char iord8( access_type );
// Write
void iowr8( access_type, unsigned char );
// Bit clear
void ioand8( access_type, unsigned char );
// Bit set
void ioor8( access_type, unsigned char );

The main idea behind this standardization method is to let a single parameter, access_type, represent or reference a complete description of how a given I/O register is connected in a given hardware platform with a given processor. It encapsulates how the I/O register should be addressed. It is the access_type parameter that enables the C/C++ syntax for hardware I/O functions to be fully standardized.

The SG standard defines a number of I/O functions for the most common I/O register sizes. See the complete list of I/O function in Listing 1.

The ioxxxbuf functions in Listing 2 are intended for use where an I/O interface contains a linear hardware data buffer, i.e. a one-dimensional array of 8-, 16-, or 32-bit registers. The functions operate on the index element in the register array, where the access_type parameter specifies the physical location of element zero.

The header file iohw.h contains the I/O function prototypes and the access_type specifications. It must be included by all C/C++ modules that perform I/O operations on hardware. Typically, the header file iohw.h further includes a separate header file iohw_ta.h, which holds the access_type parameter specifications for all I/O registers in the given processor platform. By concentrating access_type specifications in a separate header file, we achieve the benefit that iohw.h has to be implemented only once for each compiler and iohw_ta.h only once for each hardware platform.

Semantics for access_type

The definition of the access_type parameter is implementation specific. It is peculiar to a given compiler, processor, and platform. An I/O register definition should contain the following elements:

1.a symbolic name for the I/O register, which represents a complete specification of the access method for the I/O register in the given hardware platform
2. the I/O register data width: (such as 1, 8, 16, or 32 bits)
3. the address space used (if the processor supports more than one address space or address bus)
4. the physical address, which may also include a bit position (if the register is a bit in a larger register)

A typical use of the Stimuli-Gateway I/O functions is shown below. This driver function module is completely compiler independent and portable. The source code depends only on the I/O hardware register architecture on which it operates. iohw.h includes definitions of the SG functions and, via iohw_ta.h, definitions for the two symbolic access_type parameter names PORT1 and PORT2.

#include <iohw.h>
void my_portable_device
  (unsigned char data)
   {
   if (iord8(PORT1) & 0x4)
       ioand8(PORT2, data);
   else
       ioor8(PORT2, data);
   iowr8(PORT1, 0xf);
   }

If this driver function is ported to another platform using another compiler or processor, then the iohw.h file will be replaced with an iohw.h file specific for the new compiler or processor. The driver source code remains the same.

Constraints

The use and implementation of the standardized I/O functions are limited by some natural constraints which the programmer should be aware of. As a general rule the standardized I/O functions will not try to handle limitations which originate from limitations in the physical I/O hardware. The task of the SG functions is to hide the processor architecture differences, not the characteristics of the I/O hardware. For example, AND and OR operations are not allowed (are not possible) on register types that are write only, read only, or read write. The handling of limitations that are intrinsic to the I/O hardware must be done in the device driver source code. They will not be enforced by the standardized I/O functions.

The correct SG function to use with a given I/O register is defined by the capabilities of the I/O register (Not by the addressing capabilities of the processor). For example:

When writing portable code, you should also be aware of constraints originating from differences in processor architectures. Here are a few such constraints.

Atomic Operations

The standardized I/O functions do not guarantee read-modify-write operations to be atomic. For example, the operation:

sgandby( PORT3, 0xf3);

may for some processor types be implemented as separate machine operations:

in temp,PORT3;
and temp,0xf3;
out PORT3,temp;

The programmer must be aware of this when writing portable device drivers. An I/O AND operation may on some platforms allow interrupts between the buried IN and OUT operations. If this has any importance for the functionality of a given device driver implementation, then interrupt disable/enable operations should be used in that device driver source code.

Compiler Optimization

The standardized I/O functions do not guarantee that two I/O operations in a statement will always be separate operations. For instance, an optimizing compiler might choose to combine two operations to form a single read-modify-write operation, as in:

iowr8(PORT1, iord8(PORT1) + 0x10);

which can become the single operation:

ADD   PORT1,0x10

MSB-LSB Order

As a general rule, I/O registers should be addressed with an I/O instruction of the same size as the register. However, some I/O devices (chips) may contain registers whose logical size in bits is bigger than the physical databus of the chip. An I/O chip may, for instance, contain a logical 32-bit register, but because of the databus width of the I/O chip it is physically addressed as two 16-bit registers.

For portability reasons such a register should at all times be addressed with two separate 16-bit I/O operations instead of a single 32-bit I/O operation. This is because the order of addressing the component 16-bit words may differ among processors and compilers. The handling of such MSB-LSB addressing-order problems should be captured in the device description and handled uniformly by the device driver code.

Here is an example of a fully portable solution:

#include <iohw.h>
unsigned long int get_count (void)
   {
   unsigned int val_l, val_h;
   val_l = iord16(COUNTL); /* LSB counter value */
   val_h = iord16(COUNTH); /* MSB counter value */
   return ((unsigned long) val_h) * 0x10000L +
          ((unsigned long) val_l));
   }

A much faster MSB-LSB handling solution uses a union for type conversion. However, with unions the programmer should be aware of the compiler data representation and how the compiler handles data alignment. This solution is therefore faster but less portable:

/* Compiler specific word-to-long
   conversion union (in iohw.h) */
typedef union
   {
   unsigned long ul;  /* 32-bit */
   long l;
   struct {           /* 16-bit */
      unsigned int w0;   /* LSB */
      unsigned int w1;   /* MSB */
      } w;
   } sg_union32;
/* Portable I/O driver module */
#include <iohw.h>
/* Faster I/O driver function */
unsigned long int get_count (void)
   {
   sg_union32 val;
   val.w.w0 = iord16(COUNTL); /* LSB counter value */
   val.w.w1 = iord16(COUNTH); /* MSB counter value */
   return (val.ul);
   }

Variable access_type

Typically, driver code uses access_type parameters as constant values. This gives usually the fastest and most compact code. For example:

/* I/O function using constant
   access_type parameters */
void nibble_wr1(char value)
   {
   /* Low nibble */
   iowr8( PORT1, value & 0xf );          
   /* High nibble */
   iowr8( PORT1,(value >> 4) & 0xf);
   }

But if the same driver function is used with multiple hardware chips of the same type in the same platform, then it can be useful to have access_type as a variable. For instance:

/* I/O function using variable
   access_type parameters*/
void nibble_wr2
  ( access_type adr, char value)
   {
   /* Low nibble */
   iowr8( adr, value & 0xf );          
   /* High nibble */
   iowr8( adr, (value >> 4) & 0xf );
   }

Ideally, it should be possible to use the access_type parameter both as a constant and as a variable. However, programmers should avoid the use of access_type as a variable in portable code whenever possible for two reasons:

For these reasons, the standardized SG functions are not able to support variable access_type parameters for all platforms today.

Implementation

An implementation of the I/O functions will always be processor and compiler specific. Furthermore, it cannot be expected that an implementation will implement I/O functions for register sizes which are not supported by the processor architecture itself. For instance, if a given processor architecture does not support 1-, and 32-bit operations, then SG I/O functions for those register sizes are not required to be implemented. This consideration reflects the simple fact that, if an I/O chip cannot be connected to a given processor architecture, then there is no point in making driver source code for that chip portable to the processor architecture.

With most C/C++ compilers used today for embedded applications, the SG functions can be implemented as preprocessor macros. The functions are thus effectively inline functions, so calling them does not generate any machine-code overhead.

When implemented as preprocessor functions, SG converts a statement from standardized syntax to the non-standardized I/O syntax of the target compiler. The machine code generated will therefore be exactly as efficient as the code generated when using the compiler's native syntax. Once again, standardizing the I/O syntax with compilers of today generates no machine-code overhead.

For processor architectures which have only one address space, the implementation of the SG I/O functions and the definition of access_type parameters becomes very simple and straightforward. Listing 3 shows how I/O functions and access_type parameters can be implemented and used in driver functions.

For processor architectures which have multiple address spaces, some way must be provided to specify which address space the I/O register inhabits. Listings 4 and 5 shows how the SG standardization method handles multiple address spaces with different compiler implementations. In Listing 4 the compilers are using a special pointer, where the most significant byte specifies the address space. In Listing 5 the compiler uses a mixture of pointer operations and special functions to differentiate between address spaces.

In all cases the SG I/O function implementation will generate fully optimized machine code. The I/O driver source code for a peripheral I/O chip will remain the same, independent of which processor architecture, address space, or compiler is used.

Interrupt Functions

Interrupt functions will always depend on the processor architecture, essentially because of the interrupt vector number. However, the syntax for interrupt function headers can be standardized. The SG method specifies the following standard function header for interrupt functions:

SGISR( func_name, vector_number, options )

This SGISR header enables interrupt function modules to be compiled with different compiler implementation for the same processor platform.

Two general macros are defined for enabling and disabling interrupts:

SGENABLE;
SGDISABLE;

With most platforms, these macros operate on the global interrupt flag. Some processor architectures change among several levels of interrupt priority instead of manipulating an interrupt flag. With such platforms these macros will change the interrupt priority between two fixed levels as defined by the implementation.

Conclusion

The SG method is battle proven in the embedded market place. It has been in use since 1991 and is adopted by several companies. SG header files have been implemented for the most popular 8-, 16-, and 32-bit CPU families on the market, using C and C++ compilers from different vendors. I/O chip drivers have been compiled and tested with more than 20 different compilers for more than 15 different processors, without any changes in the source code.

Developers have benefited financially through reuse of I/O device-driver code, which often represent considerable knowhow, and through increased software portability in general. Existing software applications using the SG method have been ported to new (and more powerful) CPU families with very little effort. The standardized syntax for I/O operations enable peripheral I/O chip vendors to reach a much larger market with their support software and examples of software drivers written in C.

For more information, visit our Web site at http:\\www.ramtex.dk.

My company RAMTEX can deliver a setup tool which helps the user generate Stimuli-Gateway files for a number of embedded compilers. You just have to select the processor family, define the I/O registers, and select the compiler. The headers iohw.h and iohw_ta.h will then be generated automatically with the right syntax for the selected compiler.

A final note. There are a few differences between the standardized I/O functions currently used by the Stimuli-Gateway method and the syntax for basic I/O operation which the C standardization committees is refining right now. The differences can be summarized as follows:

You should keep these differences in mind to give your code maximum portability in the future. o

Jan Kristoffersen has more than 19 years of experience in the embedded processor design and programming field. He is now the president of RAMTEX, an independent research and design company. He is the inventor of the Target Controller tools, a totally new type of development tools for embedded program applications, and he holds several patents. Jan Kristoffersen is a member of the Danish delegation to the ISO C committee WG14.