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: