Abstracting system hardware for maximum reuse - Embedded.com

Abstracting system hardware for maximum reuse


To read original PDF of the print article, click here.

Abstracting System Hardware for Maximum Reuse

Joseph Lemieux

Abstracting hardware is difficult at times, but necessary. If you do it right, the resulting software will be much easier to reuse.

There are many reasons to adopt hardware abstraction. The foremost is to enable code reuse. The term code reuse has been defined in many different ways throughout industry, from code that is borrowed and slightly modified for each project, to libraries of features that are released as object code and linked together during development. For the purpose of this article, code reuse will be defined as any code, either at a source level or in library form, that is reused completely, with only changes in the definition of constants in a header. Abstraction that requires functions to be written or macros to be defined that change executable code is considered 3borrowed2 and not 3reused.2

Another justification for hardware abstraction is maintainability of the application code. If a standard method of accessing hardware exists, all software engineers in a department can maintain the code. When hardware changes, the abstraction changes in one location, not in every source code module. For example, changing the port for a digital input from Port 1 to Port 2 would require a change in the constant fed to the I/O layer, and each module will then benefit from this change after linking.

Finally, quality of the end product increases. By reusing code and abstracting hardware, the number of new or modified lines of code is reduced for each project. Since the probability of creating a defect increases with the number of lines of code written, by reducing this number, the number of defects will also decrease.

Figure 1

Figure 1: System model

This article describes three methods of abstracting hardware from software applications. The first method, which I call the function name method, is a direct I/O access method using a different function for each input and output. This method is the fastest and easiest to use, but is also the hardest to change when the hardware changes. The second method uses an API for direct I/O. This method is more flexible and ports more easily to different hardware, but requires a slight addition in resources. Finally, the message method is an example of indirect I/O, where the I/O is obtained using a messaging system such as is found in OSEK/VDX. This message method works well in a complex environment where data is obtained both from hardware and other devices. The cost is additional resources.The system model that will be used is shown in Figure 1. The operating system, utilities, and physical layer chosen do not impact the I/O abstraction directly and are not discussed here. The I/O layer is the basis of this article, and the impact to the application tasks is discussed. In the case of the message method, the I/O layer will be separated from the application tasks by a messaging system.

Function name
The function name method takes its name from the fact that a specific function or set of functions is defined for each input or output. These functions provide the value of an input and set the value of an output. In addition, sampling routines need to be written for the inputs, and driver routines need to be written for the outputs. The steps required to use this method are:

  • Define all inputs and outputs in the application
  • Define an access function for each input and output. This may optionally be a macro
  • Create the input sampling routines required for all types of inputs. Each routine should ideally sample many inputs
  • Create the output driver routines required for all types of outputs. Again, each routine should ideally drive many outputs
  • Create support routines such as debounce routines, filtering, scaling, and so on. These may already exist in a library

This method will allow reuse of application source code. However, the I/O layers will use borrowed code instead of reused code, with the borrowed code being re-written for every application.

Define inputs and outputs
The first step in applying this method is to define all the inputs and outputs to the system. These may include digital inputs and outputs such as switches and lamps, analog inputs and outputs such as voltages and currents, time-based inputs and outputs such as frequencies and PWM signals, and so on. These should be listed in a table along with any requirements such as range, granularity, debouncing, filtering, default values, and so on.

Define an access function for each input and output
This function or macro will always take on the function prototype of: type inputfunction(void); or void outputfunction(type); , where type is the type that contains the complete range and granularity of the input. For digital inputs, this is usually BOOLEAN , which most applications equate to an unsigned character. It may be any type of integer or float available in C, and may also be a structure. The return value is always the value of the input or output. Outputs may have two functions if other application tasks need to read the current output value. Following are examples of access function names, with NAME replaced by a unique name for each input or output:

BOOLEAN CheckNAMEState(void);void SetNAMEState(BOOLEAN);U8 GetNAMEValue(void);void SetNAMEValue(U16);

After the function is defined, it needs to be created. In the case of a macro, the definition and creation are simultaneous. The listings presented here provide examples of routines for each of the previous examples. The macros are included. However, only one or the other will actually exist in the final product.

In Listing 1, the input is a discrete input that can have the value either ACTIVE or INACTIVE . Each input is packed into a byte that contains eight discrete inputs. The function must unpack the bit and return the proper value. Using a macro, the variable and mask must be defined globally so all routines can access them. The function provides some level of encapsulation of the variable.For a discrete output, Listing 2 presents an example where the output port is written directly. Again with this macro, the port must be defined globally.

For analog inputs, Listing 3 describes the access function. In this case, a structure is defined for each analog input as follows:

typedef struct AnalogInputTypetag{  U16 rawvalue;  U16 filteredvalue;  U16 defaultvalue;  BOOLEAN validvalue;} AnalogInputType;

If an error occurs, the default is used; otherwise the filtered value is used. It is up to the sampling routine to set the validvalue status.

Finally, for a timer-based output (PWM or variable frequency), Listing 4 shows how the value is buffered for output using an output driver. The value is first checked for validity and limited at the high end.

