SLTF Consulting
Technology with Business Sense

 Home | Bio | Contact Us | Site Map


 

Good programmers don't need C++ to think for them

Scott Rosenthal
July, 1992

Programming embedded systems has come a long way since the days of the earliest microprocessors. The complexity of today's embedded programs rivals that of yesterday's minicomputer and mainframe programs. Yet, it is still extremely common for a single person to design and to implement an embedded system using a haphazard methodology. The present day systems are so complex that you must make a choice; wait for the development of the ultimate language or tool that will accept any scatterbrained ideas and turn them into useful software, or take it upon yourself and craft software that's flexible, understandable, expandable, and extensible. There is no correct language choice today for embedded systems; every one of them has strengths as well as drawbacks. In my last article I went through some of the drawbacks using C++: memory usage, compilation time, and execution speed. On a workstation or even a PC the methodology gains might easily outweigh the drawbacks. An embedded system, though, is sufficiently different that C++ at this stage is probably not the panacea managers and programmers alike are looking for. The answer for embedded systems is ORGANIZATION. C, or assembly language, can produce beautiful code for an embedded system or it can produce a nightmare -- the choice is the designer's.

What Does Embedded Programming Take

Today's programmers are not necessarily embedded system's programmers. In my too many years in this business, I have only come across a handful of people that I would consider being complete, well-rounded embedded programmers. An embedded programmer has to be different from a mainframe or minicomputer programmer. Theirs is not a world of bits, bytes, cycle times, and limited address space. An embedded programmer must be intimately aware of the hardware platform(s) that the application is being developed for. Questions such as "what happens if I write a 16 bit word to an odd address?" and "what happens to my interrupt masks when an interrupt occurs?" have no relevance to most programmers outside the embedded world. In my company's Humor File (which every embedded programmer's sanity requires) we have an ad clipped from a local newspaper that says in bold letters:

YOU TOO CAN BE A PROGRAMMER IN SIX MONTHS!

This might be true in COBOL or RPG but not in programming embedded systems!

What does it take to program embedded systems? I have expressed my opinions about this in previous columns but I think it needs further repeating in a concise manner:

  1. You must have the knowledge to know that you do not know everything and that you must be flexible and secure enough in your abilities to try new ideas.
  2. You must have a streak of perfectionism in your soul that permeates your everyday activities.
  3. You must have a broad-based educational background including science, math, and English.
  4. You must have the ability to effectively listen and expand on ideas and concepts that are presented to you.
  5. You must realize that job security comes from a successful design and not from obtuse code or chaotic methodology.

A Never Ending Learning Experience

Programming embedded systems is an ongoing, never-ending learning experience. I have been programming embedded systems since (I hate to admit it) 1974 starting with the Intel 8008, I produce more than 50,000 lines of debugged program code every year, and every week I still discover another programming trick to reduce the execution time, the program size, the amount of code needed, or the complexity of an algorithm. My programming is constantly evolving, sometimes with the industry and sometimes against it, but the point is, keep what works, keep evolving, and don't become that dinosaur.

Software Standards Are A Requirement

Before you write software, you should have a document describing how to write this software. At my company we use a document I wrote called the "Software Standards and Development Manual" (SSM). This document describes how to develop program code and projects. The depth of the document is enough that there is little room for misinterpretation but still enough room for innovation. Not very surprisingly, some of C++'s ideas have been part of this manual since its inception in 1981.

Has this ever happened to you: A potential client came into my office, took out a scroll of paper, raised it above his head, and let it unravel to the floor. I asked him what it was and he said it was a program that someone else had developed for him and could I make some massive changes to it. The program had no comments, no subroutines or functions, nothing that would indicate to me how to begin changing it. Of course, the cost to modify this trash would be astronomical. His tears were real when he said that it had cost him more than $50,000.00 for its development.

Too often people programming (not programmers) create monolithic monuments that are unstable, unextendable, and useless past the first day that they got the program working. In Figure 1 are some definitions from our SSM that describe a program's organization. You can see that these definitions are very similar to the C++ idea of classes. Typically, I try to keep the length of any function to maybe 50 lines. This is approximately the length of one sheet of paper. I have found that functions longer than a page generally do not express just one idea.

