Tutorial: ESP32 -- Non-Blocking and Concurrent Function Execution

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));
}

image

3 Likes

Nice to see more ESP32 dual core stuff… This is a well written enhancement of the concept, brought up earlier this year by one of our regulars

I think it is time for me to make use of my spare ESP32s again.

I ran your sketch… then belatedly looked at it when nothing much seemed to happen :stuck_out_tongue: I still think it is bad form to program with delay() of any significant duration as that simply shuts down the “thinking” and doesn’t really equate to real work in an example, sure one core thinks while another sleeps… but what about the alluded one core works hard, while another works harder… without stepping on each others toes?

BTW I do like the way you showed the pinning options in the example… I will have to invest more time to fully get my sleepy head around it.

But what I really like it for is the foundation of a real world example of controlling a stepper from Blynk, running on one core, with the stepper code on the 2nd… at least that is what I am challenging myself to make from your tutorial. It should be a nice upgrade from my old-skool dual MCU

I still think this… but I exchanged your sleep inducing 15 second delay with something the adds some visual feedback and actual work to the Blynk process.

Add in an LED Widget to each button (V4, V5, V6) and replace each delay(15000); with this (adjusting the vPin and for variables… I used x, y, z).

    for (int x = 0; x <= 15; x++) {
    Blynk.virtualWrite(V4, 255);
    delay(100);
    Blynk.virtualWrite(V4, 0);
    delay(900);
  }

I could have used timers, but I wanted to keep your basic concept of blocking.

Like them or not, delays and blocking hardware drivers are needed for some apps and also make coding simpler (e.g. no need to hack into 3rd-party libs with SimpleTimer insertions) and easier to read (using timers on a single LED blink would be hard to read). The good news is you can use delays freely in this configuration without blocking Blynk.

This would be the code for a non-blocking stepper motor (my 3 motors are in use, so have not tested it myself, but the pins are valid on ESP32). You control it with a “V0” slider (values from -3 to 3 – to select speed and direction) and also see simulated activity in a “V1” value display or over Serial out (both enabled by default). The QR is after the code.

#include <WiFi.h>
#include <BlynkSimpleEsp32.h>

#include <Stepper.h>

#define STEPS_PER_MOTOR_REVOLUTION 100

const int mtrPin1 =  23;
const int mtrPin2 =  22;
const int mtrPin3 =   1;
const int mtrPin4 =   3;

Stepper stepper(STEPS_PER_MOTOR_REVOLUTION, mtrPin1, mtrPin3, mtrPin2, mtrPin4);

//Blynk setup
char ssid[] = "";    //type your ssid
char pass[] = "";  //type your password
char auth[] = "";

int STEP_SPEED =  30;
int MAX_SPEED =  100;
int newDir = 0;        // -3 through 3 for direction and speed -- 0 means stop

BLYNK_WRITE(V0) {
  if (abs(param.asInt() * STEP_SPEED) <= MAX_SPEED) {  //ignore input that spins motor too fast
    newDir = param.asInt(); // newDir is the main parameter control (e.g. -3 is fast backwards)
    if (!(newDir == 0)) { //do not set speed to 0 because Stepper.h will choke
      stepper.setSpeed(STEP_SPEED * abs(newDir));
    }
  }
}

void stepperLoop(void *inVar) {   //this task loops forever and changes motor as newDir is updated
  Serial.println("Stepper Task Started");
  
  while (true) {
      if (!(newDir == 0)) {  // 0 means stop, so do not step
       stepper.step(newDir > 0 ? 10 : -10); //step 10 forward or backwards
      } else {
        vTaskDelay(200); //delay a bit if motor is stopped (for visual display purposes only)
      }
      
      showAction(); //will show motor direction & speed on serial or blynk value label
      vTaskDelay(10); //keep this to feed watchdog
  }
}

void blynkLoop(void *inVar ) {  // blynk task pinned to core 0
  Serial.println("Blynk Loop Task Started");
    
  while (true) {
      Blynk.run();
      vTaskDelay(random(1,10)); //feed the watchdog
    }
}

void showAction() {
  static const bool SERIAL_DISPLAY = true;
  static const bool BLYNK_DISPLAY = true;

  static unsigned int iterations = 1;

  if (iterations++ < 15) {
    return;
  }
  
  iterations = 0;
  
  String myTxt = "123";
  if (newDir <= -3) myTxt = "<<< ";
  if (newDir == -2) myTxt = "<<  ";
  if (newDir == -1) myTxt = "<   ";
  if (newDir ==  0) myTxt = " -  ";
  if (newDir ==  1) myTxt = ">   ";
  if (newDir ==  2) myTxt = ">>  ";
  if (newDir >=  3) myTxt = ">>> ";

  if (SERIAL_DISPLAY) { //print at 1/15th of motor speed
    Serial.println(myTxt);
    if (random(1,20) > 18) Serial.println(); //so you can see screen movement, even if whole output page is full of same char's
  }
  
  if (BLYNK_DISPLAY) { //flash at 1/15th of motor speed
    Blynk.virtualWrite(V1, myTxt);
    vTaskDelay(800);
    Blynk.virtualWrite(V1, " ");
  }
}

void setup() {
  Serial.begin(9600);
  
  Blynk.begin(auth, ssid, pass);  
  while (Blynk.connected() == false) {
  }
  Serial.println();
  Serial.println("Blynk Connected");

  Blynk.syncVirtual(V0); //set slider to last speed
  
  //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, "blynk", 100000, NULL, 1, NULL, 0);

  //this is where we start the stepper control loop pinned to core 1
  xTaskCreatePinnedToCore(stepperLoop, "stepper", 10000, NULL, 0, NULL, 1);
}

void loop() {
  delay(random(1,10));
}

image

Oh, I actually use short delays at times when it doesn’t matter… but in this forum we do try to teach Blynk embedded, approved and comparatively easy to learn timing methods, without limiting to special MCUs.

Not that an ESP32 dual core tutorial is bad, perhaps overtly advanced for some, and not even required in most use cases. Not talking running Blynk on a full blown CNC here (although might be possible) I don’t see that as Blynk’s true purview.

I will give it a shot with my setup… meanwhile, I have introduced a full stepper control example for either ESP32 or ESP8266 (would work on Arduino as well), with just a single timer.

Your last QR was incorrect. I guessed and setup a slider on V0 with settings -100 to 100 (EDIT - then I saw your notes on it :blush: ). Made some pin and rotational step changes for my Nema 17 servo, and finally got it to work in CCW direction -1 & -2 (jerky) only. As for the rest, probably just needs some pin tweaking to match my servo/controller.

Well, you have proven dual core works well, and nicely shown how :+1: And despite claims, I have repeatedly shown that one doesn’t need delay(), Blynk functions don’t block (bad code inside does) and BlynkTimer is reliable and effective… so Blynk users have plenty of choices. Carry on :slight_smile:

PS - Das Timed Blinkin’ LED

1 Like