Problème communication avec DHT11 avec bibliothèque "maison"

Bonjour,
ne trouvant aucune bibliothèques pour l'utilisation de capteur DHT11 qui ne soit pas soit une machine à faire exploser la RAM ou des gros tas de while et de delay() et delayMicroseconds(), j'ai décidé d'en faire une moi même, sans aucun code bloquant (donc pas de pulseIn, pas de delay et pas de while - je fonctionne aux machines à état maintenant :wink:). On trouve nombre de tutoriels sur l'utilisation d'un tel capteur sans bibliothèque, et d'articles sur le fonctionnement (listés dans un commentaire dans le fichier header)

Fichier header : dht11.h
/*
Sources et liens utiles :
  - Datasheet du DHT11 : https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf
  Pour la communication avec le capteur, articles intéressants :
  - https://forum.arduino.cc/t/dht11-gather-data-without-library/932829
  - https://www.circuitgeeks.com/arduino-dht11-and-dht22-sensor-tutorial/
  - https://www.engineersgarage.com/articles-arduino-dht11-humidity-temperature-sensor-interfacing/
  - Manipulation bit à bit : https://www.locoduino.org/spip.php?article70
  Autres bibliothèques pour DHT11
  - https://github.com/markruys/arduino-DHT
  - https://github.com/adidax/dht11
  - https://github.com/winlinvip/SimpleDHT
  - https://github.com/amperka/dht
  - https://github.com/RobTillaart/DHTNew
  - https://github.com/RobTillaart/DHTlib
  - https://github.com/olewolf/DHT_nonblocking
  - etc...
*/

//#pragma once
#ifndef dht_h
#define dht_h

#include <Arduino.h>

class DHT11 {
  private:
    uint8_t _pinCapteur; // Numéro de la broche du capteur DHT11
    uint32_t _derniereLecture; // Timestamp de la dernière lecture réussie
    uint8_t _humidite; // Dernière valeur d'humidité lue
    int8_t _temperature; // Dernière valeur de température lue
    uint8_t _donnees[5]; // Buffer pour stocker les données brutes du capteur
    uint8_t _indexBit; // Index du bit en cours de lecture
    uint32_t _tempsDebut; // Timestamp pour les mesures de durée

    enum Etats : uint8_t
    {
      ATTENTE,
      DEMARRAGE,
      REPONSE,
      LECTURE_BIT,
      VERIFICATION
    } machineAEtatDHT; // État actuel de la machine à états

    bool readSensor(); // Fonction interne pour lire le capteur

  public:
    /// @brief Initialise le capteur DHT11
    /// @param broche Numéro de la broche Arduino à laquelle est connecté le capteur
    void init(uint8_t broche);

    /// @brief À appeller régulièrement pour mettre à jour la machine à état
    void update() {readSensor();}

    /// @brief Lit la valeur d'humidité du capteur
    /// @return uint8_t Valeur d'humidité en pourcentage
    unsigned char readHumidity();

    /// @brief Lit la valeur de température du capteur
    /// @return int8_t Valeur de température en degrés Celsius
    signed char readTemperature();
};

#endif //dht_h
Fichier programme : dht11.cpp
#include "dht11.h"

