Debug Recorder used with Motor control Loop

I am new to C++ and Arduino, so I of cause like to be guided. Only perhaps somebody else can make use of the code I make now.

This code is used for debugging, and it is used like a digital oscilloscope with a pre and post trigger function. I find the code useful for fast control loops. This is the code with some example test code to be seen at bottom:


int data = 0;   // Variable to be used for test of this debug Recorder

/*
Below, you find seven functions to help debugging by recording some global variables.
The code is used like a digital oscilloscope, with a pre and post trigger function as you desire.
The trigger function is the key feature. It is only the first call of trigger that cause action.
Subsequent calls do nothing and cause no harm. The functions are useful for debugging of time
critical code with fast loops.

The data is contained in an array of variables, with a ring buffer function. The data recorded is later
on printed out via the serial monitor. In this example integers are used, but it could be other types as well.

The data recording is stopped when the recording has been triggered and the data array has been filled
accordingly. It can also be stopped by an abort function. Afterwords the recordings are serial "printed"
to the serial monitor and subsequently by many output loop calls, by witch only some may take action
in order to avoid overflow of the serial communication. For instance the Excel DataStreamer Add can present
the data recorded in a convenient way. A restart Function can be called after the data have been printed, if
user wants to do more recordings afterwords.

The functions use no busy waits and an indication of execution time is provided for an
Arduino Nano V3 with 16 MHz clock.

The assignment of global data to be recorded needs to be included in the function, debugRecordCode().

Declarations and description of the functions to be called are given below.

This code is made by Arduino Forum user backflip in 2024, and can be used free and with no garantee of succes.
*/
const int MaxNumberOfVariables = 5;             // Number of variables to record
const int MaxNumberOfRows = 20;                 // Be aware of the amount of RAM available in the Arduino for this data array. 
const int WaitEachParameterSerialPrintMs = 20;  // Minimum number of milliseconds for each single data variable to be 
                                                // printed. It limits data speed by the millis() function.
const char DataSeparator = '\t';                // Separator of data by in Serial.Print. ',' is used as separator
                                                // by the Exceldatastreamer. '\t' would be convenient for the 
                                                // Arduino IDE Serial Monitor.

int dataRecording[MaxNumberOfRows][MaxNumberOfVariables]; //Data recording element type can be changed as desired

enum T_debugRecorderState {   
  AwaitsAction,               // Initial state. Can be changed by debugArmTrigger() or debugStartRecording()
  AwaitsTrigger,              // Special trigger inactive state. Can be changed by debugTrigRecording or debugStartRecording()
  RecordingFreeRunning,       // Data is collected to ring buffer by debugRecord(). State changes by debugTrigRecording or debugAbortRecord.
  RecordingTriggered,         // Recorder is triggered and remaining data is recorded until buffer full by debugRecord(); 
  RecordingEnded,             // Recorder data buffer is full and Recorder stopped. 
  OutputRecording,            // Data is printed out vis Serial.Print one data point each time debugOutputRecording() is called
  ActionFinished }            // Data buffer have been send out. State can only change by debugRestart();
    debugRecorderState = AwaitsAction; // State of the Recorder. In use, you progress normally only downwords.

const int NVarM1 = MaxNumberOfVariables -1;
const int NRecM1 = MaxNumberOfRows -1;

int recCol = 0;     // Current column index to recording
int recRow = 0;     // Current row index to recording
int recTrig = -1;   // First row index or according to trigger. -1 is used to indicate empty recorder.
int recStop = 0;    // The latest row index. Also used with negative values for delayed trigger.
unsigned long int recTrigMillis = 0; // Millis counter value to limit serial data flow speed
char printBuffer[8]; // Buffer used to enhance speed of Serial print function

/*
This procedure is not supposed to be called from "outside";
The user is supposed to insert code here for data to be recorded.
Insert code like:
dataRecording[RecRow][0] = value1;
dataRecording[RecRow][1] = value2;
Do not change RecRow or other parameters. 
*/
void debugRecordCode() {

  dataRecording[recRow][0] = data;       //Example use of this function
  dataRecording[recRow][1] = data+1;
  dataRecording[recRow][2] = data+2;
  dataRecording[recRow][3] = data+3;
  dataRecording[recRow][4] = data+4;
  };

