Developing 80x86-Based Embedded Systems

发表于:2007-06-08来源:作者:点击数: 标签:
Developing 80x86-Based Embedded Systems Andrew P. Beck -------------------------------------------------------------------------------- Andrew Beck is a systems engineer specializing in the design of computers for industrial and scientific

Developing 80x86-Based Embedded Systems

Andrew P. Beck

--------------------------------------------------------------------------------


Andrew Beck is a systems engineer specializing in the design of computers for industrial and scientific applications. He has been designing embedded systems more than 15 years. Andrew can be reached at (908) 806-8262.


Developing 80x86-Based Embedded Systems

Andrew P. Beck

--------------------------------------------------------------------------------


Andrew Beck is a systems engineer specializing in the design of computers for industrial and scientific applications. He has been designing embedded systems more than 15 years. Andrew can be reached at (908) 806-8262.


Introduction

Embedded systems were once programmed only by those brave souls who to dared write in assembly language and who knew enough about the hardware to program to the chip level. But as the embedded systems market continues to expand, and the need for embedded systems programmers grows, all of that has changed.
Most manufacturers are reducing the size of their products. With the shortage of experienced embedded systems programmers, more MS-DOS programmers will be required to develop embedded programs. In this article, I will try to ease the path for those of you who have never developed an embedded system. I address the fundamental issues of developing a program to run on an 80x86-based system. I explain the steps necessary to convert your C programs into embedded code, And I give you some hints on how to avoid some of the common pitfalls of embedded systems design.


A Little History

In 1978 Intel developed the 8086 microprocessor. Due primarily to IBM's use of it in their first PC, the 80x86 family has become the industry's most popular microprocessor. There are literally millions of 80x86-based computer systems in operation today. Because of the PC's popularity, scores of lowcost, high-quality software development tools have been created for it.
Soon after the introduction of the 8086, embedded systems designers started developing new products around it. These early designers had to rely on high-priced development systems to help them refine their designs and develop their software. This, along with the large number of support chips required for even a modest 8086 design, prevented many designers from using the 8086 in their embedded designs. Only those who could justify the high initial development cost and long development cycle could afford to use it.

Intel soon realized that embedded systems offered a potentially large market, so in 1982 they announced the 80186 microcontroller. The 80186 is a highly-integrated controller that includes some of the most often used peripherals on the same chip as the microprocessor. In the past couple of years there has been a dramatic increase in the number of new designs utilizing this controller. In addition to Intel, NEC and AMD currently produce microcontrollers that are code compatible with the 8086.

Users of all types of equipment, from consumer electronics to high-end instrumentation, are demanding ever more intelligent devices. Manufacturers are continuing to put more powerful systems into ever smaller boxes. One market that has seen a tremendous growth in the last few years is the field of industrial instrumentation and automation. The typical handheld instrument of today is required to outperform desktop systems produced just a few years ago. In addition, manufacturers are striving to make these systems easier to use, while introducing new products faster than ever.

All of these factors have combined to force the embedded systems designer to look for tools to help produce better products in a shorter period of time. One of the most powerful tools currently available is the C programming language. C allows the designer to develop programs faster, and with fewer bugs, than assembly language. C programs can be better structured, and therefore more portable and maintainable, than their assembly-language counterparts.

A few years ago, many designers were reluctant to use C in their designs. They felt the language used too much memory. It was often difficult to write interrupt handlers in C, and the designer had little control over placement of data or code. All of this has changed. Today's C compilers offer a high level of optimization. In large complex programs, the C implementation is often no larger than the assembly version. Most compilers provide support for generating interrupt <I>function</I> completely in C.


What is an Embedded System?

An embedded system generally consists of a microprocessor and a few peripherals that run a dedicated program. That program is usually stored in non-volatile memory, such as PROM. Embedded systems are not programmable by the end user, and usually have very limited I/O resources. While many embedded systems are designed with off-the-shelf components, the majority of them are based on custom-designed hardware.
Since many embedded systems do not run under an operating system, the programmer is responsible for supplying all of the low-level I/O <I>function</I>s. And unlike MS-DOS-based programs that can be loaded from disk, embedded systems require you to load the program into PROM. Consequently, developing and debugging these systems presents some unique challenges.


