Problème à récurrence constante mais bizarre

Bonjour à toutes et à tous,

Depuis quelques jours, sur mon application routeur photovoltaïque avec un ESP32 où j'enregistre un fichier journalier des données toutes les 30 secondes, j'observe un problème de donnée aberrante avec une récurrence bizarre. La récurrence n'est pas systématique, mais l'écart entre deux problèmes est toujours un multiple de 7h et 10 mn. Les heures où cela se produit ne sont pas calées sur l'heure du jour.

Est-ce que cette récurrence vous dit quelque chose.

Cordialement.

Pierre.

7h10 ça fait 25800 secondes. Avez vous cette valeur 25800 (ou approchante) dans votre code ?
ce n'est pas loin non plus d'un int16_t qui déborde à 32767 - avez vous un débordement ?

Autre piste - un problème dans l’algorithme de synchronisation de l’heure (NTP) ou une drift de l’horloge interne corrigeant brusquement une erreur toutes les 7h10 ?

J'avais regardé un peu toutes ces options, mais je n'ai rien vu d'approchant. En ce qui concerne l'heure, je la resynchronise tous les jours à minuit.
A un endroit, dans mon code, j' ai :

    if (tc >= P50*i/nbPt) { // Période divisée par le nombre de points de mesure

avec :

uint64_t tc;
uint32_t P50 = 20000; 
uint8_t nbPt = 48;
uint8_t i; // Compteur jusqu'à 48

Ce n'est peut-être pas génial, mais je l'ai depuis le début où je n'avais pas d'erreur.

Cordialement.

Pierre.

non c'est OK. vous avez un uint64_t à gauche et un uint32_t à droite. La première multiplication P50*i passe en uint32_t même si i est un byte il sera promu en int pour l'opération puis comme l'autre membre est non signé en unsigned int. idem ensuite pour la division.

est-ce que ça pourrait générer des division par 0 ?

regardez aussi vos pointeurs ou tableaux. un dépassement mémoire peut avoir des effets inattendus. testez à chaque fois les indices pour vous assurer que vous restez dans les clous.

Je ne vois pas dans ce qui suit ce qui pourrait provoquer une division par zéro.

          if (timeinfo.tm_yday != jourPrec) {                          // MàJ de l'heure à chaque changement de jour et ...
            configTime(0, 0, ntpServer);
            setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1); // Définir le fuseau horaire pour Europe centrale (Paris)
            tzset();
            fchHisto = remplaceFchHisto();                             // ... remplace l'ancien fichier "fchHisto" portant sur le même jour de la semaine
            timestamp = mktime(&timeinfo); // Temps UNIX de la nouvelle journée
            jourPrec = timeinfo.tm_yday;
          }

Mes tableaux ont tous un peu de marge vis çà vis de ce que j'ai à y mettre.

J'ai balayé toutes mes boucles for, while, do , je n'y ai pas trouvé de dépassement d'indice.

Je vais continuer à chercher. En attendant, j'ai un sparadrap sur mes erreurs et les enregistre quand ça arrive.

Cordialement.

Pierre.

Est-ce que ça pourrait être un facteur externe - un appareil qui se déclenche de temps en temps et perturbe votre alim électrique et ça fausse une lecture ?

Ce n'est peut-être pas le temps qui est la cause racine du problème.
Cela pourrait aussi être un compteur, par exemple, utilisé dans un calcul qui générerait une erreur en créant un débordement.

Ce n'est pas impossible, mais qu'est-ce qui gigote chez moi avec une telle récurrence :thinking: ? J'y pense.

Cordialement.

Pierre.

Je ne pense pas au temps en tant que date, heure ... mais effectivement à un compteur qui boucle mal.

Cordialement.

Pierre.

Il n'y a pas que les compteurs qui débordent. Ce pourrait être une variable qui accumule des résultats


un ballon d'eau chaude, la charge d'un véhicule électrique, ...

Passage aux heures creuses et appareils qui se déclenche ?

Je n'ai pas de contrat à heure creuse. Je n'ai pas de voiture électrique, mes voisins (30 mètres minimum), n'en ont pas non plus. Mon chauffe-eau est mis en marche quotidiennement à 13h 30. La chauffe dure entre une et une heure et demie selon l'usage qui a été fait.

