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.
When in ROM...
One of the most obvious ways to download your embedded software is to load the binary image into a read-only memory device and 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. A device programmer is a computer system with several memory sockets on the top—of varying shapes and sizes—that 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 containing executable binary images could be easily transferred to it for ROM programming. After transferring the binary image to the device programmer, the memory chip is placed into the appropriately sized and shaped socket and the device type is selected from an on-screen menu. The actual device programming process may take anywhere from a few seconds to several minutes, depending on the size of the binary image and the type of memory device you are using.
After programming 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 after the chip has been inserted fully. As soon as the power is reapplied, the processor will begin to execute the code stored inside the ROM. However, each processor has its own rules about the location of the first instruction and the initializations that must be performed right away. You must ensure that the binary image you’ve produced satisfies these requirements. If your program doesn’t appear to work at all, it could be a problem with the location of the reset vector or the implementation of the startup code. During product development, it might be helpful to turn on an LED at the end of the startup code or at the beginning of main. That way, you’ll know at a glance if your new ROM satisfies the processor’s most basic requirements for program execution.
Debugging Tip #1 |
One of the most primitive debugging techniques available is the use of an LED as indicator of success and/or failure. The basic idea is to slowly walk the LED enable code through the larger program. In other words, you first begin with the LED enable code at the reset address. If the LED turns on, then you can edit the program, moving the LED enable code to just after the next execution milestone, rebuild, and test. This works best for very simple, linearly executed programs like the startup code. But if you don’t have access to a remote debugger or any of the other debugging tools described later in this chapter, this type of debugging may be your only choice. |
The Arcom board includes a special in-circuit programmable memory, called Flash memory, that does not have to be removed from the board to be reprogrammed. In fact, software that can perform the device programming function is already installed in another memory device on the board. You see, the Arcom board actually has two read-only memory devices—one (a true ROM) contains a simple program that allows the user to in-circuit program the other (a Flash memory device). All the host computer needs to talk to the monitor program is a serial port and a terminal program. Instructions for loading an Intel Hex Format file, like blink.hex, into the Flash memory device are provided in the “Target188EB Monitor User’s Manual,” which is included with the board.
The biggest disadvantage of this download technique is that there is no easy way to debug software that is executing out of ROM. The processor fetches and executes the instructions at a high rate of speed and provides no way for you to view the internal state of the program. This might be fine once you know that your software works and are ready to deploy the system, but it’s not very helpful during software development. 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.
Remote Debuggers
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. The front-end of a remote debugger looks just like any other debugger that you may have used. It usually has a text or GUI-based main window, with several smaller windows for the source code, register contents, and other relevant information about the executing program. However, in the embedded systems case, the debugger and the software being debugged are executing on two different computer systems.
A remote debugger actually consists of two pieces of software. The front-end runs on the host computer and provides the human interface just described. But there is also a hidden back-end that runs on the target processor and communicates with the front-end over a communications link of some sort. The back-end provides for low-level control of the target processor and is usually called the debug monitor. Figure 4-1 shows how these two components work together.
The debug monitor resides in ROM—having been placed there in the manner described earlier (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 running there. 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 remote debugger can make are “read register x,” “modify register y,” “read n bytes of memory starting at address,” and “modify the data at address.” The remote debugger combines sequences of these low-level commands to accomplish high-level debugging tasks like downloading a program, single-stepping through it, and setting breakpoint.
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 cross-platform debugging. So you can build a version of the GDB front-end that runs on any supported host and yet understands the opcodes and register names of any supported target. Source code for a compatible debug monitor is included within the GDB package and must be ported to the target platform. This port can be tricky, particularly if you’ve only got LED debugging at your disposal, but it has been done this way—I’ve even done it myself more times than I care to admit.
Communication between the GDB front-end and the debug monitor is byte-oriented and designed for transmission over a serial connection. The command format and some of the major commands are shown in Table 4-1 below. These commands exemplify the type of interactions that occur between the typical remote debugger front-end and the debug monitor.
Command | Request Format | Response Format |
Read Registers | g | <data> |
Write Registers | G<data> | OK |
Read Data at Address | m<address>,<length> | <data> |
Write Data at Address | M<address>,<length>:<data> | OK |
Start/Restart Execution | c | S<signal> |
Start Execution from Address | c<address> | S<signal> |
Single Step | s | S<signal> |
Single Step from Address | s<address> | S<signal> |
Reset/Kill Program | k | no response |
Remote debuggers are one of the most commonly used downloading and testing tools during embedded software development. This is primarily because of their low cost. Embedded software developers already have the requisite host computer. In addition, the price of a remote debugger front-end does not add significantly to the cost of a suite of cross-development tools (compiler, linker, locator, etc.). Finally, the suppliers of remote debuggers often desire to give away the source code for their debug monitors, in order to increase the size of their installed user base.
As shipped, the Arcom board includes a free debug monitor in Flash memory. Together with host software provided by Arcom, this debug monitor can be used to download programs directly into target RAM and execute them. To do this, you can use the tload utility. Simply connect the SourceVIEW serial communications adapter to the target and host as instructed in the “SourceVIEW for Target188EB User’s Manual,” and issue the following command on the host PC:
tload -g blink.exe SourceView Target Loader v1.4 Copyright (c) Arcom Control Systems Ltd 1994 Opening 'blink.exe'... download size 750H bytes (2K) Checking COM1 (press ESC key to exit)... Remote ident: TDR188EB version 1.02 Download successful Sending 'GO' command to target system
The -g option tells the debug monitor to start executing the program as soon as the download is complete. So, this is the RAM equivalent of execution direct out of ROM. In this case, though, we want to start with the relocatable program. The tload utility will automatically locate the program for us, at the first available location in RAM.
For remote debugging purposes, Arcom’s debug monitor can be used with Borland’s Turbo Debugger as the front-end. Turbo Debugger can then be used to step through your C/C++ and assembly language programs, set breakpoints in them, and monitor variables, registers, and the stack as they execute. Here’s the command you would use to start a debugging session for the Blinking LED program:
tdr blink.exe tver -3.1 Target Debugger Version Changer v1.2 Copyright (c) Arcom Control Systems Ltd 1994 Checking COM1 (press ESC key to exit)... Remote ident: TDR188EB version 1.02 TDR88 set for TD version 3.1 td -rp1 -rs3 blink.exe Turbo Debugger Version 3.1 Copyright (c) 1988,92 Borland International Waiting for handshake from remote driver (Ctrl-Break to quit)
The tdr command is actually a batch file that invokes two other commands. The first tells the on-board debug monitor which version of Turbo Debugger you will be using and the second actually invokes it. Again we use the relocatable version of the program, since we will be downloading the program into RAM and executing it from there. Both of these commands need to be issued each time you want to start a remote debugging session with the Arcom board. The tdr.bat batch file exists solely to combine them into a single command line.
The debugger startup options -rp1 and -rs3 establish the parameters for the communications link to the debug monitor. -rp1 means “remote-port=1” (COM1) and -rs3 means “remote-speed=3” (38,400 baud). These are the parameters required to communicate with Arcom’s debug monitor. After establishing a connection to the debug monitor, Turbo Debugger should start running. If it does not, there may be a problem with the serial link. Compare your setup of the link to the one in the SourceVIEW User’s Manual.
Once you’re in Turbo Debugger, you will see a dialog box that says: “Program out of date on remote, send over link?” When you select “Yes,” the file blink.exe will be downloaded to the target RAM. The debugger will then set an initial breakpoint at main and instruct the debug monitor to execute the program until that point is reached. So the next thing you should see is the C source code for main, with a cursor indicating the embedded processor’s program counter is at the entry point to that routine.
Using normal Turbo Debugger commands, you can step through the program, set breakpoints, monitor the values stored in variables and registers, and do all of the other things debuggers allow. Or you can simply press the F9 key to immediately execute the rest of the program. If you do this, you should then see the green LED on the front of the board start blinking. When you are satisfied that the program and the debugger are both working properly, press the reset switch attached to the Arcom board. This will cause the embedded processor to be reset, the LED to stop blinking, and Turbo Debugger to again respond to your commands.
Emulators
Remote debuggers are very helpful for monitoring and controlling the state of embedded software, but only an in-circuit emulator (ICE) allows you to examine the state of the processor on which that program is running. In fact, an ICE actually takes the place of—or emulates—the processor on your target board. It is itself an embedded system, with its own copy of the target processor, RAM, ROM, and its very own embedded software. As a result, in-circuit emulators are usually pretty expensive—often more expensive than the target hardware. But they are a very powerful tool, and in a tight debugging spot nothing else will help you get the job done better.
Like a debug monitor, an emulator also uses a remote debugger for its human interface. In some cases, it may even be possible to use the same debugger front-end 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 powerful debugging features like hardware breakpoints and real-time tracing, in addition to the features provided by any debug monitor.
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 executing 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 wide variety of events. These events include not only instruction fetches, but also memory and I/O reads and writes, and interrupts. For example, you might set a hardware breakpoint on the event “variable foo contains 15 and register AX becomes 0.”
Another useful feature of an in-circuit emulator is real-time tracing. Typically, an emulator incorporates a large block of special-purpose RAM dedicated to storing information about each of the processor cycles that are executed. This feature allows you to see in exactly what order things happened, so it can help you answer questions like “did the timer interrupt occur before or after the variable bar became 94?” In addition, it is usually possible to either restrict the information that is stored or post-process the data prior to viewing it in order to cut down on the amount of trace data to be examined.
ROM Emulators
One other type of emulator is worth mentioning at this point. A ROM emulator is a device that emulates a read-only memory device. Like an ICE, it is also an embedded system that connects to the target and communicates with the host. However, this time the target connection is via a ROM socket. To the embedded processor, it looks just like any other read-only memory device. But to the remote debugger, it looks like a debug monitor.
ROM emulators have several 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.
Simulators and Other Tools
Of course, many other debugging tools are available to you, including simulators, logic analyzers, and oscilloscopes. A simulator is a completely host-based program that simulates the functionality and instruction set of the target processor. The human interface is usually the same as or similar to that of the remote debugger. In fact, it may be possible to use one debugger front-end for the simulator back-end as well, as shown in Figure 4-2. 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.
Debugging Tip #2 |
If you ever encounter a situation in which the target processor is behaving differently than you think it should from reading the data book, try running the very same software in a simulator. If your program works fine there, then you know it’s a hardware problem of some sort. But if the simulator exhibits the same weirdness as the actual chip, you’ll know you’ve been misinterpreting the processor documentation all along. |
By far, the biggest disadvantage of a simulator is that it only simulates the processor. And embedded systems frequently contain one or more other important hardware devices. 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 valuable. So you probably won’t do too much with the simulator once you have the actual embedded hardware available to you.
Once you have access to your target hardware—and especially during the hardware debugging—logic analyzers and oscilloscopes can be indispensable debugging tools. They are most useful for debugging the interactions between the processor and other chips on the board. Because they can only view signals that lie outside the processor, however, they cannot control the flow of execution of your software like a debugger or an emulator can. This makes these tools significantly less useful by themselves. But coupled with a software debugging tool like a remote debugger or an emulator they can be extremely valuable.
A logic analyzer is a piece of laboratory equipment that is designed specifically for troubleshooting digital hardware. It may 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 4-3. Most logic analyzers will also let you begin capturing data, or ‘trigger’, on a particular pattern. For example, you might make the 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.”
Debugging Tip #3 |
Occasionally it is desirable to coordinate your observation of some set of electrical signals on the target with the embedded software that is running there. For example, you might want to observe the bus interaction between the processor and one of the peripherals attached to it. A handy trick is to add an output statement to the software, just prior to the start of the interaction you’re interested in. This output statement should cause a unique logic pattern to appear on one or more processor pins. For example, you might cause a spare I/O pin to change from a zero to a one. The logic analyzer can then be setup to trigger on the occurrence of that event and capture everything that follows. |
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 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 about 4) and advanced triggering logic is not often available. You’ll definitely be better off if you know what an oscilloscope looks like, but it’ll only rarely be useful to you as a software debugging tool.
Most of the debugging tools described in this chapter will be used at some point or another in every embedded project. Oscilloscopes and logic analyzers are most often used to debug hardware problems, simulators during early stages of the software development, and debug monitors and emulators during the actual software debugging. To be most effective, you should understand what each tool is for and when and where to apply it for the greatest impact.