Multitâche — FreeRTOS#

← Web · Programme · Protocoles →

L'ESP32 tourne sous FreeRTOS, un système temps réel. Même un programme Arduino « classique » est déjà multitâche :

Tâche Cœur Rôle
Wi-Fi, Bluetooth, TCP/IP 0 pile réseau
loopTask 1 appelle setup() puis loop() en boucle
IDLE0, IDLE1 0 et 1 quand rien d'autre ne tourne (et chien de garde)

On peut créer ses propres tâches : une boucle de mesure à fréquence fixe, une tâche réseau, une animation de LEDs… qui tournent en parallèle, sans millis() ni machine à états.

delay() sur ESP32 = vTaskDelay() : la tâche s'endort et laisse tourner les autres. Une boucle qui ne rend jamais la main (while (true) {} sans pause) déclenche le chien de garde et redémarre la carte.


Créer des tâches#

Les exemples de cette page utilisent le même montage : capteur sur GPIO 4, LED sur GPIO 5, bouton sur GPIO 6.

Montage

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

// Une tâche = une fonction qui ne se termine jamais, avec sa propre pile
void blinkTask(void* param) {
  int periodMs = (int) (intptr_t) param;          // paramètre passé à la création
  pinMode(5, OUTPUT);
  for (;;) {
    digitalWrite(5, !digitalRead(5));
    vTaskDelay(pdMS_TO_TICKS(periodMs));          // pause SANS bloquer les autres tâches
  }
}

void measureTask(void* param) {
  for (;;) {
    Serial.printf("[cœur %d] capteur = %d\n", xPortGetCoreID(), analogRead(4));
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

void setup() {
  Serial.begin(115200);
  //                      function     name       stack  param           priority  handle  core
  xTaskCreatePinnedToCore(blinkTask,   "blink",   2048,  (void*) 250,    1,        NULL,   1);
  xTaskCreatePinnedToCore(measureTask, "measure", 4096,  NULL,           1,        NULL,   0);
}

void loop() {
  // loop() est elle-même une tâche (cœur 1, priorité 1)
  Serial.printf("[cœur %d] loop, RAM libre %d\n", xPortGetCoreID(), ESP.getFreeHeap());
  delay(2000);                                   // delay() = vTaskDelay() : les autres tâches tournent
}

Files d'attente : passer des données entre tâches#

#include <Arduino.h>

struct Sample {
  uint32_t time;
  int value;
};

QueueHandle_t queue;   // file d'attente thread-safe entre tâches

// Producteur : mesure très régulièrement (1 kHz)
void sensorTask(void*) {
  TickType_t lastWake = xTaskGetTickCount();
  for (;;) {
    Sample sample = {millis(), analogRead(4)};
    xQueueSend(queue, &sample, 0);                         // 0 : ne pas attendre si la file est pleine
    vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(1));      // période fixe, même si le code prend du temps
  }
}

// Consommateur : traite les mesures quand elles arrivent
void processTask(void*) {
  Sample sample;
  int maxValue = 0;
  uint32_t start = millis();
  for (;;) {
    if (xQueueReceive(queue, &sample, portMAX_DELAY)) {    // attend (sans consommer de CPU) un élément
      maxValue = max(maxValue, sample.value);
      if (sample.time - start >= 1000) {                 // résumé une fois par seconde
        Serial.printf("max sur 1 s : %d (éléments en attente : %d)\n", maxValue, uxQueueMessagesWaiting(queue));
        maxValue = 0;
        start = sample.time;
      }
    }
  }
}

void setup() {
  Serial.begin(115200);
  queue = xQueueCreate(64, sizeof(Sample));
  xTaskCreatePinnedToCore(sensorTask, "sensor", 2048, NULL, 3, NULL, 1);       // priorité haute
  xTaskCreatePinnedToCore(processTask, "process", 4096, NULL, 1, NULL, 0);
}

void loop() {
  vTaskDelete(NULL);   // loop() ne sert plus : on supprime sa tâche
}

Mutex : protéger des données partagées#

#include <Arduino.h>

// Données partagées entre deux tâches + un mutex pour les protéger
struct State {
  int x, y;
} state;
SemaphoreHandle_t lock;

void writerTask(void*) {
  for (int i = 0;; i++) {
    xSemaphoreTake(lock, portMAX_DELAY);   // section critique : personne d'autre ne touche à "state"
    state.x = i;
    state.y = -i;                             // x et y restent cohérents
    xSemaphoreGive(lock);
    vTaskDelay(1);
  }
}

void setup() {
  Serial.begin(115200);
  lock = xSemaphoreCreateMutex();
  xTaskCreatePinnedToCore(writerTask, "writer", 2048, NULL, 1, NULL, 0);
}

void loop() {
  xSemaphoreTake(lock, portMAX_DELAY);
  State snapshot = state;                         // on copie vite, puis on libère
  xSemaphoreGive(lock);

  if (snapshot.x != -snapshot.y) Serial.println("incohérence !");   // n'arrive jamais grâce au mutex
  Serial.printf("x=%d y=%d\n", snapshot.x, snapshot.y);
  delay(500);
}

ISR → notification de tâche#

Le schéma classique : l'interruption ne fait que signaler, une tâche fait le travail.

#include <Arduino.h>

#define BUTTON_PIN 6
TaskHandle_t buttonTaskHandle;

// L'ISR ne fait presque rien : elle réveille la tâche
void IRAM_ATTR onEdge() {
  BaseType_t higherPriorityWoken = pdFALSE;
  vTaskNotifyGiveFromISR(buttonTaskHandle, &higherPriorityWoken);
  portYIELD_FROM_ISR(higherPriorityWoken);
}

// Le vrai travail (anti-rebond, Serial, réseau…) se fait ici, hors interruption
void buttonTask(void*) {
  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);   // dort jusqu'à la prochaine notification
    vTaskDelay(pdMS_TO_TICKS(30));             // anti-rebond
    if (digitalRead(BUTTON_PIN) == LOW) {
      Serial.printf("appui à %lu ms\n", millis());
    }
    ulTaskNotifyTake(pdTRUE, 0);               // ignore les rebonds arrivés pendant l'attente
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  xTaskCreatePinnedToCore(buttonTask, "button", 3072, NULL, 2, &buttonTaskHandle, 1);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onEdge, FALLING);
}

