Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Building An Embedded System


March 1992/Building An Embedded System

Keith Cox is currently employed at NH Research Inc. of Irvine, CA, where he is the software lead for the S6000 project. He has over six years experience with ATE-oriented embedded systems based on Motorola 8-and 16-bit microprocessors. Keith was a member of Who's Who in the Computer Industry in 1989-90 and is the president of Perfect Circle Computing, a private San Diego consulting firm.

For the conventional C programmer, creating an application consists of sitting at a keyboard, editing some code, compiling to an executable, typing the filename, and pressing ENTER. The operating system takes control, loads the program into memory where it belongs, and executes. The program relies heavily on library functions written by someone else to perform such tasks as interfacing with the hardware and communicating with the user. It is completely normal for the programmer to have little or no knowledge of how these tasks are actually accomplished. If there is a problem, the underlying support platform in most cases will announce it by sending a descriptive error message to the monitor. The bug can be edited out, the code recompiled, and program promptly executed again, in search of the next run time error.

If this describes your current software development method, creating an application to run on an embedded system may pose a particular challenge. If you now face the prospect of working in this environment, gone are the days when the flip of a switch and a few seconds of power on testing would allow you to begin computing to your heart's content. In an embedded system, turning on the switch will do little for you. The best you can hope for is that the microprocessor and its peripheral chips will receive a reset pulse of the proper duration. After that, you become the proud operator of a collection of warming silicon devices, many of them in an unknown state. If there is a display, it will taunt you with gibberish. You can press the keys on the keyboard (again, if there even is one), and nothing will change. Where do you go from here?

Embedded systems programming is a unique niche in the software development field. Most of today's embedded applications are sophisticated enough that a well meaning but improperly trained hardware engineer is not up to the task of implementing the system's intricate control schemes. On the other hand, a programmer familiar only with data structures and control statements, oblivious to the world of digital electronics, who knows little about the inner workings of a microprocessor and cares less, will find himself scratching his head in bewilderment when the time comes to make the hardware do something. The embedded systems programmer must have his feet firmly planted in both the hardware and software worlds to be successful.

I have suffered through many a long night laboring over unforgiving components and their unintelligible documentation, trying to conjure up the exact micro-incantation that would bring the inanimate chips to life. From my mistakes I have learned a few secrets I wish I had known when I started. My purpose here is to present some some basic techniques of embedded systems programming by way of describing the implementation of a recently completed project, so that you who must now make the transition to this hybrid world might have at least some idea of where to start.

The S6000 Power Subsystem

The instrument I will use to illustrate the process of programming an embedded system is the S6000 Power Subsystem recently released by NH Research Inc. of Irvine, Ca. The S6000 provides modular AC and DC power and DC loads at precisely programmable levels and states for Automated Test Systems. Included in each production system is at least one and up to six chassis that communicate with the modules and each other via an RS422 serial protocol. Each of these chassis may contain up to six separate power devices.

There is one CPU board per system, which contains the main control firmware. The board is populated by a Motorola 68000 family microprocessor, RAM, EPROM, EEPROM, serial and GPIB interface chips, and timers. Interface is provided for an optional front panel keyboard and 40 character by eight line LCD display.

Defining The Project

In any software development project one must first determine exactly what is expected of the envisioned executable. From consulting the engineers responsible for the hardware design, I learned that the CPU firmware was required to

1. Provide the system with an orderly power on sequence.

2. Perform a power on self test and report the results via front panel indicator lights.

3. Determine the installed system configuration, compare it with the configuration stored in EEPROM, and report discrepancies via the IEEE 488 (GPIB) bus.

4. Command all installed modules to initialize and perform self test.

5. Enter a continuous loop whose purposes would be primarily to maintain system status information and supervisory control and secondarily to relay command data from the outside world to the modules and status information from the modules to the outside world.

The Development Environment

At the beginning of a project the programmer is faced with a number of fundamental design questions, some of which are faced by all software designers, and some which are unique to embedded systems. In the former category, a programming environment and its related tools must be selected, and in the latter it must be determined whether to use a real time kernel.

While embedded systems as recently as four or five years ago were written almost exclusively in assembly language for the host microprocessor, C has in recent years gained a great deal in popularity among embedded firmware designers. There is good reason for this increased acceptance. C can be easily adapted to embedded systems. It is versatile enough to make possible the development of low-level hardware drivers in assembly language where necessary, while executing the bulk of the code in a high-level language. This makes development and debugging of firmware faster and easier than was possible when writing exclusively in assembly language, while providing the speed and control available only with assembler.

