Implementing FreeRTOS Solutions on ESP 32 Devices using Arduino

Tutorial Lesson 2 of 4

Tom Wilson
17 min readAug 20, 2024

In the previous lesson we built the breadboard circuit used for the tutorials and tested it with a simple program downloaded using the Arduino IDE. This time we’re going to use the same circuit but modify the program to utilize FreeRTOS techniques. Before jumping in let’s discuss the FreeRTOS approach to real time programming.

FreeRTOS Programming Overview

A typical C ++ program written in the Arduino IDE consists of three main parts:

  • A header section that contains definitions, declarations, and included libraries.
  • A setup section that initializes variables and program objects, and starts services such as console connections, web services, etc. It also includes any code that is only executed once.
  • A loop section that runs in a continuous fashion until the program is completed and the execution stops.

The programmer defines the execution path that the processor(s) take, using conditional branches to guide the direction of execution. There is typically one program running at a time, although multiple threads can be defined to effect multiple processing branches running simultaneously.

In the FreeRTOS environment the approach is to define multiple code threads (called tasks) that can be run independently and let the operating system determine which task(s) to execute at any given time based on the task priorities and current states of the tasks. The heart of the system is the task scheduler which controls the execution flow. In FreeRTOS as implemented in the Arduino IDE, this scheduler uses a “non-cooperative preemptive round robin” scheduling algorithm to assign time slices of processing to the various tasks based on their assigned priorities and readiness for execution. At the start of each time slice (1 millisecond by default) the system determines which task should be executed and then swaps the current running task for a different one if necessary. Each task has an assigned area of heap memory for its use and a Task Control Block (TCB) area which contains information about it’s current processing state when last executed. Process switching from one task to another then involves moving the program pointer to the TCB of the new task and pointing to the memory area of the new task. But what in the world does “non-cooperative preemptive round robin” mean?

  • Non-cooperative means the scheduler can swap out a task without getting permission from it. Tasks cannot refuse to relinquish processing, but they can be assigned (programmatically) a higher priority to improve their liklihood of staying in execution if they are performing critical functions at the time. For highly critical processing needs there are other ways to prevent swapping, but they are not covered in this tutorial .
  • Preemptive means that certain events such as high priority interrupts and semaphore state changes can cause the process swapping to occur prior to the completion of a time slice.
  • Round-robin means that if multiple tasks of the same priority are ready to run at the start of a time slice, the system will swap between them at each time slice to balance the processing between tasks. This increases the operating system overhead somewhat, but since context swapping can be done quickly it is generally considered a good tradeoff.

There are other variations of FreeRTOS operation that can be configured and also other real-time operating systems that provide different capabilities, but these are the dafault features as implemented in Arduino IDE and ESP 32 hardware, and they work well for most hobbyist needs.

At any given time, each task will be in one of four states, and the operating systems keeps track of each one. At the begining of each time slice the states are updated by the OS. The states are:

  • RUNNING — the task (or tasks in multiprocessing systems) currently executing.
  • READY — available to continue processing whenever a processor is available.
  • BLOCKED — not ready to run. The task may be waiting for a resource to become available, waiting for a timer event, or waiting for an interupt or queue message.
  • SUSPENDED — programatically placed in a wait state.

There are many situations where the execution of tasks need to be coordinated and timed to occur in the right sequence. Timers, semaphores, mutexes, queues, and interrupts are commonly used for this purpose, and they will be explored in later lessons. Today we will look at creating and executing multiple tasks, and sharing information between those tasks.

Program Overview

In this lesson we will create several tasks as described below:

  • Blinker Task — to cycle an LED on and off. We will create a generic function and pass it a struct variable containing the GPIO pin, cycle time, time multiplier, and current status values. The function will use common Arduino code to cycle the LED and update the status of the LED. Since the struct is defined as a global variable, we will be able to read and modify the values from other tasks. We then create three tasks using this one function definition to control the red, green, and blue LED lights independently.
  • Tally Task — to display the current number of LEDs lit at any given time. This task will read the status values from the three tasks above and display the total number that are lit on the OLED display. We will set it to update the display every 0.1 seconds.
  • Highwater Task — this will display the amount of free space for each running task on the serial console. We will set it to run once 5 seconds after startup and then suspend itself from further processing. (More on this later.)
  • Speedup Task — increase the cycle speed 50% each time the button is pushed. This will illustrate a simple interupt system — it only runs when the button is pushed.

As we go through the program description below you be able to see how these pieces fit together, so let’s get started.

Program Code