bool DHT11::readSensor()
{
  uint32_t tempsActuel = millis();

  switch (machineAEtatDHT)
  {
  case ATTENTE:
    if(tempsActuel - _derniereLecture >= 1000) // temps min d'échantillonnage du DHT11
    {
      machineAEtatDHT = DEMARRAGE;
      _tempsDebut = tempsActuel;
      digitalWrite(_pinCapteur, LOW);
      pinMode(_pinCapteur, OUTPUT);
      Serial.println(F("Démarrage!"));
    }
    else return false;
    break;
  case DEMARRAGE:
    // Envoyer le signal de démarrage pendant 18ms
    if (tempsActuel - _tempsDebut >= 18)
    {
      pinMode(_pinCapteur, INPUT_PULLUP);
      machineAEtatDHT = REPONSE;
      _tempsDebut = micros();
    }
    break;

  case REPONSE:
    // Attendre la réponse du capteur
    if (micros() - _tempsDebut > 100)
    {
      machineAEtatDHT = LECTURE_BIT;
      _indexBit = 0;
      _tempsDebut = micros();
      Serial.println(F("Réponse!"));
    }
    break;

  case LECTURE_BIT:
    // Lire les 40 bits de données
    if (digitalRead(_pinCapteur) == HIGH)
    {
      uint32_t duree = micros() - _tempsDebut;
      if (duree > 100)
      {
        machineAEtatDHT = ATTENTE;
        return false;
      }
      _donnees[_indexBit / 8] <<= 1; // équivalent à <<, décalage d'un cran à gauche des bits
      if (duree > 40) _donnees[_indexBit / 8] |= 1; // Si la durée est supérieure à 40 microsecondes, le bit est un '1'
      _indexBit++;
      if (_indexBit == 40)
      {
        machineAEtatDHT = VERIFICATION;
        Serial.println(F("End lecture!"));
      }
      _tempsDebut = micros();
    }
    break;

  case VERIFICATION:
    /*
    effectue un "ET" bit à bit avec 0xFF (11111111 en binaire), pour
    ne garder que les 8 bits de poids faible du résultat de l'addition.
    parce que l'addition des 4 octets pourrait donner un résultat sur plus de 8 bits.
    la somme de contrôle n'utilise que 8 bits. donc par exemple
    - si l'addition donne 260 (100000100 en binaire)
    - 260 & 0xFF donne 4 (00000100 en binaire)
    */
    if (_donnees[4] == ((_donnees[0] + _donnees[1] + _donnees[2] + _donnees[3]) & 0xFF))
    {
      _humidite = _donnees[0];
      _temperature = _donnees[2];
      _derniereLecture = tempsActuel;
      machineAEtatDHT = ATTENTE;
      return true;
    }
    else
    {
      machineAEtatDHT = ATTENTE;
      return false;
    }

  default: break;
  }

  return false;
}

void DHT11::init(uint8_t broche)
{
  _pinCapteur = broche;
  _derniereLecture = _humidite = _temperature = 0;
  delay(2000); // temps nécessaire pour que le DHT11 soit prêt à la transmission de données après le démarrage de l'Arduino.
  machineAEtatDHT = ATTENTE;
}

unsigned char DHT11::readHumidity()
{
  readSensor();
  return _humidite;
}

signed char DHT11::readTemperature()
{
  readSensor();
  return _temperature;
}
Fichier d'exemple - main.cpp/main.ino
#include "dht11.h"

#define PIN_DHT 6

DHT11 dht11;

unsigned char lastHumidite = 0; // uint8_t
signed char lastTemperature = 0; // int8_t
unsigned char humidite = dht11.readHumidity(); // uint8_t
signed char temperature = dht11.readTemperature(); // int8_t

void afficherDonnees(void)
{
  Serial.print("Humidité : ");
  Serial.print(humidite);
  Serial.print("%, Température : ");
  Serial.print(temperature);
  Serial.println("°C");
}

void setup(void)
{
  Serial.begin(115200);
  dht11.init(PIN_DHT);
  Serial.println("DHT11 Test");
  // Lecture des valeurs actuelles
  humidite = dht11.readHumidity(); // uint8_t
  temperature = dht11.readTemperature(); // int8_t
  afficherDonnees();
}

void loop(void)
{
  dht11.update();

  humidite = dht11.readHumidity(); // uint8_t
  temperature = dht11.readTemperature(); // int8_t

  // verif si les valeurs ont changé
  if (humidite != lastHumidite || temperature != lastTemperature) {
    afficherDonnees();

    lastHumidite = humidite;
    lastTemperature = temperature;
  }
}

Sauf que... Normalement je n'ai rien oublié, mais dans le moniteur série, je vois des print de débug en continus, ce qui normalement n'est censé arriver que toutes les 1s (fréquence d'échantillonnage maximale du DHT11 de 1 Hz). Donc je sors beaucoup trop souvent de ATTENTE, comme si if(tempsActuel - _derniereLecture >= 1000) était toujours vrai ?!

Démarrage!
Réponse!
End lecture!
Démarrage!
Réponse!
End lecture!
Démarrage!
Réponse!
End lecture!
...

Pourtant, les valeurs d'humidité et de température ne change jamais et son toujours à 0... Sauf si je vire les print de débug dans la bibliothèque (un print, c'est long donc ça doit fausser les temps de la machine à état!), là de temps en temps j'ai l'affichage de valeur de température et d'humidité, genre ça :