void debugStartRecording() {    //procedure to be called when you want to start the free recording into the ring buffer.
                                //Action is taken at first call only. Later calls do nothing.
                                //A way to switch off debug recording in your code is to comment out the call to this function.
  switch (debugRecorderState) {
    case AwaitsAction:
    case AwaitsTrigger:                            
      debugRecorderState = RecordingFreeRunning;
      break;
    };
  };

void debugArmTrigger() {      //procedure to enter a trigger state at which recording do not happen. Later calls do nothing.
  if ((debugRecorderState) == AwaitsAction) {
    debugRecorderState = AwaitsTrigger; };
  };

/*
Function is to be called in a loop for recording a row of variables.
When recording 5 integer variables, the execution time is typically 11 us.
When the data buffer is full and a trigger was provided the recording will finish,
and then this procedure do nothing anymore.
With no action, the execution time is typically 5 us.                             
*/
void debugRecord() {
  switch (debugRecorderState) {
    case RecordingFreeRunning:
      debugRecordCode();
      recStop = recRow;
      recRow ++;
      if (recRow > NRecM1) {
        recRow = 0; };  
      if (recStop == recTrig) {     // data have been overwritten   
        recTrig = recRow;}          // regTrig set to new oldest data    
      else
        if (recTrig == -1) {        // if buffer was empty
          recTrig = recStop; };
      break;
    case RecordingTriggered:
      if (recStop < 0) {      //start of data recording to be used is later than now. RecStop is used as a counter for the delayed start
        recStop ++; } 
      else {
        debugRecordCode();
        recStop = recRow;
        recRow ++;
        if (recRow > NRecM1) {
          recRow = 0; };         
        if (recStop == recTrig) { //test if previous data have been overwritten intentionally
          recTrig = recRow;}      //regTrig set to new oldest data
        else {
          if (recTrig == -1) {       //if buffer was empty before this call
            recTrig = recStop; }; 
          };
        if (recRow == recTrig) {     //if last data in buffer space have just been recorded
          debugRecorderState = RecordingEnded; };
        };  
      break;
    };
  };

/*
The function is the trigger and sets the number rows to be kept stored or skipped before or after this function is called.
If numberOfPreRecordings = 5, then 5 data rows of data remains to be output before this trigger happen. (Pre-trigger view)
If numberOfPreRecordings = -10, then the recordings will start at 11 recordings (10 skipped) later than trigger happen.
If numberOfPreRecordings is equal or above MaxNumberOfPreRecordings then no more data is collected and recording end.
If numberOfPreRecordings is above the obtained number of data the first data point is given value 99 to signal error to user.
Action is taken on only first meaningful call of this procedure. Later calls do nothing.
No action is not taken in AwaitAction state. You need to call other start functions before that.
The execution time is about 5 us.
*/
void debugTrigRecording(int numberOfPreRecordings) {   
  switch (debugRecorderState) {                   
    case AwaitsTrigger:                           //no data is recorded yet
      debugRecorderState = RecordingTriggered;
      if (numberOfPreRecordings < 0) {
        recStop = numberOfPreRecordings; };       //recStop is used for counting delayed trig
      if (numberOfPreRecordings > 0) {
        dataRecording[0][0] = 99;                 // Cannot provide data not recorded.
        recStop = 0;                               // Signals this error to user by setting 99 in first row.
        recTrig = 0;
        recRow = 1;
        };
      break;
    case RecordingFreeRunning:
      if (numberOfPreRecordings <= 0) {
        recStop = numberOfPreRecordings;      //negative no in recStop indicate comming date to be stored later
        recTrig = -1;                         //Sets status to empty buffer
        recRow = 0;
        debugRecorderState = RecordingTriggered;} 
      else {  
        if (numberOfPreRecordings >= MaxNumberOfRows) {
          if (numberOfPreRecordings > MaxNumberOfRows) {  // User wants more data than data rows in buffer
            dataRecording[recTrig][0] = 99; }             // Signals error to user by setting 99 in first row.
          if (recRow == recTrig) {                        // Buffer is full all data is valid and recording can end
            debugRecorderState = RecordingEnded; }
          else                                            // collect remaining data to data buffer by state change
            debugRecorderState = RecordingTriggered;
          }  
        else {                                      // here you need to recalculate pointers
          if ((numberOfPreRecordings > (recStop - recTrig + 1))
               && (recStop >= recTrig)) { 
                                                    // you ask for more pre recordings than available.                                         
                                                    // can only happen here when also recTrig = 0;
            dataRecording[0][0] = 99; }           // Signals this error to user by setting 99 in first row.
          else {
            recTrig = recRow - numberOfPreRecordings;
            if (recTrig < 0) {
              recTrig += MaxNumberOfRows; };
            }; 
          debugRecorderState = RecordingTriggered;
          };
        };  
      break;  
    };
  };