Open up a new project in your Arduino IDE and you will be able to copy and paste the code below as we progress. For more information about the FreeRTOS functions that are being used, please refer back to the manual previously cited and available here. Here is the first section of code:

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 3 - define task functions
void TaskBlink(void *pvParameters);
void TaskTally(void *pvParameters);
void TaskSpeedup(void *pvParameters);
void TaskHighWater(void *pvParameters);


// 4 - define global variables for led status
int greenLED = 0; // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED; //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

In the section shown above we are initializing and defining a number of things.

  1. We are defining FreeRTOS settings for the ESP 32. Most ESP 32 boards include two process cores running in a symmetric multiprocessor mode. When we create tasks we can specify which core to run on.
  2. Set up parameters for the OLED display and include the necessary libraries. This is standard Arduino code.
  3. Create function prototypes for the functions that will be used to create tasks. All functions must have a VOID return argument and a generic pointer to the data being passed to it. One other key point — functions must never return to the calling program. They run in an endless loop until they are deleted programatically.
  4. Here we are defining global variables that can be read or updated from multiple tasks. There are variables to contain the status of each LED (i.e greenLED) as well as variables to contain pointers to the LED status (i.e. *greenPtr). By passing the pointer to the LED status we can allow the tasks to read and update the values. We also define a float variable called speedMult to allow us to change the LED cycle time.

Next section of code:

// 5 - define structure for data to pass to each task
struct BlinkData {
int pin;
int delay;
float *speedMult;
int *ptr;
};

// 6 - load up data for each LED task
static BlinkData blinkGreen = { 15, 2500, &speedMult, &greenLED };
static BlinkData blinkRed = { 4, 3300, &speedMult, &redLED };
static BlinkData blinkBlue = { 16, 1800, &speedMult, &blueLED };

// 7 - set up task handles for the RTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;

// 8 - define function for highwater mark
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

// 9 - define ISR routine to service button interrupt
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush()
{
vTaskResume(taskSpeed); // run the speed program once
}

5. Define a struct datatype called BlinkData to exchange information with the tasks. The GPIO pin number and initial delay are entered as values and the speed multiplier and status variable are copied by reference, as they will change dynamically as the program runs.

6. Since we are going to create three tasks using the TaskBlink function, we create separate BlinkData struct variables for each LED with the values to be used. Again note we are passing values for the pin and delay, and pointers for the speed multiplier and the status.

7. When FreeRTOS creates each task, the system returns a handle structure (defined by FreeRTOS) that can be used to refer to the task. We are creating the global TaskHandle_t variables to contain these task references.

8. Here is a function prototype for the FreeRTOS function used to get the highwater mark for a running task. The highwater mark describes the amount of memory (in bytes) that has been allocated to the task but never used. It can be used to adjust the allocation that is specified when the task is initialized. You will see how this is used later.

9. This is used to tell the system what to do when the button is pushed. It is a standard Arduino interrupt service request that will be linked to a GPIO pin in the next section of code

Next we are ready to start executing the startup code…


// the setup function runs once when you press reset or power the board
void setup() {

// 10 - initialize serial communication at 115200 bits per second:
Serial.begin(115200);

// 11 - setup button for interupt processing
pinMode(pinButton, INPUT_PULLUP);
attachInterrupt(pinButton, buttonPush, FALLING);

// 12 - Now set up tasks to run independently.
xTaskCreatePinnedToCore
(
TaskBlink, // name of function to invoke
"TaskBlinkGreen" , // label for the task
4096 , // This stack size can be checked & adjusted by reading the Stack Highwater
(void *)&blinkGreen, // pointer to global data area
2 , // Priority,
&taskGreen, //pointer to task handle
ARDUINO_RUNNING_CORE // run on the arduino process core
);

xTaskCreatePinnedToCore
(
TaskBlink,
"TaskBlinkRed",
4096,
(void *)&blinkRed,
2,
&taskRed,
ARDUINO_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskBlink,
"TaskBlinkBlue",
4096,
(void *)&blinkBlue,
2,
&taskBlue,
ARDUINO_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskTally,
"TaskTally",
4096,
NULL,
2,
&taskTally,
SYSTEM_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskSpeed,
"Speed",
4096,
NULL,
2,
&taskSpeed,
SYSTEM_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskHighWater,
"High Water Mark Display",
4096,
NULL,
2,
&taskHighwater,
SYSTEM_RUNNING_CORE
);

} // end of setup

10. Typical Arduino routine to start the serial console.

11. Here we are setting up the GPIO pin 23 to create an interrupt called buttonPush on a falling value. This will trigger the routine defined in step 9 above.

