I can remember the exact instant when I realized that a large part of my life from then on was going to be spent in finding mistakes in my own programs.
Once you have an executable binary image stored as a file on the host computer, you will need a way to download that image to the embedded system and execute it. The executable binary image is usually loaded into a memory device on the target board and executed from there. And if you have the right tools at your disposal, it will be possible to set breakpoints in the program or to observe its execution in less intrusive ways. This chapter describes various techniques for downloading, executing, and debugging embedded software in general, as well as focuses on the techniques available on our development environment.
With most embedded systems, there are several means to get an image onto the target and run the program, some more challenging than others. In this section, we investigate the methods available for downloading the Blinking LED program onto the Arcom board, as well as some other methods that may be useful for other projects.
The software development cycle for a PC and an embedded system include many of the same stages. Figure 5-1 is a general diagram of the embedded software development cycle.
As shown in Figure 5-1, the software development cycle begins with design and the first implementation of the code. After that, there are usually iterations of the build, download and debug, and bug-fixing stages. Because a lot of time is spent in these three stages, it helps to eliminate any kinks in this process so that the majority of time can be spent on debugging and testing the software. (This diagram does not take into account the handling of feature creep inevitably inflicted by the marketing department.)
Because this is a very basic diagram, other stages that may be necessary are profiling and optimization. Profiling allows a developer to determine various metrics about a program, such as where the processor is spending most of its time. Optimization is the process by which the developer tries to eliminate bottlenecks in software using various techniques, such as implementing time-critical code in assembly language. Very often, optimization techniques are compiler-, processor-, or system-specific.
Another task at this stage is integration. Once the development cycle is complete, system-level testing is typically done. And after a product ships, the software enters its maintenance phase for the duration of the product’s life cycle, when the code must be supported and sustained. The debugging techniques shown in this chapter apply to the maintenance stage as well.
Because code must be repeatedly tested on the target hardware, a quick and straightforward method for loading software onto the target is ideal. Let’s take a look at the techniques and tools we can use for this task.
A debug monitor, also called a ROM monitor, is a small program that resides in nonvolatile memory on the target hardware that facilitates various operations needed during development. One of the tasks that a debug monitor handles is basic hardware initialization. A debug monitor allows you to download and run software in RAM to debug the program.
Most debug monitors include many other useful features to assist in the development cycle. For example, most debug monitors incorporate some kind of com mand-line interface (CLI) where commands can be issued to the debug monitor for execution on the target hardware—e.g., via serial port. These commands include downloading software, running the program, peeking (reading) and poking (writing) memory and processor registers, comparing or displaying blocks of memory, and setting initialization configurations for the hardware.
In some systems, a debug monitor is incorporated in the production units in the field as well. This can be used to update the firmware to add new features or fix bugs after the unit is deployed.
Some processors include a program similar to a debug monitor in on-chip memory. For example, the TMS320C5000 series DSPs from Texax Instruments include a program called a bootloader. This bootloader is used to transfer software from an external source (off-chip) to internal memory (on-chip), allowing code to reside in slower memory for storage and be transferred into faster memory prior to execution. The bootloader determines where to load code based upon the boot mode setting, which is determined by sampling particular DSP I/O pins during power up.
Be aware that the execution speeds of RAM and ROM typically are very different. Code running from RAM typically executes much faster than code running from ROM, which could cause the software to behave differently. Certain types of bugs, such as timing errors, may only reveal themselves when run from just one type of memory.
The Arcom board includes a debug monitor called RedBoot, which is described in the sidebar “Debug Monitors” in Chapter 4. RedBoot resides in the bootloader flash on the Arcom board and uses the board’s serial port COM1 for its command-line interface. The VIPER Technical Manual and VIPER-I/O Technical Manual describe how to connect the various modules to the Arcom board.
Once you have the Arcom board connected properly, you need to connect the Arcom board’s COM1 port to a serial port on your computer using the cable included in the development kit.
Baud rate: 115200
Data bits: 8
Stop bits: 1
Flow control: None
Now you are ready to power on the Arcom board.
Redboot executes a script after it runs to automatically boot embedded Linux (if present on your version of the Arcom board). In order to prevent the script from running, hit the keys Ctrl and C together when RedBoot outputs its initialization message.
Ethernet eth0: MAC address 00:80:12:1c:89:b6 No IP info for device! RedBoot(tm) bootstrap and debug environment [ROM] Non-certified release, version W468 V3I7 - built 10:11:20, Mar 15 2006 Platform: VIPER (XScale PXA255) Copyright (C) 2000, 2001, 2002, 2003, 2004 Red Hat, Inc. RAM: 0x00000000-0x04000000, [0x00400000-0x03fd1000] available FLASH: base 0x60000000, size 0x02000000, 256 blocks of 0x00020000 bytes each. == Executing boot script in 1.000 seconds - enter ^C to abort ^C RedBoot>
Because we have not entered an Internet Protocol (IP) address
for the Arcom board, RedBoot outputs the message:
device! This message can be ignored for
now. Another thing to notice is that we have stopped the boot script
from running (and loading Linux) by entering Ctrl-C (shown in the
preceding code as
RedBoot is started.
The RedBoot initialization message contains information regarding the build and version of the RedBoot image. The available memory is also listed before the prompt, including the RAM and flash memory address ranges.
You can enter
help at the
prompt to get a list of the supported commands. A description of the
RedBoot commands can be found online at http://ecos.sourceware.org.
Now that RedBoot is up and running, we are ready to download and run the Blinking LED program. RedBoot is able to load and run ELF files. Therefore, we use the blink.exe file built in Chapter 4 as the program image to run on the Arcom board.
To initiate the download, enter the following
load command at the RedBoot prompt:
load –m xmodem
To begin the file transfer using Windows HyperTerminal, select Transfer→Send File... from the menu (use a similar command if you have a different terminal program). This brings up the Send File dialog box; select Xmodem for the protocol. Browse to the location of the blink.exe program and select it. Then click Send. A transfer statistics dialog box will be displayed showing the status of the file transfer.
Once the transfer has successfully completed, RedBoot outputs a message similar to the following:
Entry point: 0x00400110, address range: 0x00000024-0x0040014c xyzModem - CRC mode, 24(SOH)/0(STX)/0(CAN) packets, 2 retries
This shows the entry point of the program—in this case, 0x00400110. If you refer to the map file generated by the linker during the build process, blink.map, the entry point address should look familiar, as shown in this portion of the map:
Name Origin Length .text 0x004000b0 0x9c blink.o 0x00400110 main
The map file shows that the routine
main resides at 0x00400110, which is the
entry point for execution of the Blinking LED program. The value
0x9C is the total length of the object file blink.o.
After you press Enter, RedBoot hands control of the Arcom board over to the Blinking LED program. If everything is successful, you should now see the green LED blinking on the add-on board.
Another way to download embedded software is to load the binary image into a ROM device and physically insert that chip into a socket on the target board. Obviously, the contents of a truly read-only memory device could not be overwritten. However, as you’ll see in Chapter 6, embedded systems commonly employ special read-only memory devices that can be programmed (or reprogrammed) with the help of a special piece of equipment called a device programmer or burner. A device programmer is a computer system that has one or more IC sockets on the top—of varying shapes and sizes—and is capable of programming memory devices of all sorts.
In an ideal development scenario, the device programmer would be connected to the same network as the host computer. That way, files that contain executable binary images could be easily transferred to it for ROM programming. After the binary image has been transferred to the device programmer, the memory chip is placed into a socket of the appropriate size and shape, and the device type is selected from an on-screen menu. The actual device programming process can take anywhere from a few seconds to several minutes, depending on the size of the binary image, the type of memory device you are using, and the quality and speed of your device programmer.
After you program the ROM, it is ready to be inserted into its socket on the board. Of course, this shouldn’t be done while the embedded system is still powered on. The power should be turned off and then reapplied only after the chip has been carefully inserted.
Care should be taken when removing and inserting any part that is socketed. Pins can become bent with surprising ease, and a bent or broken pin can cause all sorts of problems that are difficult to debug.
As soon as power is applied, a processor will begin to fetch and execute the code that is stored inside the ROM. However, be aware that each type of processor has its own rules about the location of its first instruction. For example, when the ARM processor is reset, it begins by fetching and executing whatever is stored at physical address 0x00000000. This is called the reset address, and the instructions located there are collectively known as the reset code. In the case of the Arcom development board, the reset code is part of the RedBoot debug monitor.
If your program doesn’t appear to be working, something could be wrong with your reset code. You must always ensure that the binary image you’ve loaded into the ROM satisfies the target processor’s reset rules. During product development, we often find it useful to turn on one of the board’s LEDs just after the reset code has been completed. That way, we know at a glance that any new code either does or doesn’t satisfy the processor’s most basic requirements.
The Arcom board includes a type of memory called flash, which is in-circuit programmable. Even when socketed for easy removal, flash memory does not have to be removed from the board to be reprogrammed. The RedBoot debug monitor includes software that can perform the device programming function.
RedBoot also contains several commands to manage a flash filesystem, called the Flash Image System (FIS). The FIS allows you to specify regions in flash, similar to a filesystem on a hard disk drive. Using the FIS, you can create, write, and erase locations of flash based on “filenames” you select.
Be extremely careful not to corrupt the existing images or configuration data residing in flash on the Arcom board. If this happens, you could render the board unusable.
To see what is contained in the FIS, enter the following command:
which will output a listing similar to this one:
Name FLASH addr Mem addr Length Entry point FIS directory 0x00000000 0x00000000 0x0001F000 0x00000000 RedBoot config 0x0001F000 0x00000000 0x00001000 0x00000000 filesystem 0x00020000 0x00000000 0x01FE0000 0x00000000
list command shows the
images currently available in the RedBoot FIS. There are a few other
FIS commands supported by RedBoot. For details on the
other FIS-related commands and options, refer to the RedBoot User’s
Guide online at http://ecos.sourceware.org.
You have some decisions to make when deciding how and where to download a program to the hardware. The biggest disadvantage of using flash memory for downloads is that there is no easy way to debug software that is executing out of flash memory or ROM. When single-stepping or executing to a breakpoint, the debugger replaces the subsequent instruction with a software interrupt, which is used to halt the processor’s execution. Thus, a debugger doesn’t work in any form of read-only memory, such as flash. Of course, you can still examine the state of the LEDs and other externally visible hardware, but this will never provide as much information and feedback as a debugger. So, flash might be fine once you know that your software works and you’re ready to deploy the system, but it’s not very helpful during software development.
Some processors can work around the issue of executing out of flash or ROM. In some cases, the processor includes a TRACE instruction that executes a single instruction and then automatically vectors to an interrupt. On other processors, a breakpoint register gets you back to the debug monitor.
If available, a remote debugger can be used to download, execute, and debug embedded software over a serial port or network connection between the host and target (also called cross-platform debugging). The program running on the host of a remote debugger has a user interface that looks just like any other debugger that you might have used. The main debugger screen is usually either a command-line interface or graphical user interface (GUI). GUI debuggers typically contain several smaller windows to simultaneously show the active part of the source code, current register contents, and other relevant information about the executing program.
Note that in the case of embedded systems, the debugger and the software being debugged are executing on two different computer systems. Remote debugger software runs on the host computer and provides the user interface just described. But there is also a backend component that runs on the target processor and communicates with the host debugger frontend over a communications link. The debugger backend provides low-level control of the target processor. Figure 5-2 shows how these two components work together.
The debug monitor resides in ROM—having been placed there either by you or at the factory—and is automatically started whenever the target processor is reset. It monitors the communications link to the host computer and responds to requests from the remote debugger host software. Of course, these requests and the monitor’s responses must conform to some predefined communications protocol and are typically of a very low-level nature. Examples of requests the host software can make are “read register x,” “modify register y,” “read n bytes of memory starting at address z,” and “modify the data at address a.” The remote debugger combines sequences of these low-level commands to accomplish complex debugging tasks such as downloading a program, single-stepping, and setting breakpoints.
It is helpful to build the program being tested to include symbolic debug information, which we did with the –g option during the compilation step of the build procedure in Chapter 4. The –g option causes the compiler to place additional information in the object file for use by the debugger. This debug information allows the debugger to relate between the executable program and the source code.
One such debugger is the GNU debugger (gdb). Like the other GNU tools, it was originally designed for use as a native debugger and was later given the ability to perform remote debugging. The gdb debug monitor that runs on the target hardware is called a gdb stub. Additional information about gdb can be found online at http://sources.redhat.com/gdb.
The GNU software tools include gdb. The version installed is CLI-based, so there are a few commands to learn in order to run the debugger properly. There are several GUIs available for gdb, such as Insight (http://sources.redhat.com/insight) and DataDisplay Debugger (http://www.gnu.org/software/ddd).
RedBoot contains a gdb-compatible debug monitor. Therefore, once a host attempts to communicate with the target using the gdb protocol, RedBoot turns control of the target over to the gdb stub for the debug session.
As described earlier, the host and target use a predefined protocol. For gdb, this protocol is the ASCII-based Remote Serial Protocol. To learn more about the gdb Remote Serial Protocol, go to http://sources.redhat.com/gdb. Another good resource for information about gdb is Debugging with GDB: The GNU Source-Level Debugger, by Richard Stallman, Roland Pesch, and Stan Shebs (Free Software Foundation). 
Remote debuggers are one of the most commonly used downloading and testing tools during development of embedded software. This is mainly because of their low cost. Embedded software developers already have the requisite host computer. In addition, the price of a remote debugger does not add significantly to the cost of a suite of cross-development tools (compiler, linker, locator, etc.).
However, there are some disadvantages to using a debug monitor, including the inability to debug startup code. Another disadvantage is that code must execute from RAM. Furthermore, when using a debug monitor, a communication channel must exist to interface the target to the host computer.
gdb is able to operate over serial or TCP/IP network ports. RedBoot also supports gdb debug sessions over either of these ports. For the example that follows, we use the serial port. We then cover some of the basic gdb commands that are in the example.
In order to demonstrate some additional debug capabilities, we
have added a global variable,
gChapter, to the Blinking LED
To prepare for the debugging examples, cycle power on the Arcom board and halt the RedBoot boot script by pressing Ctrl-C. Once the RedBoot initialization message is output, you’re ready to start.
Invoke gdb, passing the name of the program to debug as an argument, by using the following command:
gdb outputs a message similar to this one:
GNU gdb 6.3 Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "--host=i686-pc-cygwin --target=arm-elf"...
If you use the wrong executable that does not contain debugging information with gdb, the following message is output:
(no debugging symbols found)
There should also be a (gdb) prompt waiting for input. Next, issue the command to have gdb connect to the Arcom board. The following command assumes that the computer’s serial port that is connected to the target board is COM1 (if a different PC serial port is used, modify the command accordingly):
target remote /dev/ttyS0
After gdb successfully connects to the target, a response similar to this one will follow:
Remote debugging using /dev/ttyS0
The host computer running the gdb command-line interface is now connected to the gdb stub residing on the target hardware within RedBoot.
Next download the
program onto the target with the command:
When program loading completes successfully, a message similar to this one is output from gdb:
Loading section data, size 0x4 lma 0x400000 Loading section text, size 0x148 lma 0x400004 Start address 0x400110, load size 332 Transfer rate: 2656 bits in <1 sec, 166 bytes/write.
Now you are ready to start debugging!
Let’s start by setting a breakpoint in the code. A breakpoint is an address or condition where the debugger will halt execution of the program. The debugger sets a breakpoint by replacing a given instruction with a software interrupt (that gets sent back to the debugger). Removing a breakpoint removes the software interrupt and restores the preplaced instruction.
Use the following command to set a breakpoint that is hit when
gdb commands are not case-sensitive (though symbols are) and can be abbreviated to the shortest unique string. For example, you can set a breakpoint with any of these commands:
Each command accomplishes the same goal—i.e., setting a
breakpoint at the routine
ledToggle. The RedBoot CLI commands
After successfully setting the breakpoint, gdb responds with information about the breakpoint as follows:
Breakpoint 1 at 0x400070: file led.c, line 66.
The response shows the breakpoint number (1 in this case), the address of the breakpoint (0x400070), the file where the function is located (led.c), and the line number in that file where the breakpoint is set (66).
If you need to check which breakpoints are set within a program for a gdb session, you can use the command:
The response from gdb is something like this:
Num Type Disp Enb Address What 1 breakpoint keep y 0x00400070 in ledToggle at led.c:66
Next, run the program by entering the
Once gdb hits the breakpoint, it will halt execution and output the source code line that is to be executed next:
Breakpoint 1, ledToggle () at led.c:66 66 if (GPIO_0_LEVEL_REG & LED_GREEN)
The first line of output shows the breakpoint that was hit, along with the description. The second line shows the line number in the file, 66 in this case, with the source code for that line. The green LED should be lit now because that is its initial state in the program.
To have gdb show the source
code where the current program is stopped, use the
This will dump, by default, 10 source code lines. To dump the
next 10 source code lines, simply enter the
list command again.
To repeat the last command in a gdb session, simply press Enter.
print /x gChapter
The /x option formats the output for hexadecimal. The response from gdb is:
$1 = 0x5
$1 is a special history
number that gdb assigns, and
gdb allows reference back to this
value at a later time. The current value of
gChapter is 5.
The response from gdb is:
$2 = 0xc
This shows that the new value of
gChapter is 0xC (12 decimal).
Another very useful feature of a remote debugger is the
ability to step through lines of source code, an action commonly called
single-stepping. There are several different
types of single-step commands, such as stepping a single machine
instruction and stepping a single source code line. The following
next, steps a single
source code line:
When debugging, it is also important to realize that using compiler optimization can affect the behavior of the code in the debug session. Most compilers have an option for enabling optimization. For example, with gcc, the –O option invokes optimization. This option has various levels, which are indicated by a number (0, 1, 2, or 3) that follows the option switch—for example, –O2. The optimization switch –Os optimizes for size. By default, no optimization is selected. Details about the specific optimizations for each level can be found in the gcc documentation located online at http://gcc.gnu.org/onlinedocs.
The reason compiler optimization needs to be considered when debugging is because the compiler can reorder code and remove entire sections of code and/or variables without telling you. It is for this reason that most debugging is done with optimization turned off. Keep this in mind when single-stepping source code.
The source code line to be executed next is output by gdb as shown here:
69 GPIO_0_SET_REG = LED_GREEN;
gdb provides two commands
for stepping one line at a time:
next. The difference between them is that
when you reach the start of a function call,
step enters the function and runs the first
statement within the function, whereas
next runs the whole function.
Now run the program again with the
You might notice that the LED is now off.
Execution will halt once again when gdb encounters the breakpoint at
gdb allows you to check which functions have executed by using the
bt command. The
backtrace command shows how your program got
to where it currently is. Enter the
backtrace command as follows:
The gdb output looks something like this:
#0 ledToggle () at led.c:66 #1 0x00400140 in main () at blink.c:75
The response from gdb shows
the most recently executed function (indicated by
#0), followed by the function that called it
#1), and so on. The
preceding response shows that the routine
main called the routine
To print the value of a specific register, use the command:
This command outputs the current value of the program counter register in hexadecimal format.
You should now have a good understanding of how to use gdb. We’ve examined the most important commands, but it may be helpful to play around with some others at this point.
Then confirm the removal of all breakpoints. You can now continue running the program and watch the LED blink.
To halt execution, press Ctrl-C. However, the program may not respond to this command. If this is the case, gdb will ask if you want to stop debugging the program.
commands can be found by using the gdb
command. Assistance with a specific command can be obtained by
help followed by the
command name (e.g.,
breakpoint). There is also a gdb quick-reference guide available at
An in-circuit emulator (ICE) provides a lot more functionality than a remote debugger. In addition to providing the features available with a remote debugger, an ICE allows you to debug startup code and programs running from ROM, set breakpoints for code running from ROM, and even run tests that require more RAM than the system contains.
An ICE typically takes the place of—or emulates—the processor on your target board. (Some emulators come with an adapter that clips over the processor on the target.) The ICE is itself an embedded system, with its own copy of the target processor, RAM, ROM, and embedded software. In-circuit emulators are usually pretty expensive. But they are powerful tools, and in a tight spot, nothing else will help you get the debug job done better.
Like a debug monitor, an emulator uses a remote debugger for its host interface. In some cases, it is even possible to use the same debugger frontend for both. But because the emulator has its own copy of the target processor, it is possible to monitor and control the state of the processor in real time. This allows the emulator to support such powerful debug features as hardware breakpoints and real-time tracing. Additional information about in-circuit emulators can be found in the November 2001 Embedded Systems Programming article “Introduction to In-Circuit Emulators,” which can be found online at http://www.embedded.com.
With a debug monitor, you can set breakpoints in your program. However, these software breakpoints are restricted to instruction fetches—the equivalent of the command “stop execution if this instruction is about to be fetched.” Emulators, by contrast, also support hardware breakpoints. Hardware breakpoints allow you to stop execution in response to a wider variety of events—not only instruction fetches, but also interrupts and reads and writes of memory. For example, you might set a hardware breakpoint on the event “address bus = 0x2034FF00 and data bus = 0x20310000.”
Another useful feature of an in-circuit emulator is real-time tracing. Typically, an
emulator incorporates a large block of special-purpose RAM that is
dedicated to storing information about each processor cycle executed.
This feature allows you to see in exactly what order things happened, so
it can help you answer questions such as, “Did the timer interrupt occur
before or after the variable
became 12?” In addition, real-time trace features are often able to
either restrict the information stored or post-process the data prior to
viewing it, to cut down on the amount of memory wasted.
Another type of debug tool similar to an ICE is a background debug mode (BDM), or JTAG (pronounced “jay-tag”) debugger. JTAG debuggers are typically less expensive than in-circuit emulators but offer much of the same functionality. These tools rely on a debug interface (the JTAG interface) and on-chip test circuitry found in modern processors. Additional information about JTAG emulators can be found in the February 2003 Embedded Systems Programming article “Introduction to On-Chip Debug,” located online at http://www.embedded.com.
The article “How to choose an in-circuit emulator” in the July 2002 issue of Embedded Systems Programming is useful for learning how to select an ICE.
Another type of device that emulates a read-only memory device is a ROM emulator. Like an ICE, it is an embedded system that connects to the target and communicates with the host. However, with this device, the target connection is via a ROM socket. To the embedded processor, it looks like any other read-only memory device. But to the remote debugger, it looks like a debug monitor.
ROM emulators have some advantages over debug monitors. First, no one has to port the debug monitor code to your particular target hardware. Second, the ROM emulator supplies its own serial or network connection to the host, so it is not necessary to use the target’s own, usually limited, resources. And finally, the ROM emulator is a true replacement for the original ROM, so none of the target’s memory is used up by the debug monitor code.
Some disadvantages of using a ROM emulator are that it does not provide general debugging capabilities (i.e., reading processor register values) and is useless in systems that lack external memory.
This section is an introduction to other tools that many software developers find useful.
A simulator is a completely host-based program that simulates (hence the catchy name) the functionality and instruction set of the target processor. The user interface is usually the same as or similar to that of the remote debugger. In fact, it might be possible to use one debugger host for the simulator target as well, as shown in Figure 5-3. Although simulators have many disadvantages, they are quite valuable in the earlier stages of a project when there is not yet any actual hardware for the programmers to experiment with. If you cannot get your hands on a development board, a simulator is the best tool for getting a jump-start on the software development.
By far, the biggest disadvantage of a simulator is that it simulates only the processor. Embedded systems frequently contain one or more important peripherals. Interaction with these devices can sometimes be imitated with simulator scripts or other workarounds, but such workarounds are often more trouble to create than the simulation is worth. So you probably won’t do too much with the simulator once the actual embedded hardware is available.
As mentioned before, one of the key aspects that differentiates the embedded developer from the typical software developer is “closeness” to the hardware. Several tools are available to assist you with finding out what is going on with the hardware. A basic understanding of how to use these tools is essential to developing good debugging skills, particularly since these same tools are very useful for low-level software debugging.
Once you have access to your target hardware—and especially during hardware debug—logic analyzers and oscilloscopes can be indispensable debug tools. A logic analyzer and an oscilloscope are most useful for debugging the interactions between the processor and other chips on the board. Because they can view only signals that lie outside the processor, however, these tools cannot control the flow of execution of software. This lack of software execution control makes them significantly less useful by themselves, but coupled with a software debug tool such as a remote debugger or an emulator, they can be extremely valuable.
A logic analyzer is a piece of laboratory equipment designed specifically for troubleshooting digital hardware. It can have dozens or even hundreds of inputs, each capable of detecting only one thing: whether the electrical signal it is attached to is currently at logic level 1 or 0. Any subset of the inputs that you select can be displayed against a timeline as illustrated in Figure 5-4. Most logic analyzers will also let you begin capturing data, or trigger, on a particular pattern. For example, you might make this request: “Display the values of input signals 1 through 10, but don’t start recording what happens until inputs 2 and 5 are both zero at the same time.”
An oscilloscope is another piece of laboratory equipment for hardware debugging. But this one is used to examine any electrical signal—analog or digital—on any piece of hardware. Oscilloscopes are sometimes useful for quickly observing the voltage or signal waveform on a particular pin, or, in the absence of a logic analyzer, for something slightly more complex. However, the number of inputs is much smaller (there are usually two to four), and advanced triggering logic is not often available.
One of the most primitive debug techniques available is the use of an LED as an indicator of success or failure. The basic idea is to slowly walk the LED enable code through the larger program. In other words, first begin with the LED enable code at the reset address. If the LED turns on, you can edit the program—moving the LED enable code to just after the next execution milestone—and then rebuild and test. Because this technique gives you very little information about the state of the processor, it is most appropriate for very simple, linearly executed programs such as the startup code. But if you don’t have access to a remote debugger or any of the other debug tools, this type of debugging might be your only choice.
If an LED is not present on your hardware platform, you can still use this debug technique with an I/O signal and an oscilloscope. In this case, set the I/O signal to a specific level once you reach a particular execution milestone. Using the oscilloscope, you can then probe that I/O pin to determine whether the code has set it appropriately. If so, you know that the code executed successfully up to that point, and you can now move the I/O signal code to the next milestone.
The method of using an I/O signal and an oscilloscope can also be used as a basic performance measurement tool. An I/O pin can be used to measure how long a program is spending in a given routine, or how long it takes to execute a particular fragment of code. This can show potential bottlenecks in the program.
For example, to precisely measure the length of time spent in
delay_ms routine (when passed in a parameter
of 1), we could set an I/O pin high when we enter the routine and then
set the same I/O pin low before exiting. We could then attach an
oscilloscope lead to this I/O pin to measure the amount of time that
the I/O pin is high, which is the time spent in the
delay_ms routine. The oscilloscope screen
should look similar to the image in Figure 5-5.
As shown in Figure 5-5, channel 1 (CH 1) is the probe that captured the signal. The dotted horizontal lines indicate voltage increments—in this case, 2 volts per division; the dotted vertical lines indicate time increments—in this case, 500 microseconds per division. The “T” at the top of the screen, along with the arrow, indicate when the oscilloscope triggered on the falling edge of the I/O pin. The I/O signal goes from (approximately) 0 to 3.3 volts.
Incidentally, this test shows that the actual
delay_ms routine that is supposed to delay
for 1 millisecond is a little off, because the time from setting the
I/O pin high to setting the I/O pin low is a bit longer than two
If I/O pins are available and several inputs are supported by the oscilloscope, you can use multiple I/O signals simultaneously in order to get a snapshot of the entire system. In more complex systems, you can move the I/O set calls around in the various routines and measure how each routine is performing.
A lint program is a tool for statically checking source code for portability problems and common coding syntax errors, such as ignored return values and type inconsistencies. A compiler provides some of this error checking, but a lint program verifies these areas of a program much more carefully and therefore aids in the development of more robust software.
Setting up lint is similar to setting up a compiler, where different options are passed into the program to control the type of output produced. In fact, you can augment your build procedure to include a lint check that sends its output to a file for you to review at a later time.
A good introduction to using lint is the article “Introduction to Lint” from the archives of Embedded Systems Programming. This article can be found online at http://www.embedded.com. For additional information, pick up Checking C Programs with Lint, by Ian F. Darwin (O’Reilly).
Several commercial and open source lint programs are available. One such program is called Splint. Additional information about Splint as well as the latest release of it can be found online at http://www.splint.org.
Version control software allows for storage of source code in a repository that can be updated as the project progresses through the different development stages. Various version control programs offer several features such as logging, file comparisons, and tagging releases, as well as tracking bug fixes and code updates for new features. Version control software can also assist in finding bugs that were introduced after changes were made to a stable code release.
Version control software can be useful not just for source code but for all files associated with a specific project or product, including programs, the tools used to build the software, and documentation. There are several open source version control programs available. Here are a few:
A system that allows many people to work on large sets of files simultaneously and can even combine changes from different people to a single file. Essential CVS, by Jennifer Vesperman (O’Reilly) covers CVS. There are also several graphical host applications for CVS, such as the Windows-based program WinCVS (http://www.wincvs.org).
A follow-on to CVS that solves some of CVS’s problems for large projects and is gaining adherents. Version Control with Subversion, by Ben Collins-Sussman, Brian W. Fitzpatrick, and C. Michael Pilato (O’Reilly) covers Subversion version control software. 
A free software (GNU project) version of a traditional Unix source control program adequate for small projects.
Most of the debugging tools described in this chapter will be used at one point or another in every embedded project. Oscilloscopes and logic analyzers are most often used to debug hardware problems; simulators to test software before the hardware is available; and debug monitors and emulators during the integration and software debug. Lint and version control software are typically used throughout the entire project. To be most effective, you should understand what each tool is for and when and where to apply it for the greatest impact.
On many occasions, software engineers don’t want anything to do with the hardware, but this attitude lessens the software engineer’s usefulness. Most projects are successful because the team members have a variety of skills and can assist in areas other than the discipline in which they are trained. Don’t look at a hardware problem as something that can be solved only by a hardware engineer. Look at it as something that you can learn from and help solve.
Don’t be afraid to get in there—alone or with the hardware engineer—and find out what is going on. If you don’t understand the issue, sit in the lab with the hardware engineer for a while to get a better idea of what the problem is. You might even be able to write a piece of code that exacerbates the problem and, as a result, uncovers its cause.