Now that the processor's reset, here's how to get the code running
Scott Rosenthal
May, 1994
We'll do it in C. We'll use an RTOS. We'll purchase a communications library. You want
to create how many icons? These are a few of the typical thoughts that come into play at
the beginning of a software design, especially an embedded design. However, one thought I
rarely hear is how to kick-start the software into performing all its miraculous
activities. My last column (reference) addressed what
happens in the first few microseconds after a microcomputer powers-up. This time, I'm
going to take that discussion a step further and describe some of the activities a program
must perform when a user first turns on the power.
Before the beginning
If you write a C program on a PC, you know it must include a function named main()
that serves as the starting place for the program code. However, if main() was
truly the program's starting place, then that function would also need to initialize a
stack, set up the software floating-point package and initialize the data space. Yet I've
never seen a main() function perform any of these feats. So how do these
initialization steps take place? When you link a program (which is a whole story in
itself), the linker also loads a special module. Depending on the development environment,
this module goes by many names. For example, with the Manx Aztec C tools I use the name of
the function is $begin(), but to keep things simple, I'll just refer to it as the
loader(). Its job is to take the handoff from the operating system, perform the necessary
initialization and run main().
Specifically at run time, DOS loads a program into the lowest available memory
locations, allocates a small stack area for it and then transfers control to the start
address stored in the .EXE header information. This address is the starting location of loader().
It's now up to loader() to perform all other housekeeping details necessary to
successfully launch main() and the rest of the program.
The following steps outline tasks loader() must typically perform, but the list is by
no means complete, and the sequence isn't necessarily important. Many compiler
manufacturers have special requirements for their versions of loader(). What's
important is to see the number of steps needed to successfully launch a program:
- Create a local stack area for the program.
- Clear the uninitialized memory-data area.
- Create and initialize a heap-memory area.
- Initialize the disk file and device-handling logic.
- Open communication channels to the standard C devices: stdin, stdout, stdprt
and stderr.
- Initialize the floating-point package or math-coprocessor chip.
- Call main() to start the program running.
As you can see, loader() does a lot of work behind the scenes that significantly
reduces your programming chores. If instead of coding in C you used assembly language,
you'd have to supply a custom loader(). Thus, it's easy to appreciate how a
high-level language can eliminate the mundane and allow you to concentrate on solving
specific design issues.
The embedded twist
The PC world-compared to embedded development-can sometimes lull you into a false sense
of security. One pitfall waiting for the uninitiated is the matter of a loader().
Not only must you worry whether the language supplier includes a loader(), you must
also make sure the loader() works in your application. Even if you're lucky enough
to get one (many compilers-such as Intel's C96 tool-don't come with one) the chances of it
working with the widget you're developing are slim because no two embedded applications
incorporate the same resources and so have different initialization requirements. For
instance, some systems use disk drives and displays, while others have neither.
At this point in the discussion, one of several twists comes into play. In the PC
world, DOS hands off to the loader(), which then hands off to the main().
But in an embedded system, which module hands off to the loader()? The answer was
in my last column-the microcomputer's reset address. When the CPU first starts running at
its reset address, the short piece of reset code should branch to the loader().
In my experiences, every one of my embedded systems' loader() functions has a
few things in common: They all set up a stack area in memory; they all clear system RAM so
that, when debugging, my programs always have the same starting memory images; and they
set up pointers to a free-memory area that becomes the system's heap space. Other than
these simple steps, all other functions I either group into a function called from within main()
or let the functions self-initialize.
A monolithic approach
As far as I know, these two alternatives are the only techniques that exist for
initializing an embedded system. Deriving from my years of programming in assembly
language, I've always included a function, which I inherited from another engineer, that
performed a massive system initialization. Originally named sysint(), short for
system initialization (our language at the time only allowed 6-character names), and later
renamed sysinit() (when a more-advanced language allowed 8-character names), it
initialized all hardware to a known state, set global locations to their starting values
and performed a bevy of other calls to set-up other functions such as a keypad FIFO.
As time went on and programs grew in size, sysinit() ballooned to ridiculous
proportions. I developed a guideline for my office, as documented in our Software
Standards and Development Manual, that says a function should be no more than 50 lines
in length. So my choices were to either ignore a standard I authored or find another way
to initialize my embedded systems. Deciding that sleeping well at night was more important
than temporary expediency, I took the latter course. The technique I implemented was to
allow functions, and therefore systems, to initialize themselves. This change in approach
reduced the size of sysinit() while eliminating the potential programming error of
failing to incorporate into sysinit() all the systems that needed initialization.
In C terminology, I use a static variable within a function as a flag to say whether
the function has run yet. Remember when I said that during initialization my loader()
clears all system memory? Due to this fact, I know that at initialization, my flags must
be False until the function sets them True. For example, assume I declare a
flag in a function named Initialized (see listing). I can test this variable each time the
function runs. The first time the function executes, the flag is False, so the code
runs the execution routines, sets the flag True and continues with whatever the functions
should do. With each subsequent test, the True flag prevents the initialization
code from running again.
Listing: self-initializing function logic
void SampleFunc(void) {
static int Initialized;
if (!Initialized) {
// ... perform the initialization ...
Initialized = TRUE;
}
// ... function guts ...
}
Some people might ask, why initialize memory when you can assign a value to a static
variable? That point is valid on a PC, but many times embedded systems run from ROM and
can't initialize static variables. The technique I outlined above works in both
situations. Other people might argue that this method is wasteful of a computer's time and
memory, and I agree. However today, processors are so fast and memory is so cheap that a
little bit of waste in return for function encapsulation, better documentation and
potentially fewer programming problems, is a dynamite tradeoff.
One caveat to keep in mind with this technique is reentrancy. If you're using an RTOS
and all functions must be reentrant, be careful with these flags. Try to incorporate them
into the data structure that the function works on. Finally, let your mind wander and find
other techniques that keep initialization out of the monolith and put it where it
belongs-as part of the function. PE&IN
Reference
Rosenthal, S, "System initialization requires as much care
as the rest of your design," PE&IN, Mar 1994, pgs 73-74.
Adapted from an article that appeared in Personal Engineering & Instrumentation
News.
Return to the article index.
|