Home | pfodApps/pfodDevices | WebStringTemplates | Java/J2EE | Unix | Torches | Superannuation | | About Us
 

Forward Logo (image)      

Simple Multi-Tasking Arduino
without using an RTOS

by Matthew Ford 11th September 2019 (original 4th September 2019)
© Forward Computing and Control Pty. Ltd. NSW Australia
All rights reserved.


How to keep your Arduino loop() responsive

Introduction

The tutorial describes how to run multiple task on your Arduino without using an RTOS. Your 'tasks' are just normal methods, called directly from the loop() method. Each 'task' is given a chance to run each loop. You can either use a flag to skip 'tasks' that don't need to be run or, more often, just return immediately from the method call if that task has nothing to do. Each task is called in a round robin manner.

As a practical application, this tutorial will develop a temperature controlled, stepper motor driven damper with a user interface. The entire project can be developed and testing on just an Arduino UNO. Because this page is concentrating on the software the external thermocouple board and stepper motor driver libraries are used, but the hardware is omitted and the input temperature simulated in the software. Finally the same project code is moved from the UNO to an ESP32 so that you can control it remotely via WiFi, BLE or Bluetooth.

If you search for 'multitasking arduino' you will find lots of results. Most of them deal with removing delays or with using an RTOS. This page goes beyond just removing delays, that was covered in How to code Timers and Delays in Arduino, and covers the other things you need to do for multi-tasking Arduino without going to an RTOS.

This tutorial also covers moving from an Arduino to an FreeRTOS enabled ESP32 board and why you may want to keep using “Simple Multi-tasking” approach even on a board that supports an RTOS.

Parts List

Hardware
Arduino UNO or any other board supported by the Arduino IDE. All the code developed here can be tested with just an Arduino UNO.
Optional - an ESP32 e.g. Sparkfun ESP32 Thing. The last step in this tutorial moves the code, unchanged, to an ESP32 and adds remote control.

Software
Install the Arduino IDE V1.8.9+

Install the following libraries, using Arduino's Sketch -> Include Library -> Add.ZIP library :-
millisDelay.zip, loopTimer.zip and pfodParser.zip (for pfodBufferedStream and pfodNonBlockingInput),
The loopTime library has the sketches used here in its examples directory. Open Arduino's
File → Examples → loopTimer for a list of them.

For the temperature controlled stepper motor drive damper example sketch:-
Temperature input library MAX31856_noDelay.zip. Adafruit-MAX31855-library-master.zip is also used for illustration purposes, but because it uses
delay() it is replaced by MAX31856_noDelay.zip
Stepper motor control, AccelStepper-1.59.zip

Optionalinstall ESP32 Arduino support, see ESP32_Arduino_SetupGuide.pdf

Simple Multi-tasking

Here is an example of multi-tasking for Arduino

void loop() {
 callTask_1(); // do something
 callTask_2(); // do something else
 callTask_1(); // check the first task again as it needs to be more responsive than the others.
 callTask_3(); // do something else
}

That is very simple isn't it. What is the trick?
The trick is that each callTask..() method must return quickly so that the other tasks in the loop get called promptly and often. The rest of this tutorial covers how to keep your tasks running quickly and not holding everything up, using a temperature controlled, stepper motor driven damper with a user interface as a concrete example.

Why not use an RTOS?

A 'Real Time Operating System' (RTOS) adds another level of complexity to your programs as well as needing more RAM and taking more time to execute. There are a number of RTOS (Real Time Operating Systems) systems for Arduino such as https://github.com/feilipu/Arduino_FreeRTOS_Library, https://github.com/PeterVranken/RTuinOS, https://github.com/ftrias/TeensyThreads and https://github.com/arduino/ArduinoCore-nRF528x-mbedos The preemptive RTOS systems, work by dividing the CPU's time up into small slices, 2mS to 15mS or more, and sharing these slices between competing tasks. The cooperative multi-tasking RTOS systems depend on each task pausing to let another task run.

