[Design View / Design Solution]
Take The Guesswork Out Of Debugging
Move past the hit-and-miss game of “printf()” debugging, and instead look to streamline the task with run-time visualization tools.
Matthew Gordon
ED Online ID #21365
June 25, 2009
Copyright © 2006 Penton Media, Inc., All rights reserved. Printing of this document is for personal use only.
Reprints
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.
printf()
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().
WATCH WINDOWS
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.
RUNTIME VISUALIZATION
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 OTHER TOOLS
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.
REFERENCE
1. Goering, Richard, “Embedded
developer survey reveals debugging challenges,” EE Times, May 11, 2007.
|