12. Finally, we are going to create our FreeRTOS tasks. We are using the FreeRTOS function xTaskCreatePinnedToCore which tells the system which processor core to run on. Alternatively, you can xTaskCreate which omits the last argument and allows the task to run on any available processor. The arguments passed to the task creation function include:

  • function to call to initiate the task
  • name to call the task (text)
  • memory allocation for the task (bytes). We can tweak this after reading the highwater mark on first execution.
  • variable passed to the function by reference (or NULL)
  • task priority (higher gets processed first)
  • task handle name
  • processor core to run on

Next is the most uninteresting section of code

  // Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.

void loop() {
// Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
// DO NOTHING HERE...

/*--------------------------------------------------*/
/*---------------------- Tasks ---------------------*/
/*--------------------------------------------------*/
}

At the end of the setup function, the FreeRTOS scheduler is automatically started if there are calls to FreeRTOS resources. The FreeRTOS function vTaskStartScheduler is invoked here by the system. Typically the void loop() function does nothing — all the execution occurs in the tasks. If you add code here it will run as part of the FreeRTOS IDLE task, which runs at the lowest priority.

Next we implement the task functions. Let’s look at each one:

TaskBlink

void TaskBlink(void *xStruct)  // This is a task template
{
BlinkData *data = (BlinkData *)xStruct; //passed data cast to BlinkData structur
// unpack data from the BlinkData structure passed by reference
int pin = data->pin;
int delay = data->delay;
float *speedMult = data->speedMult;
int *statePtr = data->ptr;

// set pinMode on output pin
pinMode(pin, OUTPUT);


while(true) // A Task shall never return or exit.
{
int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
digitalWrite(pin, HIGH); // turn the LED on (HIGH is the voltage level)
*statePtr = 1; // set the LED state to 1
vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for on cycle time
digitalWrite(pin, LOW); // turn the LED off by making the voltage LOW
*statePtr = 0; // set the LED state to 0
vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle
}
}

  1. In this function we are first unpacking the data passed by refererence in the BlinkData structure to the variables pin and delay, and the reference pointers to speedMult and the pin state pointer for the appropriate pin color (greenPtr, redPtr, bluePtr).
  2. Set the pin mode for the GPIO pin to output.
  3. Enter into a processing loop that never exits. In this loop we determine the current on/off delay time based on the initial setting and the current speed multiplier, turn the LED on and off , and change the pin state variable to 0 (off) or 1 (on).
  4. The delay between state changes is implemented using a FreeRTOS function called vTaskDelay which takes an argument that represents the number of time slices (ticks) to wait. We also use a function called pdMS_TO_TICKS that converts milliseconds to ticks. In this manner the delay time we enter correseponds to milliseconds regardless of the time slice setting of the Free RTOS installation.
  5. Note that the vTaskDelay function is a non-blocking delay. While this task is waiting it is moved to the BLOCKED status and other tasks can be scheduled.

TaskTally

void TaskTally(void *pvParameters)  // This is a task template
{
(void)pvParameters; // Not used anywhere - the input is NULL

// 1 - initialization
TickType_t xLastWaitTime; // define the last update time (in ticks)

// initialize xLastWaitTime
xLastWaitTime = xTaskGetTickCount();

// initialize the lcd display (once)
Adafruit_SSD1306 Display(OLED_RESET);
Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
Display.clearDisplay();
Display.display();

// 2 - display loop
while (true) // A Task shall never return or exit.
{
// calcualte numer of lamps lit from global variables
int numLit = redLED + greenLED + blueLED;

//display data on LCD display
Display.clearDisplay();
Display.setTextSize(1);
Display.setTextColor(WHITE);
Display.setCursor(40, 0);
Display.println("LEDs Lit");
Display.setTextSize(2);
Display.setTextColor(WHITE);
Display.setCursor(60, 15);
Display.println(String(numLit));
Display.display();
// short unblocked delay between readings (1/10 second)
xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
}
}
  1. The initialization section defines a variable called xLastWaitTime and initializes it with the current tick count. It also initializes the OLED display using typical Arduino calls.
  2. In a never-ending loop the function calculates the number of LEDS lit and updates the OLED display with this information. It then uses the xTaskDelayUntil function to set the next execution time to 0.1 seconds after the previous execution. Instead of a defining the wait time between function runs, like vTaskDelay, it sets the clock time difference between execution starts.

TaskHighWater