Today there are many excellent C programming tools available from a variety of vendors for embedded programming. A rule of thumb to follow when investigating the choices is to select an environment that is either intended for or makes specific provision for firmware development. The environment chosen for the S6000 project was the Microtec ANSI C compiler for the Motorola 680X0 family microprocessors, but there are many others available for not only the Motorola microprocessors but those of other manufacturers as well. The firmware designer will find that for his purposes using one of these will be much less baffling than using a C environment created for a specific platform. In addition, such compilers generally come with Documentation directed specifically to problems encountered in implementing embedded firmware.

One question a firmware designer is bound to encounter is whether to use a real-time kernel. A real-time kernel is software developed by a third party that provides some of the basic features of an operating system such as task prioritization and management, interrupt handling, and basic I/O. There are several of these available for use on a variety of host microprocessors. The decision to use one must be weighed between their high cost (both in initial expense and in licensing fees) and the complexity of the firmware system to be implemented. In a relatively simple system such as the S6000, which must switch between only two or three tasks, the expense of the real time kernel is unjustified. On the other hand, in a system where tens or maybe hundreds of events must be managed, the cost of implementing algorithms to handle them might easily exceed the cost of a packaged real-time operating system. My own experience with these systems is that they are expensive in terms of RAM, ROM, and dollars, are impossible to troubleshoot, and that their vendors provide less than adequate technical support. In my opinion, for small to medium applications such as the S6000 they are not worth the trouble and expense. With that in mind, no kernel was included in the S6000 design.

The C/Assembly Language Interface

It is rare for a completed embedded system to be programmed entirely in C. Usually, assembly language is used at least to enter the vectors, and often to perform the system startup and device initialization routines. Why use assembly language instead of C? In many cases the code directly interfacing with the hardware is required to be compact and fast. Depending on the tools used and the expertise of the programmer, it may be possible to accomplish this in C, but in reality the compiler is rarely able to provide assembly language output as tight as the code you can produce yourself. Additionally, with assembly language you have full control over the hardware and the data presented to it, something you may not have, or that you must be very careful to achieve, using C.

When implementing a mixed language system, the assembly-language code is entered in separate files from the C code. Since compilation is generally a two-step process (from C to assembler, then from assembler to object code), the assembler simply skips the first part of the process when producing the object code. Your makefile should be set up to instruct the development tools how to process the assembly code. The linker will act on the object output of the assembler in exactly the same way as the output from the C compiler, because they are in exactly the same format.

Something to keep in mind when calling functions written in assembly from the C code is that the compiler will often add an underscore (or in some cases a dot) to the names of functions located in and called from a C program. For example, a call to the function write_char(), located in an assembly language file, will cause the instruction

jsr _write_char
to be contained in the C module's assembly-language output. If your routine in the assembly-language file is named write_char instead of _write_char, the linker will not know where to find it, and will generate an error. To overcome this difficulty, equate the name of the assembly-language routine with the name the compiler will generate to call it. For example, the line

_write_char equ write_char
at the end of the write_char routine would enable the linker to find the correct address. An even simpler method is to merely add the underscore to the routine's declaration in the assembly-language file.

There may be some instances when you would rather write some of the low-level access in C, and in this case it will be necessary to know how to perform direct memory reads and writes. The code fragment below illustrates the task of writing a character to a hardware address.

void func(char c)
{
char *ch;
ch = (char *) 0xFFFF0400;
*ch = c;
}
Here, a pointer variable named ch has been declared. This variable could have been a pointer of any type, depending on the width of the hardware port being written (for example, short * for a 16 bit port, or long * for one that is 32 bits wide). The first code line causes the variable ch to point to address 0xFFFF0400, an arbitrary address that could be any address in your memory map. The second code line causes the value of c to be written to the selected address.

Reading a memory address is performed following a similar process, as shown below.

char func()
{
char *ch;
ch = (char *) 0xFFFF0400;
return(*ch};
}
In this case the variable ch is declared and assigned as in the write operation. The last code line returns the 8-bit value located at address 0xFFFF0400 to the calling function.

The Memory Map