Au vu des erreurs que j'obtiens, ç'est une variable qui accumule des valeurs, mais pour arriver à ce cumul :

...
30/07/2025 02:47:00,-76.3,5.1,231.9 // Puissance Réseau, Puissance Onduleur, Tension Réseau
30/07/2025 02:47:30,-3958348428869632.0,7.9,230.9
30/07/2025 02:48:00,-73.4,6.8,232.3
...
30/07/2025 09:56:30,544.4,619.7,233.0
30/07/2025 09:57:00,21497539677126656.0,617.8,232.2
30/07/2025 09:57:30,542.3,616.4,232.1
...

Passer de -76.3 à -3958348428869632.0 ou passer de 544.4 à 21497539677126656.0, je ne vois pas comment car mon cumul se ferait en 30 secondes au lieu de une.

Cordialement.

Pierre.

Quel est le type de cette variable et la formule qui le calcule (et le type des autres éléments)

Si la relecture du code est une piste de recherche, la compilation de ce code est-elle effectuée avec le niveau maximal des warnings car par exemple une variable locale non initialisée peut faire que le programme dysfonctionne aléatoirement

A suivre...

Dans les Préférences/Avertissement du compilateur, j'ai activé tout.

Suite à la compilation, et une floppée d'information relatives au bibliothèques utilisées, je n'ai aucun warning relatifs au sketch lui-même.

Cordialement.

Pierre.

C'est compliqué, mais voilà :

1 - La procédure de calcul des puissances, courant et tension :

float Infos_UIP(uint8_t v0, uint8_t vIn, uint8_t iIn, float kV, float kI, uint8_t nbpt, float echV[nbPt], float echI[nbPt], float *vEff, float *iEff){
  float V0, V, I, V2 = 0, I2 = 0, P = 0;
  float vr, ir;
  uint8_t i = 0;
  uint32_t P50 = 20000; // Durée en µS d'une période de 50 Hz
  uint64_t tc, tcPrec = 0;
  uint64_t dT; // Intervale entre deux mesures
  uint64_t Td = micros();
  do {
    tc = micros()-Td;
    if (tc >= P50*i/nbPt) { // Période divisée par le nombre de points de mesure
      V0 = analogReadMilliVolts(v0); // Lecture de la tension du point commun
      V = analogReadMilliVolts(vIn); // Lecture de la tension
      I = analogReadMilliVolts(iIn); // Lecture du courant
      vr = (V-V0)*kV;
      ir = (I-V0)*kI;
      dT = tc - tcPrec;
      V2 += sq(vr) * dT;
      I2 += sq(ir) * dT;
      P += vr * ir * dT; // Puissance active
      echV[i] += vr/50.0;
      echI[i] += ir/50.0;
      i++;
      tcPrec = tc;
    }
  } while (i < nbPt); // nbPt = 48 --> la période de 50 Hz est découpée en 48 tronçons
  *vEff = sqrtf(V2 / P50); // Tension efficace
  *iEff = sqrtf(I2 / P50); // Courant efficace
  return P / P50; // Puissance active
}

Dans la suite, cette procédure - activée 50 fois par seconde - est utilisée en alternance toutes les secondes (boolean Vu):

  • une fois pour ce qui concerne le réseau
  • une fois pour ce qui concerne l'onduleur.
    A chaque seconde noCycle == 0:
  • je fais le cumul des infos de la dernière seconde
  • et toutes les 30 secondes, je mets les données dans un fichier.

2 les variables intéressantes :

float pA_Reseau;      // Valeur de la puissance active sur une période de 50 Hz
float sumPA_R;        // Somme partielle de pA_Reseau
float pA_Onduleur;    // Valeur de la puissance active sur une période de 50 Hz
float sumPA_O;        // Somme partielle de pA_Reseau
float vRMS, iRMS;     // Valeur efficace pour la tension et le courant
float cosPhi_Res, cosPhi_Ond;
float ptsV[nbPt];
float ptsI_R[nbPt];
float ptsI_P[nbPt];
String infUI; // Chaîne de caractères représentant la forme de la tension et du courant
char info_PPDD[32]; // Chaîne de caractères représentant les puissances et les déphasages réseau et onduleur
char info_PPU[32]; // Chaîne de caractères pour l'écriture des données

