Simple Multitasking in Arduino on Any Board

18,225

82

32

Introduction: Simple Multitasking in Arduino on Any Board

Update 6th Jan 2021 – loopTimer class now part of the SafeString library (V3+) install it from Arduino Library manager or from its zip file

Update 15th Dec 2020 – Revised to use SafeString readUntilToken and BufferedOutput for non-blocking Serial I/O, loopTimer now displays its print time as prt:
Update 27th Sept 2020 – Added note about using multiple thermocouples/SPI devices
Update 21st Nov 2019
- Added comparison examples for Arduino_FreeRTOS and frt compared to Simple Multi-tasking in Arduino

Also see Arduino For Beginners – Next Steps
How to write Timers and Delays in Arduino
Safe Arduino String Processing for Beginners
Simple Arduino Libraries for Beginners
Simple Multi-tasking in Arduino (this one)
Arduino Serial I/O for the Real World

Introduction

The instructable describes how to run multiple tasks 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 instructable will develop a temperature controlled, stepper motor driven damper with a command user interface. The entire project can be developed and tested on just an Arduino UNO. Because this instructiable 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 is 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 (instructable), and covers the other things you need to do for multi-tasking Arduino without going to an RTOS, such as avoiding Arduino Serial and using the SafeString non-blocking alternative.

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

This instructable is also available online at Simple Multitasking Arduino

The following topics will be covered:-

Simple Multi-tasking in Arduino -- Step 2

  • Add a loop timer -- Step 2
  • Rewriting the Blink Example as a task -- Step 3
  • Another Task -- Step 3
  • Doing two things at once -- Step 3
  • Get rid of delay() calls, use millisDelay -- Step 4
  • Buffering Print Output -- Step 5
  • Getting User Input without blocking -- Step 6

Temperature Controlled Damper -- Step 7

  • Adding the Temperature Sensor -- Step 7
  • Modifying Arduino libraries to remove delay() calls -- Step 8

Giving Important Tasks Extra Time -- Step 9

ESP32 Damper Remote Control -- Step 10

Simple Multi-tasking versus ESP32 FreeRTOS -- Step 11

Supplies

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 instructable moves the code, unchanged, to an ESP32 and adds remote control.

Software
Install the Arduino IDE V1.8.9+
Install the SafeString library (V3+) from the Arduino Library Manager. It includes the millisDelay class and the loopTimer class used here.

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

Optional – install ESP32 Arduino support, see ESP32_Arduino_SetupGuide.pdf

Step 1: Simple Multi-tasking

Here is an example of multi-tasking for Arduino

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

That is very simple isn't it. What is the trick?

The trick is that each call to a task..() method must return quickly so that the other tasks in the loop get called promptly and often. The rest of this instructable 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

The preemptive RTOS systems, work by dividing the CPU's time up into small slices and sharing these slices between competing tasks. The cooperative multi-tasking RTOS systems depend on each task pausing to let another task run.

For example, comparing Arduino_FreeRTOS and frt to Simple Multi-tasking in Arduino using the Blink_AnalogRead examples to read the analog input as fast as possible. Adding a loopTimer to the AnalogRead task shows that:-

Each of the Arduino_FreeRTOS and frt use an extra 0.6Kb of program memory and an extra 150 bytes of RAM, but, more importantly, because the analogRead task has the highest priority, they each need to include a minimum 'delay' in the analogRead task to allow other less important tasks a chance to run. Simple Multi-tasking in Arduino does not need that extra delay. As we will see in the stepper motor control below, that 15mS delay is prohibitive. Simple Multi-tasking in Arduino is smaller and simpler than RTOS alternatives and does not need extra added delays. Note also that the time taken to print the loop timings (prt: ) to Serial is significant but is NOT included in the loop times.

These frameworks add extra program code, use more RAM and involve learning a new 'task' framework, the FreeRTOS manual is 400 pages long. 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.

All computers take time to do tasks. While the cpu is occupied with that task it can miss other signals. The “Real Time” in RTOS is a misnomer. An UNO has limited RAM and Flash memory available to run an RTOS. In contrast to RTOS systems, the approach here uses minimal RAM and 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 instructable 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.

Step 2: 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. Using Arduino Serial for I/O will also slow down your loop().

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:2000026
 sofar max:2000028 avg:2000026 max - prt:24996

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.

