You are on page 1of 69

Writing Code For Debug

What is Debugging?
Debugging is the art of removing bugs
from software.
Debugging is identifying the problem
and providing the solution to the
software program.

Debugging

An error in a computer program is called a bug.


The process of finding & fixing these bugs is called
debugging.
There are several tools to help us debug programs.
When an error occurs, we dont know which line
causes the error.
To find the error, we can either:
Read the code, think about the logic, and hopefully
find the error
Failing that, we can print relevant variables at
different points of the code, and see where the
variables values deviate from what you expect

Aspects of debugging C code

Noticing

Localising

Identify the area of the bug

Understanding

Notice the bug by running test case.

Understand fully is programming mistake or


algorithm is in correct.

Repairing bugs

Modifying the code with fix. Check for any side


effects.

Types of bugs
Semantic bugs
Using uninitialized variables
Array boundaries
Using out of range value
Pointer bugs

Trying to use memory that has not been allocated


yet.
Trying to access memory that has been deallocated already.

Common Mistakes
Programming without thinking
Writing code in an unstructured
manner
Works for some cases.
Too many Bugs
Hard to understand
Hard to debug
Hard to fix bugs

Thinking about Programming


First design the program by thinking
about the numerous ways the
problem's solution may be found.
How does one design a program
before coding?
Different techniques (top-down
design)
Top-down design divides the program
into sub-tasks.

Thinking about Programming


Each sub-task is a smaller problem
that must be solved.
Problems are solved by using an
algorithm. Functions (and data) in the
program implement the algorithm.
Trace with different inputs to check
algorithm manually.
When confident that algorithm works
then start implementing the code.

Structured Programming
Writing structured programs helps
greatly in debugging the code.
Some of the features of a structured
program:

Lots of well-defined functions!


Using structured loop constructs (i.e., while and
for) instead of goto.
Using variables that have one purpose and
meaningful names.
Using structured data types to represent complex
data.

Structured Programming
When a program is structured, it is
divided into sub-units that may be
tested and debugged separately.
Storing the sub-units of a program into
separate source files can make it
easier to debug them separately.
Sub-units can often be reused in other
programs.

Designed & Structured Code


Program with NO or very few bugs
Easy to understand the program by
any one.
Easy to debug the code.
Easy to fix the bug.

Other Important things.

Following Coding
Conventions
Set of rules for naming the file names
Ex: module_init.c, module_init.h
Function and variable naming
Ex: module_open(), module_close()
Ex: bool g_module_init_done;

Documentation
Documentation helps a lot to
understand and debug the code.
Write comments

About the file


Function Description
Any complex mathematical operation
Magic values
Complex conditions

Adding Debug Prints


Add prints to log the execution flow.
Enable prints for debug only.
Add prints for all error conditions. And
enable all the time.

Memcpy()
/* memcpy-- copy a non overlapping memory block */
void *memcpy(void *pvTo,void *pvFrom, size_t size)

byte *pbTo
byte *pbFrom

= (byte *) pvTo;
= (byte *) pvFrom;

if ( pvTo == NULL || pvFrom == NULL )


{
fprintf(stderr, Bad args in memcpy\n);
halt();
}
while ( size-- > 0 )
{
*pbTo++ = *pbFrom++;
}
return( pvTo );
}

Memcpy() with DEBUG


/* memcpy-- copy a non overlapping memory block */
void *memcpy(void *pvTo,void *pvFrom, size_t size)

byte *pbTo
byte *pbFrom

= (byte *) pvTo;
= (byte *) pvFrom;

#ifdef DEBUG
if ( pvTo == NULL || pvFrom == NULL )
{
fprintf(stderr, Bad args in memcpy\n);
halt();
}
#endif
while ( size-- > 0 )
{
*pbTo++ = *pbFrom++;
}
return( pvTo );
}

Add asserts
The assert is used (with a booleanexpression parameter) to check
assumptions
If the expression is TRUE nothing
happens, if FALSE, a message is
printed and the program can be
stopped.

Memcpy() with Assert


/* memcpy-- copy a non overlapping memory block

*/

void *memcpy(void *pvTo,void *pvFrom, size_t size)

{
byte *pbTo

= (byte *) pvTo;

byte *pbFrom

= (byte *) pvFrom;

assert( !((pvTo == NULL) || (pvFrom == NULL)) );


while ( size-- > 0 )
{
*pbTo++ = *pbFrom++;

}
return( pvTo );
}

Debugger functions

No matter which debugger or IDE youre using, a


debugger always has the following basic
functionalities:

Start: Start executing the program in debug mode.


Set/remove breakpoint: Set/remove a breakpoint at a
line of code. When the program runs to a line of code
with a breakpoint set, it pauses and waits for user
command
Step: Run one line of code
Show variable: Display the value of a variable at this
time
Continue: Continue running the program (until the
next breakpoint is reached)
Move up the stack: Examine the variables at other

Debugging Techniques
Rule of Thumb: Write good, bug-free
code from beggining
Testing/Debugging embedded
software is more difficult than
application software
Post-shipment application problems
are more tolerable than embedded
(real-time or life-critical) software

Testing on Host Machine

Some reasons why you cant test on target


machine:
Test early (target may not ready or
completely stable)
Exercise all code, including exceptions (real
situations may be difficult to exercise)
Develop reusable, repeatable test (difficult to
do in target environment, and likelihood of
hitting the same bug is low)
Store test results (target may not even have
disk drive to store results)

Testing on Host Machine


Target system on the left: (hardwareindep code, hardware-dep code, hw)
Test system (on host) on the right:
(hardware-indep code same,
scaffold rest)
Scaffold provides (in software) all
functionalities and calls to hardware
as in the hardware-dep and hardware
components of the target system
more like a simulator for them!

Testing on Host Machine

Radio.c -- hardware independent code

Radiohw.c hardware dependent code (only interface


to hw: inp() and outp() supporting vTurnOnTransmitter()
and vTurnOffTransmitter() functions

Inp() and outp() must have real hardware code to


read/write byte data correctly - makes testing harder!!

Replace radiohw.c with scaffold, eliminating the need


for inp() and outp() both are simulated in software a
program stub!!

Figure 10.2 A Poor Plan for Testing


/* File: radio.c */
void vRadioTask (void)
{
.
.
.
!! Complicated code to determine if turning on the radio now
!! is within FCC regulations.
.
.
.
!! More complicated code to decide if turning on the radio now
!! makes sense in our protocol.
If (!! Time to send something on the radio)
{
vTurnOnTransmitter (FREQ_NORMAL);
!! Send data out
vTurnOffRadio ();
}
}
----------------------------------------------(continued)

Figure 10.2 (continued)


/* File: radiohw.c */
void vTurnOnTransmitter (int iFrequencyValue)
{
BYTE byPower;
/* Byte read from device controlling power. */
int
i;
/* Turn on main power for the radio. */
disable_interrupts ();
byPower = inp (PWR_CONTROL_ADDR);
byPower |= PWR_CONTROL_RADIO_MAIN;
outp (PWR_CONTROL_ADDR, byPower);
enable_interrupts ();
/* Shift the frequency value out to hardware. */
for (i = 0; i < 16; ++i)
{
/* Send out the lowest bit of iFrequencyValue */
if (iFrequencyValue & 0x0001)
{
/* The data is a binary 1 */
/* Put a '1' on the data line; pulse the clock line. */
outp (FRQ_CONROL, DATA_1 & CLOCK_LOW)
outp (FRQ_CONROL, DATA_1 & CLOCK_HIGH);
}
(continued)

Figure 10.2 (continued)


else
{
/* The data is a binary 0 */
/* put a '0' on the data line; pulse the clock line. */
outp (FRQ_CONROL, DATA_0 & CLOCK_LOW)
outp (FRQ_CONROL, DATA_0 & CLOCK_HIGH);
}
/* Shift iFrequencyValue to get the next lowest bit. */
iFrequencyValue >>= 1;
}
/* Turn on the receiver. */
byPower = inp (PWR_CONTROL_ADDR);
byPower |= PWR_CONTROL_RADIO_RECEIVER;
outp (PWR_CONTROL_ADDR, byPower);
enable_interrupts ();
}
void vTurnOffRadio (void)
{
BYTE byPower;

/* Byte read from device controlling power. */

/* Turn off main power for the radio. */


disable_interrupts ();
byPower = inp (PWR_CONTROL_ADDR);
byPower &= ~PWR_CONTROL_RADIO_MAIN;
outp (PWR_CONTROL_ADDR, byPower);
enable_interrupts ();
}
-------------------------------------------

(continued)

Figure 10.2 (continued)


/* File: test.c */
void outp (int Address, BYTE byData)
{
#ifdef
LET_USER_SIMULATE_HARDWARE
PRINTF ("program wrote %02x to %04x. ", byData, iAddress);
#endif
#ifdef
SIMULATE_HARDWARE
!! Remember that software wrote byData to iAddress
!! Update state of simulated hardware.
#endif
}
BYTE inp (int iAddress)
{
int iData;
#ifdef
LET_USER_SIMULATE_HARDWARE
PRINTF ("program wrote %02x to %04x. Enter value. ",
iAddress);
scanf ("%x", &iData);
#endif
#ifdef
SIMULATE_HARDWARE
!! Figure out what the real hardware would return
#endif
return ((BYTE) iData);
}

Figure 10.3 Better Plan for Testing


/* File: radio.c */
void vRadioTask (void)
{
.
.
.
!! Complicated code to determine if turning on the radio now
!! is within FCC regulations.
.
.
.
!! More complicated code to decide if turning on the radio now
!! makes sense in our protocol.
If (!! Time to send something on the radio)
{
vTurnOnTransmitter (FREQ_NORMAL);
!! Send data out
vTurnOffRadio ();
}
}
----------------------------------------------(continued)

Figure 10.3 (continued)


/* File: test.c */
static BOOL
fRadionOn;
static int
iRadioFrequencyValue;
void vTurnOnTransmitter (int iFrequencyValue)
{
/* Record the state of the radio. */
fRadionOn = TRUE;
iRadioFrequencyValue = iFrequencyValue;
/* Tell the user */
printf ("Radio turned on with frequency %04x", iFrequencyValue);
}
void vTurnOffRadio (void)
{
/* Record the state of the radio. */
fRadioOn = FALSE;
/* Tell the user */
printf ("Radio now off");
}

Testing on Host Machine

Calling Interrupt Routines

Embedded systems are interrupt-driven, so to test based on interrupts

1) Divide interrupt routines into two components