To aid in understanding the topics that follow it may be instructive to consider the memory map of the S6000 CPU board, which is representative of many 680X0 implementations. Figure 1 shows that the memory map can be divided into three sections: EPROM, RAM, and Hardware Ports. The EPROM, beginning at the lowest physical address, contains the initial vector table and all of the program code. The RAM, using all of the populated address space between the EPROM and the I/O ports, contains the final vector table, all of the static data, the ZEROVARS RAM section (which is used by the C runtime libraries), the heap, and the stack.

For those who may be unfamiliar with the operation of the heap and stack, the stack provides temporary storage during runtime (such as for local variables), and grows from the highest address in RAM down toward the end of the static data. The heap consists of all unused RAM space and grows up from the end of the static data area toward the stack. The heap is the dynamic memory area managed by such functions as malloc() and free(). Improper management of this area may result in the infamous and unrecoverable Heap/Stack Collision error, for obvious reasons.

The Vector Table

When programming an embedded system the first code to be written is normally the vector table. This table is a series of pointers in a specific location in the microprocessor's address space. The vector table defines the system's fundamental behavior. The beginning of the 680X0's vector table is always located at physical address 0x00000000. Microprocessors are designed in such a way that when certain events occur control is passed to code pointed to by one of the vectors. For example, after the power-on reset pulse is applied to the 68000 microprocessor's reset pin, the Program Counter (PC) will automatically be filled with the contents of address 0x00000004 (the second vector, since each vector is four bytes wide), and program execution will begin at that address. The microprocessor will know where the initial stack is located, and where to find code to execute in case of such events as bus error, address error, illegal instruction, and divide by zero, by values located at predetermined addresses in the vector-table. A description of the vector table contents can be found in the documentation provided with the microprocessor.

Although the vector table is located at address 0x00000000 in the physical address space and must initially be located in EPROM, many hardware designers redirect the vectors (other than the first two) to addresses in RAM. This allows the firmware designer to change the contents of the vector table on the fly, a feature which can be useful in a variety of cases, one of which is described later in this article.

How do you actually write the vector table? The easiest way is to write it in assembly language as shown in Listing 1. At the top of the assembly language file, statements instruct the assembler that certain names (of functions and interrupt service routines) are to be found elsewhere. With most 680X0 assemblers, this is done using the xref directive. Thus the assembler makes space for the vector table without actually knowing the actual data that will ultimately reside there. The linker will fill in the details at link time.

After listing all external references, the ORG statement is used to tell the assembler where to put the code it is about to encounter (0x00000000). Following the ORG statement, all the vectors are defined using the define long constant directive or its equivalent for the assembler in question. At compile time, this module is assembled to a .obj or equivalent file which the linker can include in the final executable.

The Application Startup Code

The startup code begins at the address pointed to in the Initial PC vector. The purpose of this code will vary between applications. In the S6000, the startup process performs various self tests and initializes the hardware ports and devices.

Because the hardware is in an unknown state at power on, it is wise before performing any other task to mask out external interrupts while initializing the system. This is accomplished by the first instruction in the startup code. The contents of the vector table are then copied to the first 400 (hex) addresses in RAM so that they can be changed as needed. Once this has been accomplished, a check is made to see how much RAM is installed in the system.

The CPU board is designed in such a way that the amount of RAM it can hold varies between 16K bytes and 256K bytes in 16K byte blocks. It is important to know the actual amount installed so that the address of the top of the stack can be registered in the microprocessor. Determining the amount of RAM present is accomplished by writing a value to the first address of each 16KB boundary. If RAM is present, the address counter is incremented by 16KB and the operation repeated until the maximum address is reached. If there is no RAM present at any address being written to, a bus error occurs, and code at the address entered in the Bus Error vector is executed. In the S6000 this interrupt service routine assigns the stack pointer in the SP (A7) register to the value found by this process and changes the bus error vector (now located in RAM) to the value of the real Bus Error interrupt service routine.

The startup code then performs a check of RAM and EEPROM memory, reporting any errors, and each of the initialization routines for the specific hardware devices are called. These routines might be provided by the hardware vendor or a third party, but most likely the system designer will have to create them. For the S6000, initialization and I/O routines were designed and implemented in assembly language for GPIB I/O, serial I/O, timer interrupt management, keyboard input, and display output.

Finally, the external interrupts are unmasked, and control is passed to routines that will initialize the C runtime environment.

The C Runtime Environment