3 - son utilisation :

void loop() {
  ArduinoOTA.handle();
  etatNouv = digitalRead(pin_DetSinus);
  if (etatNouv == HIGH && etatPrec == LOW) {  // Il n'y a changement d'état que si le réseau est présent
#endif    
    t1 = 0;                                   // Réinitialisation du compteur de défaut
    pb = false;                               // Si on passe dans cette boucle, c'est que le réseau est présent
    statut0 = "Normal";
    digitalWrite(ledPB, LOW);  // on éteint la LED de défaut
    noCycle++;
    if (noCycle >= nbP50Hz) {  // On recommence la série de nbP50Hz mesures
      noCycle = 0;
    }
    if (Vu) { // Alternance des mesures du courant Réseau et Onduleur toutes les secondes
      pA_Reseau = Infos_UIP(pin_Commun, pin_Tension, pin_I_Reseau, 0.935, 0.1, nbPt, ptsV, ptsI_R, &vRMS, &iRMS);  // Mesure toutes les 20 mS ; Sonde 50 A/V
      sumPA_R += pA_Reseau;       // On fait le cumul des différents mesures pour faire un moyennage
      cosPhi_Res = -pA_Reseau/(vRMS*iRMS);
    } else {
      pA_Onduleur = Infos_UIP(pin_Commun, pin_Tension, pin_I_Onduleur, 0.935, 0.1, nbPt, ptsV, ptsI_P, &vRMS, &iRMS);  // Mesure toutes les 20 mS ; Sonde 50 A/V
      sumPA_O += pA_Onduleur;       // On fait le cumul des différents mesures pour faire un moyennage
      cosPhi_Ond = pA_Onduleur/(vRMS*iRMS);
    }
    if (noCycle == 0) {    // Commencement de la série de nbP50Hz mesures  
      lanceLecture = true; // Lance le processus d'acquisition des températures des sondes
      infUIoK = false;
      infUI = "";
      for (uint8_t i = 0; i < nbPt; i++) {  // nbPt est le nombre de points de mesure à l'intérieur d'une période
        infUI += String(ptsV[i]) + "," + String(-ptsI_R[i]) + "," + String(ptsI_P[i]) + "\n";
        ptsV[i] = 0;
      }
      if (Vu) { // Comme on alterne les mesures, on alterne les effacements
        for (uint8_t i = 0; i < nbPt; i++)
          ptsI_P[i] = 0;
      } else {
        for (uint8_t i = 0; i < nbPt; i++)
          ptsI_R[i] = 0;
      }
      Vu = !Vu;
      digitalWrite(ledOK, Vu);
      forceWiFi = String(WiFi.RSSI());        // Mesure de l'intensité du signal WiFi
      pActifReseau = sumPA_R / nbP50Hz;       // Valeur moyenne de la puissance sur les nbP50Hz mesures
      pActifOnduleur = sumPA_O / nbP50Hz;     // Valeur moyenne de la puissance sur les nbP50Hz mesures
      if (getLocalTime(&timeinfo)) {
        strftime(info_DH, 24, "%d/%m/%Y %H:%M:%S,", &timeinfo); // Mise en forme de la date : JJ/MM/AAA HH:MM:SS
        if (abs(pActifReseau) > 10000) { // Sparadrap pour masquer une potentielle erreur
          pActifReseau = pActifReseauPrec;
          logPb(1);
        }
        if (abs(pActifOnduleur) > 10000) { // Sparadrap pour masquer une potentielle erreur
          pActifOnduleur = pActifOnduleurPrec;
          logPb(2);
        }
        snprintf(info_PPU, 32, "%.1f,%.1f,%.1f", pActifReseau, pActifOnduleur, vRMS);
        snprintf(info_PPDD, 32, "%.1f,%.1f,%.2f,%.2f\n", -pActifReseau, pActifOnduleur, cosPhi_Res, cosPhi_Ond);
        infUIoK = true;
        if (timeinfo.tm_sec % 30 == 0) {                               // Enregistrement toutes les 30 secondes
          appendFile2(LittleFS, fchHisto, info_DH, info_PPU, 114688);  // Enregistrement d'une nouvelle ligne de données
          if (timeinfo.tm_yday != jourPrec) {                          // MàJ de l'heure à chaque changement de jour et ...
            configTime(0, 0, ntpServer);
            setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1); // Définir le fuseau horaire pour Europe centrale (Paris)
            tzset();
           //configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);  // On configure le seveur NTP
            fchHisto = remplaceFchHisto();                             // ... remplace l'ancien fichier "fchHisto" portant sur le même jour de la semaine
            timestamp = mktime(&timeinfo); // Temps UNIX de la nouvelle journée
            jourPrec = timeinfo.tm_yday;
          }
          if (WiFi.status() != WL_CONNECTED) { // Si problème de connexion au WiFi
            File file = LittleFS.open(Suivi, FILE_APPEND);
            file.print(info_DH);
            file.print(',');
            file.print("Déconnexion ... ");
            WiFi.disconnect();
            WiFi.begin(ssid, password);
            tr = millis();
            while (WiFi.status() != WL_CONNECTED && millis()-tr < 10000) {
              delay(100);
            }
            if (millis() - tr > 10000 ) 
              file.print("Echec reconnexion\n");
            else
              file.print("Reconnexion OK \n");
            file.close();
          }
          affOLED(2);
        }
      }
      if (pActifReseau < 0)  // Si il y a surplus de production
        nbP_CmdTriac += 1 + 50 * pActifReseau / pMaxSurplus;
      else
        nbP_CmdTriac -= 1 + 50 * pActifReseau / pMaxSurplus;
      nbP_CmdTriac = constrain(nbP_CmdTriac, 0, nbP50Hz - 1);
      if (Vu)
        sumPA_R = 0;  // Remise à zéro de la puissance moyenne toutes les nbP50Hz périodes
      else
        sumPA_O = 0;  // Remise à zéro de la puissance moyenne toutes les nbP50Hz périodes
      if (nbP_CmdTriac == 0)
        digitalWrite(pin_CmdTriac1, LOW);
      else
        digitalWrite(pin_CmdTriac1, HIGH);
      modeFonctionnement();           // Modifie la gestion du surplus de production
    }
  }
  if (nbP_CmdTriac < noCycle)        // Remise à zéro de la commande du triac
    digitalWrite(pin_CmdTriac1, LOW);  // Attention, cette commande doit arriver avant le front montant de la tension EDF
  etatPrec = etatNouv;
  t1++;
  pb = statut();
  if (digitalRead(pin_ResetTotal) == LOW && LittleFS.exists(FchWiFi)) { // Réinitialisation totale par effacement du fichier "FchWiFi"
    Serial.println("Réinitialisation totale");
    LittleFS.remove(FchWiFi);
  }
}