Embedded Systems Development Tools

Due to the popularity of the PC, there are a number of low-cost, high-quality development tools available for the 80x86 processors. Two of the most popular C compilers for the PC are produced by Borland and Microsoft. Because of their popularity, these compilers have led to the development of many third-party design tools that support embedded systems.
To create an 80x86-based embedded system, you will need a standard MS-DOS-based assembler, compiler, and linker. You will also need a locate program. This program converts a standard MS-DOS .exe file into a form that can be burned into PROM, typically a hex or binary file.

If this is your first embedded system design, I'd recommend that you purchase a third-party embedded-systems development package. In addition to the locate program, the vendor will supply the requisite startup code and runtime support <I>function</I>s for your compiler. Most vendors will also supply their own libraries, or a set of utilities to remove non-ROMable <I>function</I>s from your compiler's library.


Basic Principles

Programs written for MS-DOS-based systems run in the system's RAM. When a program is executed, the program loader locates an available block of memory and loads the program into it. MS-DOS .exe files are simply relocatable files. As the program is loaded, the program loader uses a table stored in the file to resolve the relocatable segment addresses.
Embedded systems require two types of memory, volatile RAM and non-volatile PROM (or ROM). All of the program's <I>var</I>iables, as well as the stack, are located in RAM. The executable portion of the program is stored in PROM.

Constants, such as string data and initialized <I>var</I>iables, are initially stored in the PROM. However, before the program can be executed, this data must be copied to RAM. This is the responsibility of the system's startup code. This code is also responsible for initializing the segment registers, clearing the uninitiliazed RAM, and setting up the heap. Finally the startup code must call the program's main <I>function</I>.

The startup code is the most important piece of software in the system. It is also the most difficult for the first-time embedded systems programmer to develop. If you purchase a third-party embedded development package, the vendor will supply the requisite startup code.

When the 80x86 processor begins running after reset, it executes the code located at 0xffff:0xfff0. Normally the only instruction at this location is a jump to a lower address in PROM, where the actual program is stored. The first piece of code to be executed after the initial jump is the startup code. The startup code is responsible for establishing an environment that the C program can run in, consequently it is always written in assembly.

All embedded C programs require that at least three segments be defined. The code segment contains executable instructions — your program. The data segment contains all of the program's static <I>var</I>iables. Finally, the stack segment is where non-static <I>var</I>iables, passed arguments, and <I>function</I> return addresses are stored. Depending on the memory model used, some programs will also require a far data segment. Some advanced applications may make use of multiple code, data, or stack segments.

Since your programs, constants, and initialized data will be stored in PROM, the startup code must copy them into RAM. It must also zero out the uninitialized data area so that all other static <I>var</I>iables will be initialized to zero. Finally it has to setup all of the segment registers and the stack pointer.

Most of the time the startup code will also be responsible for setting the system's interrupt vectors. Interrupts 0 through 6 are special interrupts generated by the CPU. Interrupts 0, 4, 5, 6 and 7 are processor exception interrupts. They should point to an exception-handler <I>function</I>. Interrupt 2 is the nonmaskable interrupt (NMI). If your hardware utilizes the NMI, its vector must be initialized to your NMI handler's address.

If you are using a microcontroller, its on-board peripherals will use several other interrupts. If you operate them in the interrupt mode, you must initialize their vectors. The remaining interrupts are available for your application. All unused interrupts should be initialized to point to a dummy <I>function</I> that simply performs a return. This prevents an errant interrupt from crashing the system.

In some systems, the startup code may also be responsible for initializing some, or all, of the system's hardware. Once the initialization is complete, the startup code calls the main <I>function</I>. Command-line arguments can be emulated by pushing data onto the stack before main is called. These arguments can then be aclearcase/" target="_blank" >ccessed via the standard argv and argc <I>var</I>iables. Typically, these would be used to indicate the status of the system's setup switches, or some other hardware interface.

