Un troisième temporisateur de surveillance du ESP8266, version simplifiée
2018-06-10
Récupérer automatiquement des redémarrages en boucle du ESP8266

Dernièrement, j'ai corrigé une erreur grossière dans un croquis Arduino qui réalisait un temporisateur de surveillance du ESP8266. Cela m'a permis d'examiner mon code avec plus de recul pour me rendre compte qu'il était souhaitable de l'améliorer. J'ai aussi décidé qu'il était temps de traduire les trois billets en anglais sur ce sujet qui datent d'août et septembre de l'année dernière. Encore là, le recul apporte conseil, et il me semble préférable de réécrire plutôt que traduire dans l'espoir qu'une présentation en deux étapes du logiciel sera plus digeste. Pour continuer avec cette métaphore alimentaire, j'espère que mes programmes et billets sont comme ma sauce spaghetti; meilleurs réchauffés.

Alors que les temporisateurs de surveillance de nature matérielle et logiciel du ESP8266 sont essentiels, ils ne sont pas suffisants pour assurer le niveau de fiabilité nécessaire dans un dispositif de l'Internet des objets. Ci-dessous, je propose un troisième chien de garde pour améliorer la fiabilité des appareils basés sur ce circuit intégré fabriqué par Espressif. Il s'agit d'une version simplifiée du chien de garde, la version complète sera présentée dans un billet ultérieur.

