Real-Time Programming


Building Embedded Systems With Turbo C

Pat Villani


Pat Villani received his B.S.E.E. from Polytechnic Institute of Brooklyn and his M.S.E.E. from Polytechnic Institute of New York. Pat has developed applications ranging from avionics and guidance to computer peripherals. He is president of Network Software Systems, Ltd., which specializes in C applications and embedded control systems. Readers may reach him at (908) 206-0320.

Embedded systems have traditionally represented a specialized area of computer science and electrical engineering, an application where both disciplines merge. System design often requires tradeoffs between programming convenience and system cost. Quite often, code produced for embedded systems is created by both software and hardware engineers.

The first microprocessors used in embedded systems implemented a lot of code in assembly langauage because of the type of software development packages available for these processors. These packages were often expensive and required the manufacturer's development system or a large machine to build code. Only large corporations could afford these programming platforms. As microprocessor capabilities increased, manufacturers introduced programming langauges geared toward embedded systems. These new languages were limited to the same platforms, so the trend of limited embedded system development continued.

Concurrently, great strides were made in desktop computers. These computers, based on the same microprocessors that were used in embedded systems, were capable of running applications and small software development packages that targeted these machines. Further developments in desktop computers enabled the developers of these packages to include more advanced features. The result: powerful langauges such as the Borland Turbo languages and Microsoft languages that include debuggers and integrated development environments that allow rapid development of applications.

Today, low-cost clone motherboards, hybrids, and single chip computers based on XT and AT architectures exist. When combined with low-cost peripherals, these motherboards and devices present a unique opportunity to system developers. Low-cost and small-volume controllers, data loggers, and specialized processors are feasible. Adapting langauges such as Turbo C to produce code for these environments makes these designs even more affordable.

Basic Principles

Compilers such as Borland Turbo C generate efficient code that is independent of any operating system. What ties the compiler to an operating system or any environment is the system startup code and the libraries that are linked to produce the executable object file. Replacing these files with new files specific to embedded systems allows the code to work in an embedded system.

The first file you must replace is the startup file. Turbo-C uses one of the files COx. OBJ (where x is T, S, L, etc., depending on the memory model used) found in \TC\LIB. This file transforms the unique parameters of MS-DOS passed at startup into the conventional C environment argc, argv, envp, and errno. The file also sets up the stacks and aligns the segments of memory.

The next file that you must replace is the corresponding flavor of \TC\LIB\Cx.LIB. This file contains all the operating system calls, such as read and write. It also contains library functions such as strcpy and tolower. Our approach is to replace this file with a new library emulating these calls.

Design Advantages

Rapid development is a key factor in today's market. This approach helps the system developer achieve this goal by reducing development time. The project is developed using the Turbo C integrated environment or Turbo Debugger. When the developer is satisfied with the code's functionality, the code is recompiled and linked using make files and the command line environment. A commercial or public domain locater is then used to create an EPROM for the target system.

The project is quickly debugged in a convenient environment in which code changes are rapidly made and revision control is maintained. Using the Turbo Debugger, nearly all C code and assembly code can be debugged, registers examined, breakpoints set, etc., without the need for a costly in-circuit emulator. If a suitable ROM monitor is used during the final debug cycle, an in-circuit emulator may not be needed at all.

Since the code is transported from an MS-DOS environment to a target system, the functional code can be easily transported to a different 80x86-based target system by simply changing the device drivers and library I/O files. This built-in hardware independence aids the developer in writing reusable code, further reducing development time in future projects.

Example Project

The development of a simple 8086 ROM monitor illustrates this design approach (Listing 1) . The monitor, called mon86, performs two functions:

It uses a simple table-driven executive that can be extended by adding new table entries and suitable functions to execute these new commands. An error function terminates the table.

The monitor uses only Turbo C calls that comply with both SVID and POSIX definitions, improving portability. DOS-unique system calls, such as INT21, etc., are not used. Using them would complicate the code by requiring the use of conditional preprocessor directives to selectively include the non-portable sections of code. It also complicates library coding, since it requires that DOS code also be emulated.

For this project, the C library is broken into two libraries: a system call emulation and a portable C library. The example project uses simple I/O calls such as read and write. The functions open, close, and ioctl are also implemented for completeness. Restricting I/O function calls to this small set reduces the work required to implement the emulation package.

The monitor C library calls are limited to simple getc and putc. These calls are implemented as links to Turbo C calls _fputc and _fgetc, which allow for full use of Turbo C's <stdio.h>, further enhancing portability. Also, simple formatted output is required for memory display. To achieve this with an eye on portability, the library provides an integer-only printf. The choice of a limited printf has two justifications:

A machine library, called libm. lib, is also needed. This library is a collection of all subroutines required to supplment C operations not supported by the processor itself, such as long divides, multiplies, etc. These subroutines are naturally operating system independent. It is best to extract them from the Turbo C library as needed during the ROM build portion of the project. A limited library is built at that time by linking the final MS-DOS code, noting the routines used in the code map, and extracting and creating a new library of just those routines.

Startup Assembly Code

Probably the most important piece of code for an embedded system is the system startup code (Listing 2) . This code will vary from compiler to compiler, but the same operations generally appear in some form. The first part of the module is usually initialization related to the assembler or compiler. For Turbo C, this is the specification of segments. It is here that the order of segments for the module is first specified. Turbo C requires three segments, MS-DOS requires one.

MS-DOS requires a stack segment. In a .EXE file, this is the only segment specified in the header that is separate from the rest. That is why the stack segment is used by some locate utilities as a place to separate RAM code from ROM code when generating hex files suitable for EPROM programmers.

