David designed a two-stage bootloader that allows application
firmware to be updated in the field to support bug fixes and
additional features for specific end-user applications. It also
adds capabilities to the native boot processing of the Blackfin
chip. Although some details are specific to the Blackfin family of
DSPs, some general features may be helpful on other CPUs.
Not long ago, I was working on an inertial measurement unit (IMU)
that was based on the highly integrated ADIS16350 inertial sensor
from Analog Devices that Tom Cantrell wrote about in his column in
Issue 208("Thanks for the MEMS," 2007)。 This is a six-axis MEMS
sensor (three axes of angular rate and three axes of
acceleration)in a compact and rugged package. My client wanted to
marry an Analog Devices Blackfin DSP chip to it in order to create
a self-contained inertial measurement solution.
After a couple of design iterations,we came up with the board shown
in Photo 1, which holds the Blackfin processor and various
interface and power-supply components. A group of finished units in
their boxes is shown in Photo 2. A key aspect of the implementation
was that the firmware would need to be updated in the field,after
the unit had left the controlled environment of the factory, in
order to support both bug fixes to the basic functionality and
additional features for specific end-user applications.


The structure of a loader file consists of a series of blocks of
various types. Each block contains a header,and the header contains
a target address, a length, and several flags. The flags include
IGNORE, FINAL,ZEROFILL, and INIT. The block will also contain data
bytes following the header, as long as the length is nonzero and
the ZEROFILL flag is not set.
The Boot ROM firmware processes blocks from the external SPI EEPROM
one at a time, stopping only when it gets to a block that has its
FINAL flag set. After processing that block, the Boot ROM jumps to
the address contained in the Blackfin‘s reset event vector, which
should point to the cold-start entry point of the newly loaded
code. The Boot ROM initializes this register to a default value,
but it is possible to modify this register (and other aspects of
the state of the Blackfin)
through the use of INIT blocks.
A block that has its IGNORE flag set may or may not contain data
bytes (it usually does), but the Boot ROM skips over such blocks
altogether-it simply adds the length of the block to its current
SPI EEPROM address value and looks for the next block. Such blocks
can be used to hold information for a second-stage bootloader
program or for the application itself.
Any block that does not have the IGNORE flag set is either a data
block or a ZEROFILL block. In the first case,the data associated
with the block is copied to the specified target address for the
specified length. In the second case, there is no data associated
with the block in the EEPROM, but the specified target memory
address is filled with bytes of zero for the specified length.
A data block that has its INIT flag set is called an "INIT block,"
and it must contain Blackfin code that begins execution at its load
address. When the Boot ROM encounters such a block, it loads the
data bytes into memory starting at the specified target address and
then executes a subroutine call to that same address. The INIT
block code must finish with a return instruction in order to allow
the Boot ROM to continue processing blocks.
BUILDING THE SOFTWARE
Blackfin software is built in the usual way: compile/assemble,
link,and load. The result of compiling or assembling source files
is a set of object files. The linker is used to combine object
files into a single executable image, which is stored in a file
that has a .DXE extension. The loader converts one or more .DXE
files into a single loader file (。LDR extension) that can be stored
in a boot device, and contains the blocks that the Boot ROM will
process. Among other things, the loader omits information in the
.DXE file that isn‘t needed, such as debugging symbols and other
metadata.
The loader includes the important feature that it can combine
multiple。DXE files into a single loader file. It does this by
inserting an IGNORE block at the beginning of each one. This IGNORE
block includes a 4-byte data field that contains the total length
of the entire set of blocks that represent that particular .DXE
file. If a second-stage bootloader or other software wants to skip
over the .DXE image, it can simply read the IGNORE block and then
add its value to the EEPROM address pointer, which will then cause
it to point to the next item in the EEPROM following the .DXE
image.
Furthermore, the loader can optionally mark the code block of the
first。DXE file as an INIT block. This means that both the first and
second。DXE files in a multi-.DXE loader file will get loaded and
executed in sequence by the Boot ROM.
SUPPORTING FIELD UPDATES
In order to support firmware updates in the field in a robust
manner,it is necessary to have the capability of storing more than
one copy of the application code in the SPI EERPOM-the one
currently executing,and the newer one being installed. Until the
install process is completed and verified, the DSP will execute the
older version on any hardware reset. Furthermore, it is a
requirement that the build process for the application is supported
by the standard ADI VisualDSP++ development environment (the IDDE
GUI) and that the build process for field upgrades be exactly the
same as for factory-installed software. As I will show later, the
capabilities of the Boot ROM and the build tools outlined
previously are sufficient to achieve these requirements without
resorting to custom tools in the build chain.
SUPPORTING NEWER EEPROM
There is a second issue related to the specific SPI EEPROM device
used on our IMU, which is an Atmel AT45DB081D, a member of their
DataFlash family. The Blackfin‘s onchip Boot ROM supports older
versions of the DataFlash family, but does not correctly support
this newer member(the one with the -D suffix)。 The specific issue
is that the -D device now implements the 0x03 "legacy read"
command, while the older devices did not. This causes the Boot ROM
to identify it as a generic 24-bit addressable SPI flash rather
than a DataFlash, and the Boot ROM then assumes that the device has
256 bytes per page. As a result, the Boot ROM will work correctly
only if the AT45DB081D is permanently set to its 256-byte Page
mode.
In order to fully support the 264-byte Page mode, a second-stage
boot kernel is required. This works as long as all of the block
headers for the INIT block and the boot kernel itself fit into the
first 256 bytes of the DataFlash device. As long as the boot kernel
comprises a single code block,it can be arbitrarily long (well, up
to 65,534 bytes) and extend beyond the first DataFlash page.
TWO-STAGE BOOT PROCESS
Therefore, we needed to implement a two-stage boot process. This
consists of the boot ROM executing, which loads and then executes a
second-stage boot kernel. The boot kernel, in turn,loads and
executes the actual application firmware.
In order to support multiple copies of the application firmware in
EEPROM at the same time, it is necessary to have some storage
reserved in the EEPROM to indicate which copy is the "active" copy
of the firmware at any given point in time. Within the context of
the standard software development toolchain,the only way to
accomplish this is to include an INIT block. The INIT block
contains just a"return" instruction, along with four additional
bytes of space that get initialized to all zeros. When the boot
kernel gets control, it examines those 4 bytes to determine the
base address of the "active" loader file in the EEPROM. It will
then skip the first two。DXE images in this file (the INIT block and
the boot kernel itself) and load the third .DXE image, which is the
application firmware.
However, there is one additional twist. When the boot kernel is
executing,it will be loading the application code, so the boot
kernel itself must be located in code memory not needed by the
application. There are two ways to achieve this-the boot ROM could
load the boot kernel at the default address and then the boot
kernel could relocate itself to a different address higher up in
memory before continuing. But in many ways, it's simpler to just
have the boot kernel load and run at the higher memory address to
begin with. This adds the requirement that our INIT block must set
the reset vector to that other address-so that the Boot ROM will
jump to that address when it finishes loading the boot kernel-and
that the boot kernel itself must restore the default reset vector
value before it loads the application code. This still leaves open
the possibility that the application can have its own INIT block
that sets a non-default reset vector value. But constructing a
loader file with two (or more) INIT blocks, while possible, would
require a non-standard development toolchain.
As a result of all of this, we‘re going to build each application
image (。LDR file) as a concatenation of three separate DSP
executable programs (。DXE fields)。 The first is our INIT block that
performs two functions: It provides a place in the EEPROM to store
a pointer to the current application image,and it sets up the
environment (a nondefault reset vector value) in which the
second-stage boot kernel runs (see Figure 2)。
点击Figure 2
Figure 3 shows the details of the EEPROM layout. As a result of the
space taken up by various data structures within the loader file,
the code of our INIT block begins at address 0x00018. The first
thing located here must be an executable instruction, so we place a
jump instruction here (2 bytes)that jumps past the next 4
bytes,which is where we‘re going to store our application pointer.
Therefore,the pointer starts at address 0x0001A(decimal 26)。