The loopTimer excludes the time it takes to print its results (prt: 24996) from the loop time. As you can see it takes about 25.5mS just to print the loopTimer output to Serial. So each time the loopTimer prints, the loop() takes 25mS longer to run. You should always remove the loopTimer once you have completed your testing. As we will see below just added debugging print statements, using Serial, can seriously delay the rest of your loop()

Step 3: 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)

10033
loop uS Latency
 5sec max:5007284 avg:5007284
 sofar max:5007284 avg:5007284 max - prt:24992
15072

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

14032
loop uS Latency
 5sec max:7000284 avg:7000284
 sofar max:7000284 avg:7000284 max - prt:24992
21065

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

Step 4: 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

25000
loop uS Latency
 5sec max:7276 avg:12
 sofar max:7276 avg:12 max - prt:15512

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.

Step 5: Buffering Print Output - Avoid Serial

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 to Arduino Serial

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
loop uS Latency
 5sec max:62396 avg:12
 sofar max:62396 avg:12 max - prt:10436
The built-in board led, pin 13, is being turned ON
40072

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

What is happening? Well the Serial.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 83 bytes long so every 5sec it fills the buffer and the “led ON” “led OFF” message is blocked waiting for 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 Serial.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.

The simple fix is to NOT use any Serial statements in your loop() code, use BufferedOutput instead.

BufferedOutput class from the SafeString library, can be used to avoid blocking the loop() code due to prints(..) by providing a larger buffer to print to and also discarding any excess chars so that the loop() is not delayed waiting for the Serial port. It actually runs as another task, called from the nextByteOut() call. See Arduino Serial I/O for the Real World for a complete tutorial.

Install the SafeString library (V2.0.5+) from the Arduino library manager, which contains the BufferedOutput class and then run the BufferedPrintTime_Blink.ino example. The print(..) statements don't block and with an extra 80 byte buffer, no output is discarded. The bufferedOut is connected to the Serial and thereafter the sketch prints to the bufferedOut. At the top of the loop() a call to bufferedOut.nextByteOut() is added to release a buffered characters as there is space in the Serial Tx buffer. This is like running another background task releasing buffered characters to Serial.

#include <loopTimer.h>
// install the loopTimer library from https://www.forward.com.au/pfod/ArduinoProgramming/RealTimeArduino/loopTimer.zip
// loopTimer.h also needs the millisDelay library installed from https://www.forward.com.au/pfod/ArduinoProgramming/TimingDelaysInArduino.html
#include <BufferedOutput.h>
// install SafeString library from Library manager or from https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html
// to get BufferedOutput. See https://www.forward.com.au/pfod/ArduinoProgramming/Serial_IO/index.html for a full tutorial
// on Arduino Serial I/O that Works
#include <millisDelay.h>

//Example of using BufferedOutput to release bytes when there is space in the Serial Tx buffer, extra buffer size 130
createBufferedOutput(bufferedOut, 80, DROP_UNTIL_EMPTY);