DHT11 Test
Humidité : 0%, Température : 0°C     <--------  dans le setup tout est à 0
Humidité : 10%, Température : 17°C     <--------  alors qu'il fait 24°C ?!
Humidité : 0%, Température : 0°C     <--------  et après ça repasse à 0 ?

Sans analyseur logique va falloir que je me débrouille avec des print, c'est compliqué mais pour l'instant je sèche... Auriez vous une idée sur

Donc je sors beaucoup trop souvent de ATTENTE, comme si if(tempsActuel - _derniereLecture >= 1000) était toujours vrai ?!

Merci d'avance!

Cordialement
Pandaroux007

bravo :slight_smile:

mais ce n'est pas toujours bien quand on a un timing précis à respecter

c'est parce que ces composants sont sensibles aux durées:

  • L'impulsion de démarrage doit avoir une durée minimale de 18 millisecondes pour que le capteur puisse commencer à transmettre des données.
  • Après l'impulsion de démarrage, la ligne de données doit être maintenue basse pendant au moins 20 à 40 microsecondes par le microcontrôleur pour permettre au capteur de préparer les données.
  • Le capteur DHT11 répondra à l'impulsion de démarrage en tirant la ligne de données basse pendant 80 microsecondes, suivie d'une impulsion de 80 microsecondes tirant la ligne de données haute pour indiquer qu'il est prêt à transmettre des données.
  • Le capteur envoie les données sous forme de 40 bits consécutifs. Chaque bit est envoyé en tirant la ligne de données basse pendant 50 microsecondes pour représenter un "0", suivi d'une période où la ligne de données est maintenue haute pendant différentes durées pour représenter un "1".
  • Après avoir transmis les 40 bits de données, le capteur envoie un bit de checksum pour vérifier l'intégrité des données.

il faut que votre machine à état traite correctement ces durées ce qui veut dire qu'elle doit être appelée hyper souvent surtout pendant la réception des données. C'est là que tout se joue et vous pouvez peut être rater des bits...

sinon

vous devriez traiter cela dans la machine à état et ne pas avoir de délai de 2s lors de l'appel à init()


En C++ et en C, les noms de variables commençant par un underscore suivi d'une lettre majuscule ou d'un autre underscore sont réservés par le langage pour une utilisation spéciale. De plus, les noms de variables commençant par un underscore suivi d'une lettre minuscule sont également réservés dans le contexte global (c'est-à-dire en dehors de toute fonction ou classe).

Ici vous les utilisez dans une classe, donc c'est OK mais en pratique on évite.


Bonjour @J-M-L et merci pour votre réponse!

C'est pour un usage perso, donc ça me dérange pas de coder en fonction de ma bibliothèque. Mais oui en effet vous avez raison faut appeler update souvent :slight_smile:

Oui, c'est ce que j'ai essayé d'implémenter avec ma machine à état. Si on décortique le code ça donne :

case ATTENTE:
    if(tempsActuel - _derniereLecture >= 1000) // temps min d'échantillonnage du DHT11
    {
      machineAEtatDHT = DEMARRAGE;
      _tempsDebut = tempsActuel;
      digitalWrite(_pinCapteur, LOW);
      pinMode(_pinCapteur, OUTPUT);
      // Serial.println(F("Démarrage!"));
    }
    else return false;
    break;

L'état ATTENTE est là pour vérifier si on peut demander au capteur d'envoyer les données en fonction de son temps minimal d'échantillonnage (qui est de 1Hz selon la datasheet). Si la fonction est appelée alors que la condition est vraie, alors on commence à envoyer un signal bas sur la pin, et on passe en mode DEMARRAGE.


  case DEMARRAGE:
    // Envoyer le signal de démarrage pendant 18ms
    if (tempsActuel - _tempsDebut >= 18)
    {
      pinMode(_pinCapteur, INPUT_PULLUP);
      machineAEtatDHT = REPONSE;
      _tempsDebut = micros();
    }
    break;

Si, alors qu'on est dans le mode DEMARRAGE, on voit que depuis qu'on est passé à cet état il s'est écoulé 18ms, alors on passe en mode REPONSE. C'est là que selon vous je dois faire cela:

Je vais implémenter cela. Je n'avais pas vu ça dans les autres bibliothèques il me semble...