These frameworks add extra program code, use more RAM and involve learning a new 'task' framework. Some of them only run on specific Arduino boards. In general they aim to 'appear' to execute multiple 'tasks' (code blocks) at the same time, but in most cases just put one block of code to sleep for some milliseconds while executing another block. Because of this task switching taking place at a low level it is difficult to ensure any particular tasks will respond in a given time. E.g. running the AccelStepper stepper motor library on an RTOS system can be difficult because the run() method needs to be called every 1mS for high speed stepping. Later we will look at how “simple multi-tasking' lets you run these types of libraries on an ESP32 which runs FreeRTOS.

On an UNO there is not really enough RAM available to run an RTOS. The “Real Time” in RTOS is a misnomer. All computers take time to do tasks. While the cpu is occupied with that task it can miss other signals. In contrast to RTOS systems, the approach here uses the minimal of RAM, follows the standard Arduino framework of first running the setup() and then repeatedly running loop() method. The “simple multi-tasking” examples below are run on an Arduino UNO.

Keep the loop() fast and 'responsive'.

Why do you want a fast and 'responsive' loop()? Well for simple single action programs like blinking one led, you don't need it to be fast or responsive. However as you add more tasks/functions to your sketch, you will quickly find things don't work as expected. Inputs are missed, outputs are slow to operate and when you add debugging print statements everything just gets worse. This tutorial covers how to avoid the blockages that cause your program to hang without resorting to using a 'Real Time Operating System' (RTOS)

To keep the appearance of 'real time' the loop() method must run as quickly as possible without being held up. The approach here aims to keep the loop() method running continually so that your tasks are called as often as possible.

There are a number of blockages you need to avoid. The 'fixes' covered here are generally applicable all Arduino boards and don't involve leaning a new framework. However they do involve some considered programming choices. The following topics will be covered:-

Simple Multi-tasking in Arduino
Add a loop timer
Rewriting the Blink Example as a task
Another Task
Doing two things at once
Get rid of delay() calls, use millisDelay
Buffering Print Output
Getting User Input without blocking

Temperature Controlled Damper
Adding the Temperature Sensor
Modifying Arduino libraries to remove delay() calls

Giving Important Tasks Extra Time

ESP32 Damper Remote Control

Simple Multi-tasking versus ESP32 FreeRTOS

Simple Multi-tasking in Arduino

Add a loop timer

The first thing to do is to add a loop timer to keep track of how long it takes your loop() method to run. It will let you know if one or more of your tasks in holding things up. As we will see below, third party libraries often have delays built in that will slow down your loop() code.

The loopTimer library (which also needs millisDelay library) provides a simple timer that keeps track of the maximum and average time it take to run the loop code. Download these two zip files, loopTimer.zip and millisDelay.zip, and use Arduino IDE menu Sketch -> Include Library -> Add.ZIP library to add them in. Insert #include <loopTimer.h> at the top of the file and then add loopTimer.check(&Serial); to the top of your loop() method. The loopTime library uses millisDelay library, which is why you need to install that as well.

#include <loopTimer.h>
…
void setup() {
  Serial.begin(9600);}
void loop() {
 loopTimer.check(&Serial);.
}

loopTimer.check(&Serial) will print out the results every 5sec. You can suppress the printing by omitting the Serial argument, i..e loopTimer.check() and then call loopTimer.print(&Serial) later. You can also create extra named timers from the loopTimerClass that will add that name to their output. e.g. loopTimerClass task1Timer("task1");

The loopTimer library includes a number of examples. LoopTimer_BlinkDelay.ino is the 'standard' Blink code with a loop timer added

Running the LoopTimer_BlinkDelay.ino gives the following output

loop uS Latency
 5sec  max:2000028 avg:2000028
 sofar max:2000032 avg:2000029 max

As this shows the loop() code takes 2sec (2000000 uS) to run. So not even close to 'real time' if you are trying to do anything else.

Rewriting the Blink Example as a task.

Lets rewrite the blink example as task in Simple Multi-tasking Arduino, BlinkDelay_Task.ino

