Debugging Code
Most modern programming languages, such as C# or Java, and their respective development environments provide extensive tools for profiling and debugging code. As such, developers take for granted the ability to set break points or alter variable values and code on the fly. The Arduboy with its limited resources provides none of these facilities and debugging is often performed by logging status information to the console.
Therefore developing on the Arduino and Arduboy is like stepping back in time for some developers - forcing them to re-think how to debug applications.
Debugging Code is Valuable
Due to the limited memory in an Arduino environment, most developers will add temporary logging within a troublesome procedure and then strip it out when the issue is resolved. Over the life of a project, the same logging code may be added and removed from a single procedure a number of times!
I think logging code is valuable and should remain in the application at all times and be turned on or off at the flick of a switch.
Consider the following code:
001 #define DEBUG 002 003 #ifdef DEBUG 004 #define DEBUG_print(x) Serial.print(x) 005 #define DEBUG_printDec(x) Serial.print(x, DEC) 006 #define DEBUG_println(x) Serial.println(x) 007 #else 008 #define DEBUG_print(x) 009 #define DEBUG_printDec(x) 010 #define DEBUG_println(x) 011 #endif
It uses C / C++ prototypes to define an identifier DEBUG
(line 001) that indicates whether the application is in debug mode or not. Lines 004 – 006 define function prototypes that will log a string or decimal value to the serial port using a syntax similar to the print()
and println()
functions.
If the DEBUG
identifier is not defined, the prototype functions are declared again but this time with no body (lines 08 – 10). This has the result of effectively dropping the function at compile time – resulting in a smaller compilation unit. Rather than deleting the '#define DEBUG
line, I simply prefix the identifier with a few characters to change its value.
Now that we have defined both the identifier and the prototype functions, we can use them in the following way. To log a simple message or variable value, use the prototype function:
DEBUG_print(“Player X position : “); DEBUG_println(x);
For more complex logging, conditional include the code if the DEBUG
identifier is defined:
#ifdef DEBUG for (byte x = 0; x < card_count; x++) { Serial.println(card[x]); } #endif
But how do I turn sections on and off?
As a program grows and more debugging code is added, you will invariably run into two problems – firstly, you have so much information being logged that you cannot see the forest for the trees and, secondly, that the actual logging code is taking up a significant portion of the minimal memory available.
The following technique allows you to enable and disable logging overall and at the procedural level.
In the previous example, a single identifier named DEBUG
was used to turn on or off all debugging code. Identifier definitions can be nested in the Arduboy environment to create a hierarchy of debugging flags as shown below.
#define DEBUG #ifdef DEBUG #define DEBUG_PROC_A #endif #ifdef DEBUG #define DEBUG_PROC_B #endif
This definition provides an overall debugging identifier and individual identifiers for two procedures, A
& B
. Overall debugging can be enabled or disabled by changing the DEBUG
identifier to something else. Subsequent definitions of the identifiers DEBUG_PROC_A
and DEBUG_PROC_B
will be skipped as they are conditioned on the presence of identifier DEBUG
.
Debugging of individual procedures can be controlled by altering their identifiers to include or exclude them.
The sample application below shows these flags in action:
#define DEBUG #ifdef DEBUG #define DEBUG_PROC_A #endif #ifdef DEBUG #define DEBUG_PROC_B #endif void loop() { #ifdef DEBUG Serial.println(“Loop(): Start”); #endif proc_A(); proc_B(12, 14); } void proc_A() { #ifdef DEBUG_PROC_A Serial.println(“proc_A(): Start”); #endif } void proc_B(uint8_t param1, uint8_t param2) { #ifdef DEBUG_PROC_B Serial.print(“proc_B(): Start); #endif }
This technique can be extended to provide granular logging within a procedure itself. The example below shows how the level of logging could be refined in Procedure B<code.
#define DEBUG
#ifdef DEBUG
#define DEBUG_PROC_A
#endif
#ifdef DEBUG
#define DEBUG_PROC_B
#endif
#ifdef DEBUG_PROC_B
#define DEBUG_PROC_B_CALL
#endif
#ifdef DEBUG_PROC_B
#define DEBUG_PROC_B_PARAMS
#endif
Then, within the procedure itself, the new logging levels are incorporated to provide more information as the logging level is increased.
In the example below, if the DEBUG_ PROC_B_CALL
identifier is defined without DEBUG_PROC_B_PARAMS
identifier, the call to the procedure will only log the
text proc_B(): Start
. Including identifier DEBUG_PROC_B_PARAMS
will also log out the parameter values.
void proc_B(uint8_t param1, uint8_t param2) {
#ifdef DEBUG_PROC_B_CALL
Serial.println(“proc_B(): Start”);
#endif
#ifdef DEBUG_PROC_B_PARAMS
Serial.print("> param1 = ");
Serial.println(param1);
Serial.println("> param2 = ");
Serial.println(param2);
#endif
}
Almost there ..
Previously we defined a number of prototype functions to allow us to conditionally include a Serial.print()
or Serial.println()
in our code.
These prototypes can be expanded to include additional information, such as the function (procedure) name, file name and line number, as shown below.
#define DEBUG
#ifdef DEBUG
#define DEBUG_println(x) Serial.println(x)
#define DEBUG_printDec(x) Serial.println(x, DEC)
#define DEBUG_println(str)
Serial.print(__FILE__);
Serial.print(", ");
Serial.print(__FUNCTION__);
Serial.print(": ");
Serial.print(__LINE__);
Serial.print(" (");
Serial.print(millis());
Serial.print(") ");
Serial.println(str);
#endif
#else
#define DEBUG_print(x)
#define DEBUG_printDec(x)
#define DEBUG_println(x)
#endif
Last - but certainly not least – my final tip. When developing and testing logic games, it is often desirable to have the pseudo-random number generator produce the same series of numbers each time thus allowing you to test a sequence of events again and again until your code performs correctly.
I use the DEBUG
identifier to condition the seeding of the number sequence – if I am debugging the code, I want the seed to be a constant otherwise I seed the generator with another random number (in this case the value read from an analogue port with a floating pin).
#ifdef DEBUG
int seed = 1;
#else
int seed = analogRead(3);
#endif
randomSeed(seed);