case REPONSE:
    // Attendre la réponse du capteur
    if (micros() - _tempsDebut > 100)
    {
      machineAEtatDHT = LECTURE_BIT;
      _indexBit = 0;
      _tempsDebut = micros();
      // Serial.println(F("Réponse!"));
    }
    break;

Bon, je pense que le code ici est assez explicite, mais je me rends compte que j'interprète mal la réponse du capteur... C'est p'tète ça qui fait mon bazar ? Ici je lis

Response: After receiving a request signal from the host, DHT11 sends a response signal to indicate that it is ready to transmit the sensor data. The response pulse is a logical LOW of 80 microseconds followed by a logical HIGH of 80 microseconds.

Et vous semblez confirmer, donc faut que je recode ça.


case LECTURE_BIT:
    // Lire les 40 bits de données
    if (digitalRead(_pinCapteur) == HIGH)
    {
      uint32_t duree = micros() - _tempsDebut;
      if (duree > 100)
      {
        machineAEtatDHT = ATTENTE;
        return false;
      }
      _donnees[_indexBit / 8] <<= 1; // équivalent à <<, décalage d'un cran à gauche des bits
      if (duree > 40) _donnees[_indexBit / 8] |= 1; // Si la durée est supérieure à 40 microsecondes, le bit est un '1'
      _indexBit++;
      if (_indexBit == 40)
      {
        machineAEtatDHT = VERIFICATION;
        //Serial.println(F("End lecture!"));
      }
      _tempsDebut = micros();
    }
    break;

Bon on passe ici, quand on a reçu la réponse du capteur à chaque tour. Si la pin du capteur est à l'état haut, on regarde si ça fait depuis trop longtemps, si c'est le cas on revient à l'état d'ATTENTE, sinon on décale d'un cran les bits de données déjà reçu et on y inscrit la valeur du bit suivant (interprété selon si il dure plus ou moins de 40µs - là encore je me suis trompé d'après l'article et vous même c'est 50µs!)


/*
    effectue un "ET" bit à bit avec 0xFF (11111111 en binaire), pour
    ne garder que les 8 bits de poids faible du résultat de l'addition.
    parce que l'addition des 4 octets pourrait donner un résultat sur plus de 8 bits.
    la somme de contrôle n'utilise que 8 bits. donc par exemple
    - si l'addition donne 260 (100000100 en binaire)
    - 260 & 0xFF donne 4 (00000100 en binaire)
    */
    if (_donnees[4] == ((_donnees[0] + _donnees[1] + _donnees[2] + _donnees[3]) & 0xFF))
    {
      _humidite = _donnees[0];
      _temperature = _donnees[2];
      _derniereLecture = tempsActuel;
      machineAEtatDHT = ATTENTE;
      return true;
    }
    else
    {
      machineAEtatDHT = ATTENTE;
      return false;
    }

