Debug Macros - Good or Bad?

Recently a bright idea visited my head:

#ifndef NO_DEBUG
    #define P(x) Serial.print(F(x))
    #define PL(x) Serial.println(F(x))
    #define PF(...) Serial.printf(__VA_ARGS__)

#else
    #define P(x)
    #define PL(x)
    #define PF(...)
#endif

The following code facilitates the debug purpose messages in code and can control their presence in the final code, if NO_DEBUG is defined every macro statement is just switched by an empty place in the code (At least that is what I think).

  1. The F() macro is usable on the print and println statements, does someone know how to use it on printf arguments ?

  2. In general, is this code considered 'bad'?

  3. Are there built-in alternatives to this purpose?

This is a common technique.

Search printf

1 Like

Is there a way to use printf with the F() macro?

Don't have the time now, however these might be interesting to you:

//***************************************************************
//If you comment the line:    #define DEBUG
//the DPRINT & DPRINTLN lines are defined as blank, thus would be ignored by the compiler
#define DEBUG  // if this line is NOT commented, these macros will be included in the sketch
//examples:
// DPRINTLN("Testing123");    
// DPRINTLN(0xC0FFEEul,DEC);
// DPRINTLN(12648430ul,HEX);
// DPRINTLNF("This message came from your flash");  

#ifdef DEBUG
#define DPRINT(...)    Serial.print(__VA_ARGS__)
//OR, #define DPRINT(args...)    Serial.print(args)
#define DPRINTLN(...)  Serial.println(__VA_ARGS__)
#define DRINTF(...)    Serial.print(F(__VA_ARGS__))
#define DPRINTLNF(...) Serial.println(F(__VA_ARGS__)) //printing text using the F macro
#define DBEGIN(...)    Serial.begin(__VA_ARGS__)

#else
#define DPRINT(...)     //blank line
#define DPRINTLN(...)   //blank line
#define DPRINTF(...)    //blank line
#define DPRINTLNF(...)  //blank line
#define DBEGIN(...)     //blank line

#endif

And:

//***************************************************************
//   Example of use: 
//   #define DEBUG  //                              <---<<< this line must appear before the include line
//
//   #include <DebugMacros.h>
// 
//If you comment the line:    #define DEBUG
//the Macro lines are defined as blank, thus would be ignored by the compiler
//#define DEBUG  // if this line is NOT commented, these macros will be included in the sketch
//examples:
//  This  converts to >>>>----------------------->  This OR a Blank Line.  
// DPRINTLN("Testing123");                          Serial.println("Testing123");  
// DPRINTLN(0xC0FFEEul,DEC);                        Serial.println(0xC0FFEEul,DEC); 
// DPRINTLN(12648430ul,HEX);                        Serial.println(12648430ul,HEX); 
// DPRINTLNF("This message came from your flash");  Serial.println(F("This message came from your flash"));
// DPRINT(myVariable);                              Serial.print(myVariable);
// DELAY(100);                                      delay(100);
// PINMODE(9600);                                   pinMode(9600);
// TOGGLEd13;                                       PINB = 0x20;  // D13 Toggle,for UNO ONLY
//
// Also, this works  #define INFO(...)  { Console->printf("INFO: "); Console->printf(__VA_ARGS__); }   >>>--->   where {} allows multiple lines of code.
// See: http://forum.arduino.cc/index.php?topic=511393.msg3485833#new

#ifdef DEBUG
//OR the next two lines
//#define DEBUG 1
//#if DEBUG || (another thing etc.)

//examples:
//#define DPRINT(args...)  Serial.print(args)  OR use the following syntax:
#define SERIALBEGIN(...)   Serial.begin(__VA_ARGS__)
#define DPRINT(...)        Serial.print(__VA_ARGS__)
#define DPRINTLN(...)      Serial.println(__VA_ARGS__)
#define DRINTF(...)        Serial.print(F(__VA_ARGS__))
#define DPRINTLNF(...)     Serial.println(F(__VA_ARGS__)) //printing text using the F macro
#define DELAY(...)         delay(__VA_ARGS__)
#define PINMODE(...)       pinMode(__VA_ARGS__)
#define TOGGLEd13          PINB = 0x20                    //UNO's pin D13
#define PULSEd13           PINB = 0x20; PINB = 0x20       //a 62.5ns pulse is output on pin 13 "UNO"

#define DEBUG_PRINT(...)   Serial.print(F(#__VA_ARGS__" = ")); Serial.print(__VA_ARGS__); Serial.print(F(" ")) 
#define DEBUG_PRINTLN(...) DEBUG_PRINT(__VA_ARGS__); Serial.println()

//***************************************************************
#else

#define SERIALBEGIN(...)  
#define DPRINT(...)       
#define DPRINTLN(...)     
#define DPRINTF(...)      
#define DPRINTLNF(...)    
#define DELAY(...)        
#define PINMODE(...)      
#define TOGGLEd13      
#define PULSEd13

#define DEBUG_PRINT(...)    
#define DEBUG_PRINTLN(...)  