/*
This function is specific to output data. It do nothing unless the Recording have finished,
The function can be called in a less time critical loop of the code, but it can be called 
at the same place as the recording loop with debugRecord().
When data is printed, the execution time is typically 80 us each 20 ms, for each integer printed.
(I hope one day someone wiser than me optimize time consumption of the Serial.Print function)
Print of one variable is not done on each call. 
You can shorten the print time of integers to about 50 us by output of int HEX (look
the applicable print code lines below).
*/
void debugOutputRecording() {
  switch (debugRecorderState) {
    case RecordingEnded:
      recCol = 0;
      recRow = recTrig;
      debugRecorderState = OutputRecording;
      recTrigMillis = millis() + WaitEachParameterSerialPrintMs;
      break;
    case OutputRecording:
      if (millis() > recTrigMillis) { // ensures a slow printout, so no buffers are filled. In order to limit execution time
                                      // only one datapoint is printed each time.
        recTrigMillis = millis() + WaitEachParameterSerialPrintMs;
        if (recCol == NVarM1) {                                     // Prints last variable in row
          if (recCol > 0) {
            Serial.print(DataSeparator);};                          // Prints data separator
          itoa( dataRecording[recRow][recCol], printBuffer, 10 );   // Faster way to convert and print a 2-byte integer
          Serial.println( printBuffer );                            //
          // Serial.println ( dataRecording[recRow][recCol], Hex ); // Faster alternative with hexidecimal integer (saves about 30 us)
          // Serial.println ( dataRecording[recRow][recCol] );      // Alternative for other data types          
          if (recRow == recStop) {
            debugRecorderState = ActionFinished; }                  // This was last variable printed
          else {
            recCol = 0;                                             // Prepare to print next row in next call
            recRow ++;
            if (recRow > NRecM1) {
              recRow = 0; };
            };
          }
        else {                                                     // Prints variables except last variable in row
          if (recCol > 0) {
            Serial.print(DataSeparator);};
          itoa( dataRecording[recRow][recCol], printBuffer, 10 );  // Faster way to convert and print a 2-byte integer
          Serial.print( printBuffer );                             //
          // Serial.print( dataRecording[recRow][recCol], Hex );   // Faster alternative with hexidecimal number (saves about 30 us)
          // Serial.print( dataRecording[recRow][recCol] );        // Alternative for other data types          
          recCol ++;
          };
        };
      break;  
    };
  };

/*
The function below is not needed to be called, but you can stop the data recording when they happen,
and then the data out serial print sequence commence.
If the function is called when the data is printed out, then print stops and data is lost.
Subsequent calls do nothing.
The execution time is about 5 us.
*/
void debugAbortRecording() {          
  switch (debugRecorderState) {
    case AwaitsAction: 
    case AwaitsTrigger:
    case RecordingEnded:
    case OutputRecording:
      debugRecorderState = ActionFinished;
      break;
    case RecordingFreeRunning:
    case RecordingTriggered:
      debugRecorderState = RecordingEnded;
      break;
    };
  };

void debugRestart() {           //Procedure to be called to restart the recorder to the initial await state.
                                //The Procedure do noting unless a recording alread have finished.
  if (debugRecorderState == ActionFinished) {
    debugRecorderState = AwaitsAction;
    recCol = 0;   
    recRow = 0;   
    recTrig = -1;  
    recStop = 0;  
    };
  }; 

// Example code to test the debug recorder is given below

void setup() {
  Serial.begin(9600);       // Sets baud speed of Serial Monitor devise
  delay(100);
  data = 0;
  Serial.println("start");
  debugStartRecording();    // Sets Recorder to free running mode to activate use of debugRecord()
  };

