The next platform for your embedded application will use a new processor, and the original software must run on it next week. Depending on the original system architecture and the tools available, the task can be either a breeze or a nightmare.
This scenario illustrates the problem today's designers face when attempting to jump to new platforms while keeping the same software. Many choices exist, but each has its own pros and cons. Let's take a look at those options.
The table outlines the three general approaches to porting code: recompilation, virtual machines, and machine-code translation. Each has different benefits, requirements, and drawbacks. For example, recompilation is only possible with source code present.
Source code availability is preferable even when employing technologies like virtual machines and machine-code translation because of debugging considerations. The big problem with any porting exercise is always compatibility, and 100% compatibility is hard to come by, especially when the new target adds features or peripherals.
Another consideration is the support code necessary by an application (see "Making Apps Run," p. 78). Small microcontroller applications can often run without additional support code, but anything larger includes at least an operating system. Building an operating system from scratch for an embedded application is still a viable alternative, although using a third-party operating system is pretty standard.
Unfortunately, the source code for support programs can be expensive, which is one reason why Linux, with its General Public License (GPL), is a popular embedded solution. Many real-time operating-system (RTOS) vendors include source code as part of their base package. A number of them still keep their source code closed, yet Microsoft permits peeking under the covers.
Portable Source Code: In addition, the destination must have a compatible compiler. C and C++ are the two most popular compilers, but this can be refined further as C++ provides undesirable or unnecessary features for embedded environments.
Embedded C++ (EC++) is one common subset that works with compilers from a number of companies, such as Altium Tasking. It eliminates advanced features like multiple inheritance and templates. The idea behind it is to make EC++-based applications less complicated in terms of language features. As a subset of C++, EC++ lists the things that won't be implemented.
Subsetting the C runtime also is useful for embedded environments and portability. The POSIX standards define a range of interfaces for everything from file access to multitasking support. Migration at the source level is significantly simpler if the application can adhere to this more limited support interface. POSIX works with most operating systems.
Another operating environment is OSEK/VDX. Primarily employed for distributed control in vehicles, the OSEK standard is helpful in other embedded environments as well. Green Hills Software's Multi 2000 Integrated Development Environment runs on OSEK-compliant operating systems.
Unfortunately, even with proper programming practices, a lot more details can crop up when switching from one platform to another. Some issues are memory mapped versus port-mapped I/O, big- and little-endian numeric encoding, and even floating-point formats. The object of migration is getting a system to function with an existing infrastructure, which could entail exchanging data across devices or platforms. Switching from an x86 environment to a PowerPC can grow tedious even with source code.
Virtual Machines: Hiding the underlying platform from the application and interposing a virtual one between the application and hardware eases migration. Many years ago, the Pascal P-System took this approach, allowing Pascal applications to be compiled once and run on a wide range of hardware without modification.
Among others, Sun's Java is the most notable implementation that uses a virtual-machine (VM) architecture. Both Microsoft's .NET CLR (Common Language Runtime) and Vita Nuova's Inferno use VMs. Every one defines a bytecode instruction set similar to the Pascal P-System. With this approach, the VM's machine code is tailored to the language that it was designed for.
Although a VM can be optimized for a particular language, it's not prevented from running applications that generate the appropriate bytecodes. The Pascal P-System functioned with numerous compilers in the same fashion that Microsoft's .NET supports a range of languages from C# to Visual Basic .NET to Eiffel .NET. Even Sun's Java VM, or JVM, contains compilers for other languages like Forth and Scheme (a version of Lisp).
The advantage of a VM specification is that a programmer only needs to deploy the instruction set so that the application will run accordingly. Initial VM implementations are typically bytecode interpreters, but this is just the tip of the iceberg.
Companies like NewMonics, IBM, and HP offer Java VMs that compile Java bytecode applications to native code. Just-in-time (JIT) compilers do this on-the-fly as a program is executed, while ahead-of-time (AOT) compilers generate native machine code before the program runs. Advanced VM implementations can mix JIT, AOT, and interpreted operation, because there's usually a space/performance tradeoff when using native code.
While native code generated from a C++ compiler is typically faster than native code generated by a JIT or AOT JVM compiler, the difference is due to the kind of checking performed by a JVM. This is part of the language specification. Checks verify that array indices are within range.
Another source of overhead is garbage collection. Although not a VM requirement, most modern VMs work with languages that have garbage collection. This minimizes programming errors such as dangling pointers and memory leaks, typical with C and C++, among others.
Another benefit of garbage collection for code portability is that the memory management feature is hidden in the VM. So it doesn't matter that the source and destination VMs use different kinds of garbage-collection algorithms. There can still be an impact on application performance when moving between VMs, especially if there are real-time considerations. Standards like the Java Real-Time Standard address these issues, letting developers use high-level controls on real-time performance and garbage collection.
Many times, a new target system's performance influences code migration to new platforms. Putting the VM in hardware is another way of speeding up performance. A wide range of companies—Ajile, Digital Communications Technology, InSilicon, Nazomi, Parallax, Vulcan Machines, and Zucotto Wireless—provide various hardware implementations of Java. Some, like the Parallax Javelin and the Ajile aJ-100, are standalone Java processors. Others work with existing processor architectures, allowing native and Java code to be executed by the same hardware.
Only the Pascal P-System and Forth have had some hardware implementations of note. It will be interesting to see if Microsoft's .NET VM garners similar support.
Machine-Code Translation: Taking the VM idea one step further, the VM can be made into the original platform's processor architecture. This approach has surfaced in recent years in both hardware and software for embedded applications.
This didn't happen earlier, nor is it typical today, because this type of emulation is complex. The nuances of the original processor must be considered, plus the emulation has to deliver good or better performance than the original platform.
System emulation isn't really new. IBM did it for years as the company's product line changed. Running 1401 Autocoder programs on a 370 mainframe was regular practice for a long while and faster too. Even the Intel 80386 ran a VM emulation for x86 real-mode applications. In this case, the architectural similarities between different modes was minimized because the basic register set was a subset of one found in the more advanced execution environment.
Transmeta's line of Pentium-compatible processors uses an entirely different method with techniques that are common to JIT VM implementations. The processors execute x86 applications, but the machine code executed is actually very-long-instruction-word (VLIW) instructions for a custom VLIW processor designed by Transmeta. Blocks of x86 instructions are compiled into blocks of VLIW instructions, which are placed in a cache in main memory. With the cache area hidden from the x86 applications, an application can't corrupt the underlying VLIW-based system.
As with many JIT VM systems, Transmeta's translation software, also called code morphing software, performs subsequent optimizations to code used repeatedly. Essentially, the more the code is executed, the faster it will run.
Transmeta has done remarkably well with its machine-code translation system. The VLIW firmware is downloaded when the system boots, giving it the added advantage of updates to correct problems or increase performance. Plus, the VLIW processor can use a different chip for each generation. In fact, most Transmeta processors implement slightly different VLIW cores.
While Transmeta's VLIW processor architecture is somewhat attuned to the x86 architecture, it's really a general-purpose unit. Transitive Technologies' Dynamite product handles machine code migration across standard hardware platforms such as the x86 to MIPS. Architecturally, it and Transmeta's approach are very similar. The major difference is that Transitive's targets are standard processors.
Dynamite uses the same kinds of JIT techniques already mentioned, implementing a caching system with only one major difference from the Transmeta system. Dynamite compiles to an intermediate code that then compiles to native code. Both the intermediate and compiled code are cached.
The two-level system is used for subsequent optimizations as Dynamite tracks converts blocks of machine code in the same fashion as Transmeta's processors. But it's easier to optimize the intermediate each time, instead of regenerating it.
Among JIT systems, the main common thread is a big cache. With megabits of memory, that's an easy requirement to fill. A 16-kbyte cache or larger is typical.
While Transmeta's VLIW code is always created on-the-fly, Dynamite can also act as an AOT compiler. This is important in embedded applications where the initial compilation latency can be a killer. AOT compilation eliminates the latency.
Machine-code translation features lots of benefits, including migration speed. It's not unusual to migrate applications to a new platform, then slowly migrate them via recompilation and possibly redesign at a later date.
The major problem with this procedure is migration of the runtime support. It's possible to bring over the entire system from the old platform. But more often, the application alone is moved. In this case, an interface must be created from the application to the native operating system and related support system. The scope of such a job varies greatly, depending on the scope of the application interface and the application size.
Nothing Is Problem Free: So far, the "write-once, run-everywhere" promise of Java hasn't come true. The bulk of a Java application will migrate between most Java implementations, but taking advantage of a VM-specific feature causes porting problems. Even playing by the rules and not using these features can result in difficult-to-diagnose problems due to how a VM is used. Being slightly out of spec, or having an interface not precisely defined or not used accordingly, means that applications will run differently on separate platforms.
Unfortunately, porting problems will always be with us. The trick is to minimize the problems so developers can concentrate on the application.
|Need More Information?|
Digital Communications Technology
Green Hills Software
Embedded C++ Technical Committee
Vita Nuova Ltd.
Vulcan Machines Ltd.
+44 (0) 1763 247624