// the task method
void blinkLed13() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second  
}

// the loop function runs over and over again forever
void loop() {
  loopTimer.check(&Serial);
  blinkLed13(); // call the method to blink the led
}

Another Task

Now lets write another task that prints the current time in mS to the Serial every 5 secs, PrintTimeDelay_Task.ino

// the task method
void print_mS() {
  Serial.println(millis());   // print the current mS
  delay(5000);              // wait for a 5 seconds
}

Here is some of the output when that task is run just by itself (without the blink task)

10021
loop uS Latency
 5sec  max:5007288 avg:5007288
 sofar max:5007288 avg:5007288 max
15049

The millseconds is printed every 5secs and the loopTimer shows the loop is taking about 5secs to run.

Doing two things at once

Putting the two task in one sketch clearly shows the problem most people face when trying to do more than one thing with their Arduino. PrintTime_BlinkDelay_Task.ino

void loop() {
  loopTimer.check(&Serial);
  blinkLed13(); // call the method to blink the led
  print_mS(); // print the time
}

A sample of the output now shows that now the loop() takes 7 secs to run and so the blinkLed13() and the print_mS() tasks are only call once every 7secs

14021
loop uS Latency
 5sec  max:7000288 avg:7000288
 sofar max:7000288 avg:7000288 max
21042

Clearly the delay(5000) and the two delay(1000) are the problem here.

Get rid of delay() calls, use millisDelay

The PrintTime_Blink_millisDelay.ino example replaces the delay() calls with millisDelay timers. See How to code Timers and Delays in Arduino for a detailed tutorial on this.

void blinkLed13() {
  if (ledDelay.justFinished()) {   // check if delay has timed out
    ledDelay.repeat(); // start delay again without drift
    ledOn = !ledOn;     // toggle the led
    digitalWrite(led, ledOn?HIGH:LOW); // turn led on/off
  } // else nothing to do this call just return, quickly
}

void print_mS() {
  if (printDelay.justFinished()) {
    printDelay.repeat(); // start delay again without drift
    Serial.println(millis());   // print the current mS
  } // else nothing to do this call just return, quickly
}

Running this example code on an Arduino UNO gives

15001
loop uS Latency
 5sec  max:7280 avg:13
 sofar max:7280 avg:13 max
20004

So now the loop() code runs every 7.28mS and you will see the LED blinking on and off every 1sec and the every 5sec the milliseconds will be printed to Serial. You now have two tasks running “at the same time”.
The 7.2mS is due to the print() statements as we will see next.

Buffering Print Output

However delay() is not the only thing that can hold up your loop from running quickly. The next most common thing that blocks your loop() is print(..) statements.

The LongPrintTime_Blink.ino adds some extra description text as the LED is turned On and Off.

void blinkLed13() {
  if (ledDelay.justFinished()) {   // check if delay has timed out
    ledDelay.repeat(); // start delay again without drift
    ledOn = !ledOn;     // toggle the led
    Serial.print("The built-in board LED, pin 13, is being turned "); Serial.println(ledOn?"ON":"OFF");
    digitalWrite(led, ledOn?HIGH:LOW); // turn led on/off
  } // else nothing to do this call just return, quickly
}

When you run this example on an Arduino UNO board, the loop() run time goes from 7.2mS to 62.4mS.

. 
The built in board LED, pin 13, is being turned OFF
The built in board LED, pin 13, is being turned ON
The built in board LED, pin 13, is being turned OFF
40000
loop uS Latency
 5sec  max:62400 avg:13
 sofar max:62400 avg:13 max

As you add more debugging output the loop() gets slower and slower.

What is happening? Well the print(..) statements block once the TX buffer in Hardware Serial is full waiting for the preceding bytes (characters) to be sent. At 9600 baud it takes about 1mS to send each byte to the Serial port. In the UNO the TX buffer is 64 bytes long and the loopTimer Latency message is 66 bytes long so every 5sec it fills the buffer and the “led ON” “led OFF” message is blocked waiting for 53 bytes of the Latency debug message to be sent so there is room for the ON/OFF message. The print mS also blocks waiting for another 7 bytes (including the /r/n) to be sent. The net result is that the loop() is delayed for 62mS waiting for the print() statements to send the output to Serial.