#endif
//***************************************************************

BTW, do you have access to an oscilloscope ?

The print methods that use F() macro don't work since it takes only one argument and VA_ARGS are 1 or more. it is useful to use it only on the blank print statements.

Sorry, I don't, I am not even sure what is it.

Oh but the #VA_ARGS seems to be interesting.

My advice would be to avoid macros as much as possible. In almost all cases C++ offers better, type safe solutions like templates, inline functions, constants, etc. You may want to read: So, what's wrong with using macros? near the bottom of the page.

as long as you don't proof with a working example how to get a switchable debug print like in the starting post which uses zero program space when switched off, this example is exactly that 0,1% of cases which make makros still valuable.

2 Likes

ESP32 offers selectable Debug Levels that automatically include / exclude logging statements at compile time.

The logging statements follow the printf convention. For example:

  log_e("Values: %s, %d, %5.2f", x, y, z);
  
  log_i("Values: %s, %d, %5.2f", x, y, z);
   
  log_d("Values: %s, %d, %5.2f", x, y, z);

If the Debug Level were set to "Info" as in the image above, the "Error" and "Info" information would be printed. The "Debug" log would not.

1 Like

Also, log_x printing is faster on a ESP32 then Serial.prints().

Faster how? Faster putting data into the UART's TX Buffer? Given that the UART is still bound by the selected Baud (say 115200), is it noticeable to the user?

For log_x printing there is not serial baud rate setting.

in this setup()

void setup()
{
  gpio_config_t io_cfg = {}; // initialize the gpio configuration structure
  io_cfg.mode = GPIO_MODE_OUTPUT;
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_0) ); //bit mask of the pins to set, assign gpio number to be configured
  gpio_config(&io_cfg);
  gpio_set_level( GPIO_NUM_0, LOW); // 
  //
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);// using GPIO 34  
  // hardware timer 4 set for one minute alarm
  hw_timer_t * timer = NULL;
  timer = timerBegin( 3, 80, true );
  timerAttachInterrupt( timer, &onTimer, true );
  timerAlarmWrite(timer, 60000000, true);
  timerAlarmEnable(timer);
  ///
  co2Serial.begin( 9600 , SERIAL_8N1, 25, 26 ); // pin25 RX, pin26 TX
  //
  x_eData.WD.reserve(50);
  x_message.topic.reserve( payloadSize );
  //
  xQ_WindChillDewPoint = xQueueCreate( 1, sizeof(stu_eData) );
  xQ_Message  = xQueueCreate( 1, sizeof(stu_message) );
  xQ_eData    = xQueueCreate( 1, sizeof(stu_eData) ); // sends a queue copy of the structure
  //
  sema_PublishPM = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_PublishPM );
  sema_mqttOK    =  xSemaphoreCreateBinary();
  xSemaphoreGive( sema_mqttOK );
  sema_CollectPressure = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_CollectPressure );
  sema_eData = xSemaphoreCreateBinary();
  xSemaphoreGive ( sema_eData );
  //
  eg = xEventGroupCreate(); // get an event group handle
  //
  xTaskCreatePinnedToCore( fparseMQTT, "fparseMQTT", 7000,  NULL, 5, NULL, 1 );
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 5000, NULL, 6, NULL, 1 );
  xTaskCreatePinnedToCore( DoTheBME680Thing, "DoTheBME280Thing", 20000, NULL, 5, NULL, 1);
  xTaskCreatePinnedToCore( fmqttWatchDog, "fmqttWatchDog", 5000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDoTheDisplayThing, "fDoTheDisplayThing", 30000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fGetCO2, "fGetCO2", 4500, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fParseDewPointWindChill, "fParseDewPointWindChill", 4500, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fSolarCalculations, "fSolarCalculations", 10000, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fProcessAirPressure, "fProcessAirPressure", 5000, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fFindDewPointWithHumidity, "fFindDewPointWithHumidity", 5000, NULL, 2, NULL, 1 );
  xTaskCreatePinnedToCore( fLowSideSwitchTest, "fLowSideSwitchTest", 2000, NULL, 5, NULL, 1 );
} //void setup()

You can see there is no Serial baud rate setting. Log_x printing uses 921600 baud. Yet log_i happily prints using the ESP32's default baud rate of 921600.

I notice log_x printing to be faster.

there's not one good way to do debugging. sometimes prints work, but at other times they can't be fast enough and trace buffers are needed and dumped after some real-time event occurs

unless you're memory constrained, not sure using DEBUG macros is worth the effort. you may need to enable some debugging at run-time when a problem occurs.

And in my experience and from what i've heard from other professionals, is debugging needs to be enabled for specific parts of the code, not the classic set of levels

i use a debug bitmap

    if (DBG_MENU & debug)
        printf ("%s: '%c', digit %d\n", __func__, s [digit], digit);

multiple debug options can be enabled at the same time

#define DBG_ENGINE   2
#define DBG_BRAKE    4
#define DBG_CYLPRESS 8