Bon courage pour déchiffrer tout ça :face_with_diagonal_mouth:

Cordialement.

Pierre

Je vous livre ma reflexion

Pour trouver où chercher, je me concentre sur votre analyse - la régularité du phénomène avec des périodes multiples de 7h10 soit 25800 secondes. Le temps semble donc un élément important dans l'erreur.

S'il n'y a pas d'élément externe lié au temps venant perturber les acquisitions, c'est que le souci vient vraiment du code. Si vous avez éliminé les dépassement mémoire, il reste des soucis de conversions et de calcul.

Le cadencement du temps dans votre code est géré par micros() et le débordement de micros() est à 232 microsecondes soit quand il atteint 4 294 967 296 µs.

Si vous prenez vos 25800 secondes en µs et que vous divisiez 25800000000/2**32 = 6,00703060627. On est presque à un multiple exact ➜ Cette proximité mathématique est troublante.

Donc cela m'emmène à soupçonner grandement

d'où ma question sur les types. L'arithmétique en non signé est souvent cause de souci en C++ car les règles de promotion sont complexes à mémoriser (par exemple un byte est converti en int dans un calcul et ce même int est promu en entier non signé s'il y a un autre élément non signé dans l'opération et son type éventuellement étendu pour aller vers le type le plus large).

Ici tc et Td sont des uint64_t, tandis que micros() retourne un uint32_t. Il sera donc promu en uint64_t selon les règles du C++ puisque l'autre opérande est un type plus grand.