This is a common problem when adding debug print statements and other output. Once you print more than 64 chars in the loop() code, it will start blocking. Increasing the Serial baud rate to 115200 will reduce the delay but does not remove it.

pfodBufferedStream can be used to avoid blocking the loop() code due to prints(..) by providing a larger buffer to print to and then slowly releasing characters to the Serial port. It actually runs as another task.

Install the pfodParser library which contains the pfodBufferedStream class and then run the BufferedPrintTime_Blink.ino example. With a 130 byte buffer the print(..) statements don't block.
The bufferedStream is connected to the Serial and thereafter the sketch prints to the bufferedStream. Calling any bufferedStream print/read method will release a buffered character if appropriate, so a call to
bufferedStream.available(); is added to the loop to ensure characters are released. This runs like another background task releasing buffered characters to Serial at 9600 baud.

void setup() {
  Serial.begin(9600);
  for (int i = 10; i > 0; i--) {
    Serial.println(i);
    delay(500);
  }
  bufferedStream.connect(&Serial);  // connect buffered stream to Serial
  pinMode(LED_BUILTIN, OUTPUT);
  ledDelay.start(1000); // start the ledDelay, toggle every 1000mS
  printDelay.start(5000); // start the printDelay, print every 5000mS
}

// the task method
void blinkLed13() {
  if (ledDelay.justFinished()) {   // check if delay has timed out
    ledDelay.repeat(); // start delay again without drift
    ledOn = !ledOn;     // toggle the led
    bufferedStream.print("The built-in board LED, pin 13, is being turned "); bufferedStream.println(ledOn?"ON":"OFF");
    digitalWrite(led, ledOn?HIGH:LOW); // turn led on/off
  } // else nothing to do this call just return, quickly
}

// the task method
void print_mS() {
  if (printDelay.justFinished()) {
    printDelay.repeat(); // start delay again without drift
    bufferedStream.println(millis());   // print the current mS
  } // else nothing to do this call just return, quickly
}

// the loop function runs over and over again forever
void loop() {
  loopTimer.check(&bufferedStream); 
  bufferedStream.available(); // call buffered stream task to release a char as necessary.
  blinkLed13(); // call the method to blink the led
  print_mS(); // print the time
}

A sample of the output is.

The built-in board LED, pin 13, is being turned ON
10000
loop uS Latency
 5sec  max:1304 avg:17
 sofar max:1304 avg:17 max

Now the loop() is running every 1.3mS. Of course if you add more print( ) statements then eventually you will exceed the bufferedStream's capacity. To avoid blocking at all, you can choose to set the pfodBufferedStream to just discard any bytes that won't fit in the buffer

  pfodBufferedStream bufferedStream(9600,buf,bufSize, false); // false sets blocking == false, i.e. excess bytes are just dropped once buffer is full 

The false argument means blocking is false, i.e. non-blocking. With this setting you may loose some output, but you will not slow down the loop() code waiting for print statements to be sent.
If you think your extra print statements are blocking the loop, just add the extra false argument to the pfodBufferedStream constructor and see if the loop() speeds up.

Getting User Input without blocking

Another cause of delays is handling user input. The Arduino Stream class, which Serial extends, is typical of the Arduino libraries in that includes calls to delay(). The Stream class has a number of utility methods, find...(), readBytes...(), readString...() and parseInt() and parserFloat(). All of these methods call timedRead() or timedPeek() which enter a tight loop for up to 1sec waiting for the next character. This prevents your loop() from running and so these methods are useless if you need your Arduino to be controlling something as well as requesting user input.

The next example also illustrates how easy it is to pass data between tasks. Use either global variables or arguments to pass in values to a task and use global variables or a return statement to the return the results. No special locking is needed to ensure things work as you would like.

