Sie sind auf Seite 1von 11

RETURN

f e a t u r e NIGEL JONES

Efficient C Code for


Eight-Bit MCUs
The 8051, 68HC11, and PIC are popular MCUs, but they aren’t necessarily easy to
program. This article shows how the use of ANSI and compiler-specific constructs
can help generate tighter code.

G
etting the best possible performance out of digress with a discussion of the philosophy involved. The
an eight-bit microcontroller C compiler microcontrollers I mentioned are popular for reasons of
isn’t always easy. This article concentrates size, price, power consumption, peripheral mix, and so on.
mainly on those microcontrollers that were Notice that “ease of programming” is conspicuously missing
never designed to support high-level lan- from this list. Traditionally, these microcontrollers have
guages, such as the 8051 family, the 6800 been programmed in assembly language. In the last few
family (including the 68HCll), and the PIC line of micro- years, many vendors have recognized the desire of users to
controllers. Newer eight-bit machines such as the Philips increase their productivity, and have introduced C compil-
8051XA and the Atmel Atmega series were designed explic- ers for these machines—many of which are extremely good.
itly to support HLLs, and as such, may not need all the tech- However, it’s important to remember that no matter how
niques I describe here. good the compiler, the underlying hardware has severe lim-
My emphasis is not on algorithm design, nor does it itations. Thus, to write efficient C for these targets, it’s
depend on a specific microprocessor or compiler. Rather, I essential that we be aware of what the compiler can do eas-
describe general techniques that are widely applicable. In ily and what requires compiler heroics. In presenting these
many cases, these techniques work on larger machines, techniques, I have taken the attitude that I wish to solve a
although you may decide that the trade-offs involved aren’t problem by programming a microcontroller, and that the C
worthwhile. compiler is a tool, no different from an oscilloscope. In
Before jumping into the meat of the article, let’s briefly other words, C is a means to an end, and not an end in

66 NOVEMBER 1998 Embedded Systems Programming


To write efficient C for these targets, it is essential that we be
aware of what the compiler can do easily and what requires
compiler heroics.