A) a component that deals with the hardware

B) a component of the routine which deals with the rest of the system

2) To test, structure the routine such that the hardware-dependent component (A)
calls the hardware-independent part (B).

3) Write component B in C-language, so that the test scaffold can call it

Hw component (A) is vHandleRxHardware(), which reads characters from the hw

Sw component (B) is vHandleByte, called by A to buffer characters, among others

The test scaffold, vTestMain(), then calls vHandleByte(), to test if the system works
[where vTestMain() pretends to be the hardware sending the chars to vHandleByte()]

Figure 10.4 Dividing Interrupt Routines into Two Parts


/* File: serial.c */
#define CR 0x0d
#define SIZEOF_CMD_BUFFER
200
BYTE a_byCommandBuffer[SIZEOF_CMD_BUFFER];
/* Queue to send message to command-handling task. */
extern unsigned long
qidCommands;
void interrupt vHandleRxHardware (void)
{
BYTE byChar;
/* The character we received */
int
iHwError;
/* Hardware error, if any */
iHwError = !! Get status from hardware;
if (iHwError == CHARACTER_RXD_OK)
{
/* We received a character; deal with it. */
byChar = !! Read byte from hardware;
vHandleRxByte (byChar);
}
else
!! Deal with hardware error
!! Reset the hardware as necessary.
!! Reset interrupt controller as necessary.
}

(continued)

Figure 10.4 (continued)


void vHandleRxByte (BYTE byReceived)
{
static BYTE
*p_byCommandBufferTail = a_ byCommandBuffer;
extern BYTE
*p_byCommandBufferHead;
unsigned long
a_ulMessage[4];
/* Message buffer. */
/* Advance the tail pointer and wrap if necessary */
++ p_byCommandBufferTail;
if (p_byCommandBufferTail == &a_ byCommandBuffer
[SIZEOF_CMD_BUFFER])
p_byCommandBufferTail = a_ byCommandBuffer;
/* If the buffer is not full . . . . */
if (p_byCommandBufferTail != p_byCommandBufferHead)
{
/* Store the character in the buffer. */
*p_byCommandBufferTail = byReceived;
/* If we got a carriage return, wake up the command-handling task. */
if (*p_byCommandBufferTail == CR)
{
/* Build the message . . . */
a_ulMessage[0] = MSG_COMMAND_ARRIVED;
a_ulMessage[1] = 0L;
a_ulMessage[2] = 0L;
a_ulMessage[3] = 0L;
(continued)

Figure 10.4 (continued)


/* . . . and send it. */
q_send (qidCommands, a_ulMessage);
}
}
else
{
/* Discard the character; move the pointer back. */
if (p_byCommandBufferTail == a_ byCommandBuffer)
p_byCommandBufferTail ==
&a_ byCommandBuffer[SIZEOF_CMD_BUFFER];
-- p_byCommandBufferTail;
}
}
-------------------------------------------/* File: test.c */
void vTestMain (void)
{
BYTE

a_byTestCommand[] = "THUMBS UP\x0dSIMON SAYS THUMBS UP\x0d";

BYTE *p_by;
.
.
/* Send each of the characters in a_byTestCommand */
p_by = a_byTestCommand;
while (*p_by)
{
/* Send a single character as though received by the interrupt */
vHandleRxByte (*p_by);
/* Go to the next character */
++p_by;
}
.
.
}

