In the classic board game Battleship, an adversary arranges a fleet of tiny, plastic combat vessels on a grid that’s hidden from view. After an analogous fleet is set up on a separate grid, the objective is to guess the locations of the opponent’s boats. Likewise, the opponent’s goal is to divine the whereabouts of your miniature ships. The game proceeds with opponents alternately wagering guesses by calling out grid coordinates (C5, A2, etc.).
At first glance, this patently low-tech and old school game may seem to have little in common with embedded systems development. However, many engineers actually develop and debug software using the same trial-and-error tactics required for Battleship. Just as participants in a game of Battleship call out grid coordinates in attempts to locate ships, these engineers painstakingly insert p r i n t f ( ) statements into various locations in their code, hoping to eventually identify the sources of troublesome bugs.
Unfortunately, when p r i n t f ( ) is the only means of gathering helpful information from an embedded system, ridding the code of bugs is a tall order. The continued widespread use of such an outdated technique may help explain why so many engineers find debugging to be the most challenging aspect of their projects. In one recent survey of embedded software developers, a significant portion of the respondents indicated that they spend more than 50% of their time debugging.1
Although such dismal statistics seem to imply that debugging is necessarily an arduous process, many of the difficulties faced by engineers when developing and debugging embedded software could be avoided altogether. Whereas the rules of the game require Battleship players to somewhat blindly guess where opponents’ ships are hidden, today’s software developers needn’t speculate about the inner workings of their embedded systems. By using some helpful tools, engineers can glean valuable information from their embedded systems and quickly identify bugs that might otherwise lead to missed deadlines, excessive costs, and squandered opportunities.
For the most part, the tools described here don’t incorporate groundbreaking technology. Furthermore, most of these tools are exceedingly easy to use. Why, then, do so many software developers continue to use p r i n t f ( ) to gather information from embedded systems?
Many engineers (including myself) were introduced to software development via a “Hello World” application. Listing 1 shows an example “Hello World” that’s written in C and utilizes printf(), though there are certainly other forms of “Hello World.”
A particularly clever variation that relies on the human eye’s persistence of vision is described in Programming 16-bit Microcontrollers in C: Learning to Fly the PIC24 by Lucio Di Jasio. When a development board running this “Hello World” application is waved back and forth, the board’s rapidly blinking LEDs actually form the image of the word “hello.”
No matter how “Hello World” applications are implemented, they do nothing more than visually indicate that they’re running. Thus, for many engineers, “Hello World” isn’t simply the first piece of code that we write. It also introduces us to instrumented code. In other words, it shows us how printf()and other output routines can help us confirm that our code is running correctly. Consciously or not, much of the embedded community continues to follow the example of “Hello World” by using p r i n t f ( ) to monitor and debug code.
Since p r i n t f ( ) seems to be perfectly appropriate for “Hello World,” the drawbacks of developing and debugging complex applications with the assistance of this function (or with a similar output routine) may not be immediately apparent. However, this debugging strategy is hardly ideal. While you shouldn’t be discouraged from ever invoking p r i n t f ( ), there are plenty of reasons to be wary of this function.
One factor that should be considered before using p r i n t f ( ) in any project is memory usage. Because p r i n t f ( ) is a highly flexible library function capable of processing a wide range of options, it has substantial memory requirements. Notably, p r i n t f ( ) can increase the amount of stack space required by the application. Therefore, when attempting to debug a newly written application with this seemingly innocuous function, new bugs may be brought about via a stack overflow.
Supposing you can avoid any such memory issues, the size of p r i n t f ( ) still poses a potential problem. As a complex function that depends on an I/O device, p r i n t f ( ) typically has a relatively lengthy execution time. Software developers who rely on p r i n t f ( ) then must account for what’s commonly deemed the Heisenberg effect.
A debugging tool that produces the Heisenberg effect actually alters the behavior of the system being debugged. To understand the dangers of this phenomenon, imagine that the code snippet shown in Listing 2 is responsible for writing an initialization sequence to a flash memory controller. Assume that the sequence is 32 bits long and must be written, in two chunks, to a 16-bit register, access to which is provided by a function named Flash_Reg_Wr().
Continue on Page 2
Now, consider what might happen if, unbeknownst to this function’s author, the register involved in initialization could only be accessed once every hundred clock cycles. During the debugging process, no problems would be detected, because the first p r i n t f ( )call shown in Figure 2 would provide the requisite separation between the initialization sequence’s two register accesses. Upon removal of p r i n t f ( ), however, the second register access would most likely fail, possibly causing subsequent code to malfunction.
Listing 2 hints at another shortcoming of p r i n t f ( ): this function might not allow you to monitor all of your application’s code. Libraries and operating systems can’t be monitored with p r i n t f ( ) unless you possess the source code for these components. Clearly, if Flash_Reg_Wr() were available only in object code, p r i n t f ( ) would be a cumbersome means of debugging the flash-controller initialization sequence.
Even if you could obtain the source code for Flash_Reg_ Wr(), you would be wise to avoid polluting this code with unnecessary function calls. Typically, p r i n t f ( ) calls, like those shown in Listing 2, must be removed in the latter stages of the development process, either by redefining a configuration constant or by manually deleting code. Until then, the function calls simply constitute additional code that you and your colleagues must maintain.
In a perfect world, this additional code would provide helpful monitoring and debugging capabilities. For engineers who operate in the real world, however, p r i n t f ( ) affords meager benefits. The cryptic messages that rapidly scroll across your debugger’s console as this function is invoked leave you ill-prepared to resolve tricky bugs. If you intend to gather valuable information from your embedded system, you must look beyond printf().
Conveniently enough, the tool chain already used probably provides alternatives to Watch WindoWs. In particular, the debugger likely incorporates a watch window. As the example shown in Figure 1 indicates, watch windows provide snapshots of an application’s variables.
The basic watch-window theme has multiple variations. Essentially, all watch windows operate similarly. Most display variables in a tabular format. A watch window may require a table to be filled with variable names. Alternatively, this table may be automatically populated with names by the debugger. In either case, whenever a debugger halts execution of an embedded system (for instance, when triggering a breakpoint), the watch window will display the current value of each variable that’s listed.
Usually, the engineer is responsible for interpreting the numbers displayed by the watch window. If, for instance, the watch window indicates that a value of zero was returned by a particular function, it must be determined whether zero means that a grievous problem occurred or that the function completed successfully.
However, there’s a notable exception to this rule. Debuggers that offer what’s commonly known as kernel awareness are specially equipped to assist engineers whose applications incorporate a real-time operating system (RTOS). These debuggers translate variable values into helpful, RTOS-specific status information that’s then displayed in a watch window or a similar interface. Figure 2 shows the output of one such debugger, IAR’s C-SPY.
In many regards, a watch window, even one that’s incapable of displaying RTOS statistics, is a marked improvement over p r i n t f ( ). By using a watch window, the engineer isn’t forced to add a new line of code to an application whenever monitoring a new variable. Consequently, this diminishes the likelihood that debugging efforts will actually become the source of bugs. Freed of the burden of maintaining line upon line of ad hoc debugging code, it’s possible to focus efforts on code that won’t ultimately be deleted. While watch windows can certainly benefit engineers struggling to meet deadlines, they are by no means perfect. Perhaps the most disappointing limitation of watch windows is that they typically don’t allow a running embedded system to be monitored. When debugging efforts involve a watch window, it’s mandatory to stop the execution of code to view fresh variable values.
Regularly halting processors to view a particular variable’s status might not be problematic when debugging, say, an implementation of Quicksort. Code that simply manipulates arrays of integers can be stopped indefinitely. The data required by the code will still be available when execution resumes.
The code running on most embedded systems, though, requires regular data from the outside world. Such I/O-bound code (as opposed to the CPU-bound code of Quicksort) usually can’t be stopped without negative consequences. Accordingly, a watch window is poorly suited for monitoring I/O-bound code.
If you’re unconvinced of this point, consider how to use a watch window to monitor a hypothetical USB device’s software. Imagine that this device regularly receives hundreds of packets from a PC and that the software progresses through multiple states based on the contents of those packets. There would likely not be problems with monitoring the variables associated with the application’s initial state. Engineers could simply stop the processor and consult the watch window.
Continue on Page 3
By doing so, however, engineers would temporarily prevent an application from processing additional packets. Thus, when restarting execution, the application may not complete its expected state transitions. In fact, in response to the code’s lack of activity, the attached PC could assume that the device malfunctioned.
Given the drawbacks of stopping code, one might revert to p r i n t f ( )in this situation. However, that would be trading one set of problems for another. Neither p r i n t f ( ) nor a watch window would be an optimal means of monitoring the USB application’s variables.
To avoid the hassles of p r i n t f ( ) and watch windows, engineers can turn to a tool such as the µC/Probe. Developed by Micrium, µC/Probe monitors an application’s data non-intrusively, meaning that there’s no need to halt the processor to view updated variable values. Furthermore, µC/Probe isn’t text-based, since it presents data in a graphical format. As such, it’s considered a runtime visualization tool.
When using this tool, there are few limits on what can be observed. Importantly, it’s not necessary to write a new line of code whenever a new variable is viewed. Access to all global variables is gained by adding just a small amount of code to the application. No matter how much data one gathers, the amount of code that must be added to the application doesn’t change.
Since µC/Probe can leverage the advanced debugging features of ARM’s Cortex-M3 processor, engineers who are working with this processor can actually use the tool without adding any new code to their applications. All of the supplemental code needed to support µC/ Probe on these processors comes with the tool. The pseudocode in listing 3 provides an overview of this simple code’s functionality.
The code represented by Listing 3 can be viewed as one of two components that make up µC/Probe. You select, via #define constants, whether this code will comprise one of the application’s ISRs or (for RTOS users) will be repeatedly executed within a low-priority task. In either case, the Heisenberg effect is minimal. The tool doesn’t adversely impact system responsiveness.
The other component, which is an application that runs on a PC, regularly transmits commands that are ultimately processed by this code. Commands can be passed between the two components via any of the popular communication protocols supported by µC/Probe. Because this list of protocols includes JTAG, TCP/IP, USB, and RS-232, the tool can be used in the absence of debug probes and other specialized hardware.
As Listing 3 indicates, each of the commands transmitted by the Windows portion of µC/Probe instructs the embedded system to read or write a particular memory address. The addresses specified in the commands correspond to the application’s variables. The tool extracts these addresses from an executable file, provided that this file is in ELF format. Since most compilers can produce an ELF file, µC/Probe can be used with almost any tool chain, including, most likely, the one currently used to develop code.
Unlike p r i n t f ( ) or any watch window, the tool enables the creation of a visually appealing user interface for the embedded system (Fig. 3). Within the aforementioned Windows application comes a palate of graphical components, including gauges, graphs, dials, and switches. Dragging and dropping these components onto a µC/Probe data screen makes it possible to put together a control panel that allows you to read and write any application variables.
While the graphical paradigm underlying µC/Probe should be refreshing to users of p r i n t f ( ) and watch windows, it’s not unprecedented. For years, engineers have been using National Instruments’ LabVIEW to graphically develop virtual test instruments.
LabVIEW is actually a graphical programming language, and in many ways it’s just as powerful as a standard programming language. Accordingly, it has seemingly limitless uses. In recent years, an increasing number of embedded systems developers has discovered LabVIEW, harnessing this tool to design and prototype both hardware and software.
On the software side, LabVIEW gives engineers the potential to create incredibly helpful monitoring tools. If you’re not a LabVIEW expert, unlock this potential with the help of iSYSTEM’s winIDEA Embedded Test Integration Toolkit for LabVIEW. As its name indicates, this product weds LabVIEW and iSYSTEM’s winIDEA development environment.
With regards to monitoring an application’s data, the most important of the multiple tools that comprise the product is the LabVIEW Debugging Workbench for winIDEA VI Communication. Using this tool, it’s possible to combine a variety of graphical components into a front end for the embedded system, without actually doing any LabVIEW programming.
Continue on Page 4
Without question, iSYSTEM’s LabVIEW solution and Micrium’s µC/Probe can both help gather valuable information from an embedded system. However, tools that allow you to read and write an application’s variables are only one part of a successful debugging strategy. To deliver innovative products ahead of schedule, engineers need a multifaceted debugging plan that exploits the many tools available today.
To round out a debugging arsenal, keep in mind today’s numerous trace tools. Such tools are available from Green Hills, IAR, Keil, Lauterbach, and many other vendors. Green Hills’ TimeMachine trace package has deservedly attracted much attention for enabling embedded software developers to easily step backward through code and locate insidious bugs.
TimeMachine, like other trace products, can be used alongside a run-time visualization tool. An engineer using this combination can view and even manipulate live data while maintaining a vast execution history.
Every tool has drawbacks, whether it enables tracing, run-time visualization, or something entirely different. Without the proper tools, debugging, much like Battleship, is simply a guessing game.
1. Goering, Richard, “Embedded developer survey reveals debugging challenges,” EE Times, May 11, 2007.