System initialization requires as much care as the rest of your design
Scott Rosenthal
March, 1994
One of the last things many system designers consider is how to initialize a system.
Yet this start up probably takes more time getting right than almost anything else in a
design. The difficulty lies in software first dealing with hardware vagaries and then
initializing low-level data structures. In addition, software must also handle inadvertent
system resets, interrupts, stacks (or lack of) and proper initialization sequences. To
complicate matters further, each embedded system is unique, so what worked before might
not function on another system. This column summarizes some initialization techniques that
have worked for me during almost two decades of system design.
No magic allowed
The goal of the initialization process is to enable you to just flip on power to an
embedded system so it starts running, as if by magic. Unfortunately, it's not magic, and
you're not a magician. In brief, microcomputer databooks state that when a circuit
releases the RESET signal, a microcomputer starts executing instructions at a specified
memory location. This explanation is the same simplistic response I get from my kids when
I ask what happened, and they say, "It broke." In both cases, there's a whole
lot more to the story.
The databook is correct in that the microcomputer begins executing at a particular
memory address called the reset address. The problem is that each chip designer has a
different idea of what's the best reset address, so you'll find them all over the memory
map. For example, the Z80 reset address is 0000H, the 80C196 resets at 2080H with some
assistance from information at 2018H, and the 80X86 resets 16 bytes down from the top of
its memory space. From a software viewpoint, an address is an address and the physical
location means nothing. Yet to hardware, the reset address' location is of vital
importance.
To see why, consider the miserable example of the 80C196, which is a memory-design
nightmare. Not only is its reset address in the middle of a memory map, its first 100H
memory bytes are devoted to onboard registers, and 2000H to 207FH are off limits to
program code. If you locate ROM at the bottom of memory, you lose the first 100H bytes,
and a ROM gap exists between 2000H to 207FH, with initialization code starting at 2080H.
Alternately, you can fill the bottom of memory with RAM (except, of course, for the first
100H bytes), switch to ROM at 2000H and then switch back to RAM at a higher address. It's
a good thing that each user doesn't need to keep this addressing straight!
Databooks typically instruct you to place a jump to program code at the reset address.
Nowadays, programmers typically store the first instruction in ROM, but things didn't work
that way not that long ago when UVEPROMs held only 256 bytes. For example, I first learned
about programming microprocessors on the original 8-bit engine, Intel's 8008. The chip
wasn't very powerful and didn't integrate any peripherals or memory to speak of, and fancy
emulators didn't exist yet. The chip's reset address was 0000H, but the target system I
was working on had RAM at that location. Each time I powered up this computer, I had to
toggle in-through front-panel switches-a jump instruction to branch to the built-in
monitor program. I can still remember toggling switches and entering the jump instruction:
44H, 00H, 38H (JMP 3800H). It's still possible to see a jump instruction at the reset
address in every PC. Just run the DOS DEBUG program and type uffff:0,1. This
instruction sits at the reset address and is therefore the first one the processor
executes on power up.
Start your engines
Armed with knowledge of a microcomputer's reset address, it's time to examine its
effect on an application. The most simplistic way to construct a program is to put its
first instruction at the reset address and build forward from there. Another technique
puts a jump instruction at the reset address that branches to the actual program code. A
strange technique I once used when I needed every memory byte to stick an RST instruction
at location 0. This 1-byte call to a fixed location allowed me to save 2 bytes in ROM-and
you don't need a stack if the code never returns. Sometimes, every bit counts.
My preference is to place a short piece of program code starting at the reset address
(see nearby listing). This code snippet first disables all interrupts. When a CPU resets,
it disables its interrupts so that the program code has a chance to start running and
initialize the system. However, I've seen situations where the program, through a coding
error, somehow manages to work its way back to its own beginning. In this case, interrupts
are still active. If the program is going to start over, I'd rather it do so in a proper
sequence. This feature also makes debugging easier because finding out why the program
restarted is generally easier than trying to debug a strange program path. In short, keep
interrupts off until both software and hardware are capable of supporting them.
Listing: Getting started
ResetAddress: di ; turn off all interrupts
;
ld sp,StackTop ; setup the stack pointer
;
; clear RAM to zero (always assume even number of bytes to clear)
ld 1ch,#02000h ; amount of RAM
sub 1ch,1ch,#mem ; byte of uninitialized memory
shr 1ch,#1 ; words to clear
ld 1eh,#mem ; starting address to clear from
up: st 0,[1eh] ; store a 0 word at pointer
add 1eh,#2 ; increment pointer
dec 1ch ; decrement word counter
jne up ; jump if more words to clear
;
jmp Main ; go do the rest
The next step in my initialization code is to clear all RAM to zero. Obviously, oodles
of RAM might preclude this step, but in most embedded systems the process is fast and
easy. This step guarantees that the program always has a fixed starting point. This point
is important for two reasons: First, one of the most difficult software problems to find
is an uninitialized variable. With this type of error-depending on how long system power
was off, the phase of the moon and maybe even your biorhythms-RAM sometimes powers up with
the right value, sometimes not. Further, emulators aren't always helpful in
troubleshooting this problem because some always load their overlay memories with a
particular bit pattern. Setting RAM to 00H guarantees that each time the program starts,
it always knows what's in every variable.
The second reason for clearing RAM is that with every uninitialized variable set to a
known value, you can use this information to allow local initialization of functions. Some
compilers with their own initialization code perform this task, whereas others don't. It
only takes a few bytes of code to implement this safeguard, so I do it just to make sure.
After clearing memory, the startup code sets up a stack for the processor to use.
Although this step might seem obvious, you must first ensure that the RAM is accessible
and functional. If the system uses static RAM with simple address decoding, you probably
don't have to perform any initialization to start using it. On one board I designed, I had
to set the ROM/RAM border to divide memory between these two types. If your system uses
DRAM, precharge it before storing information in it. Until the RAM is ready, you can't use
any local variables, subroutine calls or stack space.
With accessible RAM in place, it's possible to set up a stack pointer. In most
processors this task is simply a matter of loading a memory pointer with a location in
RAM. However, be careful of this step. I once had a linker that supplied a RAM location
for the stack top. That feature was great, but this linker supplied the stack pointer as
an odd-numbered memory location that didn't work properly with the 16-bit bus. I ended up
anding the supplied address with 0FFFEH. This technique guarantees that the stack always
starts on an even address at the cost of maybe losing 1 byte of RAM. PE&IN
Adapted from an article that appeared in Personal Engineering & Instrumentation
News.
Return to the article index.
|