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,

, a n excerpt from which

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,.

相关文档
最新文档