Home
| pfodApps/pfodDevices
| WebStringTemplates
| Java/J2EE
| Unix
| Torches
| Superannuation
|
| About
Us
|
Simple Multitasking Arduino on any board
|
by Matthew Ford 20th June 2021 (original 4th
September 2019)
© Forward Computing and Control Pty. Ltd. NSW
Australia
All rights reserved.
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 29th Dec 2020 – Revised
loopTimer to V1.1.1
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
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 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, 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 tutorial 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.
Also see Arduino For
Beginners – Next Steps
Taming
Arduino Strings
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
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 SafeString
library (V3+) from the Arduino Library Manager, it includes the
millisDelay class and the loopTimer class used here.
The
loopTimer library has the sketches used here in its examples
directory. Open Arduino's File
→ Examples → SafeString → 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
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 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.
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/Floessie/frt,
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 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:-
frt
reads the analog input every 17ms and uses 10988
bytes Flash program memory and 453 bytes RAM
(Blink_AnalogRead_frt.ino)
FreeRTOS reads
the analog input every 17ms and uses 9996 bytes Flash program memory
and 456 bytes RAM (Blink_AnalogRead_FreeRTOS.ino)
Simple
Multi-tasking in Arduino reads the
analog input every 0.1ms and uses 3822 bytes Flash program memory and
307 bytes RAM (Blink_AnalogRead_multitaksing.ino)
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 time 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 high speed 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.
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
Simple Multi-tasking versus ESP32 FreeRTOS
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. Those two classes are included in the SafeString V3+ library. Insert #include <loopTimer.h> at the top of the file and then add loopTimer.check(Serial); to the top of your loop() method.
#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()
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
}
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.
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_Tasks.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 blink delay(1000) are the problem here.
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.
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. See Arduino Serial I/O for the Real World for a complete tutorial.
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 (V3+) 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.
// install SafeString library from Library manager or from https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html // the loopTimer, BufferedOutput, SafeStringReader and millisDelay are all included in SafeString library V3+ #include <loopTimer.h> #include <millisDelay.h> #include <BufferedOutput.h> // See https://www.forward.com.au/pfod/ArduinoProgramming/Serial_IO/index.html for a full tutorial on Arduino Serial I/O that Works //Example of using BufferedOutput to release bytes when there is space in the Serial Tx buffer, extra buffer size 80 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.
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
In setup() connect the SafeStringReader to an input Stream and configure it.
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 <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 keeping the loop time to 708us (< 1ms)
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.
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) { bufferedOut.println(F("Temp reading stopped")); } else { bufferedOut.print(F("Temp:")); bufferedOut.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 loop us Latency 5sec max:708 avg:62 sofar max:712 avg:62 max - prt:1676 startTemp Start Temp Readings loop us Latency 5sec max:252948 avg:75 sofar max:252948 avg:75 max - prt:1976 Temp:0.00
As you can see before we start taking reading the loop() runs every 0.7ms. 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.
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, MAX31856_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.4ms while taking temperature readings.
startTemp Start Temp Readings Temp:0.00 loop us Latency 5sec max:1408 avg:254 sofar max:1408 avg:254 max - prt:1872
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 … }
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() { bufferedOut.nextByteOut(); // call this one or more times each loop() to release buffered chars loopTimer.check(bufferedOut); processUserInput(); 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 and inputting run 66.5 from the Arduino IDE monitor, gives the following timings
Temp:66.50
Position current:2096 Damper running
loop us Latency
5sec max:2608 avg:791
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 5sec max:960 avg:784 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 Position current:3325 Damper running loop us Latency 5sec max:844 avg:764 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.
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 Position current:3325 Damper running loop us Latency 5sec max:62 avg:43 sofar max:279 avg:43 max - prt:121
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 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 bufferedOut is now connected to the SerialBT stream at a baud rate of 115200 and a separate serialBufferedOut created to send the loopTimer output to the Serial connection. 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 5sec max:407 avg:44 sofar max:407 avg:44 max – prt:129
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.
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.
If you want to use FreeRTOS on the EPS32, search for Digikey's Introduction to RTOS series of 12 lessons with code examples.
In a preemptive RTOS systems, 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.
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, 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.
For use of the Arduino name see http://arduino.cc/en/Main/FAQ
The General Purpose Android/Arduino Control App.
pfodDevice™ and pfodApp™ are trade marks of Forward Computing and Control Pty. Ltd.
Contact Forward Computing and Control by
©Copyright 1996-2020 Forward Computing and Control Pty. Ltd.
ACN 003 669 994