Plantage esp32 : Task watchdog got triggered (résolu ?)

Bonsoir,

je me fais des nœuds au cerveau depuis cet après-midi sur un plantage que je ne comprend pas.

J'ai un ESP32 avec une acquisition de mesures, un serveur web et le wifi.

L'ESP32 fonctionne soit en client d'un réseau connu quand il est disponible et sinon, il bascule en accesspoint wifi.

J'ai une page principale index.html qui affiche les valeurs, des boutons de commande et des liens vers d'autres pages, ça fonctionne SAUF

  • pour la page qui affiche le graphique
    ET
  • quand la carte est en accesspoint (pas de pb en client wifi)

Je peux difficilement poster tout le code (7 fichiers et plus de 1000 lignes) mais j'ai pu localiser le pb

void serv_fichiers() {
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", String(), false, processor);
  });

// il y a d'autres fichiers mais ils sont OK

  // graphique
  server.on("/graph.html", HTTP_GET, [](AsyncWebServerRequest *request){
    CopieTableau();
    request->send(LittleFS, "/graph.html", String(), false, processor);   //// plantage
  });

  // d'autres fichiers
}

CopieTableau()̀ est la pour copier le tableau de mesure et laisser l'aquisition se faire dasn son coin pendant que l'on affiche le graphique.

void CopieTableau() {
  while (xSemaphoreTake(mutex, portMAX_DELAY) != pdTRUE);

  memcpy(CopieTabMesures, TabMesures, sizeof TabMesures);
  CopieIdxMesure = IdxMesure;
  xSemaphoreGive(mutex);
}

TabMesures et CopieTabMesures est le tableau de mesure et sa copie déclarés comme :

struct SDATA {
  float temp;
  long  tmillis;
  bool err;
};

SDATA TabMesures[3600];
SDATA CopieTabMesures[3600];