itself. As a result, many of my com- This is not to say that I advocate Data type size
ments will seem heretical to the purists junking the entire ANSI standard. In the embedded world, knowing the
out there. Indeed, some of the essential require- underlying representation of the vari-
ments of the standard, such as func- ous data types is usually essential. I
ANSI C tion prototyping, are invaluable. have seen many discussions on this
The first step to writing a realistic C Rather, I take the view that one should topic, none of which has been particu-
program for an eight-bit machine is to use standard C as much as possible. larly satisfactory or portable. My pre-
dispense with the concept of writing However, when it interferes with solv- ferred solution is to include a file,
100% ANSI code. This concession is ing the problem at hand, do not hesi- <types.h>, an excerpt from which
necessary because I don’t believe it’s tate to bypass it. Does this interfere appears below:
possible, or even desirable, to write with making code portable and
100% ANSI code for any embedded reusable? Absolutely. But portable, #ifndef TYPES_H
system, particularly for eight-bit sys- reusable code that doesn’t get the job #define TYPES_H
tems. Some characteristics of eight-bit done isn’t much use. #include <limits.h>
systems that prevent ANSI compliance I’ve also noticed that every compil- /* Assign a built in data type to
are: er has a switch that strictly enforces BOOLEAN. This is compiler
ANSI C and disables all compiler specific */
• Embedded systems interact with extensions. I suspect that this is done #ifdef _C51_
hardware. ANSI C provides purely so that a vendor can claim typedef bit BOOLEAN
extremely crude tools for address- ANSI compliance, even though this #define FALSE 0
ing registers at fixed memory loca- feature is practically useless. I have #define TRUE 1
tions. Consequently, most compiler also observed that vendors who strong- #else
vendors offer language extensions ly emphasize their ANSI compliance typedef enum {FALSE=0, TRUE=1}
to overcome these limitations often produce inferior code (perhaps BOOLEAN;
• All nontrivial systems use inter- because the compiler has a generic #endif
rupts. ANSI C doesn’t have a stan- front end that is shared among multi- /* Assign a built in data type to
dard way of coding interrupt ser- ple targets) when compared to ven- type CHAR. This is an eight-bit
vice routines dors that emphasize their perfor- signed variable */
• ANSI C has various type promotion mance and language extensions. #if (SCHAR_MAX == 127)
rules that are absolute perfor- Enough on the ANSI standard— typedef char CHAR;
mance killers to an eight-bit let’s address specific actions that can #elif (SCHAR_MAX == 255)
machine. Unless your system has be taken to make your code run well /* Implies that by default chars
abundant CPU cycles, you will on an eight-bit microcontroller. The are unsigned */
quickly learn to defeat the ANSI most important, by far, is the choice of typedef signed char CHAR;
promotion rules data types. #else
• Many microcontrollers have multi- /* No eight bit data types */
ple memory spaces, which have to Data types #error Warning! Intrinsic data type
be specified in order to correctly Knowledge of the size of the underly- char is not eight bits!!
address the desired variable. Thus, ing data types, together with careful #endif
variable declarations tend to be data type selection, is essential for writ- /* Rest of the file goes here */
considerably more complex than ing efficient code on eight-bit #endif
on the typical PC application machines. Furthermore, understand-
• Many microcontrollers have no ing how the compiler handles expres- The concept is quite simple.
NANCE PATERNOSTER

hardware support for a C stack. sions involving your data types can Types.h includes the ANSI-required
Consequently, some compiler ven- make a considerable difference in file limits.h. It then explicitly tests
dors dispense with a stack-based your coding decisions. These topics each of the predefined data types for
architecture, in the process elimi- are discussed in the following para- the smallest type that matches signed
nating several key features of C graphs. and unsigned one-, eight-, 16-, and 32-

Embedded Systems Programming NOVEMBER 1998 67


eight-bit code
On many compilers, declaration of an enumerated type forces the
bit variables. The result is that my compiler to generate 16-bit signed code, which, as I’ve mentioned, is
data type UCHAR is guaranteed to be an extremely inefficient.
eight-bit unsigned variable, INT is
guaranteed to be a 16-bit signed vari-
able, and so forth. In this manner, the int is_positive(int a) the exception of some RISC proces-
following data types are defined: { sors. Furthermore, a strong case exists
BOOLEAN, CHAR, UCHAR, INT, UINT, LONG, (a>=0) ? return(1) : return (0); for doing this on all machines. Those
and ULONG. Several points are worth } of you who know Pascal are aware that
making: when declaring an integer variable, it’s
The better implementation is: possible, and normally desirable, to
• The definition of the BOOLEAN data specify the allowable range that the
type is difficult. Many eight-bit BOOLEAN is_positive(int a) integer can take on. For example:
machines directly support single- {
bit data types, and I wish to take (a>=0) ? return(TRUE) : return type loopindex = 0..9;
advantage of this if possible. (FALSE); var j loopindex;
Unfortunately, since ANSI is silent }
on this topic, it’s necessary to use Upon rereading the code later,
compiler-specific compilation On an eight-bit machine we can get you’ll have additional information
• Some compilers define a char as an a large performance boost by using concerning the intended use of the
unsigned quantity, such that if a the BOOLEAN return type because the variable. For our classical C code
signed eight-bit variable is compiler need only return a bit (typi- above, the variable int j may take on
required, one has to use the unusu- cally via the carry flag), vs. a 16-bit values of at least –32768 to +32767. For
al declaration signed char value stored in registers. The code is the case in which we have uchar j, we
• Note the use of the error function also more readable. inform others that this variable is
to force a compile error if I can’t Let’s take a look at a second exam- intended to have strictly positive val-
achieve my goal of having unam- ple. Consider the following code frag- ues over a restricted range. Thus, this
biguous definitions of BOOLEAN, ment that is littered throughout most simple change manages to combine
UCHAR, CHAR, UINT, INT, ULONG, and C programs: tighter code with improved maintain-
LONG ability—not a bad combination.
int j;
In all of the following examples, for(j=0; j<10; j++) Enumerated types
the types BOOLEAN, UCHAR, and so on will { The use of enumerated data types was
be used to specify unambiguously the : a welcome addition to ANSI C.
size of the variable being used. } Unfortunately, the standard calls for
the underlying data type of an enum
Data type selection This fragment produces horribly to be an int. Thus, on many compil-
There are two rules for data type selec- inefficient code on an 8051. The cor- ers, declaration of an enumerated type
tion on eight-bit machines: rect way to code this for eight-bit forces the compiler to generate 16-bit
machines is as follows: signed code, which, as I’ve mentioned,
• Use the smallest possible type to is extremely inefficient. This is unfor-
get the job done UCHAR j; tunate, especially as I have never seen
• Use an unsigned type if possible for (j=0; j<10; j++) an enumerated type list go over a few
{ dozen elements, such that it could eas-
The reasons for this are simply that : ily be fit in a UCHAR. To overcome this
many eight-bit machines have no } limitation, several options exist, none
direct support for manipulating any- of which is palatable:
thing more complicated than an The result is a huge boost in per-
unsigned eight-bit variable. However, formance because we are now using • Check your compiler documenta-
unlike large machines, eight-bitters an eight-bit unsigned variable (that tion. It may allow you to specify via
often provide direct support for can be manipulated directly) vs. a a command line switch that enu-
manipulation of bits. Thus, the fastest signed 16-bit quantity that will typical- merated types be put into the
integer types to use on an eight-bit ly be handled by a library call. Note smallest possible data type. The
machine are BOOLEAN and UCHAR. also that no penalty exists for coding downside is, of course, compiler-
Consider the typical C code: this way on most big machines (with dependant code

Embedded Systems Programming NOVEMBER 1998 69


eight-bit code

The integer promotion rules of ANSI C are probably the most heinous
crime committed against those of us who labor in the eight-bit world. Floating-point types
Floating-point arithmetic is required
in many applications. However, since
we’re normally dealing with real-world
• Accept the inefficiency as an you really want to do. The extent of data whose representation rarely goes
acceptable trade-off for readability the casting required seems to vary beyond 16 bits (a 20-bit atod on an
• Dispense with enumerated types among compiler vendors. As a result, I eight-bit machine is rare), the require-
and resort to lists of manifest con- tend to go overboard: ments for double-precision arithmetic
stants are tenuous, except in the strangest of
res = (CHAR)((CHAR)a + (CHAR)b); circumstances. Again, the ANSI peo-
Integer promotion rules ple have handicapped us by requiring
The integer promotion rules of ANSI With complex expressions, the result that any floating-point expression be
C are probably the most heinous can be hideous. promoted to double before execution.
crime committed against those of us Fortunately, a lot of compiler vendors
who labor in the eight-bit world. I have More integer promotion have done the sensible thing, and sim-
no doubt that the standard is quite rules ply defined doubles to be the same as
detailed in this area. However, the two A third integer promotion rule that is floats, so that this promotion is
most important rules in practice are often overlooked concerns expres- benign. Be warned, however, that
the following: sions that contain both signed and many reputable vendors have made a
unsigned integers. In this case, signed virtue out of providing a genuine dou-
• Any expression involving integral integers are promoted to unsigned ble-precision data type. The result is
types smaller than an int have all integers. Although this makes sense, it that unless you take great care, you
the variables automatically promot- can present problems in our eight-bit may end up computing values with
ed to int environment, where the unsigned ridiculous levels of precision, and pay-
• Any function call that passes an integer rules. For example: ing the price computationally. If
integral type smaller than an int you’re considering a compiler that
automatically promotes the vari- void demo(void) offers double-precision math, study
able to an int, if the function is not { the documentation carefully to ensure
prototyped. (Yet another reason for UINT a = 6; that there is some way of disabling the
using function prototyping) INT b = -20; automatic promotion. If there isn’t,
look for another compiler.
The key word here is automatically. (a+b > 6) ? puts(“More than 6”) While we’re on this topic, I’d like to
Unless you take explicit steps, the : puts(“Less than or equal to 6”); air a pet peeve of mine. Years ago,
compiler is unlikely to do what you } before decent compiler support for
want. Consider the following code eight-bit machines was available, I
fragment: If you run this program, you may would code in assembly language
be surprised to find that the output is using a bespoke floating-point library.
CHAR a,b,res; “More than 6.” This problem is a very This library was always implemented
: subtle one, and is even more difficult using three-byte floats, with a long
res = a+b; to detect when you use enumerated float consuming four bytes. I found
data types or other defined data that this was more than adequate for
The compiler will promote a and b types that evaluate to a signed inte- the real world. I’ve yet to find a com-
to integers, perform a 16-bit addition, ger data type. Using the result of a piler vendor that offers this as an
and then assign the lower eight bits of function call in an expression is also option. My guess is that the marketing
the result to res. Several ways around problematic. people insisted on a true ANSI float-
this problem exist. First, many compil- The good news is that in the ing-point library, the real world be
er vendors have seen the light, and embedded world, the percentage of damned. As a result, I can calculate
allow you to disable the ANSI auto- integral data types that must be signed hyperbolic sines on my 68HC11, but I
matic integer promotion rules. is quite low, thus the potential number can’t get the performance boost that
However, you’re then stuck with com- of expressions in which mixed types comes from using just a three-byte
piler-dependant code. occur is also low. The time to be cau- float.
Alternatively, you can resort to very tious is when reusing code that was Having moaned about the ANSI-
clumsy casting, and hope that the written by someone who didn’t believe induced problems, let’s turn to an
compiler’s optimizer works out what in unsigned data types. area in which ANSI has helped a lot.

70 NOVEMBER 1998 Embedded Systems Programming


eight-bit code

While the use of static functions is good structured programming


practice, you may also be surprised to learn that static functions can functions gives the compiler the
result in smaller and/or faster code. opportunity to use an ACALL where
otherwise it might use an LCALL.
The potential improvements are
I’m referring to the key words const structure is prevented from accessing even better, in which the compiler is
and volatile, which, together with it. This technique is an admission that smart enough to replace calls with
static, allow the production of better directly accessible variables are essen- jumps. For example:
code. tial to gaining adequate performance
on small machines. void fa(void)
Key words A few other potential benefits can {
The three key words static, volatile, result from declaring module level :
and const together allow one to write variables static (as opposed to leav- fb();
not only better code (in the sense of ing them global). Static variables, by }
information hiding and so forth) but definition, may only be accessed by a
also tighter code. specific set of functions. Consequently, static void fb(void)
the compiler and linker are able to {
Static variables make sensible choices concerning the :
When applied to variables, static has placement of the variables in memory. }
two primary functions. The first and For instance, with static variables, the
most common use is to declare a vari- compiler/linker may choose to place In this case, because function fb()
able that doesn’t disappear between all of the static variables in a module is the last line of function fa(), the
successive invocations of a function. in contiguous locations, thus increas- compiler can substitute a call with a
For example: ing the chances of various optimiza- jump. Since fb() is static, and the
tions, such as pointers being simply compiler knows its exact distance from
void func(void) incremented or decremented instead fa(), the compiler can use the shortest
{ of being reloaded. In contrast, global jump instruction. For the Dallas
static UCHAR state = 0; variables are often placed in memory DS80C320, this is an SJMP instruction
switch (state) locations that are designed to opti- (two bytes, three cycles) vs. an LCALL
{ mize the compiler’s hashing algo- (three bytes, four cycles).
: rithms, thus eliminating potential On a recent project of mine, rigor-
} optimizations. ous application of the static modifier
} to functions resulted in about a 1%
Static functions reduction in code size. When your
In this case, the use of static is manda- A static function is only callable by EPROM is 95% full (the normal case),
tory for the code to work. other functions within its module. a 1% reduction is most welcome!
The second use of static is to limit While the use of static functions is A final point concerning static vari-
the scope of a variable. A variable that good structured programming prac- ables and debugging: for reasons that
is declared static at the module level tice, you may also be surprised to learn I do not fully understand, with many
is accessible by all functions in the that static functions can result in in-circuit emulators that support
module, but by no one else. This is smaller and/or faster code. This is source-level debug, static variables
important because it allows us to gain possible because the compiler knows and/or automatic variables in static
all the performance benefits of global at compile time exactly what functions functions are not always accessible
variables, while severely limiting the can call a given static function. symbolically. As a result, I tend to use
well-known problems of globals. As a Therefore, the relative memory loca- the following construct in my project-
result, if I have a data structure which tions of functions can be adjusted such wide include file:
must be accessed frequently by a num- that the static functions may be called
ber of functions, I’ll put all of the using a short version of the call or #ifndef NDEBUG
functions into the same module and jump instruction. For instance, the #define STATIC
declare the structure static. Then all 8051 supports both an ACALL and an #else
of the functions that need to can LCALL op code. ACALL is a two-byte #define STATIC static
access the data without going through instruction, and is limited to a 2K #endif
the overhead of an access function, address block. LCALL is a three-byte
while at the same time, code that has instruction that can access the full I then use STATIC instead of stat-
no business knowing about the data 8051 address space. Thus, use of static ic to define static variables, so that

72 NOVEMBER 1998 Embedded Systems Programming


eight-bit code

while in debug mode, I can guarantee pen is either via an interrupt service will reload it every time. The case to
symbolic access to the variables. routine, or as a consequence of hard- watch out for is when compiler ven-
ware action (for instance, a serial port dors offer extensions for accessing
Volatile variables status register updates as a result of a absolute memory locations, such as
A volatile variable is one whose value character being received via the serial hardware registers. Sometimes these
may be changed outside the normal port). Most programmers are aware extensions have either an implicit or
program flow. In embedded systems, that the compiler will not attempt to an explicit declaration of volatility and
the two main ways that this can hap- optimize a volatile register, but rather sometimes they don’t. The point is to
fully understand what the compiler is
doing. If you do not, you may end up
accessing a volatile variable when you
don’t want to and vice versa. For exam-
ple, the popular 8051 compiler from
Keil offers two ways of accessing a spe-
cific memory location. The first uses a
language extension, _at_ , to specify
where a variable should be located.
The second method uses a macro such
as XBYTE[] to dereference a pointer.
The “volatility” of these two is differ-
ent. For example:

UCHAR status_register _at_ 0xE000;

This method is simply a much more


convenient way of accessing a specific
memory location. However, volatile is
not implied here. Thus, the following
code is unlikely to work:

while(status_register)
; /* Wait for status register to
clear */

Instead, one needs to use the follow-


ing declaration:

volatile UCHAR status_register


_at_ 0xE000;

The second method that Keil offers


is the use of macros, such as the XBYTE
macro, as in:

status_register = XBYTE[0xE000];

Here, however, examination of the


XBYTE macro shows that volatile is
assumed:

#define XBYTE ((unsigned char


volatile xdata*) 0)

(The xdata is a memory space qualifi-

74 NOVEMBER 1998 Embedded Systems Programming


eight-bit code
Declaring function parameters const whenever possible not only
er, which isn’t relevant to the discus- makes for better, safer code, but also has the potential for generating
sion here and may be ignored.) tighter code.
Thus, the code:

while(status_register) some form of indexed addressing. I recommend that you eschew the
; /* Wait for status register to Compared to immediate address- use of const variables on eight-bit
clear */ ing, this method is normally much machines, except in the following cer-
slower tain circumstances.
will work as you would expect in this
case. However, in the case in which
you wish to access a variable at a spe-
cific location that is not volatile, the
use of the XBYTE macro is potentially
inefficient.

Const variables
The keyword const, the most badly
named keyword in the C language,
does not mean constant! Rather, it
means “read only.” In embedded sys-
tems, there is a huge difference, which
will become clear.

Const variables vs.


manifest constants
Many texts recommend that instead of
using manifest constants, one should
use a const variable. For instance:

const UCHAR nos_atod_channels = 8;

instead of

#define NOS_ATOD_CHANNELS 8

The rationale for this approach is


that inside a debugger, you can exam-
ine a const variable (since it should
appear in the symbol table), whereas a
manifest constant isn’t accessible.
Unfortunately, on many eight-bit
machines you’ll pay a significant price
for this benefit. The two main costs
are:

• The compiler creates a genuine


variable in RAM to hold the vari-
able. On RAM-limited systems, this
can be a significant penalty
• Some compilers, recognizing that
the variable is const, will store the
variable in ROM. However, the vari-
able is still treated as a variable and
is accessed as such, typically using

Embedded Systems Programming NOVEMBER 1998 75


eight-bit code

We now come to an esoteric topic. Can a variable be both const and


volatile, and if so, what does that mean and how might you use it? offender is the 8051 family, with at
least five different memory spaces.
However, even the 68HC11 has at least
two different memory spaces (zero
volatile, and if so, what does that page and everything else), together
Const function parameters mean and how might you use it? The with the EEPROM, pointers to which
Declaring function parameters const answer is, of course, yes (why else typically require an address space
whenever possible not only makes for would it have been asked?), and it modifier.
better, safer code, but also has the should be used on any memory loca- The most obvious characteristic of
potential for generating tighter code. tion that can change unexpectedly typed data pointers is their inherent
This is best illustrated by an example: (hence the need for the volatile lack of portability. They also tend to
qualifier) and that is read-only (hence lead to some horrific data declara-
void output_string(CHAR *cp) the const). The most obvious example tions. For example, consider the fol-
{ of this is a hardware status register. lowing declaration from the
while (*cp) Thus, returning to the status_regis- Whitesmiths 68HC11 compiler:
putchar(*cp++); ter example above, a better declara-
} tion for our status register is: @dir INT * @dir
zpage_ptr_to_zero_page;
void demo(void) const volatile UCHAR status_reg-
{ ister _at_ 0xE000; This declares a pointer to an INT.
char *str = “Hello, world”; However, both the pointer and its
Typed data pointers object reside in the zero page (as indi-
output_string(str); We now come to another area in cated by the Whitesmith extension,
which a major trade-off exists between @dir). If you were to add a const qual-
if (‘H’ == str[0]) { writing portable code and writing effi- ifier or two, such as:
some_function(); cient code—namely the use of typed
} data pointers, which are pointers that @dir const INT * @dir const con-
} are constrained in some way with stant_zpage_ptr_to_constant_zero_p
respect to the type and/or size of age_data;
In this case, there is no guarantee memory that they can access. For
that output_string() will not modify example, those of you who have pro- then the declarations can quickly
our original string, str. As a result, the grammed the x86 architecture are become quite intimidating. Conse-
compiler is forced to perform the test undoubtedly familiar with the concept quently, you may be tempted to simply
in demo(). If instead, output_string is of using the __near and __far modi- ignore the use of typed pointers.
correctly declared as follows: fiers on pointers. These are examples Indeed, coding an application on a
of typed data pointers. Often the mod- 68HC11 without ever using a typed
void output_string(const char *cp) ifier is implied, based on the memory data pointer is quite possible.
{ model being used. Sometimes the However, by doing so the application’s
while (*cp) modifier is mandatory, such as in the performance will take an enormous
putchar(*cp++); prototype of an interrupt handler: hit because the zero page offers con-
} siderably faster access than the rest of
void __interrupt __far memory.
then the compiler knows that out- cntr_int7(); This area is so critical to perfor-
put_string() cannot modify the origi- mance that all hope of portability is
nal string str, and as a result it can The requirement for the near and lost. For example, consider two lead-
dispense with the test and invoke far modifiers comes about from the ing 8051 compiler vendors, Keil and
some_function() unconditionally. segmented x86 architecture. In the Tasking. Keil supports a three-byte
Thus, I strongly recommend liberal embedded eight-bit world, the situa- generic pointer that may be used to
use of the const modifier on function tion is often far more complex. point to any of the 8051 address
parameters. Microcontrollers typically require spaces, together with typed data point-
typed data pointers because they offer ers that are strictly limited to a specific
Const volatile variables a number of disparate memory spaces, data space. Keil strongly recommends
We now come to an esoteric topic. each of which may require the use of the use of typed data pointers, but
Can a variable be both const and different addressing modes. The worst doesn’t require it. By contrast, Tasking

