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.
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
}
- Pile : en octets. Trop petite → plantage « stack overflow » (décodé par
esp32_exception_decoder). Pour mesurer :uxTaskGetStackHighWaterMark(NULL)renvoie la marge restante. - Priorité : plus le nombre est grand, plus la tâche est prioritaire. Une tâche prioritaire qui ne s'endort jamais affame les autres.
- Cœur :
0,1, outskNO_AFFINITY. Gardez le cœur 0 léger si le Wi-Fi est actif.
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
}
xQueueSend/xQueueReceivecopient les données : pas de variable partagée, pas de conflit.vTaskDelayUntilgarantit une période fixe (ici 1 kHz), contrairement àvTaskDelayqui attend « au moins » la durée donnée.- Depuis une interruption :
xQueueSendFromISR.
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);
}
- Sans mutex,
loop()pourrait lirexdéjà mis à jour etypas encore : données incohérentes, bugs aléatoires et difficiles à reproduire. - Gardez les sections critiques courtes : on copie, on libère, puis on travaille sur la copie.
- Pour une seule variable de 32 bits,
std::atomic<int>suffit. Serialest déjà protégé en interne, mais deux tâches qui écrivent en même temps mélangent leurs lignes.
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);
}
vTaskNotifyGiveFromISRest le mécanisme le plus léger de FreeRTOS pour réveiller une tâche (plus rapide qu'un sémaphore).portYIELD_FROM_ISR: si la tâche réveillée est prioritaire, elle s'exécute dès la sortie de l'interruption.
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);
}
auto-reload:pdTRUE= périodique,pdFALSE= une seule fois (one-shot).xTimerResetrelance le compte à rebours depuis zéro : parfait pour « éteindre 3 s après la dernière action », un délai d'inactivité, un anti-rebond.xTimerStop,xTimerChangePeriod,xTimerIsTimerActive; depuis une interruption :xTimerResetFromISR, etc.- Tous les callbacks partagent une seule tâche système : un callback lent retarde tous les autres. Pour un vrai travail, notifiez une tâche (voir ci-dessus).
- Le paramètre
idpermet de partager un même callback entre plusieurs minuteries (pvTimerGetTimerID(timer)).
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) |