➜ Lorsque vous faites la soustraction, vous perdez donc le comportement arithmétique modulo 232, le passage en uint64_t masque le débordement naturel de micros().

En conséquence, si micros() a débordé depuis la lecture de Td, la différence devient fausse et donne une valeur de tc anormalement grande. Cela fausse dT, puis les calculs d’énergie, menant à des puissances absurdes comme celles observées dans vos logs.

On peut émettre l'hypothèse que cela n'arrive pas tous les coups (il faut que la soustraction tombe au moment où le compteur a débordé).

Donc ma proposition serait lors du calcul de la différence de temps d'effectuer la soustraction entre deux uint32_t, ce qui gère automatiquement le débordement (préserver l’arithmétique modulo 2^32) et ensuite, si nécessaire, on peut convertir cette différence en uint64_t pour un usage ultérieur.

Qu'en pensez vous ? à tester...

J'étais persuadé que micros() avec un ESP32 retournait un uint64_t ; je ne sais pas où j'ai lu ça.
Je viens d 'aller faire un tour sur "Perplexity" et j'y lis :slight_smile:

Pour l'ESP32 utilisant l'environnement Arduino, la fonction micros() retourne un uint32_t (entier non signé sur 32 bits). Ce choix est en partie lié à la compatibilité avec d'autres plateformes Arduino, où le type utilisé pour micros() est généralement unsigned long (aussi sur 32 bits sur la majorité des plates-formes Arduino)

.

  • micros() = uint32_t sur l’ESP32 (avec Arduino Core)
  • Cela signifie que la valeur “roule” (overflow) toutes les ~70 minutes.

Si vous souhaitez obtenir une mesure en microsecondes sur 64 bits (pour éviter ce rollover), il existe la fonction esp_timer_get_time() fournie par l’ESP-IDF, qui retourne un uint64_t, mais ce n’est pas la même chose que micros() dans l’environnement Arduino

.

Résumé :

  • micros() sur ESP32 = uint32_t
  • esp_timer_get_time() sur ESP32 = uint64_t

Veillez donc à adapter votre code si vous avez besoin de durées très longues ou souhaitez éviter le débordement de micros()
.

Je viens donc de remplacer micros() par esp_timer_get_time(). Pour l'instant, ça fonctionne ...

Je vais voir ce que ça donne. Sinon, comme vous me le proposez, je vais passer mes variables en 32 bits et voir ...

Cordialement.

Pierre.

Je confirme par simulation qu'en passant en 32 bits, le code:

  uint32_t tc;
  uint32_t Td = micros();

  do {
    tc = micros()-Td;
    if (tc >= P50*i/nbPt) { // Période divisée par le nombre de points de mesure
    ...

fera bien ce qui est attendu :wink:

Simulation

=> Lancement proche des fameuses 70 minutes puis incrément de 50 uS
#0: simulation micros() [4294967096] Td [4294967096] tc [0]
#1: simulation micros() [4294967146] Td [4294967096] tc [50]
#2: simulation micros() [4294967196] Td [4294967096] tc [100]
#3: simulation micros() [4294967246] Td [4294967096] tc [150]

=> Débordement -> Soustraction 32 bits correcte
#4: simulation micros() [0] Td [4294967096] tc [200]
#5: simulation micros() [50] Td [4294967096] tc [250]
#6: simulation micros() [100] Td [4294967096] tc [300]
#7: simulation micros() [150] Td [4294967096] tc [350]
#8: simulation micros() [200] Td [4294967096] tc [400]
#9: simulation micros() [250] Td [4294967096] tc [450]
...

A suivre...

Bon, ça fonctionne bien avec esp_timer_get_time(). Je suis repassé en 32 bits avec micros() et ça fonctionne toujours bien.

Je pense donc que mon problème était bien lié à ma confusion/mélange entre 32 et 64 bits.

Merci pour vos aides.

Cordialement.

Pierre.