When programming for a platform such as a PC, which conforms to a hardware standard and for which the operating system is known to be present, a C compiler will add code at compile time that defines an environment for the C program to work in. This code performs initialization of the heap and makes assumptions about where data is likely to come from (the keyboard) and go to (the screen). Using these suppositions the compiler provides elementary code fragments to handle memory allocation, keyboard input, and screen output (the devices stdin for standard input, stdout for standard output, and stderr for standard error). In an embedded system, these presumptions cannot be made, since there may not be a keyboard, screen, or heap. Therefore, in order to be able to use standard library functions such as malloc and printf the programmer is obliged either to provide these routines or modify those provided with the environment.

To take full advantage of the C run-time library it will be necessary to accomplish the following steps:

1. The heap pointer must be assigned to tell the compiler where the heap is located.

2. The ZEROVARS section must be cleared.

3. The standard input, output, and error devices must be initialized. Additionally, if file I/O is to be accomplished, the file structures must be assigned and initialized.

4. Code must be provided to access the devices which input and output characters, and the library routines must know where to find it.

To locate the initial heap pointer in the S6000 a four-byte variable is declared in a section called HEAP. Using almost any linker the order of the code and data sections can be specified. In the case of the Microtec tools this is done using a linker command file. In this file, the HEAP section is declared to be last, after the ZEROVARS section, since the highest addresses the linker will be concerned with are located in RAM. This will cause the address of the variable declared in this section to be located at the end of the static variables used by the program. Thus, the address of this variable is in reality the first address of the unassigned memory between the static data area and the stack, or the heap. This address is assigned to a constant variable (called ????HEAP in the Microtec environment) that tells the compiler where the dynamic memory area begins.

Static variables that need to be cleared at the beginning of the program are located in the ZEROVARS section. Among them are various heap-management pointers. In the Microtec environment, if the application intends to use dynamic memory allocation it is important that this area be filled with zeros, otherwise memory allocation will not work properly, probably with disastrous results. The ZEROVARS section is cleared as a matter of course in systems where the program startup code is provided by the compiler. Although the code to do this is usually available to the embedded system programmer, it is not automatically included in the program or invoked. It is up to the programmer to locate the code and incorporate it in the program. If the development environment selected is meant for embedded systems programming, the compiler's documentation should tell how to do this. If not, the code to do this will be among the startup code fragments provided with the compiler, and the system developer will need to peruse these files to find it and include it in the final executable. In the Microtec environment, this code is located in a file called ENTRY.S.

In the Microtec compiler for the 68000 family there is a file called CSYS68K.C which contains the code for performing the rest of the startup sequence. The _START routine in this file initializes the heap pointers for malloc() and opens the stdin, stdout, and stderr devices. If different compiler tools are being used, there should be a file or files that will perform these same functions, again, hopefully described in the documentation.

Additional skeleton functions must be provided for handling character input and output. The tasks consist of extracting characters from the proper hardware address and returning them to the calling function, or receiving characters for output from the library routines and actually writing their value to the proper address in memory. These routines are used by such library functions as printf and getch. Without them the library functions cannot work properly. It is possible to bypass the library functions, however this generally will make the programmer's job more difficult instead of less, because he will have to provide routines that perform tasks that have already been implemented.

The Main Program

At the end of the startup activities, control is finally passed to the program's main() function. The programmer may assume that if the above steps have all been done correctly and the code can be traced to the address of main(), writing the rest of the program will be very much the same as writing any other C program. Nevertheless, certain precautions should be adhered to which the programmer may not (but probably should) observe in other programming environments.

Make sure that no variable is used that has not been explicitly initialized. The startup code cannot be relied on to zero all of the data space unless the programmer specifically directs it. Also, initialize variables in the body of the code rather than in the declaration. Some complex variables that are initialized in the declaration may be considered to be constants and placed in ROM, an unhappy situation that can be difficult to troubleshoot.

A last word of advice to the would-be embedded systems programmer is to become familiar with the hardware involved. The idea of learning digital hardware is often distasteful to software engineers but the knowledge can be indispensable when debugging embedded code. All hardware devices are documented, some better than others, and most programmable chips contain sections aimed specifically at firmware developers. Because hardware designers are often unfamiliar with the devices themselves, and because errors frequently occur in preparing prototypes, becoming familiar with the hardware in question can save the embedded systems programmer countless hours of debugging a problem which may in the end be caused by improper hardware implementation.

Embedded systems programming, while perhaps not as glamorous as other aspects of software development, can be an interesting and rewarding experience. With the knowledge gained from this endeavor the software designer will become more articulate with the actual inner workings of computers, which will make even unrelated programming tasks easier to understand and implement.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.