Serial Printing Whilst Running Motors Without Delay

Hey Guys

Im trying to serial print data collected from the QTR-8RC with a timestamp. I have gotten it to work and I can serial print every 25ms. Once I got that working I tried to implement my code for the motors so they simply pulse a set PWM signal for a set time and then reverse it so the result is the robot will just keep moving back and fourth over the line. Its a line follower by the way. When I put both pieces of code together it does as expected and runs the loop but then serial prints the line position and time stamp after its finished pulsing the motor one way and then back the other way, so every 2 seconds. My problem is I want to keep recording data every 25ms whilst simultaneously pulsing the motors back and fourth. Thanks for your help in advanced and hope I have explained my problem well. I know the example code Blink Without Delay touches on what I want to do but I cant seem to wrap my head around it.

// MOTOR MOTOR MOTOR MOTOR
int E1 = 10; // RIGHT MOTOR PWM
int M1 = 12; // RIGHT MOTOR DIRECTION
int E2 = 11; // LEFT MOTOR PWM                    
int M2 = 13; // LEFT MOTOR DIRECTION

//TIMER TIMER TIMER TIMER
  unsigned long time;

//SENSOR SENSOR SENSOR SENSOR
  #include <QTRSensors.h>
  #define NUM_SENSORS   8 // Number of Sensors Used
  #define TIMEOUT       2500 // Waits for 2500 Microseconds for Sensor Outputs to go Low
  #define EMITTER_PIN   QTR_NO_EMITTER_PIN // Eemitter is Controlled by Digital Pin 2, No Emitter Pin

  QTRSensorsRC qtrrc((unsigned char[]) {2, 3, 4, 5, 6, 7, 8, 9}, NUM_SENSORS, TIMEOUT, EMITTER_PIN); 
  unsigned int sensorValues[NUM_SENSORS];

void setup()
{
//MOTOR MOTOR MOTOR MOTOR
    pinMode(M1, OUTPUT);   
    pinMode(M2, OUTPUT); 
    pinMode(E1, OUTPUT);   
    pinMode(E2, OUTPUT);
    
// TIMER TIMER TIMER TIMER
  Serial.begin(9600);

// SENSOR SENSOR SENSOR SENOR
  for (int i = 0; i < 200; i++) // Calibration Time
  {
    qtrrc.calibrate(); // Reads All Sensors 10 Times at 2500 us Per Read (i.e. ~25 ms per call)
  }
  delay(1000);
}
void loop()
{
//MOTOR MOTOR MOTOR MOTOR

// TURN RIGHT
    digitalWrite(M1, LOW);    //RIGHT MOTOR REVERSE
    digitalWrite(M2, HIGH);   //LEFT MOTOR FORWARD    
    analogWrite(E1, 150);     //RIGHT MOTOR PWM
    analogWrite(E2, 150);       //LEFT MOTOR PWM
    delay(50);              //TIME

// STOP
    digitalWrite(M1, LOW);    //STOP
    digitalWrite(M2, LOW);    //STOP
    analogWrite(E1, 0);       //STOP
    analogWrite(E2, 0);       //STOP
    delay(1000);

// TURN LEFT
    digitalWrite(M1, HIGH);   //RIGHT MOTOR FORWARD
    digitalWrite(M2, LOW);    //LEFT MOTOR REVERSE
    analogWrite(E1, 150);     //RIGHT MOTOR PWM
    analogWrite(E2, 150);     //LEFT MOTOR PWM
    delay(50);              //TIME

// STOP
    digitalWrite(M1, LOW);    //STOP
    digitalWrite(M2, LOW);    //STOP     
    analogWrite(E1, 0);       //STOP
    analogWrite(E2, 0);       //STOP
    delay(1000);
    
// TIMER TIMER TIMER TIMER
  Serial.print("Time: ");
  time = millis();
  Serial.println(time); //Prints Time Since Program Started
  delay(25); // Time Stamp Sample Rate

// SENSOR SENSOR SENSOR SENSOR
  unsigned int position = qtrrc.readLine(sensorValues); // Read Calibrated Sensor Values and Obtain a Measure of the Line Position from 0 to 7000
  Serial.println(position); // Prints Line Positions Values from 0-7000
}