76 NOVEMBER 1998 Embedded Systems Programming


eight-bit code

The most obvious characteristic of typed data pointers is their inher-


ent lack of portability. They also tend to lead to some horrific data al use of an assert macro. For example:
declarations. void Lcd_Write_Str(UCHAR row,
UCHAR column, CHAR *str, UCHAR
attr)
takes the attitude that generic pointers These limitations notwithstanding, {
are so horribly inefficient that it man- it’s possible to gain the benefits of the assert (row < MAX_ROW);
dates the use of typed pointers (an assert() macro on even the smallest assert (column < MAX_COLUMN);
argument to which I am extremely systems if you’re prepared to take a assert (attr <
sympathetic). pragmatic approach. ALLOWABLE_ATTRIBUTES);
To get a feel for the magnitude of Before I discuss possible imple- assert (str != NULL);
the difference, consider the following mentations, mentioning why assert()
code, intended for use on an 8051: is important (even in embedded sys- /* The real work of the driver
tems) is worthwhile. Over the years, goes here */
void main(void) I’ve built up a library of drivers to var- }
{ ious pieces of hardware such as LCDs,
UCHAR array[16]; /* array is in ADCs, and so on. These drivers typi- This is a practical approach if
the data space by default */ cally require various parameters to be you’re prepared to redefine the assert
UCHAR data *ptr = array; /* Note passed to them. For example, an LCD macro. The level of resources in your
use of data qualifier */ driver that displays a text string on a system will control the sophistication
UCHAR i; panel would expect the row, the col- of this macro, as shown in the exam-
umn, a pointer to the string, and per- ples below.
for(i=0; i<16; i++) haps an attribute parameter. When
*ptr++ = i; writing the driver, it is obviously Assert #1
} important that the passed parameters This example assumes that you have
are correct. One way of ensuring this is no spare RAM, no spare port pins, and
Using a generic pointer, this code to include code such as this: virtually no ROM to spare. In this case,
requires 571 cycles and 88 bytes. Using assert.h becomes:
a typed data pointer, it needs just 196 void Lcd_Write_Str(UCHAR row,
cycles and 52 bytes. (The memory sizes UCHAR column, CHAR *str, UCHAR #ifndef assert_h
include the startup code, and the exe- attr) #define assert_h
cution times are just those for execut- { #ifndef NDEBUG
ing main()). row &= MAX_ROW;
With these sorts of performance column &= MAX_COLUMN; #define assert(expr) \
increases, I recommend always using attr &= ALLOWABLE_ATTRIBUTES; if (expr) {\
explicitly typed pointers, and paying while (1);\
the price in loss of portability and if (NULL == str) }
readability. return; #else
#define assert(expr)
Use of assert /* The real work of the driver #endif
The assert() macro is commonly goes here */ #endif
used on PC platforms, but almost }
never used on small embedded sys- Here, if the assertion fails, we sim-
tems. There are several reasons for This code clips the parameters to ply enter an infinite loop. The only
this: allowable ranges, checks for a null utility of this case is that, assuming
pointer assignment, and so on. you’re running a debug session on an
• Many reputable compiler vendors However, on a functioning system, ICE, you will eventually notice that the
don’t bother to supply an assert executing this code every time the dri- system is no longer running. In which
macro ver is invoked is extremely costly. But if case, breaking the emulator and
• Vendors that do supply the macro the code is discarded, reuse of the dri- examining the program counter will
often provide it in an almost useless ver in another project becomes a lot give you a good indication of which
form more difficult because errors in the assertion failed. As a possible refine-
• Most embedded systems don’t sup- driver invocation are tougher to ment, if your system is interrupt-dri-
port a stderr to which the error detect. ven, inserting a “disable all interrupts”
may be printed The preferred solution is the liber- command prior to the while(1) may

78 NOVEMBER 1998 Embedded Systems Programming


eight-bit code

Recursion is a wonderful technique that solves certain problems in an


elegant manner. It has no place on an eight-bit microcontroller. • Recursion relies on a stack-based
approach to passing variables.
Many small machines have no
hardware support for a stack.
be necessary, just to ensure that the here */ Consequently, either the compiler
system’s failure is obvious. #define INTERRUPTS_OFF()/* Put will simply refuse to support reen-
expression for interrupts off trancy, or else it will resort to a soft-
Assert #2 here */ ware stack in order to solve the
This case is the same as assert #1, problem, resulting in dreadful
except that in #2 you have a spare port #ifndef NDEBUG code quality
pin on the microcontroller to which extern char error_buf[80]; • Recursion relies on a “virtual stack”
an error LED is attached. This LED is that purportedly has no real mem-
lit if an error occurs, thus giving you #define assert(expr) \ ory constraints. How many small
instant feedback that an assertion has if (expr) {\ machines can realistically support
failed. Assert.h now becomes: ERROR_LED_ON();\ virtual memory?
INTERRUPTS_OFF();\
#ifndef assert_h sprintf(error_buf,”Assert If you find yourself using recursion
#define assert_h failed: “ #expr “ (file %s on a small machine, I respectfully sug-
#define ERROR_LED_ON() /* Put line %d)\n”, gest that you are either a) doing some-
expression for turning LED on __FILE__, (int) __LINE__ );\ thing really weird, or b) you don’t
here */ while (1);\ understand the sum total of the con-
#define INTERRUPTS_OFF() /* Put } straints with which you’re working. If
expression for interrupts off #else it is the former, then please contact
here */ #define assert(expr) me, as I will be fascinated to see what
#endif you are doing.
#ifndef NDEBUG #endif
Variable length argument
#define assert(expr) \ Obviously, this requires that you lists
if (expr) {\ define error_buffer[80] somewhere You should avoid variable length argu-
ERROR_LED_ON();\ else in your code. ment lists because they too rely on a
INTERRUPTS_OFF();\ I don’t expect that these three stack-based approach to passing vari-
while (1);\ examples will cover everyone’s needs. ables. What about sprintf() and its
} Rather, I hope they give you some cousins, you all cry? Well, if possible,
#else ideas on how to create your own assert you should consider avoiding the use
#define assert(expr) macros to get the maximum debug- of these library functions. The reasons
#endif ging information within the con- for this are as follows:
#endif straints of your embedded system.
• If you use sprintf(), take a look at
Assert #3 Heretical comments the linker output and see how
This example builds on assert #2. But So far, all of my suggestions have been much library code it pulls in. On
in this case, we have sufficient RAM to about actively doing things to improve one of my compilers, sprintf(),
define an error message buffer, into the code quality. Now, let’s address without floating-point support,
which the assert macro can sprintf() those areas of the C language that consumes about 1K. If you’re using
the exact failure. While debugging on should be avoided, except in highly a masked micro with a code space
an ICE, if a permanent watch point is unusual circumstances. For some of of 8K, this penalty is huge
associated with this buffer, then break- you, the suggestions that follow will • On some compilers, use of
ing the ICE will give you instant infor- border on heresy. sprintf() implies the use of a float-
mation on where the failure occurred. ing-point library, even if you never
Assert.h for this case becomes: Recursion use the library. Consequently, the
Recursion is a wonderful technique code penalty quickly becomes
#ifndef assert_h that solves certain problems in an ele- enormous
#define assert_h gant manner. It has no place on an • If the compiler doesn’t support a
#define ERROR_LED_ON() /* Put eight-bit microcontroller. The reasons stack, but rather passes variables in
expression for turning LED on for this are quite simple: registers or fixed memory loca-