Table des matières

  1. Qu'est-ce qu'un chien de garde informatique ?
  2. Les chiens de garde du ESP8266
  3. Le chien de garde loop simplifié
  4. Conclusion

  1. Qu'est-ce qu'un chien de garde informatique ?

  2. If the man stops kicking the dog, the dog will take advantage of the hesitation and bite the man.
    Traduction libre: si l'homme cesse d'assaillir à coups de pied le chien, ce dernier profitera de l'hésitation et mordra l'homme.
    Niall Murphy, Watchdog Timers.

    En principe un temporisateur de surveillance doit réagir rapidement à un dysfonctionnement matériel ou logiciel d'un dispositif en rétablissant son fonctionnement normal, le plus souvent, en effectuant une réinitialisation. Et comment vérifier que le dispositif fonctionne correctement ? Ce dernier doit confirmer à intervalles réguliers que la situation est normale. Si ce signalement n'est pas reçu dans un délai prévu, le système de surveillance passe à l'action.

    Le temporisateur de surveillance est communément appelé un chien de garde (watchdog timer en anglais, en abrégé WDT), car il y a une analogie assez forte comme révélée dans l'encadré ci-dessus. Si asséner de coups de pieds un chien virtuel est pénible, on peut remplacer la métaphore avec «nourrir le chien» ce qui est encore plus éloquent à mon avis.

    Un chien de garde peut être vu comme un compteur qui est régulièrement décrémenté. Il « mord » quand la valeur du compteur atteint zéro. Il est « alimenté » quand le compteur est remis à sa valeur initiale. Tant que le logiciel d'un dispositif comme un micro contrôleur alimente le chien de garde avant que la valeur critique du compteur ne soit atteinte, le logiciel peut continuer avec ses autres tâches normales. Si une erreur de programmation, une surtension transitoire, ou un rayonnement cosmique à la limite, écarte le logiciel de son fonctionnement prévu, vraisemblablement ce dernier n'alimentera plus le chien de garde et conséquemment l'appareil sera réinitialisé.

  3. Les chiens de garde du ESP8266
  4. Le ESP8266 possède deux chiens de garde: l'un matériel et l'autre logiciel. Le chien de garde matériel est associé à l'unique temporisateur matériel de la puce. Voici un croquis qui provoque la morsure du chien de garde logiciel.

    void setup() {  while (1) {}; } void loop() {}

    Parce que le processeur est engagé dans la boucle while qui est vide et qui ne se termine jamais, le chien de garde logiciel n'est pas nourri et il mordra en quelques secondes. Si la boucle while avait été insérée dans la fonction loop(), le résultat aurait été le même.

    Le croquis suivant provoque la morsure du chien de garde matériel.

    void setup() {  ESP.wdtDisable();  while (1) {}; } void loop() {}

    Le chien de garde matériel est plus patient que le chien de garde logiciel. On ne verrait pas la morsure du premier si le second réinitialise le ESP8266 bien avant que soient écoulées les 6 secondes qu'attend le chien de garde matériel pour repartir le ESP s'il n'est pas nourri. Voilà la raison d'être de l'instruction ESP.wdtDisable(); qui stoppe le fonctionnement du chien de garde logiciel.

    Avertissement au sujet d'un bogue du ESP8266

    Quand un croquis est téléversé puis immédiatement exécuté, le système se bloque s'il est redémarré par un des chiens de garde, ou par une exception, avec la fonction restart() et ainsi de suite. C'est un problème bien connu qui ne se produit qu'immédiatement après le téléchargement d'un micrologiciel par liaison série (voir esp8266/Arduino FAQ). Après une réinitialisation manuelle ou l'arrêt momentané de son alimentation, l'ESP redémarrera sans faute à chaque fois qu'un chien de garde mord ou qu'une exception se produise et ainsi de suite.

    Comment nourrir les chiens de garde ? En fait c'est rarement nécessaire d'invoquer explicitement la méthode ESP.wdtFeed() (ESP est une instance de la classe EspClass). Les deux chiens sont alimentés automatiquement à chaque itération de la procédure loop() du croquis, à chaque invocation de la fonction yield() et systématiquement pendant l'exécution de la fonction delay().

    Dans la version originale en anglais de ce billet, il y a une longue discussion, avec croquis à l'appuie, sur comment mesurer la période des deux temporisateurs de surveillance. Je voulais comprendre pourquoi certains disaient que le chien de garde logiciel mordait après 3,2 secondes et d'autres affirmaient que la période est d'environ 1,5 secondes. Pour faire court, les deux camps ont raison, tout dépend comment le chien de garde logiciel est initialisé. Conséquemment, il est plus prudent de miser sur un délai de 1,5 secondes maximum avant que ne morde le chien de garde logiciel et de 6,2 secondes pour le chien de garde matériel.

    Parce qu'il est au cœur du fonctionnement du système, on recommande souvent de ne pas interférer avec le fonctionnement du chien de garde matériel. Aussi bien étendre cette recommandation au chien de garde logiciel à mon avis. Or d'après ce qu'on peut lire sur divers forums, plusieurs cherchent à arrêter le fonctionnement des chiens de garde qui interrompent le micrologiciel qu'ils développent. Il me semble que ce raisonnement est fautif. Le fabricant n'a certainement pas créé ces temporisateurs de surveillance pour des raisons frivoles. Je penche dans l'autre direction et je rajoute un troisième chien de garde à mes micrologiciels. Le croquis suivant explique mon raisonnement.

    /* * Sketch showing need for a third "loop" watchdog * */ // LED flasher #define LED_TIME 200 // 200 ms on/off period #define LED_PIN D4 // = 2 for Arduino unsigned long ledTime = 0; void LedModule(void) { if (millis() - ledTime > LED_TIME) { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); ledTime = millis(); } } // Bad module #define BAD_TIME 300 // 300 ms period for printing out something unsigned long badTime; int badCount = 5; // loops before going to never never land void BadModule(void) { if (millis() - badTime > BAD_TIME) { badCount -= 1; if (badCount <= 0) { Serial.print("bye bye..."); // the ESP will be stuck in the following loop while(1) { delay(5); } // remove delay(5); and the wdt will bite } Serial.printf("bad count: %d\n", badCount); badTime = millis(); } } void setup() { Serial.begin(115200); delay(100); Serial.println("\n\nLoop Watchdog Needed Example"); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); Serial.println("GPIO LED pin intialized"); ledTime = millis(); badTime = millis(); } void loop() { LedModule(); BadModule(); delay(10); }

    La boucle while dans le module badModule représente une action qui prendra beaucoup de temps. Alors on y invoque delay() pour permettre au ESP8266 de compléter ses opérations en arrière plan dont la gestion des connexions WiFi et les communications par UART. Malheureusement, les deux chiens de garde du ESP8266 sont systématiquement nourris et ils ne captent pas le fait que l'on ne revient jamais à la boucle principale loop(). Voici ce que l'on verra avec le moniteur série de l'EDI Arduino.

    Simple Loop Watchdog Example
    GPIO LED pin intialized
    bad count: 4
    bad count: 3
    bad count: 2
    bad count: 1
    bye bye...

    À partir de là, plus rien ne se produit. Les seules façons d'interrompre la boucle sans fin dans laquelle le ESP8266 est engagé sont d'activer le signal RESET de la puce ou d'arrêter l'alimentation. Malheureusement, après sa réinitialisation, l'ESP recommencera à tourner en rond. C'est à cause de ce genre de problème qu'il faut ajouter un autre temporisateur de surveillance que je nomme loop watchdog.

  5. Le chien de garde loop
  6. Le troisième chien de garde sera nourri uniquement au début de chaque itération de la boucle loop() du croquis Arduino. C'est ainsi qu'on peut s'assurer que le ESP8266 continue d'exécuter cette boucle au complet. Non seulement le chien de garde loop mord quand la boucle principale du croquis ne s'éxécute plus, mais il indique aussi où le problème s'est produit sans qu'il soit nécessaire de le compliquer outrageusement.

    Le temps qu'on accorde au chien de garde loop (lwdt pour faire plus court) est critique. Le délai d'attente du lwdt doit être supérieur à celui des chiens de garde ESP8266 intégrés et supérieur au scénario le plus défavorable pour l'exécution de la boucle du programme. C'est une bonne pratique d'ajouter du temps supplémentaire. En revanche, un délai trop long pourrait avoir de fâcheuses conséquences si le ESP8266 est affecté à un contrôle ou au suivi d'un processus physique en temps réel.

    Dans l'exemple ci-dessous, l'attente de 12 secondes, le double du délai du chien de garde matériel, semblaient être une période raisonnable. Cet exemple reprend celui de la section précédente pour montrer comment lwdt révélera le problème qui se produit dans le module badModule.

    /* * Sketch with simple loop watchdog timer (lwdt) * */ #include <Ticker.h> // simple loop watchdog #define LWD_TIMEOUT 12000 // Restart if loop watchdog timer reaches this time out value (12 seconds) #define LWD_MARKER 0xABBA0000 #define LOOP_START 0 #define LWD_OVERWRITTEN 1 #define BAD_MODULE 2 #define LED_MODULE 3 static const char *moduleNames[] = { "start of loop()", "lwd variables overwritten", "bad module", "led module" }; volatile unsigned long lwdTime = 0; volatile unsigned long lwdTimeout = LWD_TIMEOUT; volatile unsigned long lwdWhere = LWD_MARKER | LOOP_START; volatile boolean lwdIncomplete = false; void lwdRestart(void) { int index = (lwdWhere & 0xFFFF); if (lwdIncomplete) Serial.printf("\n\nLoop watch dog: loop() not completed. Last seen routine: %s\n", moduleNames[index]); else Serial.printf("\n\nLoop watch dog timed out in routine %s\n", moduleNames[index]); Serial.flush(); ESP.restart(); } Ticker lwdTicker; void ICACHE_RAM_ATTR lwdtcb(void) { if (lwdTimeout - lwdTime != LWD_TIMEOUT) // time variables out of sync lwdWhere = LWD_OVERWRITTEN; else if (lwdWhere & 0xFFFF0000 != LWD_MARKER) lwdWhere = LWD_OVERWRITTEN; // where variable mangled else if (millis() - lwdTime < LWD_TIMEOUT) return; lwdRestart(); // lwd timed out or lwd overwritten } void lwdtStamp(unsigned long where) { lwdWhere = LWD_MARKER | (where & 0xFFFF); } void lwdtFeed(void) { lwdTime = millis(); lwdTimeout = lwdTime + LWD_TIMEOUT; lwdIncomplete = (lwdWhere != (LWD_MARKER | LOOP_START)); if (lwdIncomplete) { lwdRestart(); } } // LED flasher #define LED_TIME 200 // 200 ms on/off period #define LED_PIN D4 // = 2 for Arduino unsigned long ledTime = 0; void LedModule(void) { lwdtStamp(LED_MODULE); // always first instruction of a module if (millis() - ledTime > LED_TIME) { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); ledTime = millis(); } } // Bad module #define BAD_TIME 300 // 300 ms period for printing out something unsigned long badTime; int badCount = 5; // loops before going to never never land void BadModule(void) { lwdtStamp(BAD_MODULE); // always first instruction of a module if (millis() - badTime > BAD_TIME) { badCount -= 1; if (badCount <= 0) { Serial.print("bye bye..."); // the ESP will be stuck in the following loop while(1) { delay(5); } // remove delay(5); and the wdt will bite } Serial.printf("bad count: %d\n", badCount); badTime = millis(); } } void setup() { Serial.begin(115200); delay(100); Serial.println("\n\nSimple Loop Watchdog Example"); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); Serial.println("GPIO LED pin intialized"); // setup loop watch dog lwdTicker.attach_ms(LWD_TIMEOUT / 2, lwdtcb); // attach lwdt interrupt service routine to ticker lwdTime = millis(); ledTime = millis(); badTime = millis(); } void loop() { lwdtFeed(); // should be first instruction in loop() LedModule(); BadModule(); lwdtStamp(LOOP_START); // should be last instruction in loop() }

    Le fonctionnement du chien de garde est simple. Au début de l'exécution de la boucle loop(), le chien de garde est nourri (lwdFeed()) ce qui consiste principalement à actualiser la variable lwdTime avec la valeur de l'horloge millis(). Cette dernière mesure le temps écoulé en millisecondes depuis le dernier démarrage du ESP8266. Le début de chaque module est marqué en appelant la fonction lwdStamp() avec l'identificateur unique du module. Ainsi le chien de garde peut suivre le cheminement du croquis. Un gestionnaire d'interruption (lwdtcb), invoqué régulièrement par une minuterie de type Ticker, vérifie régulièrement si le temps écoulé depuis la dernière actualisation de lwdTime dépasse le délai du chien de garde. Le cas échéant, le gestionnaire passe le contrôle à la fonction lwdRestart() qui transmet un message d'information sur le UART de la puce et puis qui redémarre le ESP8266. L'avertissement ressemblera à ceci:

    Simple Loop Watchdog Example
    GPIO LED pin intialized
    bad count: 4
    bad count: 3
    bad count: 2
    bad count: 1
    bye bye...
    
    Loop watch dog timed out in routine bad module
    
     ets Jan  8 2013,rst cause:2, boot mode:(3,6)
    
    load 0x4010f000, len 1384, room 16 
    tail 8
    chksum 0x2d
    csum 0x2d
    v614f7c32
    ~ld
    
    
    Simple Loop Watchdog Example
    GPIO LED pin intialized
    bad count: 4
    ...

    Le code est en fait un peu plus complexe que ce qui est dit ci-dessus, car j'ai incorporé des idées de Jack Ganssle et al pour améliorer la fiabilité du chien de garde.

  7. Conclusion
  8. Le chien de garde présenté ci-dessus constitue un outil que j'ose croire utile dans le développement d'un croquis moindrement complexe. Cependant, il faut un peu de discipline lors de la création du croquis pour tirer avantage de cette fonctionnalité. En particulier, il faut morceler toutes les tâches à accomplir pendant chaque itération de la boucle du programme en modules chacun ne faisant qu'une chose. C'est ainsi que la fonction lwdStamp() peut être utilisée à bon escient.

    Il est clair que tous ne partagent pas mon enthousiasme pour l'approche modulaire qui est utilisée ici. On peut trouver de nombreux exemples de croquis Arduino ne contenant que la fonction setup() suivie d'une longue fonction loop(). Les tenants de cette façon de faire peuvent définir des identificateurs uniques et les placer stratégiquement dans la fonction loop() avec la fonction lwdStamp(). À chacun son style, le résultat sera sensiblement le même.

    Le chien de garde loop souffre d'une lacune partagée avec les deux chiens de garde intégrés et le mécanisme d'exceptions. Puisque le ESP8266 est souvent utilisé dans des dispositifs de type IdO difficilement accessible, il faut prévoir une façon de récupérer si l'appareil est pris dans une boucle de redémarrage continue. Cette question est examinée dans le prochain billet de cette série.

    References:

    Barr, Michael (2001), Introduction to Watchdog Timers.
    Murphy, Niall (2000), Watchdog Timers.
    Ganssle, Jack (2016), Great Watchdog Timers for Embedded Systems.
    Santos, Nuno (2017), ESP8266: Watchdog functions.
    Markus (Links2003) (2016), Watchdog managed by sketch only.
    Ryabkov, Deomid (2017), Reverse engineering of the ESP8266 watchdog timer.

    Téléchargements:

    lwdt_needed.ino Croquis Arduino illustrant le besoin d'un troisième chien de garde.

    lwdt_simple.ino Croquis Arduino contenant un troisième chien de garde simplifié de type loop.

Récupérer automatiquement des redémarrages en boucle du ESP8266