Messagepar Flax » 24 avr. 2023, 14:38
Dans la régulation, il faudra aussi ajouter des protections pour éviter de cramer les pannes. Dans le SW d'origine, c'est simplement un timer, qui va mettre la station en protection si la sortie PWM reste à 100% pendant trop longtemps. C'est très bourrin mais ça marche. Deux problèmes:
- Une fois en protection le code est bloqué, et le seul moyen de débloquer c'est de faire un reset de l'Arduino,
- Si on essaie de souder sur une grosse masse de cuivre (genre un gros plan de masse) avec une panne vraiment trop petite, le SW va croire qu'il y a un problème et se mettre en protection.
Pour le premier point, la solution est déjà décrite : au lieu de bloquer le SW on se met dans un mode "erreur" dont on peut sortir avec une petite manip / procédure, pas besoin de reset le SW, ça sera quand-même plus pratique.
Pour le deuxième point, il faut faire un système de détection un peu plus sophistiqué, et ne pas se contenter de regarder le PWM de sortie mais aussi la mesure de température : si le PWM est kéblo à 100% ET si la température ne bouge pas ou a une valeur aberrante, alors on met en protection.
Il ne manque plus que deux éléments : la sauvegarde en NVM et le watchdog.
Watchdog
Pour le watchdog, dans le software d'origine on utilise tout simplement le watchdog de l'AVR, avec un refresh dans la tâche de fond. Rien d'incroyable, c'est du standard. SUr le STM32L011K4, il y a deux watchdogs:
- IWDG ("independant") qui est un simple timeout, clocké sur une horloge indépendante, donc siimple mais avec une indépendance garantie,
- WWDG ("window") qui est un watchdog à fenêtre, clocké sur un prescaler du APB1, donc permet d'être plus précis sur les mesures de timings, mais n'a pas une horloge indépendante des autres périphériques.
Pour rappel, le but du watchdog est de détecter un blocage de l'application. L'idée est d'avoir un timer qui décrémente en permanence, et quand il atteint zéro il va déclencher un reset du MCU. Pour éviter cela, il faut régulièrement faire une action ("refresh") qui va remettre le timer à sa valeur de départ. Pour que ça soit utile, il faut faire cette action dans une partie "pertinente" du SW, dont on considère que la non-exécution est un signe crédible de plantage de l'applicatif. Un watchdog à fenêtre va avoir le même principe de fonctionnement, mais va aussi déclencher un reset si le refresh est déclenché trop tôt. Il y a donc une fenêtre (d'où le nom) dans lequel est autorisé le refresh. Cela permet de détecter un plantage / blocage, mais aussi un défaut du séquencement, problème d'horloge, interruption qui se déclenche en boucle, ce genre de choses.
En général, sur une application qui a des tâches cycliques en interruption timer, le watchdog "basique" (pas à fenêtre) est refresh dans la tâche de fond, l'idée étant d'identifier qu'une tâche est tankée. Ici la sanction c'est le fait de repasser suffisamment souvent dans la tâche de fond pour refresh le watchdog. Le watchdog à fenêtre serait plutôt refresh dans une tâche cyclique, pour vérifier que l'exécution des tâches reste régulière.
Dans un premier temps je vais juste utiliser le IWDG, et plus tard, peut-être, je réfléchirai à utiliser le WWDG. Étant donné que le risque principal (évènement redouté comme on dit en sûreté de fonctionnement) est de faire cramer la panne, le temps de réaction n'a pas besoin d'être incroyablement rapide. Je vais quand-même mettre 100ms pour éviter de laisser la régulation trop longtemps partir en vrille si jamais on est limite niveau charge CPU.
NVM
... Gros dosselard.
Je voudrai bien réutiliser ce module sur d'autres projets, donc je voudrais bien y mettre d'entrée de jeu tous les mécanismes dont j'aurais besoin sur une plus grosse application, en particulier en appliquant les principes que j'ai appris à mon taf.
Quand on parle de NVM on parle de quoi ? "Non Volatile Memory" ou mémoire non-volatile dans la langue de Depardieu, il s'agit de la mémorisation de valeurs pendant la vie du "produit", valeurs qui peuvent être arbitraires. Le stockage se fait soit dans un composant mémoire dédié (E²PROM, Flash externe en SPI ...) soit directement dans le MCU. Par exemple, dans les PIC et les AVR il y a une E²PROM intégrée. Sur la plupart des MCUs, ce n'est pas une E²PROM, mais une partie de la Flash programme qui est différente et mieux adaptée à des écritures régulières. Vu que j'en suis a expliquer plein de choses, autant continuer.
Pour rappel, encore la mémoire Flash a la particularité d'avoir un état au repos (0 ou 1 en fonction de la techno NAND ou NOR), et on peut écrire dedans avec une granularité assez fine, à l'octet voire au bit près. Mais une fois qu'un bit est écrit à la valeur "pas repos", on ne peut pas l'écrire dans l'autre sens. Le seul moyen de faire revenir un bit à la valeur repos est de déclencher une opération d'effacement, et cette opération ne peut se faire que sur une page complète, page dont la taille est généralement relativement grande (quelques dizaines d'octet à plusieurs ko en fonction du modèle et de l'archi de la Flash). Ce qui veut dire que si on veut modifier une valeur dans une mémoire Flash, il faut identifier la page dans laquelle cette valeur est stockée, recopier toute la page en RAM, faire la modification dans la copie en RAM, effacer la page en Flash puis la réécrire avec la copie en RAM. Tout cela consomme de la RAM, est long, et ya pas intérêt à avoir une interruption ou une coupure d'alim en cours de route. Ajouter à cela le fait que ce type de mémoire a une durée de vie assez limitée, qui se compte en nombre de cycles d'effacements / écriture, cela fait qu'on tâche d'éviter de trop souvent faire des écritures en Flash. En général, on le réserve aux opérations en debug et aux mises à jour de firmware. Pour sauvegarder des valeurs qui changent régulièrement, on va utiliser autre chose.
C'est là qu'on trouve les Data Flash, DFlash, ou E²Flash, qui sont des sections particulières de la Flash programme. Il s'agit toujours de Flash, avec le même principe d'effacement de page, mais les pages sont plus petites, et en général la durée de vie est meilleure que la Flash programme classique. Genre ici sur le STM32 on a des pages de 4 octets et une durée de vie multipliée par 10 (on passe de 10k cycles à 100k).
Si on prend des exemples un peu plus concrets (et que je connais) pour illustrer ça, sur un calculateur de bagnole, on découpe la Flash en plusieurs zones:
- Bootloader
- Application
- Calibrations
- NVFix
- NVRol
Bootloader, application et calibrations sont en Flash programme, NVFix et NVRol c'est en DFlash. Le bootloader est toujours le premier à démarrer, il checke que l'application est valide, si c'est le cas il lance l'application. Les calibrations ce sont des valeurs qui peuvent être modifiées pendant le développement du calculateur, pour de la mise au point, par contre les calibrations sont strictement identiques d'un exemplaire à l'autre. Un jeu de calibration est liée à une version d'application, le fait que ça soit séparé du code de l'application est juste une facilité pour le développement parce que les outils de mise au point peuvent ainsi accéder facilement aux valeurs, mais une fois une application programmée elles ne changent pas. Ca permet aussi de faire des corrections dans les valeurs de calibrations sans re-programmer toute l'appli. Typiquement, sur un moteur thermique (et sur une bagnole "normale"), les cartos sont en calibration, elles sont identiques d'un exemplaire à l'autre. Potentiellement il peut y avoir une appli identique avec deux calibrations différentes pour deux variantes différentes d'un même moteur, ou le même moteur sur deux modèles différents. Quand on veut changer les cartos (je ne juge pas) sur une voiture "normale" du commerce il faut re-programmer la zone de calibration.
NVFix et NVRol servent à stocker les grandeurs qui vont changer pendant la vie du produit, elles seront donc différentes d'un exemplaire à l'autre, même si elles sont potentiellement identiques quand la voiture sort de l'usine.
NVFix ce sont celles qui varient rarement, ou uniquement suite à une action explicite ponctuelle (genre, une écriture de DID en concession). Par exemple : valeur d'étalonnage d'un capteur, numéro de série, ce genre de choses.
NVRol ce sont les valeurs qui changent tout le temps, ou en tous cas son sauvegardées à chaque endormissement du calculateur. Typiquement c'est le kilométrage, la mémorisation des défaut (DTC), etc.
Ces deux mécanismes, NVFix et NVRol vont se partager la DFlash. On va la re-découper en plus petites sections, chaque section (zone) devra être un multiple de la taille d'effacement, et suffisamment grande pour contenir l'intégralité des données qu'on veut y mettre. L'idée, c'est de ne pas écrire dans la même zone deux fois de suite, mais d'alterner, voir de tourner sur un pool de zones. Par exemple si on alloue deux zones, on écrira alternativement sur l'une, puis sur l'autre. ça permet d'éviter de se retrouver avec les données écrites uniquement en RAM (et donc susceptibles de disparaitre si une coupure a lieu) donc d'avoir une sorte de backup, et ça permet aussi de répartir les cycles d'effacement sur la mémoire, au lieu de concentrer sur une sous-partie qui serait écrite en permanence, ou de faire un effacement systématique de toute la DFlash. De cette façon, on allonge la durée de vie, en gros on la multiplie par le nombre de zone (mais on l'use sur "toute sa surface").
Pour savoir quelle zone est valide à un instant T, on utilise des patterns qu'on va écrire. En général il y en a 2 : validité, invalidité. Ce sont deux sous-zones / variables qui sont clairement identifiées dans chaque zone (en général à la fin) et qui sont soit "vierges" / non-écrite, soit écrites avec une valeur unique spécifique (genre 0x5A5A). Une fois qu'une nouvelle zone est écrite avec un nouveau contenu (et qu'on a vérifié son intégrité via un CRC) on écrit le pattern de validité. Cela permet, en passant sur toutes les zones, d'identifier celles qui ont passé cette étape écriture + check. Puis on vient écrire le pattern d'invalidité de la zone précédente.
Si deux zones se suivent et ont chacune leur pattern de validité, mais aucune n'a de pattern d'invalidité, alors c'est que l'opération d'écriture a été interrompue après le check, mais avant l'écriture du pattern d'invalidité. S'il n'y a que deux zones, impossible de savoir laquelle a été écrite en dernier -> les deux sont considérées HS. S'il y a plus de deux zones dans le pool, alors on connaît le "sens" d'écriture des zones, et on donc on peut identifier la plus "récente", et finir l'opération en écrivant le pattern d'invalidité sur la plus "ancienne". Au démarrage, l'appli va passer en revue toutes les zones, pour trouver celle qui est valide, qui va donc soit être la seule à avoir son pattern de validité mais pas son pattern d'invalidité, soit en trouver deux avec validité mais sans invalidité et prendre la plus "récente".
Si aucune zone ne correspond à ces caractéristiques, alors on considère que la NVM est cassée "beyond repair" et on revient aux valeurs par défaut, qui elles sont stockées dans la zone des constantes dans la Flash programme. Plus précisément, on vient écrire les pattern d'invalidité sur toutes les zones, puis on reprend la première du pool de zones, on l'efface, on vient écrire les valeurs par défaut et on écrit son pattern de validité.
En général on prend deux zones pour la NVFix et plus que deux zones pour la NVRol. L'idée c'est que la NVFix est généralement écrite dans des conditions moins "risquées" niveau perturbations et pertes d'alim, donc elle a moins de risque de se retrouver dans la situation où les deux banques sont valides + non-invalidées.
Ça c'est pour la gestion de la zone de DFlash en elle-même. Pour gérer les données qu'elle contient, il faut une "carte" des variables contenues dans une zone. Pour cela, soit on le définit explicitement avec des constantes en #define qui indiquent les adresses individuelles de chaque variable, soit on utilise une structure, voire mieux on stocke chaque variable avec un index qui permet d'identifier de quelle grandeur il s'agit, ou encore mieux, mais qui prend énormément de place, stocker la valeur avec une chaîne de caractère permettant de l'identifier. Pour ce projet, je pense que je vais faire une structure. Au démarrage, il faudra charger le contenu de la structure dans les variables associées.
Avant chaque sauvegarde, on vient reconstruire le contenu de la structure avec le contenu des variables courantes, et on compare avec le contenu correspondant dans la NVM, si la valeur est différente on lance une écriture d'une nouvelle zone avec les nouvelles valeurs.
Pour la station de soudure, je pense qu'il n'y aura que de la NVRol, et on va sauvegarder : consigne de température, consigne en standby, gain. Ce sont des uint16_t, donc il faut au moins 6 octets. Si on ajoute un CRC il faut un peu plus. Vu que la granularité est de 4 octets, je pense pertinent de faire un CRC sur 4 octets, ce qui fait donc au moins 12 octets avec la quantification.
La DFlash fait 512 octets, largement suffisant. On peut y découper en zones de 32 octets pour se prendre un peu de marge, ça fait 16 zones, 2 pour la NVFix (qu'on n'utilisera pas ici) et 14 pour la NVRol. Ce découpage est arbitraire, on pourrait prendre des valeurs différentes.
Celui-là il va prendre du temps à coder et tester ...