Memory Considerations

Since constants are stored in PROM and then copied to RAM, you pay a penalty in terms of memory usage. Constants require twice as much memory in embedded systems as they do in MS-DOS-based system. Careful design can reduce this penalty. One way to reduce memory usage is by eliminating storage of duplicate constants. Both the Turbo-C and Microsoft compilers can be instructed to merge duplicate string data. However, they can eliminate duplicate strings only within a single program module. If you use a lot of strings, careful attention to where and how you display them can help you conserve your system's precious memory.
You can place all of your display <I>function</I>s, along with their associated strings, in one module. Or you can simply place all of your text in one module and reference it via pointers. Consider how your strings are constructed. If you use a lot of common substrings, you may want to break them into separate strings so that the compiler can eliminate duplicates for you.

For example, consider an application in which you need to display three labels across the bottom of your screen. The text of each label <I>var</I>ies depending on which mode the system is in. You could simply generate separate print statements for each set of labels as shown below:


printf("DOWN  RUN     UP");
printf("LEFT  RUN  RIGHT");
printf("      RUN   STOP");
However, if you had several dozen statements like these, you would find that you had a lot of duplicate substrings. In that case a better approach would be to create pointers to each substring as depicted below. You could then direct the compiler to merge duplicate strings, and save a considerable amount of memory.

printf("%s %s %s", "DOWN ", " RUN ", "   UP");
printf("%s %s %s", "LEFT ", " RUN ", "RIGHT");
printf("%s %s %s", "     ", " RUN ", " STOP");
Another technique for conserving memory is to place all of your strings into their own far data segment and reference them via far pointers. This technique does not need the data to be copied to RAM. Unfortunately the Turbo-C compiler does not support the printf far string pointer argument (%Fs), so this technique is only valid if you are using the Microsoft compiler.

printf("%Fs %Fs %Fs", down_text, run_text, up_text);
printf("%Fs %Fs %Fs", left_text, run_text, right_text);
printf("%Fs %Fs %Fs", blank_text, run_text, stop_text);
Building an Application

You can program the bulk of your embedded program just as you would an MS-DOS-based program. However, you must avoid using any library <I>function</I>s that are not ROMable. The ROMability of <I>function</I>s <I>var</I>ies tremendously from library to library. If you are using an embedded-system development package, the vendor will indicate which <I>function</I>s can be ROMed.
Many of the low-level I/O <I>function</I>s in MS-DOS, such as putch and getch, interface to the hardware via MS-DOS's interrupt 21 handler. High-level <I>function</I>s, such as printf and scanf, use this same interrupt. If you emulate the MS-DOS interrupt 21 handler you can use most of your library's standard I/O <I>function</I>s. Except for the disk-I/O <I>function</I>s, all of Turbo C's I/O <I>function</I>s are fully-ROMable if you supply an interrupt 21 handler. If you use your library's standard <I>function</I>s, instead of developing your own, your development time will be reduced and your program will be more portable.

If your development package supports it, you can also include floating-point math in your application. You will have to supply a runtime interface to the floating-point emulator, or coprocessor, as well as a floating-point exception handler. Most embedded-systems development packages supply the requisite code.

If you are developing your own embedded support <I>function</I>s, you have to make sure you don't inadvertently use a library <I>function</I> that is MS-DOS dependent. The library that comes with Borland's Turbo C compiler tends to be more independent of MS-DOS than does Microsoft's. For example, if you supply emulation for MS-DOS's interrupt 21 console I/O <I>function</I>s, you can use Turbo C's printf <I>function</I> without any other support. On the other hand, Microsoft's printf depends on several low-level MS-DOS <I>function</I>s and cannot be embedded unless you supply emulations of them. If you are using Microsoft's C, refer to their C Compiler User's Guide for a list of the <I>function</I>s that are MS-DOS independent. Both Microsoft and Borland will sell you the source code for most of their library <I>function</I>s, the exceptions being their math and graphics libraries.