PfodNonBlockingInput, available in the pfodParser library, provides two non-blocking methods, readInputLine() and clearInput(), to collect user input while keeping your loop() code running.

Install the pfodParser library which contains the pfodNonBlockingInput class and then run the Input_Blink_Tasks.ino example. This cascades a nonBlockingInput with a bufferedStream. The readInputLine() method has an option to echo the user's input.

A small buffer is used to capture the user input

pfodNonBlockingInput nonBlocking;
const size_t lineBufferSize = 10; // 9 + terminating null
char lineBuffer[lineBufferSize] = ""; // start empty

The task to collect user input is

// task to get the first char user enters, input terminated by \r or \n
// return 0 if nothing input
char getInput() {
  char rtn = 0;
  int inputLen = nonBlocking.readInputLine(lineBuffer, lineBufferSize, true); // echo input
  if (inputLen > 0) {
    // got some user input either 9 chars or less than 9 chars terminated by \r or \n
    rtn = lineBuffer[0]; // collect first char for rtn.
  }
  return rtn;
}

The blinkLed13 task now takes an argument to stop the blinking

void blinkLed13(bool stop) {
  if (ledDelay.justFinished()) {   // check if delay has timed out
    ledDelay.repeat(); // start delay again without drift
    if (stop) {
      digitalWrite(led, LOW); // turn led on/off
      ledOn = false;
      return;
    }
    ledOn = !ledOn;     // toggle the led
    digitalWrite(led, ledOn ? HIGH : LOW); // turn led on/off
  } // else nothing to do this call just return, quickly
}

The loop() is now

void loop() {
  loopTimer.check(&nonBlocking);
  char in = getInput(); // call input task, this also releases buffered prints
  if (in != 0) {
    bufferedStream.print(F("User entered:")); bufferedStream.println(in);
    if (in != 's') {
      stopBlinking = false;
    } else {
      stopBlinking = true;
    }
    lineBuffer[0] = '\0'; // clear buffer for reuse
    // prompt user again
    if (stopBlinking) {
      bufferedStream.println(F("Enter any char to start Led Blinking:"));
    } else {
      bufferedStream.println(F("Enter s to stop Led Blinking:"));
    }
    nonBlocking.clearInput(); // clear out any old input waiting to be read.  This call is non-blocking
  }
  blinkLed13(stopBlinking); // call the method to blink the led
  print_mS(); // print the time
}

A sample of the output is below. The loop() time is still ~1mS even while clearing out old Serial data and waiting for new user input.

s
User entered:s
Enter any char to start Led Blinking:
15000
loop uS Latency
 5sec  max:1164 avg:36
 sofar max:1164 avg:36 max

So now the 'simple multi-tasking' sketch is controlling the Led blinking via a user input command while still printing out the milliseconds every 5 sec.

Temperature Controlled Damper

Now that we have a basic multi-tasking sketch that can do multiple things “at the same time”, print output and prompt for user input, we can add the temperature sensor and stepper motor libraries to complete the Temperature Controlled Damper sketch.

Adding the Temperature Sensor

The next task in this project is to read the temperature that is going to be used to control the damper. Most Arduino sensor libraries use calls to delay() to wait for the reading to become available. To keep your Arduino loop() running you need to remove these calls to delay(). This takes some work and code re-organization. The general approach is to start the measurement, set a flag to say a measurement is under way, and start a millisDelay to pick up the result.

For the temperature sensor we are using Adafruits's MAX31856 breakout board. The MAX31856 uses the SPI interface which uses pin 13 for the SCK, so the led in the blinkled13 task is moved to pin 7. You don't need the breakout board to run the sketch, it will just return 0 for the temperature.

As a first attempt we will use the Adafruit's MAX31856 library (local copy here). The sketch TempDelayInputBlink_Tasks.ino, adds a readTemp() task. For simplicity this task does not check for thermocouple faults. A full implementation should.

// return 0 if have new reading and no errors
// returns -1 if no new reading
// returns >0 if have errors
int readTemp() {
  tempReading = maxthermo.readThermocoupleTemperature();
  return 0;
}

