Debugging Code: Difference between revisions

From Arduboy Wiki
Jump to navigation Jump to search
Line 85: Line 85:


void loop() {
void loop() {
   #ifdef DEBUG
   #ifdef DEBUG
     Serial.println(“Loop(): Start”);
     Serial.println(“Loop(): Start”);
Line 92: Line 91:
   proc_A();
   proc_A();
   proc_B(12, 14);
   proc_B(12, 14);
}
}



Revision as of 02:48, 17 August 2024

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 008 – 010). 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 
  ... actual code.
}

void proc_B(uint8_t param1, uint8_t param2) {
  #ifdef DEBUG_PROC_B
     Serial.print(“proc_B(): Start);
  #endif
  ... actual code.
}

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);