I know the example code Blink Without Delay touches on what I want to do but I cant seem to wrap my head around it.

The principle is simple.

Save the millis() value at the time that the start action happens. Then, each time through loop(), check whether the required wait period has elapsed by subtracting the start time from the millis() value now. If the period has elapsed then act accordingly and maybe save the start time for the next activity. If not, then go round loop() again, perhaps taking other actions and/or reading inputs, but don't block the free running of loop().

Have a look at Several things at the same time

Try this using the SCoopME library. Anytime you are doing real time control and need to manage several tasks at once you should use a RTOS. Even a simple one such as this relieves you of much of the tedious synchronization coding and reduces errors. Ultimately, if you do it the hard way you will be writing an RTOS anyway, but at the same time making your code error prone and hard to read.

Divide your task into many smaller tasks. Define each of these tasks in addition to specifying the control parameters of each task such as when it runs, goes to sleep, wait on another task or flag, etc. When this is done you just launch the whole thing and it goes. Simple as that.

In the sample I have posted here for you the main loop is just the single statement yield(); That launches everything and it runs itself from then on.

The sample as written conforms to you original. If you leave out the lines marked "remove for simple version" then you will separate the motor drive and data sampling/printing tasks. Then these two tasks will run independently and asynchronously. Data acquisition/printing will run every 25ms and motor drive will do its own thing.

Hope this helps ;D