et IdxMesure est juste un entier (l'index de la dernière mesure).

Le plantage à l'air de se faire dans la copie du tableau.

La suite normale du processus : processor() formate les données pour les envoyer à la page et les traiter par un JS (et là je pense qu'on a quitté le problème)

String processor(const String& var){
    // optimiser la boucle pour réduire le nombre de points quand graphique est grand...
  int n = CopieIdxMesure/600 + 1; 
  
  if(var == "DATAS"){
    //return {x:241,y:-3.0}, etc...
    String donnees = "";
    char element[20] = "";
    for (int i = 0 ; i < CopieIdxMesure ; i = i+n) {
      sprintf((char*) element,  "{x:%lu,y:", CopieTabMesures[i].tmillis/1000);
      donnees = donnees + String(element);   
      dtostrf(CopieTabMesures[i].temp, 3, 1, element);
      donnees = donnees + String(element);   
      donnees = donnees + String("},");   
    }
    return donnees;
  }
// d'autres traitements sans intérêt ici...

Le message d'erreur est le suivant :

23:55:52.082 -> E (56582) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
23:55:52.082 -> E (56582) task_wdt:  - async_tcp (CPU 0)
23:55:52.082 -> E (56582) task_wdt: Tasks currently running:
23:55:52.082 -> E (56582) task_wdt: CPU 0: loopTask
23:55:52.113 -> E (56582) task_wdt: Aborting.
23:55:52.113 -> 
23:55:52.113 -> 
23:55:52.113 -> Core  0 register dump:

puis l'ESP32 reboote.
Je précise que le navigateur semble chercher la page graph.html quelques secondes avant le crash.

J'espère être assez clair et s'il faut des compléments j'essayerai de les apporter

Bonjour

Je pense que la ligne de commande suivante a été mal formulée :

while (xSemaphoreTake(mutex, portMAX_DELAY) != pdTRUE);

Il manque la liste des commandes à exécuter quand la condition du while sera vraie.

Cordialement.

Merci et bien vu…

Je suis sur téléphone donc difficile de tester quoi que ce soit mais j’ai fait un test de plus hier soir et il semble que le programme sorte quand même de copietableau()

Dès que possible je vous fais un retour

Correction

edit (provisoire) : le programme déroule même jusqu'au bout de server.on

En fait non, c'est plutôt entre 2 et 3 que ça plante (voir post 9)

  server.on("/graph.html", HTTP_GET, [](AsyncWebServerRequest *request){
    Serial.println("bug - 1  <-- jusque là tout va bien");
    CopieTableau();
    Serial.println("bug - 6  <-- jusque là tout va bien");
    request->send(LittleFS, "/graph.html", String(), false, processor);   //// plantage
    Serial.println("bug - 7  <-- jusque là tout va bien");
  });

Rien modifié pour le moment au while

Bonjour,
Je suis qu'un novice, mais cela ressemble beaucoup à l'erreur que j'ai rencontrée lorsque j'ai essayé d'exécuter le programme principal sur le cœur 0 de l'ESP32. (voir ici)
Avec l'ESP32, sur le cœur 0, il est nécessaire de faire une pause d'une milliseconde pour réinitialiser le watchdog.

Salutations, bonne journée.

Je n'avais pas trouvé ton post hier, mais la fin ne me plait pas trop

Ok pour faire un sleep(1) mais où ?

Je vais aussi essayer de remettre d'aplomb le while signalé par @amic

Il y a yield() pour ça. A placer dans un code bloquant pour rendre temporairement la main à FreeRTOS. ATTENTION, il faut prendre bloquant au sens de qui dure un peu trop longtemps et fait paniquer l'OS.

OK merci je vais étudier ça...

En attendnat je vais quand même poster les modifs et les résultats de mes tests...

Modification du while foireux :

//  copie des données 
void CopieTableau() {
  Serial.println("bug - 2  <-- jusque là tout va bien");
while (xSemaphoreTake(mutex, portMAX_DELAY) != pdTRUE) {
    // on attend le sémaphore
 Serial.println("bug - 3  <-- jusque là tout va bien");
  sleep(1);
  memcpy(CopieTabMesures, TabMesures, sizeof TabMesures);
 Serial.println("bug - 4  <-- jusque là tout va bien");
  CopieIdxMesure = IdxMesure;
  xSemaphoreGive(mutex);
    // on relâche le sémaphore
 Serial.println("bug - 5  <-- jusque là tout va bien");
  }
}

Le plantage n'est pas systématique, mais très fréquent et très souvent après l'étape 2, et le mouchard 3 n'est pas affiché... Est-ce que cela apprend qqchose du problème ?

(j'ai modifié le post 3 en conséquence)

J'ai mis le yield() juste après le while je comprend toujours pas comment ça marche mais est-ce que quelqu'un peut se pencher sur ce qui suit histoire de voir si je suis complètement à côté de la plaque ?

Ça plante toujours, mais j'ai eu, parmi d'autres, un plantage un peu différent des autres :

bug - 1 <-- jusque là tout va bien
bug - 2 <-- jusque là tout va bien
bug - 6 <-- jusque là tout va bien
bug - 7 <-- jusque là tout va bien

E (10497) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (10497) task_wdt: - async_tcp (CPU 0)
E (10497) task_wdt: Tasks currently running:
E (10497) task_wdt: CPU 0: loopTask
E (10497) task_wdt: Aborting.

Les mouchards sont passé de 2 à 6 directement, comme si tout ce qui est dans le while de CopieTableau(); n'avais pas été exécuté.

Est-ce que la ligne E (10497) task_wdt: - async_tcp (CPU 0) indique que c'est le thread 0 qui a planté ?

Est-ce que par hasard CopieTableau(); ne s'exécuterait pas dans l'autre thread ? Ceci expliquerai pourquoi le programme à l'air de ne pas planter toujours au même endroit.

Est-ce que l'on peu savoir sur quel thread fonctionne telle ou telle partie de code ?

Ensuite... bin je suis quand même paumé.

C'est là que je ne vois pas, aux alentours du plantage, ce qui peut durer trop longtemps.

La copie du tableau ? L'ajout de yield() n'a rien changé
processor() ? Je l'ai vidé, il ne fait plus rie et ça plante toujours

Essaie de faire la copie en plusieurs fois avec un yield() entre 2 copies

Merci, je vais essayer ça (pas forcement dans la journée, sinon ce soir)

Autre point, peut-être important auquel j'ai pensé cette nuit : les mesures sont faites avec la bib NonBlockingDallas.h qui est asynchrone aussi. J'ai déjà ESPAsyncWebServer.h...
Or, si j'ai bien compris (pas sûr du tout :wink: ) Asynchrone = thread (un de plus ?)

Est-ce que le problème peut venir de cette cohabitation ?

Pas sûr que yield() soit la bonne solution. Chez moi, ça n'a pas fonctionné. Raison donnée par @J-M-L

Perso, je n'avais pas trouvé d'autre solution que delay(1) pour réinitialiser le watchdog. Bonne chance et bonne journée.

1 Like

En passant, ne téléversez-vous pas votre programme sur le cœur 0 de votre ESP32?

Tiens, je n'ai pas le même menu... ? :thinking:

C'est pas la même carte ! l'ESP32_C3 Je n'ai jamais testé!

Quelques points sur lesquels il faudrait creuser (mais c'est le dernier le plus important)

SDATA occupe 12 octets car le compilateur aligne sur 32 bits. vous allouez donc 2 x 3600 x 12 = plus de 84 ko de RAM sur 160 max allouables de manière statique. Avez vous d'autres gros tableaux ?

votre fonction c'est

//  copie des données
void CopieTableau() {
  Serial.println("bug - 2  <-- jusque là tout va bien");
  while (xSemaphoreTake(mutex, portMAX_DELAY) != pdTRUE) {
    // on attend le sémaphore
    Serial.println("bug - 3  <-- jusque là tout va bien");
    sleep(1);
    memcpy(CopieTabMesures, TabMesures, sizeof TabMesures);
    Serial.println("bug - 4  <-- jusque là tout va bien");
    CopieIdxMesure = IdxMesure;
    xSemaphoreGive(mutex);
    // on relâche le sémaphore
    Serial.println("bug - 5  <-- jusque là tout va bien");
  }
}

Vous avez raison de dire que vous n'êtes pas rentré dans le while, puisque vous voyez le bug 2 et bug 5. Il faudrait creuser cela (comprendre votre configuration exacte et le reste du code).

Un point à mentionner c'est que vous ne pouvez pas faire confiance à ce que vous voyez dans le terminal série pou savoir où le code "s'est planté" si vous ne mettez pas un Serial.flush(); après chaque Serial.println() car l'impression du message est asynchrone et à 115200 bauds prends "super longtemps" par comparaison à la vitesse d'exécution du processeur. vous pouvez avoir 3 messages dans le buffer de sortie et le code est déjà 100 lignes plus loin dans votre code, voire dans un autre thread au moment du plantage.

Un autre point c'est que xSemaphoreTake(mutex, portMAX_DELAY) est bloquant (environ une semaine) puisque vous mettez comme temps d'attente portMAX_DELAY, à mon avis ce n'est pas la peine de mettre un while. Vous pourriez mettre

void CopieTableau() {
  if  (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) {
    Serial.println("obtenu le sémaphore"); Serial.flush();
    memcpy(CopieTabMesures, TabMesures, sizeof TabMesures);
    CopieIdxMesure = IdxMesure;
    xSemaphoreGive(mutex);
  } else {
    Serial.println("erreur, le sémaphore n'est pas dispo"); Serial.flush();
    CopieIdxMesure = 0; // erreur on n'a pas eu le mutex
  }
}

Ensuite vous avez une fonction processor hyper couteuse en mémoire et en temps... Imaginez que vous ayez CopieIdxMesure = 3600. Votre calcul de n donne 7 (vous prenez 1 point sur 7) soit environ 514 points.
à chaque tour de la boucle for()

    String donnees = "";
    char element[20] = "";
    for (int i = 0 ; i < CopieIdxMesure ; i = i+n) {
      sprintf((char*) element,  "{x:%lu,y:", CopieTabMesures[i].tmillis/1000);
      donnees = donnees + String(element);   
      dtostrf(CopieTabMesures[i].temp, 3, 1, element);
      donnees = donnees + String(element);   
      donnees = donnees + String("},");   
    }

quand vous demandez donnees = donnees + ... vous demandez à la classe String de trouver de plus en plus de mémoire ... En effet quand vous faites
donnees = donnees + String(element);
le compilateur doit fabriquer une String temporaire pour String(element) (allocation dynamique, risque dans le tas) puis comme vous voulez l'ajouter à donnees il doit trouver de la mémoire pour le contenu de donnees avec le contenu de la String temporaire sans toucher à données, puis il copie dans cet espace le contenu de données et de la String puis enfin il y a la réaffectation à données qui recopiera tout cela et donc une nouvelle demande d'allocation d'espace pour faire grandir la variable donnees. à ce moment là vous avez en mémoire

  • la copie originale de donnees
  • la concaténation de donnees avec la String temporaire element
  • la zone mémoire future pour donnees avec la String temporaire element

➜ plus vous bouclez, plus la taille de donnees augmente, plus vous augmentez le besoin en RAM, plus vous faites des mouvement en mémoire importants (et inutiles) et vous faites cela 3 fois par boucle for, avec le risque de morceler le tas...

C'est pour cela que l'on dit qu'il ne faut jamais utiliser la forme
donnees = donnees + String(element);
pour une concaténation mais plutôt
donnees += String(element);
qui aura l'avantage d'éviter deux des allocations intermédiaires.
De plus sprintf() sur ESP supporte le %f donc autant bâtir en une seule fois ce qu'il faut rajouter.

Il n'en reste pas moins que la String résultante donnees est très grande et comme la fonction la retourne par copie, il y a une nouvelle dernière duplication mémoire au moment de la fin de la fonction.

➜ peut-être avez aussi vous un souci mémoire. Vous devriez générer la réponse directement en ligne (est-ce que /graph.html est long ?)

Sinon concernant le watchdog (et c'est là à mon avis votre souci), c'est dans le processor que je mettrais un yield et que j'optimiserai la construction de la String (en espérant qu'il n'y ait pas de souci mémoire)

String processor(const String& var) {
  // optimiser la boucle pour réduire le nombre de points quand graphique est grand...
  int n = CopieIdxMesure / 600 + 1;

  if (var == "DATAS") {
    //return {x:241,y:-3.0}, etc...
    String donnees;
    char element[30];
    for (int i = 0 ; i < CopieIdxMesure ; i += n) {
      yield();
      int nb = snprintf(element, sizeof element, "{x:%lu,y:%.3f},", CopieTabMesures[i].tmillis / 1000ul, CopieTabMesures[i].temp); // https://cplusplus.com/reference/cstdio/snprintf/$0
      if (nb >= 0 && nb < sizeof element) {
        donnees += element;
      } else {
        Serial.println("erreur pas assez de place dans element");
        break;
      }
    }
    return donnees;
  }
  // d'autres traitements sans intérêt ici...

}

avec sprintf() et une seule concaténation de String "propre", la copie de 500 éléments devrait prendre environ 125ms.

testez ce code:

struct SDATA {
  float temp;
  long  tmillis;
  bool err;
};
SDATA CopieTabMesures[500] = {{3000, 123.456, false},};

String processor() {
  String donnees;
  char element[30];
  for (int i = 0 ; i < 500 ; i++) {
    yield();
    int nb = snprintf(element, sizeof element, "{x:%lu,y:%.3f},", CopieTabMesures[i].tmillis / 1000ul, CopieTabMesures[i].temp); // https://cplusplus.com/reference/cstdio/snprintf/$0
    if (nb >= 0 && nb < sizeof element) {
      donnees += element;
    } else {
      Serial.println("erreur pas assez de place dans element");
      break;
    }
  }
  return donnees;
}

void setup() {
  Serial.begin(115200);
  uint32_t t0 = micros();
  String resultat = processor();
  uint32_t t1 = micros();
  Serial.print("Temps de construction de la chaîne = "); Serial.print(t1 - t0); Serial.println(" µs.");
  Serial.print("Taille de la chaîne = "); Serial.print(resultat.length() + 1); Serial.println(" octets.");
}

void loop() {}

sur wokwi ça me dit

Temps de construction de la chaîne = 124990 µs.
Taille de la chaîne = 7004 octets.

si vous mettez votre code avec les copies de String a tout va

      sprintf((char*) element,  "{x:%lu,y:", CopieTabMesures[i].tmillis/1000);
      donnees = donnees + String(element);   
      dtostrf(CopieTabMesures[i].temp, 3, 1, element);
      donnees = donnees + String(element);   
      donnees = donnees + String("},");   

vous obtenez

Temps de construction de la chaîne = 6668798 µs.

➜ 6 ,6 secondes !!! c'est plus qu'il n'en faut pour déclencher le watchdog...

Merci pour cette réponse, je vais regarder ça. Ça m'énerve parce que je suis coincé en réunion toute la journée et je ne vais quand même pas sortir le matos devant la patronne, il parait que ça ne se fait pas :wink:

C'est vrai que j'ai lu de nombreuses fois que string n'est pas à utiliser à la légère...

Un complément par rapport à processor :

j'ai fait un test en mettant toute la partie à propos des données à passer au graphique :

  if(var == "DATAS"){
    //return {x:241,y:-3.0}, etc...
    String donnees = "";
    // char element[20] = "";
    // for (int i = 0 ; i < CopieIdxMesure ; i = i+n) {
    //   sprintf((char*) element,  "{x:%lu,y:", CopieTabMesures[i].tmillis/1000);
    //   donnees = donnees + String(element);   
    //   dtostrf(CopieTabMesures[i].temp, 3, 1, element);
    //   donnees = donnees + String(element);   
    //   donnees = donnees + String("},");   
    //}
    return donnees;
  }

// suit d'autres structures plus courtes if(var == "BLABLA"){} 

et ça plante de la même manière

OK faut alors regarder ailleurs aussi, la création et gestion du mutex par exemple.

je ne mettrais pas une attente super longue sur sa disponibilité, juste quelques ms parce que au pire vous le bloquez le temps d'écrire une entrée dans le tableau je suppose dans le core principal et mettre à jour le nombre d'éléments.

Bonsoir,

je m'y suis remis (saleté de contretemps) et j'ai essayé de faire la copie en deux temps

J'ai donc remplacé

memcpy(CopieTabMesures, TabMesures, sizeof TabMesures);

par

  memcpy(&CopieTabMesures[0], &TabMesures[0], int(sizeof(TabMesures)/2));
  yield();
  memcpy(&CopieTabMesures[sizeof(TabMesures)/2], &TabMesures[sizeof(TabMesures)/2], int(sizeof(TabMesures)/2));
  yield();

Comme je n'avais aucune idée de la manière de faire cette copie en deux temps, je me suis inspiré de

Ce qui change c'est que maintenant le plantage est systématique et le message d'erreur est différent, ce qui me laisse supposer que c'est ma correction de code qui est buggée :

bug - 2  <-- jusque là tout va bien

assert failed: vTaskPriorityDisinheritAfterTimeout tasks.c:5424 (pxTCB != pxCurrentTCB[ xPortGetCoreID() ])

Mais comme je ne suis pas du tout à l'aise avec les pointeurs je ne suis pas sûr de comprendre ce que je fais :frowning:
En fait je suis sûr de ne pas comprendre :wink:

Sinon, j'ai aussi diminué le délai du sémaphore comme suggéré par @J-M-L :

  const TickType_t delai = 2 / portTICK_PERIOD_MS; // 2 ms au pif

// while (xSemaphoreTake(mutex, portMAX_DELAY) != pdTRUE) {
  while (xSemaphoreTake(mutex, delai) != pdTRUE) {

EDIT : le nouveau message d'erreur semble plutôt venir du délai de sépmaphore. Si je remets la copie de tableau comme précédement, j'ai toujours l'erreur vTaskPriorityDisinheritAfterTimeout et si je rallong le délai à 1000 ms, j'en reviens au message d'erreur initial.