Compiling, Linking, and Locating

Once you've written your program, you can compile and link it just as you would any MS-DOS-based program. The only difference is that you will have to link in your startup code in place of the compiler's. You will also have to link in any special runtime support <I>function</I>s that are required by your system. Make sure that your startup code is the first module listed in the link list, this ensures that it is the first code executed. You must also instruct your linker to create a map file. This will be used by the locate program to aid in determining where code and data are to be placed in your target system.
The output file produced by your linker will be an .exe file, a relocatable image of your program. This file contains two parts, a relocation header and the actual program data and code. The relocation header consists of a series of offsets into the program that point to segment references. The locate program must modify these references so that they match the memory layout of your target system. The locate program will use the link map to determine the length and location of each of the segments contained in the .exe file.

As a final step the locate program must output your program in a form that can be downloaded into a test system or burned into PROM. Usually this will be a hex or binary file as dictated by the needs of your PROM programmer, emulator, or debugger. The actual locate procedure will <I>var</I>y somewhat depending on your locate program. Most of them require you to create a file that contains a memory map of the target system, along with directives indicating where each of the segments is to be located. You must also indicate which segments are to be duplicated in PROM. Typically this will consist of only the segment that contains your initialized data.


Debugging

Embedded systems present their own unique problems when it comes to debugging. If you simply burned your program into PROM and ran it, it would be virtually impossible to determine where a problem existed when it didn't run properly. Most embedded systems have very limited I/O capability — often nothing more than a DIP switch and a few LEDs. These would prove useless if you were trying to debug anything more than the simplest of programs.
Traditionally embedded systems designers have used an incircuit emulator (ICE), or a dedicated development system to debug their designs. While development systems provide the most integrated development environment, they tend to be very expensive, often costing upwards of ?,000. Many lowcost emulators are available, and they are fine for developing hardware, but most fall short when it comes to debugging complicated programs.

If you've opted to use your compiler's libraries, and you've carefully designed and structured your program, you can test a large percentage of your code right on your PC. You can generate test <I>function</I>s that emulate your target system's low-level I/O routines. These routines can make calls to MS-DOS to display data on the PC's screen and get input from the keyboard. If you've emulated the MS-DOS interrupt 21 handler in your program you may not need to generate any low-level test <I>function</I>s.

Perhaps's the best debugging environment for embedded systems is one with which most programmer's are already familiar — the source-level debugger. Most MS-DOS programmers have, at one time or another, debugged their applications with either Borland's Turbo Debugger or Microsoft's Codeview. Both of these products provide a host of options for debugging your program. They allow you view your source code in its native form, including comments. Both support multiple breakpoints and allow you to single step your program. They will also display the contents of memory as well as the CPU registers. However, their most powerful feature is their ability to display the values of complex data types such as arrays, structures, and unions.

Several vendors offer a version of Borland's Turbo Debugger for use on embedded systems. By utilizing the debugger's remote mode you can download and debug your program on your target system.

Some vendors offer their own remote debuggers. Like Turbo Debugger, most of these communicate with your target system via an RS232 serial link. Some, such as Paradigm's DEBUG/RT, can be configured to communicate with any type of interface, including parallel ports and PROM emulators.

With the power of these debuggers available to you, there is no reason to even test your code on the PC. You can perform all of your testing and debugging right on your target system. This will help you uncover subtle problems, especially timing-related ones, early in the development cycle.


Special Considerations

Embedded systems are often required to maintain some of their data even when their power has been turned off. This may be data collected and stored for later retrieval, or simply setup information such as the configuration of its peripherals. It's relatively easy to provide this ability in hardware. The most popular way is by equipping the system with battery-backed RAM. While system power is turned off, a small battery applies enough voltage to the RAM for it to maintain its data.
From a programmer's standpoint, non-volatile data presents something of a problem. You already know that at reset the startup code copies over any initialized <I>var</I>iables and zeros out the uninitialized area. This will reinitialize all data stored in your data segment. So you have to find a way to protect your non-volatile data from the startup code.