And the loop() is modified to allow the user to start and stop taking temperature readings. This is an example of using a flag, stopTempReadings, to skip a task that need not be run.

void loop() {
  loopTimer.check(&nonBlocking);
  char in = getInput(); // call input task, this also releases buffered prints
  if (in != 0) {
    bufferedStream.print(F("User entered:")); bufferedStream.println(in);
    if (in != 's') {
      stopTempReadings = false;
    } else {
      stopTempReadings = true;
    }
    lineBuffer[0] = '\0'; // clear buffer for reuse
    // prompt user again
    if (stopTempReadings) {
      nonBlocking.println(F("Enter any char to start reading Temp:"));
    } else {
      nonBlocking.println(F("Enter s to stop reading Temp:"));
    }
    nonBlocking.clearInput(); // clear out any old input waiting to be read.  This call is non-blocking
  }
  blinkLed13(stopTempReadings); // call the method to blink the led
  printTemp(); // print the temp
  if (!stopTempReadings) {
    int rtn = readTemp(); // check for errors here
  }
}

The print_mS() is replaced with a printTemp() task

void printTemp() {
  if (printDelay.justFinished()) {
    printDelay.repeat(); // start delay again without drift
    //bufferedStream.println(millis());   // print the current mS
    if (stopTempReadings) {
      bufferedStream.println(F("Temp reading stopped"));
    } else {
      bufferedStream.print(F("Temp:")); bufferedStream.println(tempReading);
    }
  } // else nothing to do this call just return, quickly
}

The led output will only blinks we are taking temperature readings.

Running the TempDelayInputBlink_Tasks.ino on an UNO with no breakout board attached (that is the SPI leads are not connected) gives

Enter any char to start reading Temp:
Temp reading stopped
loop uS Latency
 5sec  max:452 avg:36
 sofar max:452 avg:36 max
Temp reading stopped
loop uS Latency
 5sec  max:456 avg:36
 sofar max:456 avg:36 max
r
User entered:r
Enter s to stop reading Temp:
loop uS Latency
 5sec  max:253428 avg:388
 sofar max:253428 avg:388 max
Temp:-0.01
loop uS Latency
 5sec  max:252192 avg:251747
 sofar max:253428 avg:251747 max

As you can see before we start taking reading the loop() runs every 0.46mS. Once we start taking readings, the loop() slows to a crawl, 250mS. The problem is the delay(250) which is built into Adafruit's MAX31956 library. Searching through the library code from https://github.com/adafruit/Adafruit_MAX31856 shows that there is only one use of delay in the oneShotTemperature() method, which adds a delay(250) at the end to give the board time to read the temperature and make the result available.

Modifying Arduino libraries to remove delay() calls

Fixing this library turns out to be relatively straight forward. Remove the delay(250) at the end of the oneShotTemperature() method and delete the calls to oneShotTemperature() from readCJTemperature() and readThermocoupleTemperature().
The modified library, MAX31956_noDelay, is available here.

To use the modified noDelay library, we need to start a reading and then come back is a little while later to pick up the result. The readTemp() task now looks like

int readTemp() {
  if (!readingStarted) { // start one now
    maxthermo.oneShotTemperature();
    // start delay to pick up results
    max31856Delay.start(MAX31856_DELAY_MS);
  }
  if (max31856Delay.justFinished()) {
    readingStarted = false;
    // can pick up results now
    tempReading = maxthermo.readThermocoupleTemperature();
    return 0; // new reading
  }
  return -1; // no new reading
}

Running the modified sketch TempInputBlink_Tasks.ino, gives the output below. The loop() runs in ~2mS while taking temperature readings.

Enter any char to start reading Temp:
Temp reading stopped
loop uS Latency
 5sec  max:452 avg:29
 sofar max:452 avg:29 max
r
User entered:r
Enter s to stop reading Temp:
Temp:0.00
loop uS Latency
 5sec  max:2016 avg:233
 sofar max:2016 avg:233 max