Turbo C requires only three segments, _TEXT, _DATA, and _BSS. These closely follow the UNIX C model. The mon86 generated here is a small version, therefore, the CODE segment is text only, and the DATA segment is used for data, bss (data initialized to zeros), and stack. These segments are mapped into DGROUP.

All 80x86 processors begin executing code after reset at address 0xffff:0x0000. The usual procedure is to place a far jump at this location to the ROM entry address. For our example, the system entry point is defined as the far procedure entry. This label is used as the startup address supplied to the locater.

Next, the processor is initialized. Initializing the processor means intializing any segment registers that the processor needs for proper operation. Since the module presented here is for an 8086 or 8088 machine, only the segment registers must be initialized. Note that segment register initialization is performed with all interrupts disabled, which prevents spurious interrupts that may occur during power-up from crashing the system.

Once the processor is initialized, it usually requires the proper setting of the stack pointer. For this example, all you must do is initialize the sp and bp registers. Note that initializing the bp register is required by Turbo C code generation conventions, not by the 8086.

Special hardware initialization should be performed after the processor has been initialized. This includes initializing the interrupt vector table, moving the data segment to RAM, and initializing the _BSS segment to all zeros to match C programming conventions. The example in Listing 2 includes a hook for hardware initialization with a call to __hdw_init. (For clarity, I've omitted moving data and initializing vectors.)

After system initialization, you must enter main so the application can run. You enter main by building the stack frame containing the arguments envp, argv, and argc. You use these arguments to pass system configuration information, dip switch settings, etc. to your C code. These arguments also serve as a convenient debugging tool, since you can test different configuration switch positions by passing a command line argument during the debugging phase.

One critical piece of code that all systems require is the function exit. This code is very implementation sensitive. You may want to light LEDs and sound alarms. In certain cases, you may want only to display a message and restart. What to do when the application terminates must be decided during system design. The startup code also contains a call to exit just after the call to main, which allows main to return to its caller if an error occurs.

One final small but necessary bit of code is errno. errno is the location where C usually stores an integer value to signify the error condition that occurred during a library or system call. Although using errno this way is not necessary in all embedded systems, it is safer to follow convention than risk an unkown subroutine yielding a link error. Your system call emulation will also make use of this variable, allowing for a closer emulation.

Operating System Emulation

The design example uses a single file for operating system emulation (Listing 3) . This code closely emulates the standard C equivalents. Following this practice minimizes ROM debugging.

A small data structure, fd, tracks file status. The only parameter set by open and close are file open or close status. This structure can be extended to satisfy system requirements, but should be kept as simple as possible to assure ease of debugging. The open call checks for only a single possible file to be opened. /der/con (the control console), stdin, stdout, and stderr are also hard-wired as read-only or write-only, as appropriate. In some systems, open and close will link to the I/O drivers to perform driver initialization.

The most used portion of the emulation code is the functions ioctl, read, and write. The function ioctl is most often used for setting baud rates, screen modes, etc. It is shown here for completeness. The write function simply links to the driver call _cout (Listing 4) . It maps newlines into carriage return and line feed pairs. The read function buffers input. It terminates each request on receipt of a newline. Any necessary processing to put text in canonical C form should be performed here.

Portable Library Functions

In order to simplify debugging, standard I/O calls should be emulated. This example uses getc, puts, and printf. The functions here must closely emulate their respective MS-DOS counterparts, preventing unexpected surprises in the target system.

Examining Turbo C's <stdio.h> file reveals that getc is a macro that invokes fgetc (as is typical in many systems). However, _fgetc calls DOS through another function, _fgetc. Therefore, your code closely follows this by mapping _fgetc and _fputc to return the correct values for success and error (Listing 5) . A simple puts is implemented in a portable fashion through calls to putchar (Listing 6) .

This application uses the abbreviated version of printf described earlier (Listing 7) . This function only calls write, which minimizes unexpected dependencies on other library functions. Guaranteeing that floating point is not used helps in not wasting ROM space, usually a premium when system cost is considered. In many embedded applications, the only requirements for floating point support result from printf's floating point display options.

Programming And Debugging

Turbo C's greatest asset is its integrated programming environment. A bug can be quickly discovered and the code can be quickly recompiled from the same environment. Typically, it is in this environment that the first operational code is developed. (An interesting note: the new environment for Turbo C++ includes a register display window that works well with files assembled using the masm debugging switch). This phase should be used for all logic testing and customer demonstrations, since it is the most flexible of the development phases.

Once the main logic has been debugged, the code should be linked with the portable libraries and tested. Listing 8 shows an example make file. Both DOS and ROM version targets should be defined, so that any changes that may be necessary to the main logic resulting from target system testing can be incorporated into the DOS version and retested in the integrated environment. The make file presented here also contains two additional targets, ex0 and ex0r. These targets are typically limited versions that test the device drivers. Quite often, these versions provide repeating patterns and echo routines for testing peripherals. Upon satisfactory completion of testing, the resulting code is burned into a final EPROM and delivered.

Conclusions

This example provides a good start for an embedded system design. Although the XT and AT architecture is used for this example, the techniques presented here are not limited to them. These techniques have been used for new designs in projects ranging from simple computer peripherals, such as printers and tape transports, to specialized sonar signal analysis hardware. They provide a broad approach that can be applied to both native and cross compilations, substituting cross compilers and cross compilation targets in the make file.

Of course, an embedded system need not include support for printf, read, or write. An application can instead make simple calls directly to the device driver, which results in much smaller code. However, since many tradeoffs are considered during the system design phase, more often than not the extra functions are worth providing.