int led = 13;
// Pin 13 has an led connected on most Arduino boards.
bool ledOn = false; // keep track of the led state
millisDelay ledDelay;
millisDelay printDelay;

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(9600);
  for (int i = 10; i > 0; i--) {
    Serial.println(i);
    delay(500);
  }
  bufferedOut.connect(Serial);  // connect buffered stream to Serial

  // initialize digital pin led as an output.
  pinMode(led, 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
    bufferedOut.print("The built-in board led, pin 13, is being turned "); bufferedOut.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
    bufferedOut.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() {
  bufferedOut.nextByteOut(); // call at least once per loop to release chars
  loopTimer.check(bufferedOut); // send loop timer output to the bufferedOut
  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 OFF
25010
loop uS Latency
 5sec max:848 avg:20
 sofar max:848 avg:20 max - prt:1256
The built-in board led, pin 13, is being turned ON

Now the loop() is running every 0.8mS. Of course if you add more print( ) statements then eventually you will exceed the BufferedOutput buffer capacity. In that case some of the output will be discarded to avoid blocking the other loop() code. See Arduino Serial I/O for the Real World for a complete tutorial on how to control that.

Step 6: 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. You can use the low level read() and available() Serial methods to avoid delays, but the coding is tricky and it is easy to make mistakes handling the resulting data. The SafeString library provides high level functions that are easy to use and safe from coding error that will cause your Arduino to reboot. See Arduino Text I/O for the Real World for a complete tutorial

The next example, Input_Blink_Tasks.ino,the Serial baud rate has been increased to 115200 as recommended by Arduino Text I/O for the Real World an a SafeStringReader used to read user commands. It 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.

SafeString library provides the non-blocking SafeStringReader class that looks for text separated by one of the specified delimiters. You can also specify a non-blocking timeout to return the last token if there is not a delimiter at the end of the input. Unlike the Serial readUntil() methods, SafeStringReader.read() does not block the rest of the loop while waiting for input or for the timeout to expire. Only a couple of small SafeStrings are needed to read even very long inputs and it is easy to change the commands and add more

createSafeStringReader(sfReader, 15, " ,\r\n"); // create a SafeString reader with max Cmd Len 15 and delimiters space, comma, Carrage return and Newline
void setup() {
  Serial.begin(115200);
 . . . 
  bufferedOut.connect(Serial);  // connect bufferedOut to Serial
  sfReader.connect(bufferedOut);
  sfReader.echoOn(); // echo goes out via bufferedOut
  sfReader.setTimeout(100); // set 100mS == 0.1sec non-blocking timeout
 . . . 
}

The task to collect user input is

void processUserInput() {
  if (sfReader.read()) { // echo input and 100mS timeout, non-blocking set in setup()
    if (sfReader == "start") {
      handleStartCmd();
    } else if (sfReader == "stop") {
      handleStopCmd();
    } else {
      bufferedOut.println(" !! Invalid command: ");
    }
  } // else no delimited input
}

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 Input_Blink_Tasks.ino loop() is now

void loop() {
  bufferedOut.nextByteOut(); // call this one or more times each loop() to release buffered chars
  loopTimer.check(bufferedOut);
  processUserInput();
  blinkLed13(stopBlinking); // call the method to blink the led
  print_mS(); // print the time
}

A sample of the output from Input_Blink_Tasks.ino is below. The loop() time is ~0.6mS even while waiting for the user input to timeout if there is no space or comma or CR or NL

To control the Led Blinking, enter either stop or start
 . . .
stop  Blinking Stopped
15010
loop uS Latency
 5sec max:708 avg:69
 sofar max:708 avg:69 max - prt:1676

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.

Step 7: 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() {
  bufferedOut.nextByteOut(); // call this one or more times each loop() to release buffered chars
  loopTimer.check(bufferedOut);
  processUserInput();
  blinkLed7(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
    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 blink if 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 this output. Note the commands are startTemps and stopTemps

 Temp reading stopped<br>loop uS Latency<br> 5sec max:708 avg:62<br> sofar max:712 avg:62 max - prt:1676<br>startTemp<br> Start Temp Readings<br>loop uS Latency<br> 5sec max:252948 avg:75<br> sofar max:252948 avg:75 max - prt:1976<br> Temp:0.00

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, 252mS. 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.

Step 8: 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(). This library also add some 1mS SPI timing delays to ensure reliable
operation of the MAX31856 when used with fast processors. The MAX31856_noDelay library also supports having multiple thermocouples and other SPI devices connect to the same SPI bus. See Multiple Thermocouples below.

The modified library, MAX31956_noDelay, is available here.

To use the modified noDelay library, we need to start a reading and then come back 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 ~1.2mS while taking temperature readings.

startTemp<br> Start Temp Readings<br>Temp:0.00<br>loop uS Latency<br> 5sec max:1408 avg:254<br> sofar max:1408 avg:254 max - prt:1872

Step 9: Using Multiple Thermocouples

The MAX31856_noDelay library supports multiple thermocouples wired to the same SPI bus but with different CS pins. Here the second MAX31856 uses pin 9 for its CS pin.

The sketch, dual_MAX31856.ino, in the MAX31856_noDelay examples directory, shows how to define and setup two or more thermocouple boards. The first board is defined as before

// Use software SPI: CS, DI, DO, CLK
MAX31856_noDelay maxthermo = MAX31856_noDelay(10, 11, 12, 13);
// use hardware SPI, just pass in the CS pin
//MAX31856_noDelay maxthermo = MAX31856_noDelay(10);

The second board only needs to have a CS pin specified as it always uses the same SPI settings as the first board defined

 // create the second thermocouple object controlled by CS pin 9
MAX31856_noDelay maxthermo2 = MAX31856_noDelay(9); // NOTE: this still uses software SPI set by maxthermo above
// the SPI settings are set by the first call to MAX31856_noDelay(..) and ignored by any subsequent calls

In setup() call begin() on both boards first, BEFORE calling any of the get/set methods. The begin() method sets the SPI (if not already set) and disables the CS pin. Then the first call to a get/set method on each board will set its default setting to those set at the top of the MAX_noDelay.cpp file. You can then override the ones you want to change.

void setup() {
… 
  // call both begin() first before any other calls.
  maxthermo.begin();  
  maxthermo2.begin();  // begin second board
  // SPI interface is only started once by the first call to begin()
  // but each begin() set the CS line for that MAX31856

  // the defaults at the top of MAX31856_noDelay.cpp are set on the first call to any on of the library methods if resetDefaults() not called here
  maxthermo.setThermocoupleType(MAX31856_TCTYPE_K);  // this is the defaults for thermocouple 1
… 
}

Step 10: 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 instructable 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() {<br>  bufferedOut.nextByteOut(); // call this one or more times each loop() to release buffered chars<br>  loopTimer.check(bufferedOut);<br>  processUserInput();<br>  blinkLed7(closeDampler); // call the method to blink the led<br>  printTemp(); // print the temp<br>  int rtn = readTemp(); // check for errors here<br>  setDamperPosition();<br>  runStepper();<br>}

Running the FirstDamperControl.ino sketch and inputting run 66.5 from the Arduino IDE monitor, gives the following timings

Temp:66.50<br>Position current:2096 Damper running<br>loop uS Latency<br> 5sec max:2608 avg:791<br> sofar max:2608 avg:791 max – prt:1580

The loop() runs on average every 0.8mS. So the average maximum stepper motor speed can exceed 1000 steps/sec. However the maximum loop() time is ~2.5mS, so some times runStepper() is only called that often. A good guess would be that this is due to the print statements in the printTemp() method. We can test that by just commenting out the print statements in that methods.

void printTemp() {
  if (printDelay.justFinished()) {
    printDelay.repeat(); // start delay again without drift
      // removed all the print()s
  } // else nothing to do this call just return, quickly
}

The output is then

loop uS Latency<br> 5sec max:960 avg:784<br> sofar max:1220 avg:784 max – prt:1596

This confirms that the print() statements are the major source of the maximum loop() time. The max so far time, 1.2mS, occurs when there is a user input.

So we can say that it is only once every 5 seconds that the stepper motor's maximum speed will drop from >1000 steps/sec to ~400 steps/sec. Depending on the application this may be acceptable or it may be noticeable.

In this tutorial we are aiming for a maximum speed of 1000 steps/sec consistently so we will continue to make changes to get the max interval between calls to runStepper() to <1mS. The way to do this is to add more calls to runStepper() through out the code. Since we are now focusing on the time between runStepper() calls we move the loopTimer from the loop() into the runStepper() method to measure there.

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

Also since we have determined that the printTemp() method is the major source of the slowness, we will add extra calls to runStepper() between the print statements in that method.

void printTemp() {
  if (printDelay.justFinished()) {
    printDelay.repeat(); // start delay again without drift
  runStepper(); // <<<< extra call here
    bufferedOut.print(F("Temp:")); bufferedOut.println(simulatedTempReading);
  runStepper(); // <<<< extra call here
    bufferedOut.print(F("Position current:")); bufferedOut.print(stepper.currentPosition());
  runStepper(); // <<<< extra call here
    if (closeDampler) {
      bufferedOut.println(F(" Close Damper"));
    } else {
      bufferedOut.println(F(" Damper running"));
    }
  runStepper(); // <<<< extra call here
  } // else nothing to do this call just return, quickly
}

The output from the resulting sketch, FinalDamperControl.ino, achieves the 1000 steps/sec consistently.

Temp:66.50<br>Position current:3325 Damper running<br>loop uS Latency<br> 5sec max:844 avg:764<br> sofar max:1240 avg:815 max - prt:1824

Remember that the loopTimer.check(bufferedOut); needs to be commented out once testing is complete as its print statements add an extra 1.5mS every 5 seconds

Adding more extra calls to runStepper() does just allow us to read the 1000 steps/sec, but there is nothing left over for any more I/O or calculations on the UNO, with a 16Mhz clock. To do better we need to use a faster processor. The ESP32's clock is 80Mhz, so lets try it.

Step 11: 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:66.50<br>Position current:3325 Damper running<br>loop uS Latency<br> 5sec max:62 avg:43<br> sofar max:279 avg:43 max - prt:121<br>

So running on an ESP32, there is no problem achieving 1000 steps/sec for the stepper motor. Actually even the
FirstDamperControl.ino sketch can run at 1000 steps/sec consistently because of the faster ESP32 clock speed.

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 Android 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 bufferedOut is now connected to the SerialBT stream at a baud rate of 115200 and the buffer size increased to 180 to all for the missing Serial Tx buffer. 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.

The Serial Monitor displays the loopTimer output

loop uS Latency<br> 5sec max:407 avg:44<br> sofar max:407 avg:44 max – prt:129<br>

and the Bluetooth Terminal handles the commands and displays the damper position

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.

Step 12: 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? Using ESP32 FreeRTOS is not as straight forward as “simple multi-tasking”.

The Arduino ESP32's FreeRTOS scheduler is configured with preemption enabled. However if you have tasks of different priorities then the lower priority tasks will never run if you not add a delay() or vTaskDelay() in all the higher priority tasks. This makes the system look like a cooperative multi-tasking system when you have tasks with different priority levels, 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 instructable 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, and using the SafeString library to buffer output and get 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.

Be the First to Share

    Recommendations

    • 3D Printed Student Design Challenge

      3D Printed Student Design Challenge
    • Hour of Code Speed Challenge

      Hour of Code Speed Challenge
    • Cookie Speed Challenge

      Cookie Speed Challenge

    32 Comments

    0
    Xylopyrographer
    Xylopyrographer

    Question 6 months ago

    Hi. Great post! Thank-you.
    In Step 11, running on an ESP32 board it states:

    "The loop() code runs on core 1, leaving core 0 free to run the communication code."
    Question: How is it known that loop() is running on core 1 (the default core for Arduino IDE sketches on an ESP32) and that the communications code is running on for 0?
    Or do you mean that, although the ESP32DamperControl.ino sketch only runs on core 1, it could be altered to set the communications code to run on core 0?
    Or did I miss the point completely? :)

    0
    drmpf
    drmpf

    Answer 6 months ago

    Short answer, by default the Arduino loop() runs on core1 and the wifi/ble runs on core 0

    Long answer
    In ..\Arduino15\packages\esp32\hardware\esp32\1.0.4\tools\sdk\include\config\sdkconfig.h
    you will find
    #define CONFIG_ARDUINO_RUNNING_CORE 1
    and
    #define CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0 1

    Then in ..\Arduino15\packages\esp32\hardware\esp32\1.0.4\cores\esp32\main.cpp
    xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
    which creates the Arduino 'loopTask' on core 1

    Then in ..\Arduino15\packages\esp32\hardware\esp32\1.0.4\tools\sdk\include\esp32\esp_wifi.h

    #if CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1
    #define WIFI_TASK_CORE_ID 1
    #else
    #define WIFI_TASK_CORE_ID 0
    #endif
    which sets WIFI_TASK_CORE_ID 0 because CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0 is defined NOT CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1
    and further down
    .wifi_task_core_id = WIFI_TASK_CORE_ID

    Which is then passed to wifiLowLevelInit( ) which calls esp_wifi_init( )


    0
    Xylopyrographer
    Xylopyrographer

    Reply 6 months ago

    Well. Isn't that interesting! Thank-you for the very detailed reply. Very much appreciated.

    0
    drmpf
    drmpf

    Tip 9 months ago

    To un-install loopTimer and millisDelay, (after installing the SafeString library) just delete their directories in the libraries directory, i.e. delete the directories
    ../Arduino/libraries/loopTimer
    and
    .../Arduino/libaries/millisDelay

    0
    mirobaldo
    mirobaldo

    Tip 9 months ago

    Uninstall loopTimer and millisDelay library if you have them before SafeString

    0
    drmpf
    drmpf

    Reply 9 months ago

    Yes, they are in the SafeString library, to un-install loopTimer and millisDelay, just delete their directories in the libraries, i.e. delete
    .../libraries/loopTimer and .../libaries/millisDelay

    0
    drmpf
    drmpf

    Tip 10 months ago

    Update 6th Jan 2021 – loopTimer class now part of the SafeString library (V3+) install it from Arduino Library manager or from its zip file

    0
    queenidog
    queenidog

    11 months ago on Step 12

    Awesome guide! gotta bookmark this one.

    0
    MarcJ281
    MarcJ281

    1 year ago

    Great instructable. It is obvious that you have a deep understanding of programming languages and that you are an excellent teacher. Thank you, I will certainly use some of the knowledge learned here in future projects.

    0
    drmpf
    drmpf

    Reply 1 year ago

    Thanks. Keep an eye out for the SafeStrings tutorial I am working on at the moment.

    0
    nusnus
    nusnus

    1 year ago

    Wow!! Really awesome guide! I definately need to test your methods.
    I have made a project with similar multitasking and the code was super fast. The key was to make an interrupt that increases a counter. In the ISR is a byte and each bit represents a chosen timeslot. So if 1 ms is bit 0, then bit 0 will be toggled every 1 ms and so on.

    0
    drmpf
    drmpf

    Reply 1 year ago

    Would you like to private message me with some sample code?

    0
    nusnus
    nusnus

    Reply 1 year ago

    Sure I can do it. When I started thinking about the project, it also uses a state machine. The idea sounded simple in my head but became quite clumsy and hard to write and maintain.
    Here is a video of the device working:
    https://www.youtube.com/watch?v=oA64Dk9Zfik

    0
    drmpf
    drmpf

    Tip 2 years ago

    Update 21st Nov 2019 - Added comparison examples for Arduino_FreeRTOS and frt compared to Simple Multi-tasking in Arduino

    0
    jc508
    jc508

    Question 2 years ago

    Thanks for this and the most excellent commenting style!
    You have helped solidify my thoughts and it turns out I was heading along the path you have already travelled. But I would like to pick your brain on some methods I have not yet managed to integrate into this way of thinking.

    My project is a toy 'robot' that can wander around. It can take commands from its Raspberry Pi master via Mqtt (or from a human via InfraRed remote).
    In its 'wait-and-do-nothing' state the maain loop() is down to about 2ms.

    The 'delays' I cant seem to redesign are those deep down. In my case the worst is when the servo holding the ultrasonic ranging has to 'look-around' moving this servo over its full distance takes 600ms.
    The worst case is if the thing gets boxed in in which case it currently looks Foward, Left, Right to work out what the best direction is.
    These are tinker toy servos, they can move to an angle/position but they dont give any feedback when they get there so the best you can do is give them time.

    So, ideally, there is a series of sequential steps that it would be nice to fire off one by one with a delay/timer telling me where I am up to - and all managed from the main loop()
    The call stack for this worst case is currently....
    C_eyes_servo_delay_time = 550 /// full sweep is 600ms
    void doAvoidanceCycle ()
    ..look_at_angle (C_Fangle);
    ..directionn = suggest_direction ();
    ...int suggest_direction ()
    ......Frange = find_range_at (C_Fangle);
    ......Lrange = find_range_at (C_Langle);
    ......Rrange = find_range_at (C_Rangle);
    ........int find_range_at (int angle)
    ..........look_at_angle(angle, C_eyes_servo_delay_time);
    ..........range= eyes.Ranging(CM); // this call takes average of 20ms
    ..........void look_at_angle (int angle_to_look, int pause_time)
    ............ServoEyes.write (angle_to_look);
    ............millis_delay(pause_time); // wait for the servo motor to get there

    Sorry for such a long dump but thanks for any help or advice.
    JC

    0
    drmpf
    drmpf

    Answer 2 years ago

    You seem to have the right idea.

    Well, in these cases you often need to turn the code inside out.
    That is bring the low level calls up to the top level and use a 'state' variable to keep track of where you are up to.
    Multitasking is about doing a little bit of a lot of things in sequence so that is appears a lot of things are happening at the same time.

    Handling the Avoidance will get messy so ideally you would put all the code in its own files Avoidance.cpp and Advoidance.h files in the same directory as your robot sketch .ino file.
    Here I will just assume all the code is in the main .ino sketch and all the variables are globals.

    copy this code the the IDE and reformat it.

    // Create some states to keep track of what Avoidance is doing
    // this would be a good place to use C enums
    // but for simplicity/clarity, I am using the equivalent ints
    // the possible states
    const int AvoidanceIdle = 0;
    const int AvoidanceStart = 1;
    const int AvoidanceHaveResult = 2;
    const int AvoidanceRight = 3;
    const int AvoidanceFront = 4;
    const int AvoidanceLeft = 5;

    int AvoidanceState = AvoidanceIdle; // state variable initially idle

    millisDelay servoMovementDelay;
    unsigned long SERVO_MOVEMENT_DELAY_MS = 600;

    void startAvoidance() { // set the start state
    if (AvoidanceState != AvoidanceIdle) {
    return; // already running
    } // else
    AvoidanceState == AvoidanceStart;
    }

    void startServoMove(int angle_to_look) {
    ServoEyes.write (angle_to_look); // start servo
    servoMovementDelay.start(SERVO_MOVEMENT_DELAY_MS); // start delay to tell us when it is finished
    // if you remember the last angle of the servo you can use that
    // to reduce the delay needed here based on angle to move
    }

    int getRange() {
    int range = eyes.Ranging(CM); // this call takes average of 20ms
    // see if you can change library to start call and come back
    // here later to get results
    // would need another AvoidanceState number to keep track of this
    return range;
    }

    int calculateNewDirection() {
    // ...
    return newDirection;
    }

    void goInNewDirection() {
    // change direction
    }

    void doAvoidanceCycle() {
    if (AvoidanceState == AvoidanceIdle) {
    return; // nothing to do return quickly
    }
    // else what are we up to
    // you could use a switch statement here particularly if using enums
    // but if else works as well
    if (AvoidanceState == AvoidanceStart) {
    // start finding range Front
    AvoidanceState = AvoidanceFront;
    startServoMove(C_Fangle); // start servo
    return; // nothing to do until servo get there
    } else if (AvoidanceState == AvoidanceFront) {
    if (servoMovementDelay.justFinished()) {
    rangeFront = getRange(); // this call takes average of 20ms
    // try next direction
    AvoidanceState = AvoidanceLeft;
    startServoMove(C_Langle); // start servo
    return; // nothing to do until servo get there
    } else {
    return; // still waiting for servo
    }
    } else if (AvoidanceState == AvoidanceLeft) {
    if (servoMovementDelay.justFinished()) {
    rangeLeft = getRange(); // this call takes average of 20ms
    // try next direction
    AvoidanceState = AvoidanceRight;
    startServoMove(C_Rangle); // start servo
    // if you remember the last angle of the servo you can use that
    // to reduce the delay needed here based on angle to move
    return; // nothing to do until servo get there
    } else {
    return; // still waiting for servo
    }
    } else if (AvoidanceState == AvoidanceRight) {
    if (servoMovementDelay.justFinished()) {
    rangeRight = getRange(); // this call takes average of 20ms
    newDirection = calculateNewDirection();
    AvoidanceState = AvoidanceHaveResult;
    return; // main loop picks up newDirection and resets state to Idle
    } else {
    return; // still waiting for servo
    }
    }
    // else should not get here!!
    }

    //The main loop() would be
    void loop() {
    doAvoidanceCycle(); // call this every loop to check on millisDelay
    if (AvoidanceState == AvoidanceHaveResult) { // last call got new result
    // pick up new direction
    newDirection = newDirection;
    AvoidanceState = AvoidanceIdle; // go back to idle
    goInNewDirection();
    }
    if (stuck) { // need to start avoidance again
    startAvoidance();
    }
    }

    0
    jc508
    jc508

    Reply 2 years ago

    wow thanks for fleshing out what came to me in a dream last night! It is about turning it inside out and using states to 'click through' what was sequential code. I will go through your detail today.
    Its funny how things you learnt, or at lest were exposed to, decades ago can come back in a helpful way :)

    0
    rafununu
    rafununu

    2 years ago

    That's quite interesting, but not multitasking which implies the idea of executing different tasks in the same time, not sequencially as you do, and me as well. Multitasking needs several processors or cores. You present a clever way to manage tasks by mainly using subroutines. The time management needs a deep knowledge of the microcontroller and its interrupts.

    0
    drmpf
    drmpf

    Reply 2 years ago

    Unfortunately your definition of multi-tasking is not accurate, you only need one core.
    see https://en.wikipedia.org/wiki/Computer_multitaskin...
    "Multitasking does not require parallel execution of multiple tasks at exactly the same time."

    The time management needs a deep knowledge of the microcontroller and its interrupts
    .The aim of this instructable was to avoid having a deep knowledge and to avoid completely using interrupts.
    The loopTimer is used here to gives you the insight you need to develop a practical, useful program.
    As the title says this is 'simple' multi-tasking which avoids the complexities and board limitations of an RTOS system.

    0
    rafununu
    rafununu

    Reply 2 years ago

    So I'm wrong about the definition of multitasking, noone's perfect ! But that's the way I see it, parallel not sequential. My words weren't a critic but congrats indeed.