#define DBG_FORCE    16
#define DBG_MENU     32
#define DBG_BUT      64
#define DBG_KEYPAD  128

the debug variable can be initialized

unsigned int debug = DBG_BRAKE;

but since it's a variable, can be changed thru the serial monitor or some other interface (e.g. bluetooth, wifi, menu)

sometimes debugging just needs to be controlled piecemeal

#if 0
    printf (" %s: %6.4f %6.4f %6.4f %6.0f %6.4f %d\n",
        __func__, tractEff, whRes, grF, brkF, force, ABS(force) <= brkF);
#endif

it's hard to figure out where debugging is needed. it's common that to give instructions to a customer to enable specific debugging if there is a field problem and hope that it's sufficient to at least better isolate the problem

See the default baud rate of the ESP32,


A bit faster than 115200.

Also the ESP32's default serial buffer size is 1000 bytes for receive and 1000 bytes for transmit.

A simple example would be:

bool const debug {false};

void setup() {
  Serial.begin(9600);

  if (debug) {
    Serial.println("Hi there.");
  }
}

The compiler will eliminate the dead code (including the test) even with no optimisations enabled. Note that this is not the case when debug is not declared const.

1 Like

The only issue with this code is that you have to put if(debug) statements in every place you actually want to output text, this is not what a good code has to look like.

Do you know how adjust debug level in platformio?

I see no problems with using Debugging macros in code debugging.

After all, you are trying to find a problem.

Here is a different example you might be interested in trouble shooting problems.

Yes I know you can connect LEDs to see them flash but this example shows things in what I call a PSW (Process Status Word).

Do have to remember, adding debug code can change timing and critical events so keep things fast.

//                                          P S W
//********************************************^************************************************
//Debugging with PSW (Process Status Word)
//Used to help diagnose coding problems.
//Whenever the PSW changes, the PSW bit fields are printed.
//We can use these bits to see the state of strategic variables which might help in our debugging.
//To set a bit use bitSet(XX, YY); and to reset a bit use bitClear(XX, YY),
//where XX is the variable and YY is the bit number to be modified.
//  example:
//                111111
//                5432109876543210
//  Assume PSW =  B000000011111111
//  set bit 11    bitSet(PSW, 11);
//         PSW =  B000100011111111
//  clear bit 4   bitClear(PSW, 4);
//         PSW =  B000100011101111
//****************************************
#define           DEBUG                                                     // <-------<<<<<<
#ifdef            DEBUG

#define SET(...)            bitSet(PSW, __VA_ARGS__)
#define RESET(...)          bitClear(PSW, __VA_ARGS__)

const byte numberOfBits   = sizeof(int) * 8;

unsigned int PSW          = 0;
unsigned int oldPSW       = 0;

void updatePSW(byte BIT, byte state)
{
  if (state == HIGH)
  {
    bitSet(PSW, BIT);

    return;
  }

  bitClear(PSW, BIT);

} //END of   updatePSW()

#else
#define SET(...)
#define RESET(...)

#endif

//                              G P I O   &   V a r i a b l e s
//********************************************^************************************************
//
const byte LED1                    = 13;          //+5V---[220R]---[LED]---PIN
const byte LED2                    = 12;          //+5V---[220R]---[LED]---PIN

const byte mySwitch                = 2;           //+5V---[50k]---PIN---[Switch]---GND

//*****************
//Timing stuff
unsigned long led1Time;
unsigned long led2Time;

//********************************************^************************************************
void setup()
{
  Serial.begin(115200);

  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);

  pinMode(mySwitch, INPUT_PULLUP);

} //END of   setup()


//********************************************^************************************************
void loop()
{
  //****************************************
#ifdef DEBUG
  if (oldPSW != PSW)
  {
    //update to the new value
    oldPSW = PSW;

    //print the binary value of our PSW
    Serial.println("111111");
    Serial.println("5432109876543210");

    //add leading 0s
    for (byte i = 0; i < numberOfBits; i++)
    {
      char BIT = (PSW & (1 << (numberOfBits - 1 - i))) > 0 ? '1' : '0';
      Serial.print(BIT);
    }

    Serial.println("\n");
  }
#endif

  //****************************************
  if (millis() - led1Time >= 1000ul)
  {
    //restart this TIMER
    led1Time = millis();

    digitalWrite(LED1, !digitalRead(LED1));

#ifdef DEBUG
    //******************
    //update PSW
    updatePSW(LED1, digitalRead(LED1));
#endif
  }

  //****************************************
  if (millis() - led2Time >= 500ul)
  {
    //restart this TIMER
    led2Time = millis();

    digitalWrite(LED2, !digitalRead(LED2));

#ifdef DEBUG
    //******************
    //update PSW
    updatePSW(LED2, digitalRead(LED2));
#endif
  }

  //****************************************
#ifdef DEBUG
  //******************
  //update PSW
  updatePSW(mySwitch, digitalRead(mySwitch));
#endif

  delay(50); //just for testing

} //END of   loop()

I don't. I use Eclipse / Sloeber where the it's in the project's properties settings.