Testing on Host Machine


Calling the Timer Interrupt Routine
Design the test scaffold routine to
directly call the timer interrupt routine,
rather than other part of the host
environment, to avoid interruptions in
the scaffolds timing of events.
This way, the scaffold has control over
sequences of events in the test which
must occur within intervals of timer
interrupts.

Testing on Host Machine


Objections, Limitations, and Shortcomings
1) Hard to test parts which are truly hardware
dependent, until the target system is operational.
Yet, good to test most sw-independent parts on
host.
2) Time and effort in writing scaffold even if
huge, it is worthwhile
3) The hard to justify limitations cant tell in
scaffold until the actual test

Writing to the wrong hardware address software/hardware


interactions
Realistic interrupt latency due to differences in processor
speeds (host v. target)
Real interrupts that cause shared-data problems, where real
enable/disable is the key
Differences in network addressing, size of data types, data
packing schemes portability issues

Instruction Set Simulators


Using software to simulate:
The target microprocessor instruction set
The target memory (types - RAM)
The target microprocessor architecture
(interconnections and components)

Simulator must understand the linker/locator Map


format, parse and interpret it
Simulator takes the Map as input, reads the
instructions from simulated ROM, reads/writes from/to
simulated registers
Provide a user interface to simulator for I/O, debugging
(using, e.g., a macro language)

Instruction Set Simulators

Capabilities of Simulators:

Collect statistics on # instructions executed, bus cycles for estimating


actual times

Easier to test assembly code (for startup software and interrupt


routines) in simulator

Easier to test for portability since simulator takes same Map as the
target

Other parts, e.g., timers and built-in peripherals, can be tested in the
corresponding simulated versions in the simulated microprocessor
architecture

What simulators cant help:

Simulating and testing ASICs, sensors, actuators, specialized radios


(perhaps, in future systems!!)

Lacking I/O interfaces in simulator to support testing techniques


discussed (unless additional provision is made for I/O to support the
scaffold; and scripts to format and reformat files between the

The assert Macro


Assert works well in finding bugs early, when testing in
the host environment
On failure, assert causes a return to the host operating
systems (cant do on target, and cant print such
message on target may not have the display unit)
Assert macro that runs on the target are useful for
spotting problems:

1) disabling interrupts and spin in infinite loop effectively stopping


the system
2) turn on some pattern of LEDs or blinking device
3) write special code memory for logic analyzer to read
4) write location of the instruction that cause problem to specific
memory for logic analyzer to read (the Map can help isolate which
source code is the culprit!)
5) execute an illegal op or other to stop the system e.g., using incircuit emulators

Using Tools Hardwarefocused

Lab tools help reveal hard-to-find, very infrequently


occurring bugs
Types useful to software engineers:
Multi-meter(measure voltage/current/connectivity)
Oscilloscopes (scopes) test events that repeat
periodically monitoring one or two signals (graph of
time v. voltage), triggering mechanism to indicate start
of monitoring,
adjust vertical to know ground-signal, used as voltmeter
(flat graph at some vertical relative to ground signal),
test if a device/part is working is graph flat? Is the
digital signal coming through expecting a quick
rising/falling edge (from 0 VCC or VCC 0) if not,
scope will show slow rising/falling indicating loading,
bus fight, or other hardware problem.

Using Tools Hardwarefocused

Logic Analyzer
Like storage scopes that (first) capture many
signals and displays them simultaneously
It knows only of VCC and ground voltage levels
(displays are like timing diagrams) Real scopes
display exact voltage (like analog)
Can be used to trigger on-symptom and track
back in stored signal to isolate problem
Many signals can be triggered at their low and/or
high points and for how long in that state
Used in Timing or State Mode

Logic Analyzers in Timing


Mode

Find out if an event occurred By


external device

Measure how long it took software to


respond to an interrupt (e.g., between a
button interrupt signal and activation
signal of a responding device)

Is the software putting out the right


pattern of signals to control a hardware
device looking back in the captured
signal for elapsed time

Logic Analyzers in State


Mode

Captures signals only on clock-event occurring from the attached


hardware

