I’ve put the tutorial text into the block below to get proportional text. Let me know if there are any questions, comments or suggestions on improvement. Please also share your experience if you are using any of these techniques yourself!
This Tutorial will demonstrate some useful implimentations of FreeRTOS
on the ESP32.
(1) How to pin Blynk.run() to a specific core on ESP32 using the
built-in FreeRTOS library functions.
This is useful so that your code doesn't block the Device from
interfacing with the Blynk Server. Helpful if you need a long
delay() or you want to operating a stepping motor for more than
a couple seconds. To accomplish this, pin Blynk to core 0.
IN THE EXAMPLE: Feel the pain: Run Blynk on "Core 1" (same core
as your functions run - this is the default
configuration) then press "Called Function" button.
Try pushing it again - nothing is "heard" on the
device side, it is queued up at the Server because
Device is too busy to listen. SimpleTimer call is
no better because the workload and Blynk are operating
on Core 1.
Relief #1: Press the "FreeRTOS Button" a couple times.
Even while the blocking code is executing, the higher
priority Blynk.run() Task handles in-coming communications.
Relief #2: Select "Core 0" for Blynk. Now Blynk is not
blocked even by the SimpleTimer function workloads (as
they run on Core 1).
Note: If you select "Not Pinned" then Blynk.run() is
executing on both cores. This is buggy (hangs
occasionally) and I am not sure why yet. I attempted
to stagger Blynk.run() execution with random delay
times, but I think a semaphore is needed.
Note: A "Called Function" always blocks Blynk.run() because
it is always running on the same core.
(2) The difference between running workloads (triggered by blink events) to run
concurrently vs. consecutively.
The same built-in FreeRTOS lib has sophisticated interrupt and thread handling,
so you can control an otherwise blocking process or do other work concurrently.
Combined with the technique in (1) above, you can interrupt blocking workloads
using blynk buttons (e.g. "STOP the motor" button).
IN THE EXAMPLE: Pin Blynk to core 0, then hit the "FreeRTOS Task" button a few
times. The Tasks run concurrently and complete quickly. Try the
same with "SimpleTimer" button and queued functions line up (much
slower total execution time).
(3) Passing parameters to queued functions.
We know how to pop functions onto a SimpleTimer queue, but you cannot pass any
parameters without custom code and global arrays. When you create a "Task" with
FreeRTOS, you can pass it variables. So if a slider gets set to "150" then you
can send that data along to the Task AND have it execute on Core 1.
IN THE EXAMPLE: Notice xTaskCreatePinnedToCore passes along the "iteration" parameter.
See: https://www.freertos.org/Documentation/161204_Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
Cautions: When creating "Tasks", you'll need to estimate your function's heap requirement.
There are tools for getting this precise, but requires eduction and time to
test. I have simply made guesses and played with varying sizes to set the numbers
thus far myself.
Core 0 is used by ESP32 radio functions, so if you pin Blynk there, too, then
you must ensure any blocking code called by Blynk is moved to Core 1.
Pinning Blynk and using Tasks is not standard practice, so you will not have
much company and may not get any formal support.
Conclusions: If you have an application which has a long-running function or if you
need to speed-up functions that needlessly queue-up behind each other
then the ESP32 and FreeRHOS is worth investigating.
You will need to run the below code on an ESP32 and view serial output to see what is
happening. Find the app QR code below that for easy setup.
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
#include <SimpleTimer.h>
SimpleTimer timer;
//Blynk setup
char ssid[] = "SSID"; //type your ssid
char pass[] = "PWD"; //type your password
char auth[] = "Blynk Token";
bool BLYNK_ON_CORE_0 = true;
bool BLYNK_ON_CORE_1 = true;
BLYNK_WRITE(V0) { //Plain called function
if (param.asInt()) { //when button is pressed....
static unsigned int iteration = 1; //keep track of which instance
bool fStatus = false;
String myMsg = "C-Button - Core trigger: ";
myMsg += xPortGetCoreID();
Serial.println(myMsg);
fStatus = calledFunction(iteration); //call blocking function
if (!fStatus) { //if it didn't run
Serial.println("Function call failed -- C-Press Ignored");
}
}
}
BLYNK_WRITE(V1) { //SimpleTimer Queue
if (param.asInt()) { //when button is pressed....
int tStatus = 0; //needed to detect when SimpleTimer queue is full
tStatus = timer.setTimeout(1, queuedFunction); //add function to "work queue"
if ( tStatus != -1 ) { //if added to queue
String myMsg = "S-Button - Core trigger: ";
myMsg += xPortGetCoreID();
Serial.println(myMsg);
} else { //if queue was full and it was not added
String myMsg = "Queue at Max -- S-Press Ignored";
Serial.println(myMsg);
}
}
}
BLYNK_WRITE(V2) { //Launch a one-time FreeRTOS Task pinned to Core 1
if (param.asInt()) { // when button is pressed...
BaseType_t xStatus; //needed to capture FreeRTOS task creation return value
static unsigned int iteration = 1; //keep track which press number and pass this variable to "freeRTOSTask" Task
//this is a freeeRTOS task - ESP32 uses these libraries and we can use them too without any "includes"
xStatus = xTaskCreatePinnedToCore(
freeRTOSTask, /* Function to implement the task */
"FreeRTOS Task", /* Name of the task */
5000, /* Stack size in words */
(void *)iteration, /* Task input parameter */
0, /* Priority of the task */
NULL, /* Task handle. */
1); /* Core where the task should run */ //we'll run all "doWork" processing on Core 1
if ( xStatus == pdPASS ) { // task started OK
String myMsg = "F-Button - Core trigger: ";
myMsg += xPortGetCoreID(); //confirm which core this process is now running on
Serial.println(myMsg);
iteration++;
} else { //if we cannot allocate free memory from stack, then inform user
String myMsg = "Stack Full -- F-Press Ignored";
Serial.println(myMsg);
}
}
}
BLYNK_WRITE(V3) { //drop-down selection to select which core Blynk.run() should run on
int whichCores = param.asInt();
String myTxt = "Blynk pinned to: ";
switch (whichCores) {
case (1) : { //core 0
BLYNK_ON_CORE_0 = true;
BLYNK_ON_CORE_1 = false;
myTxt += "Core 0";
break;
}
case (2) : { //core 1
BLYNK_ON_CORE_0 = false;
BLYNK_ON_CORE_1 = true;
myTxt += "Core 1";
break;
}
case (3) : { //both - not pinned
BLYNK_ON_CORE_0 = true;
BLYNK_ON_CORE_1 = true;
myTxt += "Not pinned";
break;
}
default : {
BLYNK_ON_CORE_0 = true;
BLYNK_ON_CORE_1 = true;
myTxt += "Not pinned";
break;
}
}
Serial.println(myTxt); //confirm selection
}
bool calledFunction(unsigned int threadNo) { // just call a long-running function from BLYNK_WRITE
String workTxt = " C: ";
workTxt += threadNo;
workTxt += " STARTED";
Serial.println(workTxt);
delay(15000);
workTxt = " C: ";
workTxt += threadNo;
workTxt += " ENDED";
Serial.println(workTxt);
return(true);
}
void queuedFunction() { // run a new function using SimpleTimer queue
static unsigned int threadNo = 1;
String workTxt = " S: ";
workTxt += threadNo;
workTxt += " STARTED";
Serial.println(workTxt);
delay(15000);
workTxt = " S: ";
workTxt += threadNo;
workTxt += " ENDED";
Serial.println(workTxt);
threadNo++;
}
void freeRTOSTask(void *iteration) { // we can pass variables using FreeRTOS Tasks, unlike with SimpleTimer
unsigned int threadNo; // any variable type can be handled, but we need to cast it
threadNo = (unsigned int) iteration;
//the workload is simply to print START, delay, then print STOP
//*************************************************************
String workTxt = " F: ";
workTxt += threadNo;
workTxt += " STARTED";
Serial.println(workTxt);
delay(15000);
workTxt = " F: ";
workTxt += threadNo;
workTxt += " ENDED";
Serial.println(workTxt);
//**************************************************************
vTaskDelete( NULL ); //we must self-destruct this task explicitly
}
void blynkLoop(void *pvParameters ) { //task to be created by FreeRTOS and pinned to core 0
while (true) {
if (BLYNK_ON_CORE_0) { //if user selected core 1, then don't blynk here -- this is only for "core 0" blynking
Blynk.run();
}
vTaskDelay(random(1,10));
}
}
void setup() {
Serial.begin(9600);
Blynk.begin(auth, ssid, pass);
while (Blynk.connected() == false) {
}
Blynk.syncVirtual(V2); //retrieve last blynk core pinning selection
//this is where we start the Blynk.run() loop pinned to core 0, given priority "1" (which gives it thread priority over "0")
xTaskCreatePinnedToCore(
blynkLoop, /* Function to implement the task */
"blynk core 0", /* Name of the task */
100000, /* Stack size in words */
NULL, /* Task input parameter */
1, /* Priority of the task */
NULL, /* Task handle. */
0); /* Core where the task should run */
Serial.println("");
}
void loop() {
if (BLYNK_ON_CORE_1) { //ESP32 runs all threads in the main loop() on core 1 so it's internal radio work isn't blocked
Blynk.run();
}
timer.run(); // queued functions will all be run on core 1
delay(random(1,10));
}