Recommended way of printing to serial port from tasks on ESP32?

I’m using FreeRTOS for the first time. I’m using v10.4.3 with an ESP32 devboard in PlatformIO with the Arduino framework. I’ve got plenty of experience of writing multi tasking code for embedded systems using a few other proprietry schedulers.

I tried using vPrintLine(“text”), which seems to be the standard in the latest version of “Mastering the FreeRTOS Real Time Kernel” (sorry new user can’t include links). That gives a compiler error due to not being able to find the function’s declaration. Searching all the FreeRTOS header files also doesn’t find “vPrintLine”.

Is that because vPrintLine() is not included in the ESP32 build of free RTOS, because the doc I refer to is for a later version of free RTOS, or some other reason?

I’m currently using the normal Arduino Serial.print()/println().

I want to provide exclusive access to Serial.print()/println() to prevent one task’s partial output being merged with the output from another task.

I first tried putting Serial.print()s in critical sections using taskENTER_CRITICAL()/taskEXIT_CRITICAL(), but that caused the ESP32 to continually reboot, due to an unhandled exception, after a small amount of serial output.

I suppose it could have been a watchdog timer timing out, but at 112500 baud, none of the Serial.print()s should take much time. Can you tell me why this didn’t work?

My current solution, which seems to work fine using a semaphore, is as shown in the following code (it was the same code that kept rebooting when I used critical sections, before replacing them with the semaphore method).

#include <Arduino.h>

void vTask1( void * pvParameters );
void vTask2( void * pvParameters );

SemaphoreHandle_t serialSem;

void setup() {
  Serial.begin(115200);
  Serial.println("\nFreeRTOS Experiment 1");
  Serial.print("FreeRTOS Version = ");
  Serial.println(tskKERNEL_VERSION_NUMBER);
  serialSem = xSemaphoreCreateMutex();
  if (serialSem==NULL){
    Serial.println("xSemaphoreCreateMutex returned NULL");
    while(1);
  }
  BaseType_t result;
  result = xTaskCreate(vTask1,"Task1",1000,NULL,1,NULL);
  xSemaphoreTake(serialSem, portMAX_DELAY);
    Serial.print("xTaskCreate(vTask1...) returned ");
    Serial.println(result==pdPASS ? "pdPASS" : "pdFAIL");
  xSemaphoreGive(serialSem);
  result = xTaskCreate(vTask2,"Task2",1000,NULL,1,NULL);
  xSemaphoreTake(serialSem, portMAX_DELAY);
    Serial.print("xTaskCreate(vTask2...) returned ");
    Serial.println(result==pdPASS ? "pdPASS" : "pdFAIL");
  xSemaphoreGive(serialSem);
}

void loop() {
}

void vTask1(void * pvParameters){
  while(1){
    xSemaphoreTake(serialSem, portMAX_DELAY);
      Serial.println("Task 1 is running");
    xSemaphoreGive(serialSem);  
    vTaskDelay(1000);
  }
}

void vTask2(void * pvParameters){
  while(1){
    xSemaphoreTake(serialSem, portMAX_DELAY);
      Serial.println("Task 2 is running");
    xSemaphoreGive(serialSem);
    vTaskDelay(1000);
  }
}

Serial monitor output:

FreeRTOS Experiment 1
FreeRTOS Version = V10.4.3
xTaskCreate(vTask1...) returned pdPASS
Task 1 is running
xTaskCreate(vTask2...) returned pdPASS
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running

Is the above code a recommended way of doing it? Is there a better way?

It’s a bit clumsy. I suppose I could wrap calls to Serial.print()/println() and include the semaphore take / give in the wrapper.

Hi @DaveLowther

  1. vPrintLine is not an official FreeRTOS API function. Functions like vPrintString and vPrintLine( example ) are custom implementations used in FreeRTOS educational materials and are not included in the standard FreeRTOS headers or libraries. If you need to use this API, you will to define it in your application.
  2. The issue you faced with taskENTER_CRITICAL() / taskEXIT_CRITICAL() on the ESP32 is due to the way FreeRTOS critical sections work versus semaphores. taskENTER_CRITICAL() completely disable interrupts. The ESP32 uses a UART driver that relies on interrupts to send data. Disabling interrupts halts the UART buffer preventing transmission. If too much data accumulates, it causes a buffer overflow or a watchdog timeout which might be causing the reboot. A mutex only blocks the task that takes the mutex until it is available, but other interrupts and background system tasks keep running .
  3. Your current semaphore approach is fine, but if logging gets heavy, switch to a dedicated Logger Task with a Queue. Instead of each task printing directly, tasks can send messages to a dedicated “Logger Task” via a FreeRTOS queue.
1 Like

Thank you for that information. It answers what I didn’t understand about the behaviour.