signed OOP up-arrow comments Arduino hints and tips - Comments - 5
Dual arduinos in control panel hdr

Comments - should I and how often?


When I was in school I loved to read, but nothing spoiled my reading fun as much as having to write a book report.

When writing software I used to regard comments the same way. I figured I could write understandible code well enough. Why should I bother writing lots and lots of comments. Especially if I'm the one who will be maintaining that code...

Then I started noticing that things that seemed really obvious to me a when I wrote a program, weren't so obvious after a year or two when I was trying to find a bug or add a new function to the code.

Comments to the rescue! Now I believe that it's nearly impossible to have too many comments. Unless I'm writing a piece of throwaway code that won't last longer than a day or two, I try to discipline myself to festoon programs with useful comments.

By useful I don't mean

    
     x = x + 2;  // increment x by 2
  

If that line gets a comment, it should explain WHY x is being incremented.

C/C++ provides two styles of complementing code:

  • /* this is a comment */ - comment starts with /* and ends with */
  • // this comment extends to the end of the line

It's a good idea to develop a style of comments and stick to it.

    
     //------------------------------------------------------
     // bcsjTimer.h - class declaration for bcsjHeader class
     // 
     // This class performs the following functions:
     //   ...
     //
     // Resources used: no system resources used
     //
     // Revisions:
     //  16Nov16
     //    1. Created file
     //-------------------------------------------------------
     
     #ifndef __BCJSTIMER_H__
     #define __BCSJTIMER_H__
     
     /*
      * CLASS DECLARATION
      */
     class bcsjTimer
     {
       // Member functions
       public:
         bcsjTimer(void);  // constructor
         void start( unsigned long d );  // start timer running with duration d (micros)
         boolean active(void);           // is timer running?
         boolean done(void);             // is timer done?
         unsigned long getTimeRemaining(void); // how long to end of time period (micros)
     
       // class data
       private:
         unsigned long starttime;        // time we started timing (micros)
         unsigned long duration;         // time period we are waiting (micros)
         boolean isActive;               // true if timer is active
     };
     
     #endif
  

The header block should tell the reader what this class does so they don't need to dig through the code to figure that out. It also lists the Arduino hardware timers, interrupts, or other system resoures that are used. This is important to alert anyone using this class of possible system resource contention (a BIG issue when using classes/libraries you got from elsewhere!). Finally, it provides a revision list of bug fixes and additions.

Inside I try to give a quick definition of what the member functions do. Note that I've documented what goes in the time values - microseconds in this case. Don't leave the class users guessing about this. NASA has lost rockets because one person assumed a velocity was in miles per hour while another program though it was meters per second!

I also provide a quick definiton of what the private class data does. This is important. If there's not enough room on a single line then use as many lines as needed.

    
       // class data
       private:
         // Specifies the time, in microseconds at the beginning of the
         // intervale we are timing.
         unsigned long starttime;
         // The interval, in microseconds, we are timing.
         unsigned long duration; 
         boolean isActive;               // true if timer is active
     };
     
     #endif
  

As the program, or class in this case, is writing take the time to keep the previously written comments up to date.

At my previous gig as a software engineer, the modeling group wrote the comments for all their code first. Then when the comments were coherent enough, the programmers used the comments as a guide to writing the code. Of course this has the problem of without the code, it can be difficult to know if the proposed algorithms will actually work as intended!