void loop() {
  vTaskDelete(NULL);
}

Minuteries logicielles (xTimerCreate)#

Pour exécuter une fonction plus tard ou à intervalle régulier, sans créer de tâche ni gérer millis() à la main :

#include <Arduino.h>

#define LED_PIN    5
#define BUTTON_PIN 6

TimerHandle_t heartbeatTimer;   // périodique : fait clignoter la LED intégrée
TimerHandle_t lightTimer;       // une seule fois : éteint la LED 3 s après le dernier appui

// Les callbacks tournent dans la tâche « timer service » de FreeRTOS :
// courts, et jamais bloquants (pas de delay, pas d'attente sur une file pleine)
void onHeartbeat(TimerHandle_t timer) {
  static bool on = false;
  on = !on;
  neopixelWrite(48, 0, on ? 30 : 0, 0);
}

void onLightTimeout(TimerHandle_t timer) {
  digitalWrite(LED_PIN, LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  //                        name         period                 auto-reload  id    callback
  heartbeatTimer = xTimerCreate("heartbeat", pdMS_TO_TICKS(500),  pdTRUE,      NULL, onHeartbeat);
  lightTimer     = xTimerCreate("light",     pdMS_TO_TICKS(3000), pdFALSE,     NULL, onLightTimeout);

  xTimerStart(heartbeatTimer, 0);
}

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    digitalWrite(LED_PIN, HIGH);
    xTimerReset(lightTimer, 0);       // (re)lance le compte à rebours : 3 s après le DERNIER appui
  }

  // Changer la période à la volée : clignotement rapide tant que le bouton est appuyé
  TickType_t period = digitalRead(BUTTON_PIN) == LOW ? pdMS_TO_TICKS(100) : pdMS_TO_TICKS(500);
  if (xTimerGetPeriod(heartbeatTimer) != period) xTimerChangePeriod(heartbeatTimer, period, 0);

  delay(20);
}

Aide-mémoire#

Besoin Fonction
attendre vTaskDelay(pdMS_TO_TICKS(ms))
période fixe vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(ms))
arrêter / supprimer une tâche vTaskSuspend(handle) / vTaskDelete(handle) (NULL = soi-même)
minuterie logicielle xTimerCreate("name", pdMS_TO_TICKS(ms), pdTRUE, NULL, callback) puis xTimerStart (exemple)
liste des tâches et CPU vTaskList / vTaskGetRunTimeStats (options à activer dans la config FreeRTOS)
Éditer