I also limit the module length to about 400 lines. There are two reasons for this. The first is that trying to find the right function to edit in more than 16 to 20 screen pages is just impractical. More importantly, I limit the module length because this will reduce compilation time. Most of the time spent recompiling is because you have only changed a small part of the program. Why should the compiler have to work so hard with so much other code, just for the small change that you have done? Compilation speeds will go up, coffee break times will go down, you will be more productive, and that translates to more job security. There are very few reasons for having long modules.

The Language Is Just A Tool

In all the hoopla over new languages, it is sometimes easy to forget that a computer language is just a tool that the programmer craftsman uses to develop a program. It is a shortcut so that programmers do not have to twiddle switches and bits. It has evolved into a way for programmers to abstractly express an idea and to have the computer act in that way. The point is that the language is just a tool; it does not create a good programmer but it can sure point out the flaws in a bad one. Give the same saw to two carpenters. One carpenter can produce a beautiful work of art and the other one can produce a shoddy mess. Same tool but different skill levels. Why should programming be different?

A Better C

In my last column (May 1992) I talked about the drawbacks of using C++ in designing an embedded system. To repeat, C++ today is too memory hungry, too slow at executing, and does not exist yet for enough processors to seriously consider it for most embedded applications. Using a C++ preprocessor also is not an option since the program debugging would be horrendous. The answer is to use C, but to apply it in a smarter way than you have probably done so in the past.

Just because C++ is not acceptable for embedded systems does not mean that programmers should discard its ideas. Encapsulation, whether enforceable by the language or by the programmer, is a powerful tool that every programmer should use regardless of the language. I designed a battery-powered medical instrument back in 1983 that used an 80C85 microprocessor. The program was 5,000 lines of assembly language and used almost no global data. I controlled all access to the data through what I at the time called Table Drivers. Each type of data (analogous to structures) had its respective table driver that included functions for initializing, storing, and retrieving data. I rigidly enforced the system structure and after more than tripling the size of the program to 17,000 lines, the program was still readable and expandable. The encapsulation of the data with its code was a significant reason that the program was still manageable.