The simplest method would be eliminate the code that zeros out the uninitialized data area. This presents its own set of problems. If you didn't zero out this area you couldn't depend on uninitialized static <I>var</I>iables to be zero. An unwary maintenance programmer who wasn't aware of this particular peculiarity could easily be tripped up by this.

The best way to solve this problem is to create a segment for all of your non-volatile <I>var</I>iables. Since they're not included in either the initialized or uninitialized data areas, the startup code will not modify them.

You still face one other problem though. If your program depends on any of them being in a known state at reset time, and the startup code isn't modifying them, how do they every get initialized in the first place? Again a simple solution is at hand. You must provide a <I>function</I> that sets all of these <I>var</I>iables to default values. Then in the startup code, or at the beginning of main, you call this <I>function</I> if some predefined condition exists. For example, you could generate a checksum for all of the non-volatile <I>var</I>iables, and call your reset <I>function</I> if the checksum is invalid. Or you can test for some user-generated condition such as a certain switch setting or keyboard input.


Performance Issues

The first time you run your embedded application you may be disappointed with its performance. Often your program will simply not run as fast as expected. This problem is particularly prevalent if you test and debug your program on the PC. Most embedded systems run much slower than today's PCs. Testing your program on the target system is one way to identify performance problems early, before they overwhelm the project.
Most PCs are so fast that many MS-DOS programmers do not pay much attention to the speed issues that were a common concern just a couple of years ago. Embedded systems programming still requires that extra attention. Careful attention to selection and implementation of algorithms is paramount.

One of the simplest ways to get a little more performance out of your system is to set a couple of your compiler's command-line switches. If you're using the Intel 80186 microprocessor, set the switch that enables code generation for it. The 80186 includes several instructions, not included in the 8086, that can increase the speed of interrupts and <I>function</I> calls. If you have plenty of PROM in your system, set your compiler to optimize for speed, instead of for code size.

One area that causes a lot of problems for embedded systems programmers is the use of floating-point math. Most embedded systems lack a math coprocessor, so you'll have to depend on the floating-point emulator in your math library. Even with a highly-optimized, floating-point library, floating-point math is much slower than integer math. Many embedded applications don't require the range of numbers that floating-point supports. Often fixed-point numbers will offer more range and precision than you need. If addition to the improvement in speed, switching from floating-point to fixed-point math can save a considerable amount of RAM.

One of the biggest issues in the design of embedded systems is the tradeoff of hardware versus software. Often hardware designers will opt to eliminate a simple circuit that can be emulated in software. With the rising cost of software development, these tradeoffs have to be considered carefully. In low-volume applications, the software development cost may far exceed the savings in hardware cost.

If your application makes heavy demands on the processor, one or two simple circuits may significantly improve overall system performance. One example of a common input device that is often emulated in software is the keyboard controller. Often it will be implemented by simply connecting a few switches to one of the system's input ports. The software must then periodically poll the status of the switches to determine if any has been pressed. The software must also determine if it is a valid switch closure or simply "switch bounce" from the last key press. In simple systems this rarely taxes the processor.

In many real-time systems, however, the processor may service several I/O devices. It may not be able to spend the time necessary to poll and debounce the keyboard. In this case a simple hardware circuit can be used to decode and debounce the keyboard and interrupt the processor only when a valid key press occurs. Obviously, the better your understanding of the hardware, the easier it will be for you to spot potential problems and inform the hardware designers before the design is finalized.


Conclusion

Developing software for an embedded system is different than for a MS-DOS-based system, but it isn't necessarily harder. In this article I've only skimmed the surface of embedded systems programming. There are many issues that are beyond the scope of an introductory text. Before embarking on your first design, I'd recommend that you develop a good understanding of your target hardware platform. Also look carefully at the development tools that are available, and consider which best meet your needs and budget.

原文转自:http://www.ltesting.net