void loop() {
  debugRecord();
  debugOutputRecording();   // Outputs recordings. Look for data in Serial Monitor
  data += 10;
  if (data>=30) {
    debugTrigRecording(4);  // Asks for one row more than collected in this case
    }; 
  };

I still need to learn how to include this code from a private library.

I have used this code for a DC motor controller to record the step response seen from inside software. Excel DataStreamer was used to get the data and and for good display. This is what I made the Excel show for me:

Isn't that what "#include" does for you?

Thanks for the reply.

Yes, I guess so. But have still not succeeded to find out how to do it.

I looked for some information about it. One thing is, that when I save my code, it is in very different other directory than the libraries used with cpp and H files like HardwareSerial. When I save a file, it do not seem to make a h-file. It is called an INO file. So the IDE cannot find the file name in the #include directive. So there is some basics here that I need to learn.

Another problem is, that parts of the code needs to be changed based on what the user wants it to do. It is the size of the data buffer etc. and the function used to record the data.

Did you try putting the #include file in the same directory as the Arduino program code file?

Yes, I tried to place this INO file both in same directory as HardwareSerial is placed and in the normal directory I use for the test programs I made. There it is in a directory defined for each program I save, and the directory is named by the IDE - file - preferences. and as "Sketchbook location".

I don;t know any more. I always changed the file extension to ".h".

How do you do that?

When I started to make some programs about 4 weeks ago, I started with some proposed example code called AnalogInOutSerial. It might be part of my problem, because every time I save one of my programs, a new directory is made with the code and two figures for electronic layout and schematic to be used with this example code.

Now I tried to reference the ino file instead like:

#include <debugRecorderV2.ino>
or
#include "debugRecorderV2.ino"

This file was not found, when I placed my normal directory also seen under the IDE preferences. When I placed it in the same odd directory as HardwareSerial, then this file was found. I just do not like to mix these files with the standard software I received, so perhaps I can make a directory somewhere else the compiler can find.

I still got the problem with the constant declarations to be set by user. So the use of this code as an include file is not "nice".

You have never changed a file name?

Yes I have. This is the extension, that need to change. I think in perhaps previous versions of Windows, it was easy to do. Now with Windows 10, I had a hard time finding a way to change extension. But anyway it don not seem to be needed, when you can include the ino file as well.

Just change the name to the same name and the new extension.

Obviously this is something simple, that I do not know about.

When I try to rename a file with the windows file manager, it looks like this:

There is no option to change the type of file or the extension. If I change the name section to debugRecorderV2.h, then the name part is changed, but the extension (Type) remains.

After thinking about this, I do not think, that it make sense to make this code to an include file, because it is based on data definitions by user for the compiler. The way to use it is to copy this code into your own code in order to be able to change these compiler directives that define the data store etc. Otherwise you need to make a new include file for each application, and it makes no sense. But you can make the include file for the main part of the code, and copy paste only some definition part into your application code.

I guess that the code like this for a library needs to be based on pointers and lists, so the ram space is allocated at runtime. Furthermore the definition on what kind of variables you want to record needs to be based on some kind of function call with pointers to the variables in the setup section of the code. In this way you can transfer all use of this code to nothing else than function calls. But I may be wrong about that, and need to learn some other possibility.

Try the Windows file explorer. Always works for file name changes for me.

Try turning on "Show file extensions", or alternatively right click, select Properties and rename the file that way.

It may actually be the file explorer I use - I cannot find the name. But I found a setting of it about how to show files, that made it show file extension. And then you became able to rename the extension.

But I have to place this h-file at some predefined places, that you need to find. The directory of these libraries are found on my computer like this:

examples

HardwareSerial.h is to be found:
C:/Users/"user-name"/AppData/Local/Arduino15/packages/arduino/hardware/avr/1.8.6/cores/arduino

Servo.h is to be found:
C:/Users/"user-name"/AppData/Local/Arduino15/libraries/Servo/src

I can place an ino file in the later library directory then it is NOT found. But if I place it as an h-file, it is found. So it seems, that you need to use this pass, when you make include files by yourselves:
C:/Users//AppData/Local/Arduino15/libraries

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