Search code examples
esp32freertosplatformio

FreeRTOS on ESP32 - Task handling, priorities, interrupts


Disclaimer: I value everyone's time, appreciate this community, and do not expect people to write code for me. I'm looking for guidance on what's best to do based on my application.

I am having trouble with a project I am working on. The main hiccup is on how to make the best use of FreeRTOS.

I'm using PlatformIO on a FireBeetle 2 ESP32-E.

Attached is what I think being the most useful part of the code for what I am seeking help. I can share all .h and .cpp files if it helps.

What I want to achieve is:

  1. ReadSensorsTaskcode runs on a timer

    1. It shall never be interrupted if it's running
    2. It should wait for other tasks to complete if it has not started running yet.
  2. FanCleaningSEN55code runs when triggered by an ISR (touchpin on ESP32)

    1. It shall never be interrupted if it's running
    2. It should wait for other tasks to complete if it has not started running yet.
  3. UpdateRGBLEDTaskcode runs on a timer.

    1. It shall never be interrupted if it's running
    2. It should wait for other tasks to complete if it has not started running yet.
  4. ControlDisplayTaskcode runs when triggered by an ISR (touchpin on ESP32).

    1. It shall never be interrupted if it's running
    2. It should wait for other tasks to complete (other than UpdateRGBLED) if it has not started running yet.

I am not using Mutex, Semaphore, or any sort of queue because after many attempt, I have not figured out which one is the proper way. Binary Semaphore? Tasks are pinned to core. Is this ok? Any benefits on using both core? I plan to implement WiFi functionalities: will anything related to it "block" core 0?

Any feedback, of any kind, is much appreciated.

/************************************************************************************
 ********  FreeRTOS tasks
 **********************************************************************************/

#include "global_defs.h"
#include "leds.h"
#include "display.h"
#include "sensirionSCD41.h"
#include "sensirionSEN55.h"
#include "time.h"
#include "gsheet.h"
#include "tasks.h"

TaskHandle_t ReadSensorsTask;
TaskHandle_t FanCleaningSEN55Task;
TaskHandle_t UpdateRGBLEDTask;
TaskHandle_t ControlDisplayTask;

// Settings
static const TickType_t dim_delay = 5000 / portTICK_PERIOD_MS;

// Pins (change this if your Arduino board does not have LED_BUILTIN defined)
static const int led_pin = LED_BUILTIN;

// Globals
static TimerHandle_t one_shot_timer = NULL;

// ISR function
void IRAM_ATTR gotTouchSEN55cleaning(){
    // Resume tasks
    vTaskResume(FanCleaningSEN55Task);
}

// ISR function
void IRAM_ATTR gotTouchDisplayCarousel(){
    // Resume tasks
    buttonDisplayCount++;
    vTaskResume(ControlDisplayTask);
}

//  Triggered by TIMER
void ReadSensorsTaskcode( void * pvParameters ){

    /* DEBUG Inspect our own high water mark on entering the task. */
        //uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        
    for (;;) {
    
        // Run rightaway on first boot
        if (readingCounter == 0){
            const TickType_t xFrequency = (1000 / portTICK_PERIOD_MS);
            vTaskDelay(xFrequency );
        }
        else{
            const TickType_t xFrequency = (delayMinutesReadSensors * 60000 / portTICK_PERIOD_MS);
            vTaskDelay(xFrequency );
        }

        //vTaskSuspend(UpdateRGBLEDTask);
        
        Serial.println("** SCD41 and SEN55 Get Readings **");
        Serial.print("running on core ");
        Serial.println(xPortGetCoreID());

        lightFadeInPurpleLED();
        displayReading();
        getSCD41readings();
        getSEN55readings();
        if (readingSensorsDataflagOK == true) 
        {
            displayOKcircleThick();
        }
        else 
        {
            displayNGcircleThick();
        }
        readingSensorsDataflagOK = false;
        lightFadeOutPurpleLED();
        readingCounter++;

        // Show CO2 value if above dangerous levels
        if (co2 > dangerousValueCO2) 
        {
            displayCO2();
        }

        // Show latest sensors readings
        else
        {
            displayCO2();
            delay(3000);
            displayTempHumid();
        }

        Serial.print("Value set in memory for dangerous CO2: ");
        Serial.println(dangerousValueCO2);
        Serial.print("Value set in memory for high CO2: ");
        Serial.println(highValueCO2);
        Serial.print("Value set in memory for max CO2 gauge: ");
        Serial.println(maxco2);
        Serial.print("Value set in memory for max PM2.5 gauge: ");
        Serial.println(maxpm25);
        Serial.print("Value set in memory for max VOC gauge: ");
        Serial.println(maxVOC);

        readingCounter++;

        /* debug */
        //uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
        
    }
    
}