Create the input sampling routines
Next, create the input sampling routines and interrupt handling routines that are used to determine the value of the inputs. Each routine should sample as many inputs as possible in one function call. The following listings describe examples of some types of inputs and how they can be handled. Listing 5 shows an example where a single digital input port is sampled as a byte and debounced together. If the hardware is designed to optimize this algorithm, eight inputs are debounced at one time. The DebounceDigital routine will take the information from the sample port and the structure to determine the debounced value. This structure includes eight timers, one for each input port.

To sample analog inputs, Listing 6 shows a system that navigates a null terminated list of analog input definitions and filters the inputs. First, all analog channels are sampled. Then, for each entry in the input list, the raw value is passed to the filter function with the filtered value and filter constant, and a simple first-order filter is performed on the system and the filtered value is updated.

Create the output driver routines
The output driver routines are used to output data when direct writes to the output are not feasible, such as in a timer output that must interrupt on each edge, or when the outputs need to be periodically refreshed. Listing 7 shows an output driver for a frequency output in an Intel 87C196 processor. This interrupt routine calls a frequency handler that sets the time of the next interrupt. The structure member *output->time_register is a pointer to the actual compare register, and the member output->period is the time between toggles of the output. EPA0 has been setup to toggle the output whenever a compare occurs, which presumes that the output is a 50% duty cycle frequency output. Therefore, output->period is _ the period of the output signal. The value of FreqOutput.period has been previously set using SetFreqOutputValue() function.

Create support routines
Finally, create the support routines that will be used to debounce inputs, filter inputs, and scale inputs or outputs. These routines are general in nature and will not be covered here. The focus, however, should be on generic routines that do not use any global variables. All information that is needed is passed to the routine in the function call.

Function name advantages and disadvantages
The advantage of using the function name method is that access to data is fast. Reads are either directly to global memory, or through a quick single function call. Writes are either directly to a port, to a global memory location, or through a quick single function call. The read and write functions that are written are straightforward and simple.

The disadvantages include difficulty to set up, poor code reuse, error prone, and extensive use of global variables. Whenever the source of an input changes, multiple sections of the code may need to be re-written or new macros defined. Defining new macros also requires re-compiling. Debugging of macros is very difficult.

API calls
The API calls method requires development of a generic API that is called by the application program using structures that contain the I/O information. The amount of actual code written for each new application is minimal or non-existent. The steps to implement the API calls method can be split into two sections–one time code development and integration into the application.

One time development

  • Classify the types of I/O that your organization uses
  • Define type definitions for configurations of each type. Define a RAM type for data that changes and a ROM type for constant data
  • Define macros to create ROM and RAM variables for each I/O
  • Create routines to access, sample, and drive I/O

Integration into application

  • Create each instance of the input or output in a configuration file
  • Include the I/O files in your build scripts/makefiles
  • Include the io.h file in any application module that accesses I/O
  • Use the names defined in the configuration file to access the I/O in each application module
  • Schedule the sampling and driving tasks in your system. This is the only section where code may need to be written if the I/O functions can1t be called directly by the RTOS or to encapsulate in an ISR

Classify I/O types
In this step, each type of I/O that is used by the organization is defined and the parameters are classified. For example, discrete inputs may be defined as being from either a digital or an analog port, will be debounced, and have a default value upon power-up. Other types of I/O may include discrete outputs, analog inputs/outputs, PWM inputs/outputs, frequency inputs/outputs, multi-state inputs/outputs, and so on.

Define type definitions
For each type above, define a variable (RAM) and a constant (ROM) structure that will be used by each instance of that type of input. For the previous example of discrete inputs from a digital port, the structures shown in Listing 8 would be defined.

The definition IOHANDLE will be used for the API function calls. In the RAM structure, rawvalue is the last sampled value of the input; debouncedvalue is the debounced state of the input; and time is the time that each rawvalue has been constant in order to debounce. Each RAM structure can hold eight digital inputs. The ROM structure includes *digitalport , a pointer to the digital port; mask, the mask of the bit in the port corresponding to the input; debouncetime , the time to debounce the input in samples; ramlocation , the offset into the array of DigitalInputRam variables that holds the particular input; rammask , the mask of the RAM location to extract the digital input; and defaultstate, the default state of the input.Similarly, definitions of each of the input and output types defined previously would be created.

Define macros
Define macros that create the instances of each input or output. These macros encapsulate the definition of the structure, thereby allowing the structure to be modified to adapt to changes in the API. Examples of macros for our digital input example are in Listing 9. These macros will all appear in the configuration file.The first macro reserves RAM memory for the packed digital inputs in an array named digitalin . This array will be used later in the access and sample routines. Only one instance of this macro should exist in the configuration file.

The ROM macro defines the constant structure for each digital input. Each digital input will have one instance of this macro and it will be referenced by the unique identifier name. The member port is a global name that has been defined elsewhere and is specific to the microcontroller being used. The debouncetime argument is the number of samples to debounce the input. position and rammask is the offset position in the RAM array and the mask of the digital input in the packed RAM . The combination of position and rammask must exist only once in the configuration file. Finally, default is the default value that will be used in the RAM array for that input.