Une fois qu'on a reçu les 40 bits de données, on passe ici, à l'étape de vérification. On additionne les 4 premiers octets, puis on soumet le résultat à un & logique avec la valeur 0xFF (donc 11111111 en binaire) pour éliminer les bits de poids fort - qui ne sont pas pris en compte par somme de contrôle (qui elle est sur 8 bits - c'est le dernier octet transmis).

Si la comparaison entre le résultat et la somme de contrôle est bon, on enregistre les nouvelles valeurs dans les variables d'humidité et de température, mais si c'est pas bon, on ne change pas les valeurs. Comme ça, si l'utilisateur appelle readHumidity ou readTemperature, on passe les dernières valeurs correctes, et non des valeurs erronées.

Bon, à expliquer tout ça, je me rends compte de certaines erreurs... Faut que j'corrige ça :wink:


Je n'ai à le faire qu'une seule fois à l'initialisation normalement non ? C'est pour ça que je l'ai mis dans la fonction d'initialisation - je l'ai lu ici :

wait_for_dht11() – provides a delay of two seconds. This time is required to get DHT11 ready for data transmission after its host (Arduino) boots. If this delay is not provided, the DHT11 sensor will not respond to the host controller and, in initial attempts to read the sensor data from DHT11, there will be a timeout error.


Okey ça marche - je changerai ça dés que j'aurai le temps, là je ne suis pas là pour deux semaines :exploding_head: Ça va me faire une détox sévère, mais c'est bon aussi de partir un coup, je corrigerai toutes les âneries que j'ai fait dans le code dés mon retour :+1:

Merci encore pour votre aide!
Cordialement
Pandaroux007

:wink:

oui il faut s'aérer la tête de temps en temps !

bonnes vacances

Un programme pour lire un dht11/22 est sensible a la vitesse de lecture des gpio.
Avec un micro avr il n’est pas envigeable d’utiler la fonction Wiring/Arduino digitalRead() . Je deconseille également d’utiliser une mesure de temps comm millis().

Ce qu’il faut garder a l’esprit c’est que ce n’est pas une mesure de temps qu’il faut, c’est une comparaison entre deux ”images” de temps.
Les dht sont des circuits rapides et avec un machine a etat pour que le micro puissent faire d’autres choses, la justesse de la mesure sera très dépendante de ces autres choses.

Il faut que la récupération de l’ ”image” des deux temps soit la plus rapide possible.

Celle que j’avais réalisé était deux simples compteurs, un pour le temps passé dans le zero et l’autre pour le 1,incrémentés a chaque tour de boucle while.

C’était simplement les compteurs de timeout de la bibliothèque d’origine qu’il suffisait de comparer.

Bonjour à vous @J-M-L et @68tjs, et merci pour vos réponses

Avec du recul et vos posts je me rends compte, en effet, qu'une machine à état n'est pas adaptée dans le contexte. Mais cela veut-il dire que je suis contrains d'utiliser une bibliothèque pour DHTxx dite non bloquante mais qui l'est, manifestement, en réalité ?

Désolé, mais je ne suis pas sûr de comprendre votre allusion à des "images" de temps... De même que je ne sait pas comment faire pour créer une bibliothèque DHT (11 uniquement pour l'instant) qui soit véritablement non bloquante (j'ai besoin d'une exécution extrêmement rapide pour mon programme, donc même des delayMicroseconds posent problèmes - je ne parle même pas des whiles!). Pourriez vous développer plus avant votre raisonnement ?

Merci d'avance!
Cordialement
Pandaroux007

Tout est "bloquant" pendant un certain temps, ce qu'il faut c'est ne pas bloquer plus que de nécessaire.

Imaginez que vous devez faire clignoter 2 LEDs en alternance. Le design de base

const int led1 = 2;
const int led2 = 3;

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
}

void loop() {
  digitalWrite(led1, HIGH);
  digitalWrite(led2, HIGH);
  delay(500);

  digitalWrite(led1, LOW);
  digitalWrite(led2, LOW);
  delay(500);
}

est bloquant.

Est-ce un souci ? Non si vous n'avez rien d'autres à faire.

Mais si vous avez un autre truc à tester vous savez que vous avez 500ms où "ça coince".

➜ C'est là que vous passez à une machine à état.

ETEINT ➜ LED1 et LED2 ON ➜ 500ms ➜ LED1 et LED2 OFF ➜ 500ms ➜ ETEINT

Mais vous viendrait il à l'idée de n'allumer qu'une seule LED dans la machine à états puis à l'appel suivant d'allumer l'autre LED avant de passer à l'état d'attente ?

ETEINT ➜ LED1 ON ➜ LED2 ON ➜ 500ms ➜ LED1 OFF ➜ LED2 OFF ➜ 500ms ➜ ETEINT

➜ la réponse est sans doute non.. parce que vous vous dites que deux digitalWrite() ce n'est pas si couteux et que vous voulez les allumer "en même temps"... mais l'utilisateur ne verrait pas les quelque microsecondes en plus si vous preniez le second design.

En choisissant la première machine à état, vous avez pris une décision de design qui est que votre code est "bloquant" pour l'appel de la fonction, puis un switch et deux digitalWrite au lieu d'un seul...

est-ce que ces quelques 4 microsecondes du second digitalWrite sont critiques ? peut-être que si vous étiez revenus 4 µs avant au code principal cela changerait la donne pour une autre partie du code...

Le souci de fond et la question à vous poser quand vous gérez un processus de manière asynchrone c'est de savoir si la fréquence à laquelle vous allez vérifier/faire évoluer l'état du processus est compatible avec la vitesse d'évolution du processus et du reste du système.

Si ce n'est pas le cas, il faut remonter d'un niveau de granularité.

Dans le cas de la DHT11, la contrainte temporelle fait que ce n'est pas compatible avec une boucle potentiellement "lente" et donc le choix du design de la granularité de la machine à états doit en tenir compte.