//  Called by ISR
void FanCleaningSEN55code( void * pvParameters ){

  for(;;){
    
    Serial.print("FanCleaningSEN55 running on core ");
    Serial.println(xPortGetCoreID());
    
    //---------------------------------------------------------------------
    //--- USER ACTION ----- SEN55 fan cleaning -----------------

    int touchValue1 = touchRead(touchPinSEN55cleaning);

    if (sen55initflagOK == true && touchValue1 < threshold);

    if (sen55initflagOK == true && touchValue1 < threshold) 
    {
            displaySEN55Cleaning();
            sen55FanCleaning();
            lightFadeInOutYellowLEDfast();
    }
    else if(sen55initflagOK == false && touchValue1 < threshold)
    {
        displaySEN55InitError();
        displayNGcircleThick();
        delay(2000);
    }
    
    // Suspend tasks
    vTaskSuspend(FanCleaningSEN55Task);
    
  }
}

//  Triggered by TIMER
void UpdateRGBLEDTaskcode( void * pvParameters ){

    for(;;){
    
        const TickType_t xFrequency = (delaySecondsCO2LED * 1000 / portTICK_PERIOD_MS);
        vTaskDelay(xFrequency );

            //Serial.println("** Update RGB LED color based on CO2 **");
            //Serial.print("running on core ");
            //Serial.println(xPortGetCoreID());
            //---------------------------------------------------------------------
            //--- ONBOARD LED #2 ----- Update based on CO2 readings -----------------
            if (useOnboardLEDUser == true)
            {
                if (co2 >= dangerousValueCO2) 
                {
                    lightFadeInOutRedLED();
                }
                else if (co2 >= highValueCO2) 
                {
                    lightFadeInOutOrangeLED();
                }
                else
                {
                    lightFadeInOutGreenLED();
                }
            }
            

    }
}

//  Called by ISR
void ControlDisplayTaskcode( void * pvParameters ){

    for(;;){
    
    Serial.print("ControlDisplay running on core ");
    Serial.println(xPortGetCoreID());
    Serial.println(buttonDisplayCount);

    int touchValue2 = touchRead(touchPinDisplayCarousel);
    
    if (buttonDisplayCount > 1 && touchValue2 < threshold) 
    {
            buttonDisplayCount = 0;
    }

    if (buttonDisplayCount == 0 && touchValue2 < threshold) 
    {
            displayTempHumid();
    }
    else if (buttonDisplayCount == 1 && touchValue2 < threshold) 
    {
            displayCO2();
    }
    
    // Suspend tasks
    vTaskSuspend(ControlDisplayTask);
    
    }

}


void initializeFreeRTOStasks() {

    xTaskCreatePinnedToCore(
                        ReadSensorsTaskcode,   /* Task function. */
                        "ReadSensorsTask",     /* name of task. */
                        10000,       /* Stack size of task */
                        NULL,        /* parameter of the task */
                        10,           /* priority of the task */
                        &ReadSensorsTask,     /* Task handle to keep track of created task */
                        1);          /* pin task to core 0 */                  
    delay(500);

    touchAttachInterrupt(touchPinSEN55cleaning, gotTouchSEN55cleaning, threshold);
    xTaskCreatePinnedToCore(
                        FanCleaningSEN55code,   /* Task function. */
                        "FanCleaningSEN55Task",     /* name of task. */
                        5000,       /* Stack size of task */
                        NULL,        /* parameter of the task */
                        3,           /* priority of the task */
                        &FanCleaningSEN55Task,     /* Task handle to keep track of created task */
                        1);          /* pin task to core 0 */                  
    vTaskSuspend(FanCleaningSEN55Task);
    delay(500);

    xTaskCreatePinnedToCore(
                        UpdateRGBLEDTaskcode,   /* Task function. */
                        "UpdateRGBLEDTask",     /* name of task. */
                        5000,       /* Stack size of task */
                        NULL,        /* parameter of the task */
                        8,           /* priority of the task */
                        &UpdateRGBLEDTask,     /* Task handle to keep track of created task */
                        1);          /* pin task to core 0 */                  
    delay(500);

    touchAttachInterrupt(touchPinDisplayCarousel, gotTouchDisplayCarousel, threshold);
    xTaskCreatePinnedToCore(
                        ControlDisplayTaskcode,   /* Task function. */
                        "ControlDisplayTask",     /* name of task. */
                        10000,       /* Stack size of task */
                        NULL,        /* parameter of the task */
                        3,           /* priority of the task */
                        &ControlDisplayTask,     /* Task handle to keep track of created task */
                        1);          /* pin task to core 0 */                  
    vTaskSuspend(ControlDisplayTask);
    delay(500);

}