Create access, sample, and driver routines
The access functions should be generic and allow multiple inputs or outputs to be accessed using the same interface, with a different argument. For instance, GetDiscreteState , shown in Listing 10, is used to unpack all discrete inputs from a digital input. To expand to analog-based digital inputs, the routine would have to determine the type of argument that has been sent in the parameter list. This would require an additional parameter or structure that contains the type of the input. For simplicity, assume that GetDiscreteState will only return data on a digital input.

This function is called by passing the address of the ROM constant structure to the function. In this interface, the debounced value of the digital input is extracted from the RAM packed array. INACTIVE is returned if the bit is 0, otherwise ACTIVE is returned.

The sample routine is quite different from the one in the function name method, since it is more generic and needs to pack the inputs differently. The generic routine is shown in Listing 11.

In this function, the address of the ROM constant structure is passed and used to sample and extract the data from the digital port. The result is either 0 if low or non-zero if high. The routine, DebounceDiscrete , will use the raw data to debounce the input.

Create each instance in configuration file
A configuration file with a name such as iocfg.h needs to be created for the application. This file is included in the io.h file used by all application modules. However, the macro CREATE_MEMORY must only be defined in io.c, or an error will occur during link. The configuration file will contain all of the macros to define all of the inputs and outputs used in the application.

Use the names from the configuration file in the application moduleIn each application, use the names created in the iocfg.h file to access the inputs and outputs. An example of a task in an application is shown in Listing 12. This task is part of a round robin scheduler that checks state and drives an output.

Integrate into RTOS
The final step is to integrate the sample and driver routines into the RTOS. Each sample routine that periodically samples inputs needs to be scheduled. The advantage of these routines is that individual inputs can be sampled at different frequencies. The example in the function name method requires all inputs on a port to be sampled at the same frequency. This may require the writing of a shell routine that calls the function multiple times, once for each input. This could be made more efficient by using a null terminated list of inputs to be sampled and a small while() loop. ISRs that are specific to the application and call API routines must also be developed.

API calls: advantages and disadvantages
The advantage of this method is that little or no code is written when I/O is added, deleted, or modified. Only constants using the macros in the configuration file need to be changed. This method has high reusability and high portability since each function receives a void pointer, and changes in the structure to which the pointer is referenced do not affect the application code generated.

The disadvantages of this method are slight increases in ROM and CPU usage. However, since the difference has not been benchmarked, there may be no increase or a slight decrease in total ROM usage due to the efficiencies of integrating functions into the API instead of creating multiple functions.

The message method is almost identical to the API calls method, except that it uses the RTOS1s interprocess communication (IPC) system to transfer the status of the data. A major assumption is that the RTOS IPC provides messages that are not consumed when they are received. An example is the OSEK/VDX unqueued message in the COM specification. (See my previous OSEK/VDX articles in ESP: 3OSEK/VDX Standard: Operating System and Communication,2 March 2000, p. 90; and 3OSEK/VDX Network Manager and Implementation Language,2 April 2000, p. 96.) Unqueued messages retain the last value, and can be 3received2 multiple times without losing the value. The differences between message and API calls methods are:

  • Type definitions change slightly
  • ReceiveMessage instead of the API function Get
  • SendMessage instead of the API function Set
  • Definition of messages for all inputs required

Type definitions
From the example in API calls of the digital input, the RAM type variable does not change. However, the ROM type variable adds a reference to the message that contains the input as shown in Listing 13.

Since the ROM structure changed, a change needs to be made to the macro that creates the structure instances to include the name of the message, as shown in Listing 14.

The access routines have been replaced by the IPC routines, and the input sampling routine changes slightly as shown in Listing 15. The addition of the SendMessage function places the result in the system message for use by the algorithms. The DebounceDiscrete function returns the debounced state of the discrete.

An additional routine must be created to enable output. Since SendMessage only enters the information into the message RAM, a driver routine must be created to actually output the data to a port. This can be seen in Listing 16. With OSEK/VDX, this routine can be called whenever an output changes. This routine navigates a null terminated list of outputs that are passed in names. It receives the current message and sets the output at the port based on the state after inverting the message value for active low outputs.

Message advantages and disadvantages
The message method clearly abstracts the source of the data, whether it is a true hardware input or output, or data received over a network. Complex messages such as structures or strings can be received easily, whereas the API calls work best if all data is of a simple type (that is, U8, U16, float, and so on).

The disadvantage with this method is that more RAM, ROM, and throughput is utilized by adding another level of abstraction: the message.

Software reuse is a noble goal that has been chased by many firms for years. In reality, attaining a large level of true reuse is usually difficult and seldom achieved. Major impediments to reuse include different microcontrollers, multiple microcontrollers on a single project, hardware changes, and networks. Changing the source of data usually forces extensive changes in code. Following the tips just outlined will help increase your reuse opportunities.

Joe Lemieux is a senior applied specialist with EDS Embedded Solutions in Troy, MI. He holds an MSEE from the University of Michigan and has been writing software for embedded systems in the automotive industry for over 17 years. He can be reached by e-mail at .

2 thoughts on “Abstracting system hardware for maximum reuse

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.