Comfortable serial debug-output short to write fixed text name AND content of any variable [code example]

Hello everybody,

the democode below makes use of what is called a macro.

A macro does pre-programmed automated keyboard typing into your source-code
just right after starting the compiling-process.

This means a macro is adding lines of code to the code you see in the editor.
very simple example you might have seen before:

#define myLED_Pin 13

The macro's name is "myLED_Pin" and what it does is replacing "myLED_Pin" with number 13
So if you code

pinMode(myLED_Pin,OUTPUT);`

the macro does

pinMode(13,OUTPUT);`

of course myLED_Pin is easier to understand than "13"

Macros can do much more. You can even use "macro-variables"
This is what the debug-macro does
Here is the democode that shows how it works. For easiest understanding read the code
upload it to your microcontroller and run the code with opened serial monitor
and then re-read the code.

Here is the code

#define dbg(myFixedText, variableName) \
        Serial.print( F(#myFixedText " "  #variableName"=") ); \
        Serial.println(variableName); 
// usage dbg("fixed Text", variableName)
// example printing name and content of a variable called "myCounter"
// dbg("Demo-Text",myCounter);
// serialoutput will be '"Demo-Text" myCounter=1'
// which means this macro does three things
// 1. print the fixed text 
// 2. print the NAME of the variable
// 3. print the CONTENT of the variable 
// all in one line

int myCounter;

void setup() {
  Serial.begin(115200);
  Serial.print( F("\n Setup-Start  \n") );
  myCounter =0;
}

void loop() {
  myCounter++;
  
  dbg("Demo-Text",myCounter);
  // whenever possible replace delay() 
  // by non-blocking timing based on millis()
  delay(1000); 
}
// serial output will look like this
/*
 Setup-Start  
"Demo-Text" myCounter=1
"Demo-Text" myCounter=2
"Demo-Text" myCounter=3
"Demo-Text" myCounter=4
...
*/

The main purpose is to add such lines for

  • analysing what a code is doing
  • analysing what values do variables really have
  • when is what if-condition true etc.

For easy finding a particular place in your code give each dbg-statement a different number in the fixed text.

This enables to mark this number in the serial monitor change to the editor and do a search for this number

example
dbg(" 27: wait",myVar58);

if the characters 27: are specific to that line of code

mark 27:
Strg-C,
change to editor
Strg-f in the IDE-editor
Strg-V
Enter
brings you right to that line of code with the 27:

Additonal info how it works in post #3
best regards Stefan

1 Like

here is a version that has two different macros
the macro dbgi has three parameters where the last parameter is the interval-time fpr printing only every X milliseconds.

This enables to ad serial debug-output in fast running loops without overflooding the serial-monitor with hundreds of lines per second

#define dbg(myFixedText, variableName) \
        Serial.print( F(#myFixedText " "  #variableName"=") ); \
        Serial.println(variableName); 

#define dbgi(myFixedText, variableName,timeInterval) \
{ \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  } 

  
// non-blocking timing-function
boolean TimePeriodIsOver (unsigned long &periodStartTime, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();  
  if ( currentMillis - periodStartTime >= TimePeriod )
  {
    periodStartTime = currentMillis; // set new expireTime
    return true;                // more time than TimePeriod) has elapsed since last time if-condition was true
  } 
  else return false;            // not expired
}

// millis()-timing-variables MUST be of type unsigned long
unsigned long MyTestTimer = 0; 
boolean buttonHasBeenPressed = false;

unsigned long myDebugIntervalTimer = 0;

const byte ledPin    = 13;
const byte buttonPin =  3;

void setup() {
  Serial.begin(115200); 
  Serial.print( F("\n Setup-Start  \n") );

  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);
  buttonHasBeenPressed = false;
}

void loop() {
  int buttonState;
  buttonState = digitalRead(buttonPin);

  dbgi("0:looping",buttonState,1000);    
  dbgi("1:",buttonHasBeenPressed,4000);

  // if-condition only true once 
  // because boolean flag is changed to "true"
  if ( !buttonHasBeenPressed && (buttonState == LOW) ) {
    digitalWrite(ledPin, HIGH);

    Serial.print( F("Button pressed! \n") );
    dbg("1:Button pressed",buttonState);
    buttonHasBeenPressed = true;
    // initialise timervariable with actual time
    MyTestTimer = millis(); 
  }    

  // if-condition only true once 
  // because boolean flag is changed to "false"
  if ( buttonHasBeenPressed && TimePeriodIsOver (MyTestTimer,5000) ) {
    digitalWrite(ledPin, LOW);
    buttonHasBeenPressed = false; 
    dbg("Time is over",digitalRead(ledPin));
  }
}  

EDIT 05.04.2023

here is a demo-code with four macros

// MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START * MACRO-START *
// Take it for granted at the moment scroll down to void setup
// start of macros dbg, dbgi, dbgc dbgcf
#define dbg(myFixedText, variableName) \
  Serial.print( F(#myFixedText " "  #variableName"=") ); \
  Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope

#define dbgi(myFixedText, variableName,timeInterval) \
  { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  }
// usage: dbgi("2:my fixed text",myVariable,1000);
// myVariable can be any variable or expression that is defined in scope
// third parameter is the time in milliseconds that must pass by until the next time a
// Serial.print is executed
// end of macros dbg and dbgi
// print only once when value has changed
#define dbgc(myFixedText, variableName) \
  { \
    static long lastState; \
    if ( lastState != variableName ){ \
      Serial.print( F(#myFixedText " "  #variableName" changed from ") ); \
      Serial.print(lastState); \
      Serial.print( F(" to ") ); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }

#define dbgcf(myFixedText, variableName) \
  { \
    static float lastState; \
    if ( lastState != variableName ){ \
      Serial.print( F(#myFixedText " "  #variableName" changed from ") ); \
      Serial.print(lastState); \
      Serial.print( F(" to ") ); \
      Serial.println(variableName); \
      lastState = variableName; \
    } \
  }
// MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END * MACRO-END *


void PrintFileNameDateTime() {
  Serial.println( F("Code running comes from file ") );
  Serial.println( F(__FILE__) );
  Serial.print( F("  compiled ") );
  Serial.print( F(__DATE__) );
  Serial.print( F(" ") );
  Serial.println( F(__TIME__) );
}


// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}


void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);

  if ( TimePeriodIsOver(MyBlinkTimer, BlinkPeriod) ) {
    digitalWrite(IO_Pin, !digitalRead(IO_Pin) );
  }
}

unsigned long MyTestTimer1 =  0;                   // Timer-variables MUST be of type unsigned long
unsigned long MyTestTimer2 =  0;                   // Timer-variables MUST be of type unsigned long

const byte    OnBoard_LED  = 13;

long myLong = 0;
float myFloat = -0.1;

void setup() {
  Serial.begin(115200);
  Serial.println("Setup-Start");
  PrintFileNameDateTime();

}


void loop() {
  BlinkHeartBeatLED(OnBoard_LED, 250);

  // print value of variable myLong once every 500 milliseconds
  dbgi("dbgi1:", myLong, 500);

  // print value of variable myLong only if value has CHANGED
  // since last time it was printed
  dbgc("dbgc1:", myLong);


  // print value of variable myFloat only if value has CHANGED
  // since last time it was printed
  dbgcf("dbgcf:", myFloat);

  // check if 2000 milliseconds have passed by
  if ( TimePeriodIsOver(MyTestTimer1, 2000) ) {
    // when REALLY 2000 milliseconds have passed by
    myLong++; // increment variable myLong by 1
  }

  // check if 6000 milliseconds have passed by
  if ( TimePeriodIsOver(MyTestTimer2, 6000) ) {
    // when REALLY 6000 milliseconds have passed by
    myFloat += 0.5; // incrfement variable myFloat by 0.5
  }

}

best regards Stefan

some aditional example how it works:

using the #define "statement" to make the compiler "write" code
#define can be used to make the compiler replace text that describes something with (almost) anything else

easy example
let's assume IO-pin number 4 shall be set to work as output
the command for defining an IO-pin to be configured as output is

pinMode(4,OUTPUT);
the number "4" says nothing about the purpose of this IO-pin

let's assume the IO-pin shall switch On/Off a buzzer

you would have to add a comment to explain
pinMode(4,OUTPUT); // IO-pin 4 is buzzer

the compiler needs the number. To make the code easier to read and understand
#define BuzzerPin 4
So the command looks like this
pinMode(BuzzerPin,OUTPUT);

the descriptive word "BuzzerPin" gets replaced through the compiler by a "4"
so what the compiler compiles is still
pinMode(4,OUTPUT);

the #define can do much more than just replace a word by a single number
it can replace multiple words like

#define prHello Serial.println("Hello World!");

identifier-part: prHello

replacement-part: Serial.println("Hello World!");

The mechanism behind it is the compiler acts like if you would

deleting the identifier-part:

and then

typing the replacement-part:

into your *.ino-textfile which is your sourcecode.

This is done right after clicking the compile or the upload-button in the IDE.

macros can do even more.

You can use "MACRO"-variables. If you enter
this means the macros act on a completely different level than the program-code

macros act on the source-code-level

explaining the "variables" myFixedText, variableName, timeInterval

#define dbgi(myFixedText, variableName,timeInterval) \

the macro dbgi has round parenthesis "words" inside the parenthesises are seen as
"MACRO"-variables. MACRO-variables don't need a declaring. Because macros act on the
source-code-level. The MACRO-variable contains text and the MACRO-variable is replaced by the text it contains.

If your program is compiled and uploaded to the microcontroller things are "acting" on the program-level.

an expression like #myFixedText with a leading double-cross "#"
acts like a text-variable

#define dbg(myFixedText, variableName) \

parenthesis open myFixedText = use myFixedText as a macro-variable

a call of the macro looks like this:

dbg("entering case 1", 0);

This means all characters right behind the opening parenthese and the comma get stored into the macro-variable myFixedText

Serial.print( F(#myFixedText does

replace the character-sequence hold by the macro-variable myFixedText
so after this action the source-code looks like this

Serial.print( F("entering case 1"

The macro goes on
Serial.print( F(#myFixedText " " #variableName"=") ); \

which means after finishing the whole line your source-code looks is "transformed from

macro-call:

dbg("entering case 1", 0);

to

Serial.print( F("entering case 1  0=") );

macro-variable "variableName" holds the "0"

this part of the macro-code
Serial.print( F(#myFixedText " " #variableName"=") ); \

is replaced by the "0"
the rest
Serial.print( F(#myFixedText " " #variableName"**=") ); **

are again just characters
so at the end the source-code-line looks like this

Serial.print( F("entering case 1  0=") );

in principle the same thing happends with the rest of the macro

Serial.println(variableName);

With a modified example for the macro-call

dbg("damm! show me the content of variable",myStep);

results in

Serial.print(  F("damm! show me the content of variable myStep=") );
Serial.println(myStep);
dbg("what the heck is value of variable",myCounter);

results in

Serial.print(  F("what the heck is value of variable myCounter=") );
Serial.println(myCounter);

a do - while -loop with a fixed condition "false"

At first view this seems to be nonsense a do-while-loop where the condition always evaluates to "false" means
run down the code only once

curly braces to define a local scope

The purpose of the curly braces {} is it makes the variable intervalStartTime
local to the code inside the curly braces

This has the effect that with mutliples calls of macro dbgi
the always the same named variable intervalStartTime are each local and don't disturb each other.

This means the macro dbgi can be called at thousands places where each call has its own set of variables.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.

It is nonsense but the optimizer is smart enough to get rid of it :slight_smile:

just do a compound statement with {} no need for the while... you'll get local variables with scope and lifetime of the compound statement.

int x = 42;

void setup() {
   // here x is worth 42
}

void loop() {
  int x = 3; // local variable in the function scope, hides the global variable
  // here x is 3

  { // start a compound statement 
    int x = 7;
    // here x is 7, code doesn't see the other ones
    ...
  } // close our compound statement 

  // here x is 3 again
}