Another benefit of this encapsulation has to do with data representation. In the same instrument we developed a memory limitation problem (doesn't everyone?). The solution I adopted was to save the floating point data as three byte floats instead of four bytes. I modified the table driver to store four bytes as three and when any function in the program read the data back, the table driver converted the stored data from three bytes to four. Nothing else in the program had to change even though I had made a basic change to the data type. Another save by encapsulation. (Figure 2.)

The C language makes encapsulation even easier. The ability to group unlike data types into a cohesive unit using the structure assignment is one of the more important features of C. Unlike C++, structures in C can only group data. Your programs should not have pieces of global data floating all around it. Grouping your data in structures will reduce program global variables. This also reduces programmer confusion and increases reliability and expandability.

An Encapsulation Example In C

I have lately gotten into the habit of treating my structures as if they were streams. A stream, in case the computerese definition escapes you, is a way of looking at I/O. For example, a disk file is a continuum of bytes or characters. The data can flow into or out of your program from this one I/O path. This flow of data is a stream. With streams in C, you can open a connection to it (fopen()), close the connection to it (fclose()), read a character or byte from it (fgetc()), or store a byte to it (fputc()). Recognize the encapsulation? I use this technique for much of my data handling in my programs. Not only does it encapsulate the data, it also creates a commonality in data handling that's easy to understand even years later. A simple example of this is a FIFO handling system.

Embedded systems use FIFO (First In First Out) buffers in many places. A simple example is a keypad buffer. With each key press, the FIFO gets the key codes deposited into it. As the program has time to process the keys, it will remove the oldest key code from the buffer. The FIFO acts as temporary storage allowing the two asynchronous events (key pressing and key processing) to work independently at their respective speeds. Other examples of FIFO usage in embedded systems include serial communications and printing.

The typical embedded program deals with each of these FIFOs as unique functions; since each FIFO needs its own set of pointers and data areas, typically the programmer writes separate program code for each instance. Replicating code is a waste of the programmer's time and of the program code space. Instead, you can encapsulate the FIFO information in a structure and use one set of functions for maintaining any number of FIFOs.

Figure 3 is a header file that I use for my applications that require FIFOs. As you can see there is a new type I define named TFIFO. It is a structure containing all the information needed to implement a FIFO.

Functions control access to the FIFO much like the previously mentioned table drivers. Before using these functions though, you must create and initialize the FIFO by calling function fifo_open. Arguments to this function are the FIFO length and the size in bytes of each element. Function fifo_open then returns a pointer to a new FIFO. This pointer is of type TFIFO and contains the information and data for this FIFO. Likewise, you can just as easily open another FIFO by calling fifo_open again. Adding data to the FIFO is as simple as calling fifo_write with the TFIFO pointer from the open call and a pointer to the data. To read data from the FIFO, call fifo_read, again with the TFIFO pointer. If no data is available, the function returns a NULL pointer. Finally, when you do not need the FIFO anymore, call fifo_close, which frees the memory used by the FIFO. Very simple, very straightforward, and you can just as easily handle with these functions FIFOs of integers, floats, and doubles with no special requirements in the rest of the program code. Again, encapsulation is hiding the data storing and retrieving. One module contains all the FIFO handling functions and it is reusable and both processor and project independent.


Figure 1:

PROGRAM: A logically complete collection of modules working together to solve a problem.

MODULE: An algorithm or section of code implementing one idea or function. Ideally, a module consists of one disk file, or if the file is too large, in several related files.

FUNCTION: A subprogram, subroutine, or other related program portion that completely expresses one idea or abstraction.


Figure 2:

#define SIZE 3 /* size of each stored number */
unsigned char storage[SIZE * 100]; /* 100 entries in table */
/*
Function get_double
This will return a pointer to the double number at the specified index.
The number is stored as a 3 byte float. It is first converted from
3 bytes to a 4 byte float. Then it is converted to a double and the
address of the double is returned.
*/
double *get_double(int index)
{	
union {		
float temp;		
unsigned char buf[sizeof(double)];	
} f;	
static double d;	
memset(f.buf, sizeof(f.temp), 0); /* zero out all unused locations */	
memcpy(&f.buf[sizeof(double) - SIZE], &storage[index * SIZE], SIZE);	
d = f.temp; /* convert from float to double */	
return &d; /* return a pointer to the number */
}
/*
Function save_double
This will save a double number in the storage array as a 3 byte floating
point number. The number is first converted from a double to a float.
Then, the 3 MSBs (Most Significant Bytes) are copied into the storage
array.
*/
void save_double(int index, double *dptr)
{	
float f;	
f = *dptr; /* convert the number to float */	
memcpy(&storage[index * SIZE], (char *)&f + sizeof(float) - SIZE, SIZE);
}
                

Figure 3:

#ifndef __FIFO_H
#define __FIFO_H
/* fifo structure */
typedef struct {	
void *start; /* fifo data storage start location */	
void *temp; /* temporary buffer for returning data */	
int head; /* index to where new data goes */	
int tail; /* index to where oldest data comes from */	
int full; /* fifo full flag */	
int length; /* fifo maximum length */	
int size; /* fifo element size in bytes */
} TFIFO;
/* fifo.c */
TFIFO *fifo_open(int length, int size);
void fifo_close(FIFO *ptr);
void fifo_reset(FIFO *ptr, void *data);
int fifo_length(FIFO *ptr);
void *fifo_read(FIFO *ptr);
void *fifo_write(FIFO *ptr, void *in);
void *fifo_write_any(FIFO *ptr, void *in, int n);
void *fifo_read_any(FIFO *ptr, int n);
#endif 
             


About Us | What We Do | SSM | MiCOS | Search | Designs | Articles

Copyright © 1998-2014 SLTF Consulting, a division of SLTF Marine LLC. All rights reserved.