Typical use: instructions read/fetched by microprocessor, data read


from or written to ROM, RAM, or I/O devices

To do so, connect LA to address and data signals and RE/ signal on


the ROM (or RAM)

If triggering is on rising edge of RE/ pin, address and data signals


will be captured

Output of LA, called trace, is stored for later analysis

LA can be triggered on unusual event occurrences, then capture


signals therefrom especially for debugging purposes (e.g., writing
data to wrong address, tracking a rarely occurring bug, filtering
signals for select devices or events)

LA cant capture all signals, e.g., on fetch from caches, registers, unaccessed memory

In-Circuit Emulators (ICE)

Replaces target microprocessor in target circuitry

Has all the capabilities of a software debugger

Maintains trace, similar to that of an LAs

Has overlay memory to emulate ROM and RAM for a specified


range of address within the ICE (rather than the systems main ROM
or RAM) facilitates debugging

ICE v. LA

LAs have better trace and filtering mechanism, and easier to detail and find
problems

LAs run in timing mode

LAs work with any microprocessor ICE is microprocessor-specific

LAs support many but select signals to attach, ICE requires connecting ALL
signals

ICE is more universal

Hardware Peculiarities that Make


Debugging Difficult

Inter-pin distances/intervals for attaching


probes getting smaller
Providing sockets for debugging
hardware simply increases product
size
Use of RISC architectural designs makes
it difficult to track when read/write
happen in on-board (microprocessor)
caches different from the external RAM
or ROM
Increasingly necessary to know the lab
tool chain as it influences the design of

Software-Only Monitors

Monitors allow running an embedded system in the target


environment, while providing debugging interfaces on both the
host and target environments
A small portion of the Monitor resides in the target ROM
(debugging kernel or monitor):
The codes receives programs from serial port, network, copies
into targets RAM, and run it with full debugging capabilities to
test/debug the programs
Another portion of monitor resides on host provides debugging
capability and communicates with the debugging kernel over
serial port or network, without hardware modifications
Compiled, linked (may be located into Map) code is downloaded
from the host (by the portion on the host) to the target RAM or
flash (received by the kernel)

Logging
Logging is the process of recording
events, with an automated program.
To provide an audit trail that can be
used to understand the activity of the
system and to diagnose problems.
To understanding the activities of
complex systems particularly in the
case of applications with little user
interaction (such as server
applications).

Examples
Physical systems which have logging
subsystems include process control
systems (Telemetry).
Black box recorders in aircraft.
Many operating systems and
multitudinous computer programs
include some form of logging
subsystem.
Log messages are written to a log file /
memory.

Interpreting Logs
logs are esoteric or too verbose and
therefore hard to understand.
Logs are analyzed with special log
analysis software.
Ex: USB data packets log
Ex: Network data packets log

Transaction Logs
Database systems maintains separate
log called transaction log.
This is used to recover the database
from crash.
Maintain the data in consistent state.

Data Loggers
A data logger is an electronic device
that records data over time.
Generally they are small, battery
powered, portable, and equipped with
a microprocessor, internal memory for
data storage, and sensors.
Connected to PC to analyze the data.

Tracing
Tracing is a specialized use of logging
to record information about a
program's execution.
This information is typically used by
programmers for debugging purposes.
Provides low level information of the
programs execution.

Software tracing
Provides Developers with information
useful for debugging.
It is used both during the development
cycle and after the software is
released.
Tracing messages should be kept in
the code, they can add to the
readability of the code.

Some Considerations
Performance of the system.
Tracing data may include sensitive
information about the product's source
code. Security information like secret
key.

Disable tracing at compile time in


release software.

Diagnostics
A diagnostic program is a program
written for the express purpose of
locating problems with the software,
hardware, or any combination thereof
in a system, or a network of systems.
Preferably, diagnostic programs
provide solutions to the user to solve
issues.

Examples
Diagnostics that are run on-demand when a
user needs assistance, typically within the
primary operating system of the computer
(e.g. Linux, Windows)
"Off-line diagnostics" that are run outside the
primary operating system, typically to reduce
the masking influence of software on
hardware issues.
Background diagnostics that monitor the
system for failures and marginal events, and
provide statistical data for failure prediction,
and root cause analysis of actual failure

Types of Diagnostics
Manufacturing testing program with an
emphasis on checking assemblyrelated issues.
End-user targeted diagnostics, easy,
non-technical and an emphasis on
solutions.
Service/warranty testing, focusing on
identifying a failed unit.
Hardware components have specific
features to assist a diagnostic

Questions?

You might also like