Basebot main code: Difference between revisions

From Rsewiki
 
(28 intermediate revisions by the same user not shown)
Line 1: Line 1:
Back to [Basebot]]
Back to [[Basebot]]


=== Main code ===
== Main code ==


The main code is in ''basebot_6.ino'' (sometimes renamed to ''basebot_6.cpp).
The main code is in ''basebot_6.ino'' (sometimes renamed to ''basebot_6.cpp).


The code is based on the Arduino environment.
The code is based on the Arduino environment.
Here the normal C-main function is hidden. But ''setup()'' is called first, followed by an endless loop that calles ''loop()''.
Here, the normal C (and C++) ''main()'' function is hidden. However, ''setup()'' is called first, followed by an endless loop that calls ''loop()''.


===== Setup =====
[[file: basebot_6.ino-block.png | 400px]]


The ''setup()'' function is like this and initializes all support modules:
The Basebot API is in the "src" sub-directory.
 
== Setup ==
 
The ''setup()'' function is like this and initializes all support modules in the Basebot API:


  void setup()  // INITIALIZATION
  void setup()  // INITIALIZATION
  { // to be able to print to USB interface
  { // to be able to print to USB interface
  Serial.begin(12000000);
  Serial.begin(12000000);
  // initialize sensors, safety and display
  // start timing
  robot.setup(); // alive LED, display and battery
  sampleTimer.begin(timerInterrupt, sampleTimeUs);
  motor.setup(); // motor voltage
  // Initialise sensors, safety and display (in the basebot API)
  encoder.setup(); // motor encoders to velocity
  robot.setup(); // alive LED, display and battery
  imu2.setup(); // gyro and accelerometer - include tilt angle
  motor.setup(); // motor voltage
  encoder.setup(); // motor encoders to velocity
  encoder.pulsPerRev = 48;
  imu2.setup(); // gyro and accelerometer
  motor.setPWMfrq(80000);
  usb.setup();
}
 
==== Timing ====
 
An interrupt controls timing.
 
// Sample time can not go lower than 300us
const uint32_t sampleTimeUs = 1000; // desired sample time in us
bool isSampleTime = false;
float time_sec = 0;
 
void timerInterrupt()
{
  isSampleTime = true;
  time_sec += sampleTimeUs / 1e6;
  }
  }


===== Loop =====
== Loop ==


The ''loop()'' function:
The ''loop()'' function waits for the "isSamplingTime" flag to ensure a constant sample time.
 
Then all sensor data are updated (by the .tick() calls).
 
The "sequence" decides where to go when in a sequence of states.
 
The "control" function translates the desired sequence to motor voltage.
 
The motor voltage is then implemented by the "motor.tick()" function.
 
The essential data values are then added to the RAM-based log.
 