Someimtes comments aren't used for information. Occaisionally, during the software debug process, it can be handy to temporarily remove a line of code. Or you may have added considerable debug scaffolding (debug print statements for example) that you want to turn off, but don't want to remove from the program entirely (because there was a lot of typing in placing them there and you might be needing to put them back in again...

In that case you might turn the program lines of interest into a comment.

    
       for (int k=0; k<arraySize; ++k) {
         if (myArray[k] == 17) {
    //     Serial.print( "loop ended k=");
    //     Serial.println( k );
           break;
         }
    //   Serial.print( "loop[" );
    //   Serial.print( k );
    //   Serial.print( "] array val: ");
    //   Serial.println( myArrayk] );
       }
     
  

In the above example, the debug printing has been "turned off" by commenting out the print statements. While this could also be doen by using an "if" statement to test a debugEnabled variable, it avoids the use of extra code.

If the debug print statements are needed later in the development cycle they can be uncommented. In general, such statements should not be released with the code when it "ships" as it tends to make the source code harder to read and understand.

There's another way to comment and uncomment those print statements, conditional compilation.

C/C++ compilers make multiple passes through the source file. The first pass is often performed by a preprocessor. It scans the source file looking for compiler directives. Directives start with a '#' pound-sign and usually begin in the first column. Directives include:

  • Conditional compilation
  • Maco processing
Conditional compilation uses directives such as
  • #if <const expression>
  • #else
  • .
  • #endif
  • #ifdef <macro name>
  • #ifndef <macro name>
When an "if" directive is found, its expression is scanned and if it evaluates to true the following lines of code are included in the compilation. If false, they are skipped over until a following #else or #edif is encountered.

Note that the program is NOT yet runing so only constant values can be used.

    
       #if 0
         x = 7;       // this line not compiled
         y = x * 8;   // this line not compiled
       #endif 
       
       #if 1
         Serial.print( "timeval = ");  // this line compiled
         Serial.println( timeval );    // this line compiled
       #endif
       
       #define DEBUG_ENABLED 0
       
       #if DEBUG_ENABLED
         Serial.print( "timeval = ");  // this line not compiled
         Serial.println( timeval );    // this line not compiled
       #else
         Serial.print( "counterLimit = ");  // this line compiled
         Serial.println( counterLimit );    // this line compiled
       #endif
  

You may have noticed that my header (.h) files start with a #ifndef. What in earth is that and why is it there?

    
       //------------------------------------------------
       // header file for class myCounter
       //------------------------------------------------
       
       #ifndef __MYCOUNTER_H__
       #define __MYCOUNTER_H__
       
          // class declaration here...
          
       #endif
  

These are what are sometimes called include guards. Their purpose is to keep the contents of a header file from being included twice.

__MYCOUNTER_H__ is the name of a macro. The #ifndef looks to see if that macro has been defined already. If NOT, then the lines following the #ifndef are compiled. The first line after the #ifndef defines a value for the macro __MYCOUNTER__H__. If myInclude.h is included a second time, the macro will be present on the contents of the file won't be compiled a second time.

But if the macro has been defined, then the class declaration is NOT included in the compiled.

If we looked in the class body file - myCounter.cpp and our Arduino sketch (.ino) file both would have an include of myCounter.h in them.

    
       Arduino sketch file...
       
       #incldue <include myCounter.h >
       
       
       myCounter.cpp file...
       
       #include <include myCounter.h >
  

Without the include guard in myCounter.h it would be compiled twice. The second time through the compiler would complain that the myCounter class was being redefined.

A #define can also be used to declare numeric constants. For example:

    
       #define LED_TRACK_1 9
       #define LED_TRACK_2 10
       
       ...
       
       pinMode( LED_TRACK_1, OUTPUT );
       pinMode( LED_TRACK_2, OUTPUT );
              
       or
       
       #define NUM_LEDS 7
       
       ...
       
       for (int idx=0; idx>0; idx+=1) {
         ... handle array of LEDs ...
       }
       
       or
       
       #define DEBUG_ENABLED 0
       
       #if DEBUG_ENABLED
          .. code block being conditionally compiled ...
       #endif
       
       or
       
       #define DEBUG_ENABLED 1  // comments NOT allowed here!
  

There are a few things to keep in mind when using macro definitions:

  • These are pure text replacement. That is for the first eample aboce EVERY place the compiler finds the text LED_TRACK_1 or LED_TRACK_2 it will substitute the text to the right in the macro definition.
  • Macro replacement text in a definiton extends ALL THE WAY to the right edge of the source code page. Do NOT use a // comment in the same line as the replacement text or you will likely start finding hard-to-debug compile errors.
  • Macro replacement text can be continued on the next line by terminating the previous line of text with a backslash.
  • Macros can be redefined. If you have #define MY_MACRO in multiple places, the most recent macro declaration overrides previous ones.
  • Try to pick macro names that aren't common to avoid collisions with macros decleared in system header files. This is why I chose the syntax of my include guard macros. __MYCOUNTER_H__ is much less likely to be a collision than COUNTER.H.