void TaskHighWater(void *pvParameters)
{
while (true) //run forever
{
vTaskDelay(pdMS_TO_TICKS(5000)); // wait 5 seconds so system is fully running
// display highwater marks for all 6 tasks
Serial.println("***************************");
Serial.print("High Water Mark for Green LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
Serial.print("High Water Mark for red LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskRed));
Serial.print("High Water Mark for Blue LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
Serial.print("High Water Mark for Tally : ");
Serial.println(uxTaskGetStackHighWaterMark(taskTally));
Serial.print("High Water Mark for Highwater Task: ");
Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
//Serial.print("High Water Mark for Speedup: ");
//Serial.println(uxTaskGetStackHighWaterMark(taskSpeedup));
Serial.flush(); // make sure last data is written

vTaskSuspend(NULL); // run this task only once
}
}

This task simply waits for 5 seconds (for all the tasks to start), reads and then prints the highwater statistics for each process to the serial console, and then suspends itself. We will modify this to run periodically in a later lesson.

TaskSpeed

void TaskSpeed(void *pvParameters)  // This is a task template
{
(void)pvParameters;
vTaskSuspend(NULL); // start in suspended state

while (true) // A Task shall never return or exit.
{
speedMult = 0.667 * speedMult; // increase speed by 50% by cutting cycle time by 1/3
Serial.println("Speed increased");
vTaskSuspend(NULL); // run this task only once
}
}

This task starts by suspending itself??? Yes — remember it is triggered by the interrupt service routine tied to the push button and we don’t want it to run until the button is pushed. When the button is pushed, it changes the task status to READY and it executes itself one time and then suspends itself again. When it does execute it simply reduces the wait time on all LED delays by 67% which is the same as increasing the cycle speed by 50%.

How do we decrease the speed? I guess we could add another button. Instead, however, in the next lesson we will modify the code to allow us to set the speed from an input command on the system console.

Complete Code Listing

Here is the complete code listing for Lesson 2. Load it into your board and see what happens. Also, save it as a new sketch called rtosLesson1. We will refer to it and modify it in future lessons.

/*********
This code was developed for use with the tutorial series entitled
"Implementing FreeRTOS Solutions on ESP 32 Devices using Arduino"

Refer to https://medium.com/@tomw3115/implementing-freertos-solutions-on-esp-32-devices-using-arduino-114e05f7138a for more information.

This software is provided as-is for hobbyist use and educational purposes only.

published by Tom Wilson - September 2024 *********/

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 3 - define task functions
void TaskBlink(void *pvParameters);
void TaskTally(void *pvParameters);
void TaskSpeedup(void *pvParameters);
void TaskHighWater(void *pvParameters);


// 4 - define global variable for led status
int greenLED = 0; // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED; //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

// 5 - define structure for data to pass to each task
struct BlinkData {
int pin;
int delay;
float *speedMult;
int *ptr;
};

// 6 - load up data for each LED task
static BlinkData blinkGreen = { 15, 2500, &speedMult, &greenLED };
static BlinkData blinkRed = { 4, 3300, &speedMult, &redLED };
static BlinkData blinkBlue = { 16, 1800, &speedMult, &blueLED };

// 7 - set up task handles for the RTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;

// 8 - define function for highwater mark
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

// 9 - define ISR routine to service button interrupt
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush()
{
vTaskResume(taskSpeed); // run the speed program once
}

// the setup function runs once when you press reset or power the board
void setup() {

// initialize serial communication at 115200 bits per second:
Serial.begin(115200);

// setup button for interupt processing
pinMode(pinButton, INPUT_PULLUP);
attachInterrupt(pinButton, buttonPush, FALLING);

// Now set up tasks to run independently.
xTaskCreatePinnedToCore
(
TaskBlink, // name of function to invoke
"TaskBlinkGreen" , // label for the task
4096 , // This stack size can be checked & adjusted by reading the Stack Highwater
(void *)&blinkGreen, // pointer to global data area
2 , // Priority,
&taskGreen, //pointer to task handle
ARDUINO_RUNNING_CORE // run on the arduino process core
);

xTaskCreatePinnedToCore
(
TaskBlink,
"TaskBlinkRed",
4096,
(void *)&blinkRed,
2,
&taskRed,
ARDUINO_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskBlink,
"TaskBlinkBlue",
4096,
(void *)&blinkBlue,
2,
&taskBlue,
ARDUINO_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskTally,
"TaskTally",
4096,
NULL,
2,
&taskTally,
SYSTEM_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskSpeed,
"Speed",
4096,
NULL,
2,
&taskSpeed,
SYSTEM_RUNNING_CORE
);

xTaskCreatePinnedToCore
(
TaskHighWater,
"High Water Mark Display",
4096,
NULL,
2,
&taskHighwater,
SYSTEM_RUNNING_CORE
);
} //end of setup

// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.

void loop() {
// Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
// DO NOTHING HERE...

/*--------------------------------------------------*/
/*---------------------- Tasks ---------------------*/
/*--------------------------------------------------*/
}
void TaskBlink(void *xStruct) // This is a task template
{
BlinkData *data = (BlinkData *)xStruct; //passed data cast as BlinkData structure
// unpack data from the BlinkData structure passed by reference
int pin = data->pin;
int delay = data->delay;
float *speedMult = data->speedMult;
int *statePtr = data->ptr;

// set pinMode on output pin
pinMode(pin, OUTPUT);


while(true) // A Task shall never return or exit.
{
int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
digitalWrite(pin, HIGH); // turn the LED on (HIGH is the voltage level)
*statePtr = 1; // set the LED state to 1
vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for on cycle time
digitalWrite(pin, LOW); // turn the LED off by making the voltage LOW
*statePtr = 0; // set the LED state to 0
vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle
}
}

void TaskTally(void *pvParameters) // This is a task template
{
(void)pvParameters;

TickType_t xLastWaitTime; //updated after first call by vTaskDelaUntil

// initialize xLastWaitTime
xLastWaitTime = xTaskGetTickCount();

// initialize the lcd display (once)
Adafruit_SSD1306 Display(OLED_RESET);
Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
Display.clearDisplay();
Display.display();

while (true) // A Task shall never return or exit.
{
// calcualte numer of lamps lit from global variables
int numLit = redLED + greenLED + blueLED;

//display data on LCD display
Display.clearDisplay();
Display.setTextSize(1);
Display.setTextColor(WHITE);
Display.setCursor(40, 0);
Display.println("LEDs Lit");
Display.setTextSize(2);
Display.setTextColor(WHITE);
Display.setCursor(60, 15);
Display.println(String(numLit));
Display.display();
// short unblocked delay between readings (1/10 second)
xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
}
}

void TaskHighWater(void *pvParameters)
{
while (true) //run forever
{
vTaskDelay(pdMS_TO_TICKS(5000)); // wait 5 seconds so system is fully running
// display highwater marks for all 6 tasks
Serial.println("***************************");
Serial.print("High Water Mark for Green LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
Serial.print("High Water Mark for Red LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskRed));
Serial.print("High Water Mark for Blue LED : ");
Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
Serial.print("High Water Mark for Tally : ");
Serial.println(uxTaskGetStackHighWaterMark(taskTally));
Serial.print("High Water Mark for Highwater Task: ");
Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
Serial.print("High Water Mark for Speed: ");
Serial.println(uxTaskGetStackHighWaterMark(taskSpeed));
Serial.flush(); // make sure last data is written

vTaskSuspend(NULL); // run this task only once
}
}

void TaskSpeed(void *pvParameters) // This is a task template
{
(void)pvParameters;
vTaskSuspend(NULL); // start in suspended state

while (true) // A Task shall never return or exit.
{
speedMult = 0.667 * speedMult; // increase speed by 50%
Serial.println("Speed increased");
vTaskSuspend(NULL); // run this task only once
}
}

Results

You should now see the three LED lights blinking on and off at different rates based on the settings we passed to the blinking tasks. The OLED display will show the number of LEDs lit at any given time. Each time you press the button the LEDs will begin to flash at a higher (by 50%) speed. If you push it enough times the OLED display will not keep up since it only samples once every 0.1 seconds. Here is what it should look like.

Lesson 2 Demonstration

Now open the serial monitor in the Arduino IDE and reboot the board. After 5 seconds you should see the high watermark listings similar to that shown below.

This is indicating how much free memory is available in each of the tasks. Since the code in these tasks doesn’t increase in memory requirements over time you should be able to reduce the memory allocation in the xTaskCreatePinnedToCore statements in the code. What happens if your initial guess at the memory allocation is too low and a task runs out of memory? Your board will reboot itself unexpectedly and repeatedly.

Conclusions

Once again we have a blinky LED board as our end result. But hopefully you haved learned some skills that can be used on more practical problems. Your tasks might read temperature sensors, reed switches, proximity sensors, image recognition, lidar, or any number of inputs. Your responses might control motors, lights, servos, displays, websockets, MQTT connections, or anything else that can run on an ESP 32. In the next lesson we will look at additional ways to control and synchronize the interactions between multiple tasks - specifically timers, semaphores, mutexes, and queues.

Ready for Lesson 3? Click here

--

--

Tom Wilson

I am a retired engineer living the Pura Vida lifestyle in a beachfront community along the Pacific coast of Costa Rica.