(There may be a typo or brain freeze in this because I could not test it without your hardware, but it's pretty close to my freezer logger code so should work without much alteration)

#include <SCoopME.h>
#include <QTRSensors.h>

#define NUM_SENSORS   8
#define TIMEOUT       2500
#define EMITTER_PIN   QTR_NO_EMITTER_PIN

QTRSensorsRC qtrrc((unsigned char[]) {2, 3, 4, 5, 6, 7, 8, 9}, 
    NUM_SENSORS, TIMEOUT, EMITTER_PIN); 
unsigned int sensorValues[NUM_SENSORS];

int E1 = 10; // RIGHT MOTOR PWM
int M1 = 12; // RIGHT MOTOR DIRECTION
int E2 = 11; // LEFT MOTOR PWM                    
int M2 = 13; // LEFT MOTOR DIRECTION

enum mtr_con{RIGHT, STOP, LEFT};
bool mtrSeqComplete;            // remove for simple version

void setup() {
    Serial.begin(9600);
}

void loop() {
    yield();
}

struct sequenceMotor : SCoopTask<sequenceMotor> {
static void setup() {
    mtrSeqComplete = false;     // remove for simple version
    pinMode(M1, OUTPUT);   
    pinMode(M2, OUTPUT); 
    pinMode(E1, OUTPUT);   
    pinMode(E2, OUTPUT);
}
static void loop() {
    controlMotor(RIGHT);
    sleep(50);
    controlMotor(STOP);
    sleep(1000);
    controlMotor(LEFT);
    sleep(50);
    controlMotor(STOP);
    sleep(1000);
    mtrSeqComplete = true;      // remove for simple version
}

struct getData : SCoopTask<getData> {
static void setup() {
  for (int i = 0; i < 200; i++) qtrrc.calibrate();
}
static void loop() {
    sleepUntil(mtrSeqComplete); // remove for simple version
    mtrSeqComplete = false;     // remove for simple version
    unsigned int position = qtrrc.readLine(sensorValues);
    Serial.print("Time: ");
    Serial.println(millis());
    Serial.println(position);
    sleepSync(25);
}

void controlMotor(mtr_con cmd) {
    switch(cmd) {
    case RIGHT :
        digitalWrite(M1, LOW); 
        digitalWrite(M2, HIGH);   
        analogWrite(E1, 150);  
        analogWrite(E2, 150);  
        break;        
    case STOP :
        digitalWrite(M1, LOW);    
        digitalWrite(M2, LOW);    
        analogWrite(E1, 0);       
        analogWrite(E2, 0);       
        break;
    case LEFT :
        digitalWrite(M1, HIGH);   
        digitalWrite(M2, LOW);    
        analogWrite(E1, 150);     
        analogWrite(E2, 150);     
    }
}

jfitter:
Anytime you are doing real time control and need to manage several tasks at once you should use a RTOS.

I disagree.

I reckon they just consume CPU cycles and SRAM and make things even more complicated if (when) they don't work as the user expects.

It's not as if millis() is complicated :slight_smile:

...R

Robin2:
I disagree.

I reckon they just consume CPU cycles and SRAM and make things even more complicated if (when) they don't work as the user expects.

It's not as if millis() is complicated :slight_smile:

...R

I completely agree with you and for simple programs with just a few tasks it is quite easy to keep track of things with a timer.

The process is as you describe. Check the timer regularly and do stuff if it's due to be done. For things that need to be done right now use interrupts but that's another subject.

If that is all you do, and you have a good quality RTOS then the generated code will be just what you write yourself. Loop, check, do tasks, etc. No wasted code or cycles here. This is a form of round robin scheduling where the tasks all run to completion so no stack space is needed for task contexts.

What the RTOS offers though is much more, if you need it. Proper task synchronization and task prioritizing. Non preemptive scheduling to allow tasks to yield to the scheduler. A range of other features that ensure smooth multitasking without hidden bugs. And when you need to add additional tasks you get predictable behaviour, which is much less likely when you "roll your own" scheduler.

I guess it comes down to price. Really good (bugless) RTOS is not cheap. One meeting high reliability standards is even more costly. Some of the software I have written requires a HIREL RTOS to meet the client specification, so its use ceases to be an option.

I have a product in the wild currently residing in the safety backup system in thousands of mid-rise elevators world wide, which is based on a tiny PIC12xx series processor and is running a very lean RTOS. Even in such a miniscule processor there can be a case for an RTOS to ensure robust operation. We made hundreds of thousands of these devices and never had a single software failure (false positive).

UKHeliBob:
The principle is simple.

Save the millis() value at the time that the start action happens. Then, each time through loop(), check whether the required wait period has elapsed by subtracting the start time from the millis() value now. If the period has elapsed then act accordingly and maybe save the start time for the next activity. If not, then go round loop() again, perhaps taking other actions and/or reading inputs, but don't block the free running of loop().

Have a look at Several things at the same time

I would add to this that since the OP has already outlined discreet states, Nick's Gammon's state machine examples alongside Robin2's aforementioned tutorial may be helpful in a solution.

since the OP has already outlined discreet states, Nick's Gammon's state machine examples alongside Robin2's aforementioned tutorial may be helpful in a solution.

+1

Here's a version that uses a Finite-State Machine to step through the motor sequence, without using delay:

// MOTOR MOTOR MOTOR MOTOR
  static const int E1 = 10; // RIGHT MOTOR PWM
  static const int M1 = 12; // RIGHT MOTOR DIRECTION
  static const int E2 = 11; // LEFT MOTOR PWM
  static const int M2 = 13; // LEFT MOTOR DIRECTION

// Finite-State Machine for controlling the motors.
  enum MotorState_t { STOPPED_1, TURNING_RIGHT, STOPPED_2, TURNING_LEFT };
  static MotorState_t state = STOPPED_1; // current motor state
  static uint32_t     stateTime;  // millis() when state started
  static uint32_t     interval;

//TIMER TIMER TIMER TIMER
  static       uint32_t lastTimestamp;
  static const uint32_t TIMESTAMP_INTERVAL = 25;

//SENSOR SENSOR SENSOR SENSOR
  #include <QTRSensors.h>
  #define NUM_SENSORS   8 // Number of Sensors Used
  #define TIMEOUT       2500 // Waits for 2500 Microseconds for Sensor Outputs to go Low
  #define EMITTER_PIN   QTR_NO_EMITTER_PIN // Eemitter is Controlled by Digital Pin 2, No Emitter Pin

  static unsigned char sensorPins[NUM_SENSORS] = {2, 3, 4, 5, 6, 7, 8, 9};
  QTRSensorsRC qtrrc( sensorPins, NUM_SENSORS, TIMEOUT, EMITTER_PIN );
  unsigned int sensorValues[NUM_SENSORS];

  static const uint32_t QTR_INTERVAL = 5; // How often should you read the sensors?
  static       uint32_t lastQTRread;

void setup()
{
  //MOTOR MOTOR MOTOR MOTOR
  pinMode(M1, OUTPUT);
  pinMode(M2, OUTPUT);
  pinMode(E1, OUTPUT);
  pinMode(E2, OUTPUT);

  // TIMER TIMER TIMER TIMER
  Serial.begin(9600);

  // SENSOR SENSOR SENSOR SENOR
  for (int i = 0; i < 200; i++) // Calibration Time
  {
    qtrrc.calibrate(); // Reads All Sensors 10 Times at 2500 us Per Read (i.e. ~25 ms per call)
  }

  // Start the FSM, the QTR timer and the timestamp timer
  interval  = 1000;
  state     = STOPPED_1;
  stateTime = millis();

  lastQTRread   = stateTime;

  lastTimestamp = stateTime;
}

void loop()
{
  uint32_t time = millis();

  //MOTOR FSM
  switch (state) {
    case STOPPED_1:
      if (time - stateTime > interval) {
        turnRight();
        interval  = 50;
        state     = TURNING_RIGHT;
        stateTime = time;
      }
      break;

    case TURNING_RIGHT:
      if (time - stateTime > interval) {
        stop();
        interval  = 1000;
        state     = STOPPED_2;
        stateTime = time;
      }
      break;

    case STOPPED_2:
      if (time - stateTime > interval) {
        turnLeft();
        interval  = 50;
        state     = TURNING_LEFT;
        stateTime = time;
      }
      break;

    case TURNING_LEFT:
      if (time - stateTime > interval) {
        stop();
        interval  = 1000;
        state     = STOPPED_1; // start over
        stateTime = time;
      }
      break;
  }

// TIMER TIMER TIMER TIMER
  if (time - lastTimestamp >= TIMESTAMP_INTERVAL) {
    lastTimestamp = time;
    Serial.print( F("Time: ") );
    Serial.println(time); //Prints Time Since Program Started
  }

// SENSOR SENSOR SENSOR SENSOR
  if (time - lastQTRread >= QTR_INTERVAL) {
    lastQTRread = time;
    unsigned int position = qtrrc.readLine(sensorValues); // Read Calibrated Sensor Values and Obtain a Measure of the Line Position from 0 to 7000
    Serial.println(position); // Prints Line Positions Values from 0-7000
  }
}

void turnRight()
{
  digitalWrite(M1, LOW);    //RIGHT MOTOR REVERSE
  digitalWrite(M2, HIGH);   //LEFT MOTOR FORWARD
  analogWrite(E1, 150);     //RIGHT MOTOR PWM
  analogWrite(E2, 150);     //LEFT MOTOR PWM
}


void stop()
{
  digitalWrite(M1, LOW);    //STOP
  digitalWrite(M2, LOW);    //STOP
  analogWrite(E1, 0);       //STOP
  analogWrite(E2, 0);       //STOP
}


void turnLeft()
{
  digitalWrite(M1, HIGH);   //RIGHT MOTOR FORWARD
  digitalWrite(M2, LOW);    //LEFT MOTOR REVERSE
  analogWrite(E1, 150);     //RIGHT MOTOR PWM
  analogWrite(E2, 150);     //LEFT MOTOR PWM
}

This non-blocking approach is different, but it allows you to do multiple things at a time. In your case, there are 3 simultaneous things:

  • Motor "scanning"

  • Timestamping

  • Sensor reading

Using delay forces the tasks to wait for each other -- they would be sequential, not concurrent.

You also have to be careful about printing too much. At 9600, you can only print 1 character per millisecond. So a 5ms interval can only print 5 characters (on average). The 25ms interval can only print 25 characters. Your original sketch was printing the sensor readings too often, so I put that on a timer, too.

If you increase the baud rate, you could print more.

Nice example...

-dev:
...uses a Finite-State Machine