Efficient C Code for Eight-Bit MCUs
itself. As a result, ma ny of my com-ments will seem heretical to the purists out there.
ANSI C
The first step to writing a rea listic C program for an eight-bit machine is to dispense with the concept of writing 100% ANSI code. This concession is necessa ry beca use I don’t believe it’s possible, or even desira ble, to write 100% ANSI code for a ny embedded system, pa rticula rly for eight-bit sys-tems. Some characteristics of eight-bit systems that prevent ANSI compliance are:
?Embedded systems intera ct with
h a rdw a re. ANSI C provides
extremely crude tools for a ddress-ing registers at fixed memory loca-tions. Consequently, most compiler vendors offer la ngua ge extensions to overcome these limitations ?All nontrivia l systems use inter-rupts. ANSI C doesn’t have a stan-da rd wa y of coding interrupt ser-vice routines
?ANSI C has various type promotion rules tha t a re a bsolute perfor-m a nce killers to a n eight-bit ma chine. Unless your system ha s
bunda nt CPU cycles, you will quickly lea rn to defea t the ANSI promotion rules
?Many microcontrollers have multi-ple memory spaces, which ha ve to be specified in order to correctly address the desired variable. Thus, va ria ble decla ra tions tend to be considera bly more complex tha n on the typical PC application ?Ma ny microcontrollers ha ve no ha rdwa re support for a C sta ck.
Consequently, some compiler ven-dors dispense with a sta ck-ba sed
a rchitecture, in the process elimi-
nating several key features of C
This is not to sa y tha t I a dvoca te
junking the entire ANSI sta nda rd.
Indeed, some of the essential require-
ments of the sta nda rd, such a s func-
tion prototyping, a re inv a lu a ble.
Rather, I take the view that one should
use sta nda rd C a s much a s possible.
However, when it interferes with solv-
ing the problem at hand, do not hesi-
ta te to bypa ss it. Does this interfere
with m a king code port a ble a nd
reusa ble? Absolutely. But porta ble,
reusable code that doesn’t get the job
done isn’t much use.
I’ve also noticed that every compil-
er ha s a switch tha t strictly enforces
ANSI C a nd disa bles a ll compiler
extensions. I suspect that this is done
purely so tha t a vendor ca n cla im
ANSI complia nce, even though this
fea ture is pra ctica lly useless. I ha ve
also observed that vendors who strong-
ly empha size their ANSI complia nce
often produce inferior code (perhaps
beca use the compiler ha s a generic
front end that is shared among multi-
ple ta rgets) when compa red to ven-
dors tha t empha size their perfor-
mance and language extensions.
Enough on the ANSI sta nda rd—
let’s a ddress specific a ctions tha t ca n
be taken to make your code run well
on a n eight-bit microcontroller. The
most important, by far, is the choice of
data types.
Data types
Knowledge of the size of the underly-
ing da ta types, together with ca reful
data type selection, is essential for writ-
ing efficient code on eight-bit
ma chines. Furthermore, understa nd-
ing how the compiler handles expres-
sions involving your da ta types ca n
ma ke a considera ble difference in
your coding decisions. These topics
a re discussed in the following pa ra-
gra phs.
Data type size
In the embedded world, knowing the
underlying representation of the vari-
ous da ta types is usua lly essentia l. I
ha ve seen ma ny discussions on this
topic, none of which has been particu-
la rly sa tisfa ctory or porta ble. My pre-
ferred solution is to include a file,
appears below:
#i f n d e f T Y P E S_H
#d e f i n e T Y P E S_H
#i n c l u d e
/*A s s i g n a b u i l t i n d a t a t y p e t o
B O O L E A N.T h i s i s c o m p i l e r
s p e c i f i c*/
#i f d e f_C51_
t y p e d e f b i t B O O L E A N
#d e f i n e F A L S E0
#d e f i n e T R U E1
#e l s e
t y p e d e f e n u m{F A L S E=0,T R U E=1}
B O O L E A N;
#e n d i f
/*A s s i g n a b u i l t i n d a t a t y p e t o
t y p e C H A R.T h i s i s a n e i g h t-b i t
s i g n e d v a r i a b l e*/
#i f(S C H A R_M A X==127)
t y p e d e f c h a r C H A R;
#e l i f(S C H A R_M A X==255)
/*I m p l i e s t h a t b y d e f a u l t c h a r s
a r e u n s i g n e d*/
t y p e d e f s i g n e d c h a r C H A R;
#e l s e
/*N o e i g h t b i t d a t a t y p e s*/
#e r r o r W a r n i n g!I n t r i n s i c d a t a t y p e
c h a r i s n o t e i g h t b i t s!!
#e n d i f
/*R e s t o f t h e f i l e g o e s h e r e*/
#e n d i f
The concept is quite simple.
T y p e s.h includes the ANSI-required
file l i m i t s.h. It then explicitly tests
each of the predefined data types for
the smallest type that matches signed
and unsigned one-, eight-, 16-, and 32-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.
bit va ria bles. The result is tha t my data type U C H A R is guaranteed to be an eight-bit unsigned va ria ble, I N T is guaranteed to be a 16-bit signed vari-
able, and so forth. In this manner, the following da ta types a re defined: B O O L E A N, C H A R, U C H A R, I N T, U I N T, L O N G, and U L O N G. Severa l points a re worth ma king:
?The definition of the B O O L E A N data type is difficult. Ma ny eight-bit ma chines directly support single-bit da ta types, a nd I wish to ta ke
a dv a nt a ge of this if possible.
Unfortunately, since ANSI is silent on this topic, it’s necessa ry to use compiler-specific compilation ?Some compilers define a c h a r as an unsigned qua ntity, such tha t if a signed eight-bit v a ri a ble is required, one has to use the unusu-al declaration s i g n e d c h a r ?Note the use of the error function to force a compile error if I ca n’t
a chieve my goa l of ha ving una m-
biguous definitions of B O O L E A N, U C H A R, C H A R, U I N T, I N T, U L O N G, a nd L O N G
In a ll of the following exa mples, the types B O O L E A N, U C H A R, and so on will be used to specify unambiguously the size of the variable being used. Data type selection
There are two rules for data type selec-tion on eight-bit machines:
?Use the sma llest possible type to get the job done
?Use an unsigned type if possible
The reasons for this are simply that ma ny eight-bit ma chines ha ve no direct support for ma nipula ting a ny-thing more complic ted th n n unsigned eight-bit va ria ble. However, unlike la rge ma chines, eight-bitters often provide direct support for manipulation of bits. Thus, the fastest integer types to use on a n eight-bit m chine re B O O L E A N nd U C H A R. Consider the typical C code:i n t i s_p o s i t i v e(i n t a)
{
(a>=0)?r e t u r n(1):r e t u r n(0);
}
The better implementation is:
B O O L E A N i s_p o s i t i v e(i n t a)
{
(a>=0)? r e t u r n(T R U E):r e t u r n
(F A L S E);
}
On an eight-bit machine we can get
a la rge performa nce boost by using
the B O O L E A N return type beca use the
compiler need only return a bit (typi-
ca lly via the ca rry fla g), vs. a 16-bit
va lue stored in registers. The code is
also more readable.
Let’s take a look at a second exam-
ple. Consider the following code frag-
ment that is littered throughout most
C programs:
i n t j;
f o r(j=0;j<10;j++)
{
:
}
This fra gment produces horribly
inefficient code on an 8051. The cor-
rect wa y to code this for eight-bit
machines is as follows:
U C H A R j;
f o r(j=0;j<10;j++)
{
:
}
The result is a huge boost in per-
forma nce beca use we a re now using
a n eight-bit unsigned va ria ble (tha t
ca n be ma nipula ted directly) vs. a
signed 16-bit quantity that will typical-
ly be ha ndled by a libra ry ca ll. Note
a lso tha t no pena lty exists for coding
this wa y on most big ma chines (with
the exception of some RISC proces-
sors. Furthermore, a strong case exists
for doing this on all machines. Those
of you who know Pascal are aware that
when declaring an integer variable, it’s
possible, a nd norma lly desira ble, to
specify the a llowa ble ra nge tha t the
integer can take on. For example:
t y p e l o o p i n d e x=0..9;
v a r j l o o p i n d e x;
Upon rerea ding the code la ter,
you’ll ha ve a dditiona l informa tion
concerning the intended use of the
va ria ble. For our cla ssica l C code
above, the variable i n t j may take on
values of at least –32768 to +32767. For
the case in which we have u c h a r j, we
inform others tha t this va ria ble is
intended to ha ve strictly positive va l-
ues over a restricted range. Thus, this
simple cha nge ma na ges to combine
tighter code with improved maintain-
ability—not a bad combination.
Enumerated types
The use of enumerated data types was
welcome ddition to ANSI C.
Unfortuna tely, the sta nda rd ca lls for
the underlying da ta type of a n enum
to be a n i n t. Thus, on ma ny compil-
ers, declaration of an enumerated type
forces the compiler to generate 16-bit
signed code, which, as I’ve mentioned,
is extremely inefficient. This is unfor-
tunate, especially as I have never seen
an enumerated type list go over a few
dozen elements, such that it could eas-
ily be fit in a U C H A R. To overcome this
limitation, several options exist, none
of which is palatable:
?Check your compiler documenta-
tion. It may allow you to specify via
a comma nd line switch tha t enu-
mera ted types be put into the
sma llest possible da ta type. The
downside is, of course, compiler-
dependant code
?Accept the inefficiency a s a n acceptable trade-off for readability ?Dispense with enumera ted types and resort to lists of manifest con-
stants
Integer promotion rules
The integer promotion rules of ANSI
C a re proba bly the most heinous
crime committed a ga inst those of us
who labor in the eight-bit world. I have
no doubt tha t the sta nda rd is quite
detailed in this area. However, the two
most importa nt rules in pra ctice a re
the following:
?Any expression involving integra l types sma ller tha n a n i n t ha ve a ll the variables automatically promot-ed to i n t
?Any function ca ll tha t pa sses a n integra l type sma ller tha n a n i n t
a utoma tica lly promotes the va ri-
able to an i n t,if the function is not
prototyped. (Yet another reason for
using function prototyping)
The key word here is automatically.
Unless you ta ke explicit steps, the
compiler is unlikely to do wha t you
wa nt. Consider the following code
fra gment:
C H A R a,b,r e s;
:
r e s=a+b;
The compiler will promote a and b
to integers, perform a 16-bit addition,
and then assign the lower eight bits of
the result to r e s. Several ways around
this problem exist. First, many compil-
er vendors ha ve seen the light, a nd
a llow you to disa ble the ANSI a uto-
m a tic integer promotion rules.
However, you’re then stuck with com-
piler-dependant code.
Alternatively, you can resort to very
clumsy ca sting, a nd hope tha t the
compiler’s optimizer works out wha t you rea lly wa nt to do. The extent of
the ca sting required seems to va ry
among compiler vendors. As a result, I
tend to go overboard:
r e s=(C H A R)((C H A R)a+(C H A R)b);
With complex expressions, the result
can be hideous.
More integer promotion
rules
A third integer promotion rule that is
often overlooked concerns expres-
sions tha t conta in both signed a nd
unsigned integers. In this case, signed
integers a re promoted to unsigned
integers. Although this makes sense, it
can present problems in our eight-bit
environment, where the unsigned
integer rules. For example:
v o i d d e m o(v o i d)
{
U I N T a=6;
I N T b=-20;
(a+b>6)? p u t s(“M o r e t h a n6”)
:p u t s(“L e s s t h a n o r e q u a l t o6”);
}
If you run this program, you may
be surprised to find that the output is
“More than 6.” This problem is a very
subtle one, and is even more difficult
to detect when you use enumera ted
da ta types or other defined da ta
types tha t eva lua te to a signed inte-
ger da ta type. Using the result of a
function call in an expression is also
problematic.
The good news is tha t in the
embedded world, the percenta ge of
integral data types that must be signed
is quite low, thus the potential number
of expressions in which mixed types
occur is also low. The time to be cau-
tious is when reusing code tha t wa s
written by someone who didn’t believe
in unsigned data types.
Floating-point types
Floa ting-point a rithmetic is required
in ma ny a pplica tions. However, since
we’re normally dealing with real-world
data whose representation rarely goes
beyond 16 bits (a 20-bit a t o d on a n
eight-bit machine is rare), the require-
ments for double-precision arithmetic
are tenuous, except in the strangest of
circumstances. Again, the ANSI peo-
ple have handicapped us by requiring
tha t a ny floa ting-point expression be
promoted to double before execution.
Fortunately, a lot of compiler vendors
have done the sensible thing, and sim-
ply defined doubles to be the same as
flo ts, so th t this promotion is
benign. Be wa rned, however, tha t
many reputable vendors have made a
virtue out of providing a genuine dou-
ble-precision da ta type. The result is
tha t unless you ta ke grea t ca re, you
ma y end up computing va lues with
ridiculous levels of precision, and pay-
ing the price comput tion lly. If
you’re considering a compiler tha t
offers double-precision ma th, study
the documentation carefully to ensure
that there is some way of disabling the
a utoma tic promotion. If there isn’t,
look for another compiler.
While we’re on this topic, I’d like to
a ir a pet peeve of mine. Yea rs a go,
before decent compiler support for
eight-bit ma chines wa s a va ila ble, I
would code in a ssembly la ngua ge
using a bespoke floating-point library.
This libra ry wa s a lwa ys implemented
using three-byte floa ts, with a long
floa t consuming four bytes. I found
tha t this wa s more tha n a dequa te for
the real world. I’ve yet to find a com-
piler vendor tha t offers this a s a n
option. My guess is that the marketing
people insisted on a true ANSI floa t-
ing-point libra ry, the rea l world be
da mned. As a result, I ca n ca lcula te
hyperbolic sines on my 68HC11, but I
ca n’t get the performa nce boost tha t
comes from using just a three-byte
floa t.
Ha ving moa ned a bout the ANSI-
induced problems, let’s turn to a n
a rea in which ANSI ha s helped a lot.
I’m referring to the key words c o n s t a nd v o l a t i l e, which, together with s t a t i c, allow the production of better code.
Key words
The three key words s t a t i c, v o l a t i l e, and c o n s t together allow one to write not only better code (in the sense of information hiding and so forth) but also tighter code.
Static variables
When applied to variables, s t a t i c has two prima ry functions. The first a nd most common use is to declare a vari-a ble tha t doesn’t disa ppea r between successive invoca tions of a function. For example:
v o i d f u n c(v o i d)
{
s t a t i c U C H A R s t a t e=0;
s w i t c h(s t a t e)
{
:
}
}
In this case, the use of s t a t i c is manda-tory for the code to work.
The second use of s t a t i c is to limit the scope of a variable. A variable that is declared s t a t i c at the module level is a ccessible by a ll functions in the module, but by no one else. This is important because it allows us to gain all the performance benefits of global va ria bles, while severely limiting the well-known problems of globa ls. As a result, if I have a data structure which must be accessed frequently by a num-ber of functions, I’ll put a ll of the functions into the sa me module a nd declare the structure s t a t i c. Then all of the functions tha t need to ca n access the data without going through the overhea d of a n a ccess function, while at the same time, code that has no business knowing a bout the da ta structure is prevented from a ccessing
it. This technique is an admission that
directly accessible variables are essen-
tial to gaining adequate performance
on small machines.
A few other potentia l benefits ca n
result from decla ring module level
va ria bles s t a t i c(a s opposed to lea v-
ing them globa l). Sta tic va ria bles, by
definition, may only be accessed by a
specific set of functions. Consequently,
the compiler a nd linker a re a ble to
make sensible choices concerning the
placement of the variables in memory.
For instance, with static variables, the
compiler/linker ma y choose to pla ce
all of the static variables in a module
in contiguous loca tions, thus increa s-
ing the cha nces of va rious optimiza-
tions, such a s pointers being simply
incremented or decremented instea d
of being reloaded. In contrast, global
variables are often placed in memory
loca tions tha t a re designed to opti-
mize the compiler’s ha shing a lgo-
rithms, thus elimina ting potentia l
optimiza tions.
Static functions
A sta tic function is only ca lla ble by
other functions within its module.
While the use of sta tic functions is
good structured progra mming pra c-
tice, you may also be surprised to learn
tha t sta tic functions ca n result in
sma ller a nd/or fa ster code. This is
possible beca use the compiler knows
at compile time exactly what functions
c n c ll given st tic function.
Therefore, the rela tive memory loca-
tions of functions can be adjusted such
that the static functions may be called
using a short version of the ca ll or
jump instruction. For insta nce, the
8051 supports both an ACALL and an
LCALL op code. ACALL is a two-byte
instruction, a nd is limited to a 2K
a ddress block. L C A L L is a three-byte
instruction tha t ca n a ccess the full
8051 address space. Thus, use of static
functions gives the compiler the
opportunity to use a n ACALL where
otherwise it might use an LCALL.
The potentia l improvements a re
even better, in which the compiler is
sma rt enough to repla ce ca lls with
jumps. For example:
v o i d f a(v o i d)
{
:
f b();
}
s t a t i c v o i d f b(v o i d)
{
:
}
In this case, because function f b()
is the la st line of function f a(), the
compiler ca n substitute a ca ll with a
jump. Since f b()is s t a t i c, a nd the
compiler knows its exact distance from
f a(), the compiler can use the shortest
jump instruction. For the D ll s
DS80C320, this is a n S J M P instruction
(two bytes, three cycles) vs. an LCALL
(three bytes, four cycles).
On a recent project of mine, rigor-
ous a pplica tion of the sta tic modifier
to functions resulted in a bout a 1%
reduction in code size. When your
EPROM is 95% full (the normal case),
a 1% reduction is most welcome!
A final point concerning static vari-
ables and debugging: for reasons that
I do not fully understa nd, with ma ny
in-circuit emul a tors th a t support
source-level debug, sta tic va ria bles
a nd/or a utoma tic va ria bles in sta tic
functions a re not a lwa ys a ccessible
symbolically. As a result, I tend to use
the following construct in my project-
wide i n c l u d e file:
#i f n d e f N D E B U G
#d e f i n e S T A T I C
#e l s e
#d e f i n e S T A T I C s t a t i c
#e n d i f
I then use S T A T I C instead of s t a t-
i c to define sta tic va ria bles, so tha t
while in debug mode, I can guarantee symbolic access to the variables. Volatile variables
A volatile variable is one whose value ma y be cha nged outside the norma l progra m flow. In embedded systems, the two ma in wa ys tha t this ca n ha p-pen is either via a n interrupt service
routine, or as a consequence of hard-
ware action (for instance, a serial port
status register updates as a result of a
character being received via the serial
port). Most progra mmers a re a wa re
tha t the compiler will not a ttempt to
optimize a volatile register, but rather
will reloa d it every time. The ca se to
wa tch out for is when compiler ven-
dors offer extensions for a ccessing
a bsolute memory loca tions, such a s
ha rdwa re registers. Sometimes these
extensions ha ve either a n implicit or
an explicit declaration of volatility and
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 popula r 8051 compiler from
Keil offers two ways of accessing a spe-
cific memory location. The first uses a
la ngua ge extension, _a t_,to specify
where a va ria ble should be loca ted.
The second method uses a macro such
as X B Y T E[]to dereference a pointer.
The “vola tility” of these two is differ-
ent. For example:
U C H A R s t a t u s_r e g i s t e r_a t_0x E000;
This method is simply a much more
convenient way of accessing a specific
memory location. However, v o l a t i l e is
not implied here. Thus, the following
code is unlikely to work:
w h i l e(s t a t u s_r e g i s t e r)
;/*W a i t f o r s t a t u s r e g i s t e r t o
c l e a r*/
Instea d, one needs to use the follow-
ing declaration:
v o l a t i l e U C H A R s t a t u s_r e g i s t e r
_a t_0x E000;
The second method that Keil offers
is the use of macros, such as the X B Y T E
macro, as in:
s t a t u s_r e g i s t e r=X B Y T E[0x E000];
Here, however, exa mina tion of the
X B Y T E ma cro shows tha t v o l a t i l e is
a ssumed:
#d e f i n e X B Y T E((u n s i g n e d c h a r
v o l a t i l e x d a t a*)0)
(The x d a t a is a memory space qualifi-
er, which isn’t relevant to the discus-sion here and may be ignored.) Thus, the code:
w h i l e(s t a t u s_r e g i s t e r)
;/*W a i t f o r s t a t u s r e g i s t e r t o c l e a r*/
will work a s you would expect in this ca se. However, in the ca se in which you wish to access a variable at a spe-cific loca tion tha t is not vola tile, the use of the X B Y T E ma cro is potentia lly inefficient.
Const variables
The keyword c o n s t, the most ba dly na med keyword in the C la ngua ge, does not mea n constant! Ra ther, it mea ns “rea d 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 ma nifest consta nts, one should use a c o n s t variable. For instance:
c o n s t U C H A R n o s_a t o d_c h a n n e l s=8;
instead of
#d e f i n e N O S_A T O D_C H A N N E L S8
The ra tiona le for this a pproa ch is that inside a debugger, you can exam-ine a c o n s t va ria ble (since it should appear in the symbol table), whereas a m nifest const nt isn’t ccessible. Unfortun a tely, on m a ny eight-bit machines you’ll pay a significant price for this benefit. The two ma in costs a re:
?The compiler crea tes a genuine va ria ble in RAM to hold the va ri-able. On RAM-limited systems, this can be a significant penalty ?Some compilers, recognizing tha t the variable is c o n s t,will store the variable in ROM. However, the vari-able is still treated as a variable and is a ccessed a s such, typica lly using some form of indexed a ddressing.
Compa red to immedia te a ddress-
ing, this method is normally much
slower
I recommend tha t you eschew the
use of c o n s t va ria bles on eight-bit
machines, except in the following cer-
tain circumstances.
Const function parameters Decla ring function pa ra meters c o n s t whenever possible not only makes for better, sa fer code, but a lso ha s the potential for generating tighter code. This is best illustrated by an example: v o i d o u t p u t_s t r i n g(C H A R*c p)
{
w h i l e(*c p)
p u t c h a r(*c p++);
}
v o i d d e m o(v o i d)
{
c h a r*s t r=“H e l l o,w o r l d”;
o u t p u t_s t r i n g(s t r);
i f(‘H’==s t r[0]){
s o m e_f u n c t i o n();
}
}
In this case, there is no guarantee that o u t p u t_s t r i n g()will not modify our original string, s t r. As a result, the compiler is forced to perform the test in d e m o(). If instead, o u t p u t_s t r i n g is correctly declared as follows:
v o i d o u t p u t_s t r i n g(c o n s t c h a r*c p) {
w h i l e(*c p)
p u t c h a r(*c p++);
}
then the compiler knows tha t o u t-p u t_s t r i n g()cannot modify the origi-na l string s t r, a nd a s a result it ca n dispense with the test a nd invoke s o m e_f u n c t i o n()unconditiona lly. Thus, I strongly recommend libera l use of the c o n s t modifier on function parameters.
Const volatile variables
We now come to a n esoteric topic. Ca n a va ria ble be both c o n s t a nd v o l a t i l e, a nd if so, wha t does tha t
mean and how might you use it? The
a nswer is, of course, yes (why else
would it ha ve been a sked?), a nd it
should be used on a ny memory loca-
tion tha t ca n cha nge unexpectedly
(hence the need for the v o l a t i l e
qualifier) and that is read-only (hence
the c o n s t). The most obvious example
of this is a ha rdwa re sta tus register.
Thus, returning to the s t a t u s_r e g i s-
t e r exa mple a bove, a better decla ra-
tion for our status register is:
c o n s t v o l a t i l e U C H A R s t a t u s_r e g-
i s t e r_a t_0x E000;
Typed data pointers
We now come to a nother a rea in
which a major trade-off exists between
writing portable code and writing effi-
cient code—na mely the use of typed
data pointers, which a re pointers tha t
a re constra ined in some wa y with
respect to the type a nd/or size of
memory tha t they ca n a ccess. For
exa mple, those of you who ha ve pro-
gra mmed the x86 a rchitecture a re
undoubtedly familiar with the concept
of using the __n e a r a nd __f a r modi-
fiers on pointers. These are examples
of typed data pointers. Often the mod-
ifier is implied, based on the memory
model being used. Sometimes the
modifier is mandatory, such as in the
prototype of an interrupt handler:
v o i d__i n t e r r u p t__f a r
c n t r_i n t7();
The requirement for the near and
fa r modifiers comes a bout from the
segmented x86 a rchitecture. In the
embedded eight-bit world, the situa-
tion is often f r more complex.
Microcontrollers typic a lly require
typed data pointers because they offer
a number of disparate memory spaces,
each of which may require the use of
different addressing modes. The worst
offender is the 8051 fa mily, with a t
lea st five different memory spa ces.
However, even the 68HC11 has at least
two different memory spa ces (zero
pa ge a nd everything else), together
with the EEPROM, pointers to which
typica lly require a n a ddress spa ce
modifier.
The most obvious characteristic of
typed da ta pointers is their inherent
la ck of porta bility. They a lso tend to
lea d to some horrific da ta decla ra-
tions. For exa mple, consider the fol-
lowing decl a r a tion from the
Whitesmiths 68HC11 compiler:
@d i r I N T*@d i r
z p a g e_p t r_t o_z e r o_p a g e;
This decla res a pointer to a n I N T.
However, both the pointer a nd its
object reside in the zero page (as indi-
ca ted by the Whitesmith extension,
@d i r). If you were to add a c o n s t qual-
ifier or two, such as:
@d i r c o n s t I N T*@d i r c o n s t c o n-
s t a n t_z p a g e_p t r_t o_c o n s t a n t_z e r o_p
a g e_d a t a;
then the decla ra tions ca n quickly
become quite intimida ting. Conse-
quently, you may be tempted to simply
ignore the use of typed pointers.
Indeed, coding a n a pplica tion on a
68HC11 without ever using a typed
d a t a pointer is quit
e possible.
However, by doing so the application’s
performa nce will ta ke a n enormous
hit beca use the zero pa ge offers con-
siderably faster access than the rest of
memory.
This a rea is so critica l to perfor-
ma nce tha t a ll hope of porta bility is
lost. For exa mple, consider two lea d-
ing 8051 compiler vendors, Keil a nd
Ta sking. Keil supports a three-byte
generic pointer tha t ma y be used to
point to a ny of the 8051 a ddress
spaces, together with typed data point-
ers that are strictly limited to a specific
data space. Keil strongly recommends
the use of typed da ta pointers, but
doesn’t require it. By contrast, Tasking
takes the attitude that generic pointers are so horribly inefficient that it man-da tes the use of typed pointers (a n a rgument to which I a m extremely sympa thetic).
To get a feel for the magnitude of the difference, consider the following code, intended for use on an 8051:
v o i d m a i n(v o i d)
{
U C H A R a r r a y[16];/*a r r a y i s i n t h e d a t a s p a c e b y d e f a u l t*/
U C H A R d a t a*p t r=a r r a y;/*N o t e u s e o f d a t a q u a l i f i e r*/
U C H A R i;
f o r(i=0;i<16;i++)
*p t r++=i;
}
Using a generic pointer, this code requires 571 cycles and 88 bytes. Using a typed data pointer, it needs just 196 cycles and 52 bytes. (The memory sizes include the startup code, and the exe-cution times are just those for execut-ing m a i n()).
With these sorts of performa nce increa ses, I recommend a lwa ys using explicitly typed pointers, a nd pa ying the price in loss of porta bility a nd rea da bility.
Use of assert
The a s s e r t()ma cro is commonly used on PC pla tforms, but a lmost never used on sma ll embedded sys-tems. There a re severa l rea sons for this:
?Ma ny reputa ble compiler vendors don’t bother to supply a n a s s e r t ma cro
?Vendors tha t do supply the ma cro often provide it in an almost useless form
?Most embedded systems don’t sup-port a s t d e r r to which the error may be printed
These limita tions notwithsta nding,
it’s possible to gain the benefits of the
a s s e r t()ma cro on even the sma llest
systems if you’re prepa red to ta ke a
pragmatic approach.
Before I discuss possible imple-
mentations, mentioning why a s s e r t()
is importa nt (even in embedded sys-
tems) is worthwhile. Over the yea rs,
I’ve built up a library of drivers to var-
ious pieces of hardware such as LCDs,
ADCs, a nd so on. These drivers typi-
cally require various parameters to be
passed to them. For example, an LCD
driver tha t displa ys a text string on a
pa nel would expect the row, the col-
umn, a pointer to the string, and per-
ha ps a n a ttribute pa ra meter. When
writing the driver, it is obviously
important that the passed parameters
are correct. One way of ensuring this is
to include code such as this:
v o i d L c d_W r i t e_S t r(U C H A R r o w,
U C H A R c o l u m n,C H A R*s t r,U C H A R
a t t r)
{
r o w&=M A X_R O W;
c o l u m n&=M A X_C O L U M N;
a t t r&=A L L O W A B L E_A T T R I B U T E S;
i f(N U L L==s t r)
r e t u r n;
/*T h e r e a l w o r k o f t h e d r i v e r
g o e s h e r e*/
}
This code clips the pa ra meters to
a llowa ble ra nges, checks for a null
pointer a ssignment, a nd so on.
However, on a functioning system,
executing this code every time the dri-
ver is invoked is extremely costly. But if
the code is discarded, reuse of the dri-
ver in a nother project becomes a lot
more difficult beca use errors in the
driver invoc tion re tougher to
detect.
The preferred solution is the liber-
al use of an assert macro. For example:
v o i d L c d_W r i t e_S t r(U C H A R r o w,
U C H A R c o l u m n,C H A R*s t r,U C H A R
a t t r)
{
a s s e r t(r o w a s s e r t(c o l u m n a s s e r t(a t t r< A L L O W A B L E_A T T R I B U T E S); a s s e r t(s t r!=N U L L); /*T h e r e a l w o r k o f t h e d r i v e r g o e s h e r e*/ } This is a pra ctica l a pproa ch if you’re prepared to redefine the assert macro. The level of resources in your system will control the sophistica tion of this ma cro, a s shown in the exa m- ples below. Assert #1 This exa mple a ssumes tha t you ha ve no spare RAM, no spare port pins, and virtually no ROM to spare. In this case, assert.h becomes: #i f n d e f a s s e r t_h #d e f i n e a s s e r t_h #i f n d e f N D E B U G #d e f i n e a s s e r t(e x p r)\ i f(e x p r){\ w h i l e(1);\ } #e l s e #d e f i n e a s s e r t(e x p r) #e n d i f #e n d i f Here, if the assertion fails, we sim- ply enter a n infinite loop. The only utility of this ca se is tha t, a ssuming you’re running a debug session on an ICE, you will eventually notice that the system is no longer running. In which c se, bre king the emul tor nd exa mining the progra m counter will give you a good indica tion of which a ssertion fa iled. As a possible refine- ment, if your system is interrupt-dri- ven, inserting a “disable all interrupts” comma nd prior to the w h i l e(1)ma y be necessa ry, just to ensure tha t the system’s failure is obvious. Assert #2 This ca se is the sa me a s a ssert #1, except that in #2 you have a spare port pin on the microcontroller to which an error LED is attached. This LED is lit if a n error occurs, thus giving you instant feedback that an assertion has failed. Assert.h now becomes: #i f n d e f a s s e r t_h #d e f i n e a s s e r t_h #d e f i n e E R R O R_L E D_O N()/*P u t e x p r e s s i o n f o r t u r n i n g L E D o n h e r e*/ #d e f i n e I N T E R R U P T S_O F F()/*P u t e x p r e s s i o n f o r i n t e r r u p t s o f f h e r e*/ #i f n d e f N D E B U G #d e f i n e a s s e r t(e x p r)\ i f(e x p r){\ E R R O R_L E D_O N();\ I N T E R R U P T S_O F F();\ w h i l e(1);\ } #e l s e #d e f i n e a s s e r t(e x p r) #e n d i f #e n d i f Assert #3 This example builds on assert #2. But in this case, we have sufficient RAM to define a n error messa ge buffer, into which the assert macro can s p r i n t f() the exact failure. While debugging on an ICE, if a permanent watch point is associated with this buffer, then break-ing the ICE will give you instant infor-mation on where the failure occurred. Assert.h for this case becomes: #i f n d e f a s s e r t_h #d e f i n e a s s e r t_h #d e f i n e E R R O R_L E D_O N()/*P u t e x p r e s s i o n f o r t u r n i n g L E D o n h e r e*/ #d e f i n e I N T E R R U P T S_O F F()/*P u t e x p r e s s i o n f o r i n t e r r u p t s o f f h e r e*/ #i f n d e f N D E B U G e x t e r n c h a r e r r o r_b u f[80]; #d e f i n e a s s e r t(e x p r)\ i f(e x p r){\ E R R O R_L E D_O N();\ I N T E R R U P T S_O F F();\ s p r i n t f(e r r o r_b u f,”A s s e r t f a i l e d:“#e x p r“(f i l e%s l i n e%d)\n”, __F I L E__,(i n t)__L I N E__);\ w h i l e(1);\ } #e l s e #d e f i n e a s s e r t(e x p r) #e n d i f #e n d i f Obviously, this requires tha t you define e r r o r_b u f f e r[80]somewhere else in your code. I don’t expect tha t these three examples will cover everyone’s needs. Ra ther, I hope they give you some ideas on how to create your own assert ma cros to get the ma ximum debug- ging informa tion within the con- straints of your embedded system. Heretical comments So far, all of my suggestions have been about actively doing things to improve the code qua lity. Now, let’s a ddress those a rea s of the C la ngua ge tha t should be a voided, except in highly unusua l circumsta nces. For some of you, the suggestions tha t follow will border on heresy. Recursion Recursion is a wonderful technique that solves certain problems in an ele- ga nt ma nner. It ha s no pla ce on a n eight-bit microcontroller. The reasons for this are quite simple: ?Recursion relies on a sta ck-ba sed a ppro a ch to p a ssing v a ri a bles. Ma ny sma ll ma chines ha ve no h rdw re support for st ck. Consequently, either the compiler will simply refuse to support reen- trancy, or else it will resort to a soft- wa re sta ck in order to solve the problem, resulting in dre dful code quality ?Recursion relies on a “virtual stack” that purportedly has no real mem- ory constra ints. How ma ny sma ll ma chines ca n rea listica lly support virtual memory? If you find yourself using recursion on a small machine, I respectfully sug- gest that you are either a) doing some- thing rea lly weird, or b) you don’t understa nd the sum tota l of the con- stra ints with which you’re working. If it is the former, then plea se conta ct me, as I will be fascinated to see what you are doing. Variable length argument lists You should avoid variable length argu- ment lists beca use they too rely on a sta ck-ba sed a pproa ch to pa ssing va ri- a bles. Wha t a bout s p r i n t f()a nd its cousins, you a ll cry? Well, if possible, you should consider avoiding the use of these library functions. The reasons for this are as follows: ?If you use s p r i n t f(), take a look at the linker output a nd see how much libra ry code it pulls in. On one of my compilers, s p r i n t f(), without flo a ting-point support, consumes about 1K. If you’re using a masked micro with a code space of 8K, this penalty is huge ?On some compilers, use of s p r i n t f()implies the use of a float- ing-point library, even if you never use the libra ry. Consequently, the code pen a lty quickly becomes enormous ?If the compiler doesn’t support a stack, but rather passes variables in registers or fixed memory loca- tions, then use of va ria ble length rgument functions forces the compiler to reserve a healthy block of memory simply to provide space for variables that you may decide to use. For instance, if your compiler vendor assumes that the maximum number of arguments you can pass is 10, then the compiler will reserve 40 bytes (a ssuming four bytes per longest intrinsic data type) Fortun tely, m ny vendors re a wa re of these issues a nd ha ve ta ken steps to mitiga te the effects of using s p r i n t f(). Notwithst a nding these a ctions, ta king a close look a t your code is still worthwhile. For insta nce, writing my own w r s t r()a nd w r i n t() functions (to ouput strings a nd i n t s respectively) genera ted ha lf the code of using s p r i n t f. Thus, if all you need to format are strings and base 10 inte-gers, then the roll-your-own approach is benefici a l (while still being port a ble). Dynamic memory allocation When you’re progra mming a n a ppli-cation for a PC, using dynamic memo-ry allocation makes sense. The charac-teristics of PCs tha t permit a nd/or require dyna mic memory a lloca tion include: ?When writing a n a pplica tion, you may not know how much memory will be a va ila ble. Dyna mic a lloca-tion provides a wa y of gra cefully handling this problem ?The PC ha s a n opera ting system, which provides memory a lloca tion services ?The PC ha s a user interfa ce, such tha t if a n a pplica tion runs out of memory, it can at least tell the user a nd a ttempt a rela tively gra ceful shutdown In contra st, sma ll embedded sys-tems typically have none of these char-acteristics. Therefore, I think that the use of dynamic memory allocation on these targets is silly. First, the amount of memory a va ila ble is fixed, a nd is typica lly known a t design time. Thus sta tic a lloca tion of a ll the required nd/or a va ila ble memory ma y be done at compile time. Second, the execution time over-head of m a l l o c(), f r e e(), and so on is not only quite high, but also variable, depending on the degree of memory fragmentation. Third, use of m a l l o c(), f r e e(), and so on consumes va lua ble EPROM spa ce. And la stly, dyna mic memory allocation is fraught with danger (wit-ness the recent series from P.J. Plauger on ga rba ge collection in the Ja nua ry 1998, Ma rch 1998, a nd April 1998 issues of ESP). Consequently, I strongly recom-mend that you not use dynamic mem-ory allocation on small systems. Final thoughts I have attempted to illustrate how judi-cious use of both ANSI constructs and compiler-specific constructs ca n help generate tighter code on small micro-controllers. Often, though, these improvements come at the expense of porta bility a nd/or rea da bility. If you are in the fortunate position of being a ble to use less efficient code, then you ca n ignore these suggestions. If, however, you a re severely resource-constra ined, then give a few of these techniques a try. I think you’ll be pleasantly surprised.e s p Nigel Jones received a degree in engineering from Brunel University, London. He has worked in the industrial control field, both in the U.K. and the U.S. Presently he is working as a consultant, with the majority of his work concentrated on underwater instrumentation. He can b e reached at NAJones@https://www.360docs.net/doc/de9303394.html,.