Originally found on: http://www.embedded.com/
If your embedded system must communicate with past, present, and future technology, abstraction can take you there. It’s always a good way to create portable software.
Stealing silently through darkened labs, lying in ambush behind stacks of design documents, the specter of multiplatform development is ever eager to snare another unwary development team. Its jaws are multifarious code changes and its claws eternal technical support. Its greatest weapon is the lack of development strategies that address multiplatform software development.
In the past, developers were not afflicted with the burdens of writing code that executed on a wide variety of platforms. A company that markets I/O cards, for example, might supply example programs or device drivers for a select group of real-time operating systems. But usually these were distinct software components that were maintained as separate products. Until relatively recently, entire embedded applications were not expected to execute on different hardware platforms, under different operating systems.
At a growing number of companies, developers are being asked to design embedded systems that can run on more than one hardware platform, and sometimes under more than one operating system. The driving force behind this trend is the decreasing time between consecutive generations of a product. Not only has the duration of development cycles decreased, the time between development cycles has decreased as well!
A highly competitive marketplace demands that the next generation of a product be designed even as the previous one heads out the door. Developers find they must leverage the software from the previous generation to accelerate the next generation’s time to market. But advances in technology frustrate this goal. System designers select different devices, subsystems, and CPUs to improve the next generation product. As a result, software developers often face a porting issue that they never anticipated.
The economic downturn has slowed the pace of development, but it has also brought into play other forces that promote multiplatform efforts. Companies now also strive to cut costs by applying existing designs to a wider range of applications. A board that was the heart of a media gateway may now also become part of a wireless base-station.
When software is not designed to anticipate these possibilities, the result can be more costly than launching a new design. Major overhauls to the code, constant support calls to the original software developers, duplication of testing effort, and major redesigns all combine to confound the original goals of cost saving and accelerated time to market.
Figure 1: Components of a multiplatform software system
Is software development an art or a science? Significantly, Donald Knuth’s books were titled The Art of Computer Programming. If software development is an art, then its highest expression is the creative way in which developers solve daunting technical problems, such as multiplatform development. One of the most creative ways to approach multiplatform development is through the strategic use of abstraction.
Abstraction distills an application to its root concepts while isolating it from the details of the system implementation. The key is to compartmentalize the system such that the application’s algorithms are decoupled from the implementation environment. A camera pointing system, for example, should operate on velocities, coordinates, and distances to target. It is irrelevant whether a radar device or an optical tracking subsystem generated the ranging information. Does the algorithm really need to know that some of the rate information was produced by a Hall effect sensor?
This approach is effective over a wide variety of applications. Just as the mechanical components of a system can be abstracted, so can communications interfaces, CPU resources, RTOS facilities, and storage devices. Deft use of abstraction simplifies the application into a compact, reusable code base. It is a powerful tool when applied to the problems of multiplatform development.
The following sections provide a guideline to follow when choosing abstractions to aid multiplatform development. As always, creativity, elegance, simplicity, and pragmatism should guide the selection of abstractions.
Never call OS functions directly. Divorce the application from the underlying RTOS by encapsulating operating system functions in an OS abstraction library. This permits developers to migrate the application to different operating systems simply by porting the OS abstraction layer. The application code remains intact.
Testing and quality assurance performed previously on the application are not wasted because the application code remains unchanged. The “hardening” that the software acquired in the field will carry over to the new platform and accelerate the transition to the new environment. Also, when bugs are discovered, the OS abstraction library becomes the starting point for the debugging effort.
The OS abstraction library should export function prototypes and guarantee specific behaviors for them, regardless of the underlying operating system. For example, many operating systems have semaphores that are safe to take more than once from the same task context. Other operating systems will block the caller if a recursive semaphore take is attempted. If the OS abstraction library exports a function, OS_SemTake(), then it must proscribe a standard behavior for the function independent from the underlying operating system. In the case of a recursive semaphore take function, developers might have to implement the recursive behavior themselves. The implementation for some operating systems might involve comparing the ID of the task that last acquired the semaphore to the ID of the task attempting to take it. If they are the same, the function should increment a counter for that semaphore and not make the OS’s semaphore take system call. The corresponding give function decrements the counter each time it is called until the counter reaches zero. At that time, the OS semaphore give function is called to relinquish the semaphore.
Write wrappers even for functions you would expect to behave similarly on all operating systems. This protects the application from idiosyncrasies and even bugs in the underlying operating system implementations. “Standard” functions often aren’t, and “well-known” functions could make fortunes writing tell-all books about their secret lives.
The socket library function, select(), provides a good example of why encapsulation is so valuable. The types of devices that can be “select-ed” vary considerably from operating system to operating system. Some allow only sockets to be selected, while others permit sockets, pipes, and message queues. Abstracting the underlying implementations protects the application against an unwanted, and often messy, redesign for another operating system. In one case, an operating system failed to implement the timeout feature of select() properly. Fortunately, the work-around could be confined to the abstraction library. Otherwise, significant architectural changes to the application may have been required.
Convert physical interfaces to logical ones. As an example, consider a system that has an engineering bus to which peripherals are attached. Create a set of functions that access the bus, such as EngBus_SendTo() and EngBus_ReadFrom(), that effectively hides the implementation of the bus from the application. Whether the bus implementation is PCI, VME, 1553B, or a serial port, it is separated from the application logic.
This approach works well for networked applications. If an embedded client engages in a session with a networked server over Ethernet-TCP/IP, abstract the concept of session and partition it from the IP connection. Allow the application to invoke functions that reflect the logical session, such as Session_Open(), Session_Close(), Session_Send(), Session_Receive(), and Session_BlockForSessionData(). This allows the application to migrate to a platform where the physical interface may be a paltry serial port. The concept of the session has not changed, only the medium over which the session is conducted. If abstracted judiciously, the application can be embedded in different types of equipment with a variety of physical interfaces.
The same approach applies to applications that might be distributed among several processors in later generations of the product. Abstracting interprocess communications (IPC) mechanisms allows the software subsystems of the application to move freely from one processor to another. By partitioning the software in this manner, you can scale the application from an entry-level product to the behemoth model by adding processors to the system board. The converse occurs when bridging generations of products. Software required to translate between different generations of hardware can be removed in later generations of the product when the older technology has been completely removed.
Commanding the system is abstracted in the same manner. Whether the commands are generated by command line interface (CLI), infrared link, or an embedded SNMP agent does not matter. The mechanism by which the commands enter the system should be divided from the set of functions invoked to execute the commands.
The same is true of event logging and general I/O. By distilling the logical connections from the physical, the application can be rewired into dozens of different platforms. The advantages of this approach are a reusable code base that can be leveraged between different platforms as well as a consistent interface across different product lines from the same manufacturer.
Separate the protocol implementation from both the transport medium and the system-specific details of the application. In our camera-pointing example, the pointing algorithm was isolated from the sources of its input data (the physical sensors). The protocol implementation can be isolated from the transport medium in exactly the same way. For example, implement a packet-based protocol as a code library that accepts packets as input and produces response packets as output. For connection-oriented protocols that maintain state, the library can also accept and modify data structures that preserve state information related to the connection.
This code organization provides the freedom to embed the protocol in any number of devices at a later date. It also permits the testing of the protocol to occur in isolation, which is an invaluable capability. The implementation can be debugged prior to the appearance of hardware and used later as a static test-bed to simulate failures in the field.
It is important not to place application-specific knowledge into the protocol implementation. For example, an application-level protocol implementation that produces AAL5 packets cannot easily be placed into a device that uses IP. Similarly, an implementation that accepts a TCP socket descriptor, to which it sends responses, cannot easily be employed across a proprietary backplane.
System services are software components that provide a service to the application level software (see “At Your Service,” Steven Stolper, April 2001, p. 124). Developers employ them to abstract the details of the hardware platform into a standard set of capabilities used by the application. They can be used to manage nonvolatile storage, provide highly accurate timing through hardware support, and manage processor resources. Similar services can also be used to manage relays, switches, or other peripheral hardware manipulated at the application level.
System services can also provide software services that do not depend on the existence of specific physical hardware. Services such as interprocess communications, software health verification, event logging, and time-stamping can be rendered independently of specialized hardware.
This streamlines multiplatform development by leveraging the existing, well-tested application software. The application migrates to different platforms by porting the lower level services on which the application resides. It also allows the application code-base to be easily ported to more capable hardware when it becomes available simply by porting the underlying services.
Another way to increase the portability of the application is to place all of the platform-specific initialization into one module. When the system boots, the root task invokes a platform-specific framework module to initialize and configure the system-specific hardware. Once the platform-specific initialization is complete, the framework starts the application. The platform-specific code executes outside of the application, so the application can run under any number of frameworks.
When developing applications that might be retargeted to other platforms, it is wise to consider what other development environments might be used to build the software. Companies with heterogeneous environments that consist of both Unix and Windows computers need to keep in mind differences between the systems. Some Windows environments will accept either slashes or backslashes in file path names. Unix environments require forward slashes. Unix environments are also case sensitive. When specifying header files, the capitalization must match the way the file appears in the file system. Also consider how Unix and Windows editors deal with special characters that may be embedded in the application code such as tabs, end-of-line, and carriage return. It could be disastrous to have to modify the source of a critical application after all of the testing has been completed.
Abstraction is a powerful tool for multiplatform development. Wisely employed, it can transform the potential for multifarious code changes, insidious bugs, and eternal technical support into a triumph of engineering -and even art!
Steven Stolper is a software engineering manager at Broadcom’s Carrier-Access Business Unit. Prior to Broadcom, he helped develop embedded IP-over-satellite networks. Steve also designed flight software for NASA planetary spacecraft including the Mars Pathfinder Lander and Galileo Orbiter. His e-mail address is firstname.lastname@example.org.
Erik Guntvedt, Subbarao Mungara, and Randy Pan contributed valuable ideas and opinions that greatly influenced this article.
Forth is a niche programming language originally designed for real-time control of telescopes. Over the years, it evolved into an ANSI-standard language. While not widely used anymore, it’s still worth a look.
Forth is a niche programming language originally designed for real-time control of telescopes. As programmers from other fields discovered Forth, a grassroots effort emerged to mold it into an ANSI-standard language.
Programmers who’ve used Forth describe the language as being like a room without walls. Some thrive on such freedom, while others are uncomfortable with it. Since Forth is a type-less language, the compiler can do little checking for you before you run your program. As a result, the most common failure scenario is a system crash.
Forth is used mostly to test and debug hardware and bring up systems. Only about one in 50 embedded developers report using Forth regularly. Interestingly, some UNIX workstations boot a small Forth interpreter before the rest of the operating system. This environment provides some basic programming capabilities right out of ROM, and a small Forth bootloader stored there enables the operating system to be manually or automatically loaded from a disk drive or over a network and then run.
Forth is a language with a simple syntax and many keywords. This is in contrast to Algol-style languages (such as Pascal and C/C++), which have a complex syntax and few keywords. If you’re completely new to Forth, try to forget everything you know about programming languages as you read on.
Forth programs are made of many small procedures. Forth is compiled, yet has no compiler in the traditional sense. Essentially, it’s a population of subroutines and an interpreter. The subroutines are called words. (In this article, words will appear in UPPER-CASE.) The dictionary is a data structure that associates the compiled words with their string names. The interpreter can invoke words that perform compilation actions, thereby extending the dictionary in the middle of a program. Figure 1 shows a flow chart of a Forth interpreter. The interpreter evaluates white space-delimited strings taken from an input stream, such as a console or file, usually in one pass.
Figure 1: A Forth interpreter’s flow chart
You write a Forth program by defining new words, and run it by executing the top-level word. Forth manipulates data on a parameter stack that is separate from the call stack. (There are no registers.) Although static variables can be defined, words generally pop their parameters from the parameter stack, and push their results onto it. For example, the built-in word + pops the top two values, adds them, and pushes the sum back onto the stack. Bitwise AND operates similarly. The word < pops two values, compares them, and pushes the result (0 or -1). So a Forth programmer would code (2+3)*(4+5) as 2 3 + 4 5 + *, in reverse polish notation (RPN). The Forth standard specifies a boolean result as all 0's or all 1's, which is 0 or -1 in twos complement arithmetic. This allows you to mix arithmetic and boolean operations, for example << 7 AND. The compiler allows any kind of type mixing, as Forth is typeless. With most data kept on the parameter stack, there's little need to track variable names or addresses, and temporary storage is automatic. Several built-in words manipulate the stack by rotating, removing, copying, or displaying items from various stack positions: SWAP swaps the top two items, DROP removes the top stack item, and OVER copies the second stack item to the top of the stack, thereby increasing the stack size by one. Words that manipulate character strings generally require a pointer as the second item on the stack and the string length on the top. Branching and looping words also use the stack. IF pops and tests the value on top of the stack. If the value is non-zero, the next word executes. Otherwise, control passes to the word following the ELSE, if present. BEGIN starts an indefinite loop and the corresponding END pops and tests the top stack value, looping back to BEGIN if it's zero. DO/LOOP pairs repeat until an index (passed on the stack) increments to or beyond a limit (also passed on the stack). An important difference is that the index and limit are copied to the call stack-to avoid cluttering the parameter stack. softwaretravelsfig3 Listing 1: A Forth program and its stack
: BIGGEST OVER OVER < IF SWAP THEN DROP ; 5 9 BIGGEST . Listing 1 shows the definition and testing of a new word, BIGGEST. The word : switches to compiler mode and begins the definition of the new word. OVER OVER duplicates the values to be compared while maintaining their order. The word < compares them, popping the two values copied by OVER OVER and pushing the result of the comparison, which is subsequently popped and tested by IF. If the comparison results in a -1, control passes to SWAP, which swaps the two values. Either way, the smaller value is now on top, ready to be removed by DROP. The word ; terminates the definition and takes the interpreter out of compile mode. Once defined, we can use a new word immediately. 5 9 BIGGEST pushes 5, then 9, then removes the smaller value. The word . prints the value on top of the stack (in this case 9), and the stack is again empty. The state of the stack as the program executes is shown below the code. Forth has earned a reputation for being a write-only language. A typical Forth program defines and uses thousands of new words. In the absence of good naming conventions and comments, this can be a big maintenance headache. On the other hand, there’s no reason Forth programs can’t be useful and well documented. esp Brad Eckert holds a BS in physics from Shippensburg University. He has been a designer of both hardware and software for embedded systems for about 15 years. Brad wrote and maintains a free Forth-based framework for extensible firmware. His e-mail address is email@example.com.
Don Rowe is a consultant specializing in embedded controllers. He has over 25 years of experience with digital and analog design, software testing, and reverse engineering. Contact him at firstname.lastname@example.org.
Conklin, Edward and Elizabeth Rather. Forth Programmer’s Handbook. Hawthorne, CA: Forth Inc., 1998.