If it is not sampling time, then potential commands from the USB connection are serviced.


  void loop ( void )
  void loop ( void )
  { // init sample time
  { // init sample time
   uint32_t nextSample = 0;
   while ( true )
  '''while''' ( true )
   { // main loop
   { // main loop
    // get time since start in microseconds
    uint32_t us = micros();
     // loop until time for next sample
     // loop until time for next sample
     if (us > nextSample) // start of new control cycle
     if (isSampleTime) // start of new control cycle
     { // advance time for next sample
     { // time for next tick
       nextSample += sampleTimeUs;
       isSampleTime = false;
       // read sensors
       // read sensors
       imu2.tick();
       imu2.tick();
       encoder.tick();
       encoder.tick();
       // advance test sequence (default is wait for start button)
      // updatePose();
       testSequence();
       // advance sequence (default is wait for start button)
       // make control actions
       sequenceTwoSteps();
       controlUpdate();
       //
       if (state > 0)
      { // Only if started
        // Calculate new motor voltage
        controlUpdate();
      }
       // give value to actuators
       // give value to actuators
       motor.tick();
       motor.tick();
      // save relevant values
      updateLog();
       // support functions
       // support functions
       robot.tick(); // measure battery voltage etc.
       robot.tick();   // measure battery voltage etc.
       display.tick(); // update O-LED display
       display.tick(); // update O-LED display
     }
     }
Line 54: Line 94:
  }
  }


The loop (''while (true)'') runs as fast as it can, and if it is not time for a new sample, then the USB connection is serviced (''usb.tick()'').
=== State sequence ===
 
When the sample time has passed, the next sample time is set up. Both use the standard Arduino call ''micros()'', which returns the number of microseconds since boot.
 
At every sample, the sensors are handled first (''imu.tick()'' and ''encoder.tick()''); these will get new data from the sensor, and the encoder will estimate the motor velocity.
 
Next is ''testSequenct()'', which is intended as a state machine that changes what the robot should do, as described below.
The default is to wait for the start button to be pressed.
 
All data for the control effort is now available (''controlUpdate()''). The default controller uses the desired velocity as motor voltage, ignoring the estimated velocity. This is a feed-forward-only controller.
 
The control effort sets the motor voltages and the ''motor.tick()'' implements this as a PWM to the motor driver.
 
In the end, other service functions are called (''robot.tick()'' and ''display.tick()''), which (mostly) check the battery voltage and update the display.
 
===== State sequence =====
 
The state sequence is intended to make some small sequences usable for performance analysis and test.


/**
A state sequence is needed to implement a desired manoeuvre.
  * Global variables for test sequence */
int state = 0;              // current state
uint32_t startTime = 0;      // time, when 'start' was pressed (in milliseconds)
float desiredValue = 0;      // desired (reference) value sent to the controller.
uint32_t sampleTimeUs = 500; // sample time for the whole system (in microseconds)


The sample time (in microseconds) should not be shorter than about 300 us, as this is what the controller can handle and still send sample results over USB.
Global variables
int state = 0;              // actual sequence state
float endTime = 0;          // for current state
float desiredValue = 0;      // desired (reference) value send to controller


/**
  void sequenceTwoSteps()
  * Test sequence,
  * that is, what should happen when */
  void testSequence()
  { // this function is called at every sample time
  { // this function is called at every sample time
   // and should never wait in a loop.
   // and should never wait in a loop.
  // Update variables as needed and return.
   bool button;
   bool button = false;
   // this is a state machine
   //
   // state 0: wait for start button press
  // This is a state machine.
   // state 0: wait for the start button press.
  // Other states are part of a sequence.
   switch (state)
   switch (state)
   { // run mission, initial value
   { // run mission, initial value
     case 0:
     case 0: //''' State 0 is just inactive, waiting for the start signal.'''
      // read button value
       button = digitalReadFast(PIN_START_BUTTON);
       button = digitalReadFast(PIN_START_BUTTON);
      // A 'start' message from the USB can activate a mission.
      // This sets the robot.missionStart flag true for five samples.
       if (button or robot.missionStart)
       if (button or robot.missionStart)
       { // starting a sequence
       { // ''' Starting a sequence'''
         //
        start();
         desiredValue = 2;
         // Prepare next state
        startTime = millis();
         desiredValue = 0; // reference value to the controller
        // update of display takes too long time for fast sampling
         // to start the log with no velocity
         // so, disable during sequence.
         endTime = time_sec + 0.020; // new state to end after 20ms
         display.useDisplay = false;
        // go to next state
         state = 10;
         state = 10;
       }
       }
       break;
       break;
     case 10:
     case 10: //''' Waiting for first step.'''
       // test if this state is finished
       // test if this state is finished
       if (millis() - startTime > 500)
       if (time_sec > endTime)
       { // change to next values
       { // change to next values
         desiredValue = 6;
         desiredValue = 1.5;
         state = 20;
        endTime = time_sec + 0.48 ; // ~ 0.5 second
         state = 11;
       }
       }
       break;
       break;
     case 20:
     case 11: // ''' First step'''
      // test if this state is finished
       if (time_sec > endTime)
       if (millis() - startTime > 1000)
       { // change to next values
       { // change to next values
         desiredValue = 3;
         desiredValue = 3;
         state = 30;
        endTime = time_sec + 0.50;
         state = 12;
       }
       }
       break;
       break;
     case 30:
     case 12: // ''' Second step.'''
      // test if this state is finished
       if (time_sec > endTime)
       if (millis() - startTime > 1500)
       { // change to next values
       { // finished
         desiredValue = 6;
         desiredValue = 0;
        endTime = time_sec + 0.5;
         state = 98;
         state = 100;
       }
       }
       break;
       break;
     case 98:
     case 100: // ''' Maintain third level.'''
       // roll off velocity
       // stop after end time
       if (millis() - startTime > 2000)
       if (time_sec > endTime)
       { // finished
       { // '''stop''', but continue logging for a while (0.3 sec)
         state = 99;
         stop(0.3); // actually sets state to 999 (i.e. default:)
       }
       }
       break;
       break;
     default:
     default:
       // back to start
       // this state is needed to enable a new start
       state = 0;
       if (time_sec > endTime)
      desiredValue = 0;
        '''finished()''';
      motor.motorVoltage[0] = 0; // left motor
      motor.motorVoltage[1] = 0; // right motor
      display.useDisplay = true;
      // print som results
      Serial.print("% Set Sample time ");
      Serial.print(sampleTimeUs);
      Serial.print(" usec. Measured Sample time ");
      Serial.print(encoder.sampleTime_us);
      Serial.print(" usec.");
      Serial.println();
       break;
       break;
   }
   }
  }
  }


When 'state' is '0' and the start condition is fulfilled (start button pressed), then 'state' is changed to 10.
=== Control ===
This state also stops the display update. Display update takes a relatively long time, and if the sample time is short (<2ms), there will be samples that are not run at the desired time.
 
The control function should implement the (PID) controller
calculations.
 
The desired value (forward velocity) can here be compared with actual velocity and used to set a motor voltage that will implement the desired behaviour.
 
This can later be extended also to implement a desired turn rate.
 
void controlUpdate()
{ // do control during a mission only.
  //
  // Measured wheel velocity (positive is forward)
  // float velLeft = -encoder.motorVelocity[0];
  // float velRight = encoder.motorVelocity[1];
  //
  // Desired velocity
  float velRef = desiredValue;
  //
  // For a start, the desiredValue
  // is just used as motor voltage
  //
  // Left motor voltage (-9V to +9V)
  motor.motorVoltage[0] = -velRef;
  // Right motor voltage (-9V to +9V)
  motor.motorVoltage[1] = velRef;
}
 
=== Update log ===
 
To document the effect of the controller, data is needed.
 
An array of data structures are implemented with some of the desired data.
 
The data structure is initially set up as follows.
 
typedef struct
{
  float time; // in seconds
  int state;  // sequence state
  float desiredValue;
  float motorVel[2]; // left, right
  float motorVolt[2];// left, right
  int32_t encoder[2];// Encoder count value on each motor
  float motorCurrent[2]; // rather poor measurement of motor current
  float battery; // Battery voltage
} LogData;
 
A large portion of memory is allocated for the log:
 
// allocate memory for data log in memory block 2 available only through malloc/new
// (specific for this micro processor, a Teensy 4)
const int logsMax = 400000 / sizeof(LogData); // allocates 400kBytes
LogData * logs = (LogData*)malloc(logsMax * sizeof(LogData));
int logsCnt = 0; // next log entry to use / i.e. number of used log entries
 
A function adds data to the log.
void updateLog()
{ // step response mode
  if (state > 0 and logsCnt < logsMax)
  {
    logs[logsCnt].time = time_sec;
    logs[logsCnt].state = state;
    logs[logsCnt].desiredValue = desiredValue;
    logs[logsCnt].motorVolt[0] = -motor.motorVoltage[0]; // The left motor runs backwards, therefore the sign.
    logs[logsCnt].motorVolt[1] = motor.motorVoltage[1];
    logs[logsCnt].encoder[0] = -encoder.encoder[0];
    logs[logsCnt].encoder[1] = encoder.encoder[1];
    logs[logsCnt].motorVel[0] = -encoder.motorVelocity[0];
    logs[logsCnt].motorVel[1] = encoder.motorVelocity[1];
    logs[logsCnt].battery = robot.batteryVoltage;
    // the last part is offset of current measurement (in Amps)
    // take the initial current readings and add or subtract as needed
    // these values fits Tania: (+0.260A, -0.275A)
    logs[logsCnt].motorCurrent[0] = -(analogRead(A1) - 2024) * 0.0045 + 0.260;
    logs[logsCnt].motorCurrent[1] =  (analogRead(A0) - 2024) * 0.0045 - 0.275;
    logsCnt++;
  }
}
 
The motor current is just read from the sensor pins. This is a 12-bit (4096) A/D converter, and half that value (2048) is close to zero current. Each LSB is about 4.5mA. An offset current is added at the end. This value varies for each robot and must be calibrated.
 
Once the log is filled, it needs to be transferred to the PC.
 
All values are just printed to the USB connection, and preceded by a textual explanation of the data.
 
void printLog()
{
  LogData * d = logs;
  Serial.print("% Basebot log for ");
  Serial.println(robot.getRobotName());
  // hard-coded settings
  Serial.print("% Sample time ");  Serial.print(sampleTimeUs);
  Serial.print(" us, Gear ");  Serial.print(gear);
  Serial.print(", Wheels ");  Serial.print(wheelRadius);
  Serial.print(" m, PPR ");  Serial.println(encoder.pulsPerRev);
  Serial.print("% maximum log samples ");  Serial.print(logsMax);
  Serial.print(" each sized ");  Serial.print(sizeof(LogData)); Serial.println(" bytes");
  // Columns
  Serial.println("% 1 time (sec)");
  Serial.println("% 2 state");
  Serial.println("% 3  Desired value");
  Serial.println("% 4-5 motor voltage (left,right) (V)");
  Serial.println("% 6-7 Encoder count (left, right)");
  Serial.println("% 8-9 motor vel (left, right) (rad/s)");
  Serial.println("% 10  Battery voltage (V)");
  Serial.println("% 11-12 Motor current (left, right) (A)");
  for (int i = 0; i < logsCnt; i++)
  {
    // Serial.print(" ");
    Serial.print(d->time,4);    // 1
    Serial.print(" ");    Serial.print(d->state);          // 2
    Serial.print(" ");    Serial.print(d->desiredValue,2); // 3
    Serial.print(" ");    Serial.print(d->motorVolt[0],2); // 4 (V)
    Serial.print(" ");    Serial.print(d->motorVolt[1],2); // 5 (V)
    Serial.print(" ");    Serial.print(d->encoder[0]);    // 6
    Serial.print(" ");    Serial.print(d->encoder[1]);    // 7
    Serial.print(" ");    Serial.print(d->motorVel[0],2);  // 8 (rad/s)
    Serial.print(" ");    Serial.print(d->motorVel[1],2);  // 9 (rad/s)
    Serial.print(" ");    Serial.print(d->battery,2);      // 10 (V)
    Serial.print(" ");    Serial.print(d->motorCurrent[0],3);  // 11 (A)
    Serial.print(" ");    Serial.print(d->motorCurrent[1],3);  // 12 (A)
    Serial.println(" ");
    //delay(1);
    d++;
  }
  Serial.print("% send ");
  Serial.print(logsCnt);
  Serial.println(" log lines");
}


When the state is 10, nothing happens until 500us has passed. Then, the desired value for the controller is set, and the state is changed to 20.
=== Support functions ===


States 20 and 30 change the desired value after a given time.
A few other support functions are added to keep the code clearer.


State 98 allows the values for the stopping motors to be included in the mission.
The start function empties the log and resets what is needed for a fresh start.
Further, the display is disabled to maintain a constant sample rate.


State 99 stops the motors and allows the display to be updated.
void start()
{ // Start timing
  // reset log and encoders
  logsCnt = 0;
  // change motor PWM frequency for sampling time test
  // should not be above 100000 (100kHz), default is 80kHz.
  motor.setPWMfrq(80000);
  time_sec = 0;
  encoder.encoder[0] = 0; // left motor encoder
  encoder.encoder[1] = 0; // right motor encoder
  // Update of display takes too long for fast sampling
  // So, disable during sequence.
  display.useDisplay = false;
}
 
The stop function sets the desired velocity to 0, and extends the sequence to make the spin-down part of the log.
The spin-down time is set to 0.3 seconds.
 
void stop(float after)
{ // stop, but allow continued logging for a while
  state = 999;
  desiredValue = 0;
  // end log after an additional short time
  endTime = time_sec + after;
}
 
The finished function prepares for the next start and re-enables the display.
The function further prints the log; this is handy if the USB cable is connected during the run.
 
void finished()
{ // prepare for a new start
  // stop motors
  motor.motorVoltage[0] = 0; // left motor
  motor.motorVoltage[1] = 0;  // right motor
  // set default PWM frequency
  motor.setPWMfrq(80000);
  // start updating display
  display.useDisplay = true;
  // wait for next button press
  state = 0;
  printLog();
}

Latest revision as of 10:33, 31 August 2025

Back to Basebot

Main code

The main code is in basebot_6.ino (sometimes renamed to basebot_6.cpp).

The code is based on the Arduino environment. Here, the normal C (and C++) main() function is hidden. However, setup() is called first, followed by an endless loop that calls loop().

The Basebot API is in the "src" sub-directory.

Setup

The setup() function is like this and initializes all support modules in the Basebot API:

void setup()   // INITIALIZATION
{ // to be able to print to USB interface
 Serial.begin(12000000);
 // start timing
 sampleTimer.begin(timerInterrupt, sampleTimeUs);
 // Initialise sensors, safety and display (in the basebot API)
 robot.setup(); // alive LED, display and battery
 motor.setup(); // motor voltage
 encoder.setup(); // motor encoders to velocity
 encoder.pulsPerRev = 48;
 imu2.setup(); // gyro and accelerometer
 motor.setPWMfrq(80000);
 usb.setup();
}

Timing

An interrupt controls timing.

// Sample time can not go lower than 300us
const uint32_t sampleTimeUs = 1000; // desired sample time in us
bool isSampleTime = false;
float time_sec = 0;
void timerInterrupt()
{
 isSampleTime = true;
 time_sec += sampleTimeUs / 1e6;
}

Loop

The loop() function waits for the "isSamplingTime" flag to ensure a constant sample time.

Then all sensor data are updated (by the .tick() calls).

The "sequence" decides where to go when in a sequence of states.

The "control" function translates the desired sequence to motor voltage.

The motor voltage is then implemented by the "motor.tick()" function.

The essential data values are then added to the RAM-based log.

If it is not sampling time, then potential commands from the USB connection are serviced.

void loop ( void )
{ // init sample time
 while ( true )
 { // main loop
   // loop until time for next sample
   if (isSampleTime) // start of new control cycle
   { // time for next tick
     isSampleTime = false;
     // read sensors
     imu2.tick();
     encoder.tick();
     // updatePose();
     // advance sequence (default is wait for start button)
     sequenceTwoSteps();
     //
     if (state > 0)
     { // Only if started
       // Calculate new motor voltage
       controlUpdate();
     }
     // give value to actuators
     motor.tick();
     // save relevant values
     updateLog();
     // support functions
     robot.tick();   // measure battery voltage etc.
     display.tick(); // update O-LED display
   }
   usb.tick(); // listen to incoming from USB
 }
}

State sequence

A state sequence is needed to implement a desired manoeuvre.

Global variables

int state = 0;               // actual sequence state
float endTime = 0;           // for current state
float desiredValue = 0;      // desired (reference) value send to controller
void sequenceTwoSteps()
{ // this function is called at every sample time
 // and should never wait in a loop.
 bool button;
 // this is a state machine
 // state 0: wait for start button press
 switch (state)
 { // run mission, initial value
   case 0: // State 0 is just inactive, waiting for the start signal.
     button = digitalReadFast(PIN_START_BUTTON);
     if (button or robot.missionStart)
     { //  Starting a sequence
       start();
       // Prepare next state
       desiredValue = 0; // reference value to the controller
       // to start the log with no velocity
       endTime = time_sec + 0.020; // new state to end after 20ms
       state = 10;
     }
     break;
   case 10: // Waiting for first step.
     // test if this state is finished
     if (time_sec > endTime)
     { // change to next values
       desiredValue = 1.5;
       endTime = time_sec + 0.48 ; // ~ 0.5 second
       state = 11;
     }
     break;
   case 11: //  First step
     if (time_sec > endTime)
     { // change to next values
       desiredValue = 3;
       endTime = time_sec + 0.50;
       state = 12;
     }
     break;
   case 12: //  Second step.
     if (time_sec > endTime)
     { // change to next values
       desiredValue = 6;
       endTime = time_sec + 0.5;
       state = 100;
     }
     break;
   case 100: //  Maintain third level.
     // stop after end time
     if (time_sec > endTime)
     { // stop, but continue logging for a while (0.3 sec)
       stop(0.3); // actually sets state to 999 (i.e. default:)
     }
     break;
   default:
     // this state is needed to enable a new start
     if (time_sec  > endTime)
       finished();
     break;
 }
}

Control

The control function should implement the (PID) controller calculations.

The desired value (forward velocity) can here be compared with actual velocity and used to set a motor voltage that will implement the desired behaviour.

This can later be extended also to implement a desired turn rate.

void controlUpdate()
{ // do control during a mission only.
 //
 // Measured wheel velocity (positive is forward)
 // float velLeft = -encoder.motorVelocity[0];
 // float velRight = encoder.motorVelocity[1];
 //
 // Desired velocity
 float velRef = desiredValue;
 //
 // For a start, the desiredValue
 // is just used as motor voltage
 //
 // Left motor voltage (-9V to +9V)
 motor.motorVoltage[0] = -velRef;
 // Right motor voltage (-9V to +9V)
 motor.motorVoltage[1] = velRef;
}

Update log

To document the effect of the controller, data is needed.

An array of data structures are implemented with some of the desired data.

The data structure is initially set up as follows.

typedef struct
{
 float time; // in seconds
 int state;  // sequence state
 float desiredValue;
 float motorVel[2]; // left, right
 float motorVolt[2];// left, right
 int32_t encoder[2];// Encoder count value on each motor
 float motorCurrent[2]; // rather poor measurement of motor current
 float battery; // Battery voltage
} LogData;

A large portion of memory is allocated for the log:

// allocate memory for data log in memory block 2 available only through malloc/new
// (specific for this micro processor, a Teensy 4)
const int logsMax = 400000 / sizeof(LogData); // allocates 400kBytes
LogData * logs = (LogData*)malloc(logsMax * sizeof(LogData));
int logsCnt = 0; // next log entry to use / i.e. number of used log entries

A function adds data to the log.

void updateLog()
{ // step response mode
 if (state > 0 and logsCnt < logsMax)
 {
   logs[logsCnt].time = time_sec;
   logs[logsCnt].state = state;
   logs[logsCnt].desiredValue = desiredValue;
   logs[logsCnt].motorVolt[0] = -motor.motorVoltage[0]; // The left motor runs backwards, therefore the sign.
   logs[logsCnt].motorVolt[1] = motor.motorVoltage[1];
   logs[logsCnt].encoder[0] = -encoder.encoder[0];
   logs[logsCnt].encoder[1] = encoder.encoder[1];
   logs[logsCnt].motorVel[0] = -encoder.motorVelocity[0];
   logs[logsCnt].motorVel[1] = encoder.motorVelocity[1];
   logs[logsCnt].battery = robot.batteryVoltage;
   // the last part is offset of current measurement (in Amps)
   // take the initial current readings and add or subtract as needed
   // these values fits Tania: (+0.260A, -0.275A)
   logs[logsCnt].motorCurrent[0] = -(analogRead(A1) - 2024) * 0.0045 + 0.260;
   logs[logsCnt].motorCurrent[1] =  (analogRead(A0) - 2024) * 0.0045 - 0.275;
   logsCnt++;
 }
}

The motor current is just read from the sensor pins. This is a 12-bit (4096) A/D converter, and half that value (2048) is close to zero current. Each LSB is about 4.5mA. An offset current is added at the end. This value varies for each robot and must be calibrated.

Once the log is filled, it needs to be transferred to the PC.

All values are just printed to the USB connection, and preceded by a textual explanation of the data.

void printLog()
{
 LogData * d = logs;
 Serial.print("% Basebot log for ");
 Serial.println(robot.getRobotName());
 // hard-coded settings
 Serial.print("% Sample time ");  Serial.print(sampleTimeUs);
 Serial.print(" us, Gear ");  Serial.print(gear);
 Serial.print(", Wheels ");  Serial.print(wheelRadius);
 Serial.print(" m, PPR ");  Serial.println(encoder.pulsPerRev);
 Serial.print("% maximum log samples ");  Serial.print(logsMax);
 Serial.print(" each sized ");  Serial.print(sizeof(LogData)); Serial.println(" bytes");
 // Columns
 Serial.println("% 1 time (sec)");
 Serial.println("% 2 state");
 Serial.println("% 3   Desired value");
 Serial.println("% 4-5 motor voltage (left,right) (V)");
 Serial.println("% 6-7 Encoder count (left, right)");
 Serial.println("% 8-9 motor vel (left, right) (rad/s)");
 Serial.println("% 10  Battery voltage (V)");
 Serial.println("% 11-12 Motor current (left, right) (A)");
 for (int i = 0; i < logsCnt; i++)
 {
   // Serial.print(" ");
   Serial.print(d->time,4);    // 1
   Serial.print(" ");    Serial.print(d->state);          // 2
   Serial.print(" ");    Serial.print(d->desiredValue,2); // 3
   Serial.print(" ");    Serial.print(d->motorVolt[0],2); // 4 (V)
   Serial.print(" ");    Serial.print(d->motorVolt[1],2); // 5 (V)
   Serial.print(" ");    Serial.print(d->encoder[0]);     // 6
   Serial.print(" ");    Serial.print(d->encoder[1]);     // 7
   Serial.print(" ");    Serial.print(d->motorVel[0],2);  // 8 (rad/s)
   Serial.print(" ");    Serial.print(d->motorVel[1],2);  // 9 (rad/s)
   Serial.print(" ");    Serial.print(d->battery,2);      // 10 (V)
   Serial.print(" ");    Serial.print(d->motorCurrent[0],3);  // 11 (A)
   Serial.print(" ");    Serial.print(d->motorCurrent[1],3);  // 12 (A)
   Serial.println(" ");
   //delay(1);
   d++;
 }
 Serial.print("% send ");
 Serial.print(logsCnt);
 Serial.println(" log lines");
}

Support functions

A few other support functions are added to keep the code clearer.

The start function empties the log and resets what is needed for a fresh start. Further, the display is disabled to maintain a constant sample rate.

void start()
{ // Start timing
 // reset log and encoders
 logsCnt = 0;
 // change motor PWM frequency for sampling time test
 // should not be above 100000 (100kHz), default is 80kHz.
 motor.setPWMfrq(80000);
 time_sec = 0;
 encoder.encoder[0] = 0; // left motor encoder
 encoder.encoder[1] = 0; // right motor encoder
 // Update of display takes too long for fast sampling
 // So, disable during sequence.
 display.useDisplay = false;
}

The stop function sets the desired velocity to 0, and extends the sequence to make the spin-down part of the log. The spin-down time is set to 0.3 seconds.

void stop(float after)
{ // stop, but allow continued logging for a while
 state = 999;
 desiredValue = 0;
 // end log after an additional short time
 endTime = time_sec + after;
}

The finished function prepares for the next start and re-enables the display. The function further prints the log; this is handy if the USB cable is connected during the run.

void finished()
{ // prepare for a new start
 // stop motors
 motor.motorVoltage[0] = 0; // left motor
 motor.motorVoltage[1] = 0;  // right motor
 // set default PWM frequency
 motor.setPWMfrq(80000);
 // start updating display
 display.useDisplay = true;
 // wait for next button press
 state = 0;
 printLog();
}