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

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) \
  do { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  } while (false);

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

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.

this means the macros act on a completely different level than the program-code
They act on the source-code-level

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

dbgi has some more code that is genious! It is a brilliant idea proposed by user
Coding_Badly

So his username is self-ironic

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

so why not leave away the do-while-loop???

The purpose of adding the while-loop is it makes the variable intervalStartTime
local to the while-loop

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.