80 NOVEMBER 1998 Embedded Systems Programming


eight-bit code
If you’re using recursion on a small machine, I respectfully suggest
that you are either a) doing something really weird, or b) you don’t
understand the sum total of the constraints with which you’re working.

tions, then use of variable length tems typically have none of these char-
argument functions forces the acteristics. Therefore, I think that the
compiler to reserve a healthy block use of dynamic memory allocation on
of memory simply to provide space these targets is silly. First, the amount
for variables that you may decide to of memory available is fixed, and is
use. For instance, if your compiler typically known at design time. Thus
vendor assumes that the maximum static allocation of all the required
number of arguments you can pass and/or available memory may be
is 10, then the compiler will reserve done at compile time.
40 bytes (assuming four bytes per Second, the execution time over-
longest intrinsic data type) head of malloc(), free(), and so on is
not only quite high, but also variable,
Fortunately, many vendors are depending on the degree of memory
aware of these issues and have taken fragmentation.
steps to mitigate the effects of using Third, use of malloc(), free(), and
sprintf(). Notwithstanding these so on consumes valuable EPROM
actions, taking a close look at your space. And lastly, dynamic memory
code is still worthwhile. For instance, allocation is fraught with danger (wit-
writing my own wrstr() and wrint() ness the recent series from P.J. Plauger
functions (to ouput strings and ints on garbage collection in the January
respectively) generated half the code 1998, March 1998, and April 1998
of using sprintf. Thus, if all you need issues of ESP).
to format are strings and base 10 inte- Consequently, I strongly recom-
gers, then the roll-your-own approach mend that you not use dynamic mem-
is beneficial (while still being ory allocation on small systems.
portable).
Final thoughts
Dynamic memory allocation I have attempted to illustrate how judi-
When you’re programming an appli- cious use of both ANSI constructs and
cation for a PC, using dynamic memo- compiler-specific constructs can help
ry allocation makes sense. The charac- generate tighter code on small micro-
teristics of PCs that permit and/or controllers. Often, though, these
require dynamic memory allocation improvements come at the expense of
include: portability and/or readability. If you
are in the fortunate position of being
• When writing an application, you able to use less efficient code, then
may not know how much memory you can ignore these suggestions. If,
will be available. Dynamic alloca- however, you are severely resource-
tion provides a way of gracefully constrained, then give a few of these
handling this problem techniques a try. I think you’ll be
• The PC has an operating system, pleasantly surprised. esp
which provides memory allocation
services Nigel Jones received a degree in engineering
• The PC has a user interface, such from Brunel University, London. He has
that if an application runs out of worked in the industrial control field, both
memory, it can at least tell the user in the U.K. and the U.S. Presently he is
and attempt a relatively graceful working as a consultant, with the majority
shutdown of his work concentrated on underwater
instrumentation. He can be reached at
In contrast, small embedded sys- NAJones@compuserve.com.

Embedded Systems Programming NOVEMBER 1998 83