I tried using priorities for each task, which does not work and mcu still panik sometimes. Would a simple yield() work for my application?


Solution

  • If we ignore requirement 4.2 for a moment then your wish basically amounts to simple cooperative multi-threading (i.e. a task cannot be pre-empted unless it voluntarily yields). FreeRTOS does fancy pre-emptive multithreading, meaning the scheduler will re-evaluate running tasks after every tick to see if some thread with equal or higher priority is ready to run. I would recommend the excellent FreeRTOS book, chapter 4 as reference.

    Anyway, you can easily simulate cooperative multithreading. If I think of the simplest solution to your problem, it's a single thread which processes events (interrupts or timer ticks) as they appear in the system. Each step 1-4 simply creates an event and pushes it to the queue of the processing thread.

    Conceptual solution, not compiled or tested:

    QueueHandle_t ev_queue; // Queue for sending events to worker thread
    TimerHandle_t timer;    // Timer for generating periodic events
    
    // Event definitions
    typedef enum {
        EV_READ_SENSORS,
        EV_FAN_CLEANING,
        EV_UPDATE_RGB_LED,
        EV_CONTROL_DISPLAY,
    } event_t;
    
    // ISR generates some event
    void IRAM_ATTR gotTouchSEN55cleaning() {
        event_t ev = EV_FAN_CLEANING;
        xQueueSendFromISR(ev_queue, &ev, NULL);
    }
    
    // ISR generates some other event
    void IRAM_ATTR gotTouchDisplayCarousel() {
        event_t ev = EV_CONTROL_DISPLAY;
        xQueueSendFromISR(ev_queue, &ev, NULL);
    }
    
    // Timer generates the periodic events
    void on_timer(TimerHandle_t timer_h) {
        event_t ev;
        if (TIME_TO_READ_SENSORS()) {
            ev = EV_READ_SENSORS;
            xQueueSend(ev_queue, &ev, 0);
        }
        if (TIME_TO_UPDATE_RGB_LED()) {
            ev = EV_UPDATE_RGB_LED;
            xQueueSend(ev_queue, &ev, 0);
        }
    }
    
    // Single task handles all events in order of appearance
    void worker(void* arg) {
        while (true) {
            event_t ev;
            if (xQueueReceive(ev_queue, &ev, portMAX_DELAY)) {
                switch (ev) {
                case EV_READ_SENSORS:
                    ReadSensors(); // Your code in here, no FreeRTOS task management needed
                    break;
                case EV_FAN_CLEANING:
                    FanCleaningSEN55();
                    break;
                case EV_UPDATE_RGB_LED:
                    UpdateRGBLED();
                    break;
                case EV_CONTROL_DISPLAY:
                    ControlDisplay();
                    break;
                }
            }
        }
    }
    
    void app_main() {
        ev_queue = xQueueCreate(10, sizeof(event_t));
        assert(ev_queue);
        BaseType_t ret = xTaskCreatePinnedToCore(worker, "Worker", 10000, NULL, 10, NULL, 1);
        assert(ret == pdPASS);
        timer = xTimerCreate("MyTimer", pdMS_TO_TICKS(100), pdTRUE, NULL, on_timer);
        assert(timer);
    }
               
    

    Requirement 4.2 "other than UpdateRGBLED" does not fit neatly into this simple model. I'll leave that nut for you to crack, because I didn't come up with a clear solution in 10 seconds :)

    It should work more or less real-time (I omitted pxHigherPriorityTaskWoken in ISR for simplicity) if there are no other tasks with priority 10 or higher running on the same core. If you plan to use WiFi or BLE then those tend to create high-priority tasks on core 0, in which case running your task on core 1 is not a bad idea. Otherwise core pinning makes no difference.