Temp:0.00

Giving Important Tasks Extra Time

The last part of this simple multi-tasking temperature controlled damper is the damper's stepper motor control. Here we are using the AccelStepper library to control the damper's stepper motor. The accelStepper's run() method has to be called for each step. That means in order to achieve the maximum 1000 steps/sec, the run() method needs to be called at least once every 1mS.

As a first attempt, just add the stepper motor library and control. Since this tutorial is about the software and not the hardware, it will use a very simple control and just move the damper to fixed positions depending on temperature. 0 degs to 100 degs will be mapped into 0 to 5000 steps position. To test the software without a temperature board, the user can input numbers 0 to 5 to simulate temperatures 0 to 100 degs. The readTemp() task will still be called but its result will be ignored.

There are two new tasks setDamperPosition() to convert temp to position and runStepper() to run the AccelStepper run() method.

void setDamperPosition() {
  if (closeDampler) {
    stepper.moveTo(0);
  } else {
    long stepPosition = simulatedTempReading * 50;
    stepper.moveTo(stepPosition);
  }
}

void runStepper() {
  stepper.run();
}

The loop() handles the user input temperature simulation and adds these two extra tasks on the end

void loop() {
  loopTimer.check(&bufferedStream);
  char in = getInput(); // call input task, this also releases buffered prints
  if (in != 0) {
    bufferedStream.print(F("Cmd Entered:")); bufferedStream.println(in);
    closeDampler = false;
    if (in == '0') {
      simulatedTempReading = 0.0;
    } else if (in == '1') {
      simulatedTempReading = 20.0;
    } else if (in == '2') {
      simulatedTempReading = 40.0;
    } else if (in == '3') {
      simulatedTempReading = 60.0;
    } else if (in == '4') {
      simulatedTempReading = 80.0;
    } else if (in == '5') {
      simulatedTempReading = 100.0;
    } else {
      closeDampler = true;
      bufferedStream.println(F("Close Damper"));
    }
    lineBuffer[0] = '\0'; // clear buffer for reuse
    // prompt user again
    nonBlocking.clearInput(); // clear out any old input waiting to be read.  This call is non-blocking
  }
  blinkLed7(closeDampler); // call the method to blink the led
  printTemp(); // print the temp
  int rtn = readTemp(); // check for errors here
  setDamperPosition();
  runStepper();
}

Running the FirstDamperControl.ino sketch give the following timings

Temp:60.00
Position current:1530
loop uS Latency
 5sec  max:2352 avg:1122
 sofar max:2352 avg:1122 max

Since the loop() only runs every 2.3mS, runStepper() is only called that often. We need to add more calls to runStepper() so that it is called more frequently. Moving the loopTimer from loop() into the runStepper() task allows us to monitor how often runStepper() is called.

void runStepper() {
  loopTimer.check(&bufferedStream); // moved here from loop()
  stepper.run();
}

The FinalDamperControl.ino, adds two more calls to runStepper() around the printTemp() task

blinkLed7(closeDampler); // call the method to blink the led
  runStepper(); // <<<< extra call here
  printTemp(); // print the temp
  runStepper(); // <<<< extra call here
  int rtn = readTemp(); // check for errors here
  setDamperPosition();
  runStepper();
}

Running the FinalDamperControl.ino, gives these timings

Temp:60.00
Position current:1515
loop uS Latency
 5sec  max:1252 avg:382
 sofar max:1252 avg:382 max


Adding more extra calls to runStepper() does not noticeably improve the timings. So the UNO, with a 16Mhz clock, is just not quite fast enough to scan for user input, print output, blink the led and run the stepper at 1000 steps/sec. 1.25mS limits the maximum speed of the stepper to 800 steps/min. To do better we need to use a faster processor. The ESP32's clock is 80Mhz, so lets try it.

ESP32 Damper Remote Control

Without making any changes to the FinalDamperControl.ino sketch, recompile and run it on an ESP32 board. Here we are using a Sparkfun ESP32 Thing. The timings for runStepper() are now

Temp:60.00
Position current:379
loop uS Latency
 5sec  max:85 avg:13
 sofar max:126 avg:13 max

So running on an ESP32, there is no problem achieving 1000 steps/sec for the stepper motor. Using an ESP32 also gives you the ability to control the damper via WiFi, BLE or Classic Bluetooth.

Note that although the ESP32 is a dual core processor running FreeRTOS, no changes were needed to run the “simple multi-tasking” sketch on it. The loop() code runs on core 1, leaving core 0 free to run the communication code. You have a choice of WiFi, BLE or Classic Bluetooth for remote control of the damper system. WiFi is prone to 'Half-Open' connections and requires extra work to avoid problems. BLE is slower with smaller data packets and requires different output buffering. If you are using the free pfodDesigner Andoid app to create your control menu to run on pfodApp then the correct code for these cases are generated for you.

Here we will use Classic Bluetooth as it the simplest to code and easily connects to a terminal program on old computers as well as mobiles.

The ESP32DamperControl.ino sketch has the necessary mods. The output is now redirected to the SerialBT and the baud rate increased to 115200. Once you see “The device started, now you can pair it with Classic bluetooth!” in the Serial Monitor, you can pair with your computer or mobile. After pairing with the computer, a new COM port was created on the computer and TeraTerm for PC (or CoolTerm Mac/PC) can be used to connect and control the damper. On your Android mobile you can use a bluetooth terminal app such as Bluetooth Terminal app.

Of course now that you have finished checking the timings you can comment out the loopTimer.check() statement. You could also add you own control menu. The free pfodDesigner Android app lets you do that easily and generate the menu code for you to use with the, paid, pfodApp.

Simple Multi-tasking versus ESP32 FreeRTOS

Given that “simple multi-tasking” works on any Arduino board, why would you want to use ESP32 FreeRTOS or other RTOS system? Well probably the most compelling reason is that RTOS systems generally are tolerant of delay() calls so you can use third-party libraries unchanged. Other then that, using ESP32 FreeRTOS is not as straight forward as “simple multi-tasking”.

ESP32's FreeRTOS is a cooperative multi-tasking system, so you have to program delays into your tasks to give other tasks a chance to run. You need to learn new methods for starting tasks and if you use the default method, your task can be run on either core, so you can find your task competing with the high priority Radio tasks for time. Also if you have multiple tasks distributed across the two cores, you have to worry about safely transferring data between the tasks in a thread safe manner, i.e. locks, semaphores, critical sections etc. Finally due to a quirk in the way the ESP32 implements the task switching, you can find your task is not called at all, or called less often then you would expect. You can code around this problem, but it takes extra effort.

In a preemptive RTOS system as used by TeensyThreads it can be difficult to force a tasks like the AccelStepper run() method to run as often as you want.

All RTOS systems add an extra overhead of support code with its own set of bugs and limitations. So all in all, the recommendation is to code using the “simple multi-tasking” approach that will run on any Arduino board you choose. If you want to add a communication's module, then the ESP32's second core provides it without impacting your code and having two separate cores minimizes the impact of the underlying RTOS.

Conclusions

This tutorial presented “simple multi-tasking” for any Arduino board. The detailed example sketches showed how to achieve 'real time' execution limited only by the cpu's clock, by replacing delay() with millisDelay, buffering output and getting user input without blocking. The loopTimer lets you bench mark how responsive your sketch is. As a practical example a temperature controlled, stepper motor driven damper program was built.

Finally the example sketch was simply recompiled for an ESP32 that provides a second core for remote control via WiFi, BLE or Classic Bluetooth, without impacting the responsiveness of original code.




The General Purpose Android/Arduino Control App.
pfodDevice™ and pfodApp™ are trade marks of Forward Computing and Control Pty. Ltd.


Forward home page link (image)

Contact Forward Computing and Control by
©Copyright 1996-2018 Forward Computing and Control Pty. Ltd. ACN 003 669 994