Lire les capteurs environnementaux

L’ensemble des sketchs de notre projet peuvent être téléchargés ici.

Le sketch « read_sensors.ino » montre comment interroger les capteurs d’humidité, de température, de pression et de CO2. Commençons par les déclarations globales :

010 /*----------------------------------------------------------------------------*/
011 /* Variables globales, includes, defines                                       */
012 /*----------------------------------------------------------------------------*/
013 #define MAIN_LOOP_DELAY (10000) // Temporisation de la boucle principale (10s)
014 #define SET_REC_MOD (1) // Sur-période de mesures et enregistrements
015 uint32_t rec_mod;
016 #define STLK_BDRATE (230400) // Débit du port série du ST-LINK
017 
018 // Structure pour les données mesurées
019 struct Measure {
020   char datetime[18];
021   float temperature;
022   uint16_t pressure;
023   uint8_t humidity;
024   uint16_t co2;
025 };
026 
027 Measure measure; // Variable de type Measure
028 
029 #include <Wire.h> // Référence pour le bus I2C1
030 
031 // Références pour les capteurs du X-NUCLEO IKS01A2
032 #include <HTS221Sensor.h>
033 #include <LPS22HBSensor.h>
034 
035 // Pointeurs vers les classes des capteurs
036 HTS221Sensor *HT;
037 LPS22HBSensor *PT;
038 
039 // Offsets pour les capteurs (constantes)
040 #define TEMP_OFFSET (0.0) // en Celsius
041 #define PRES_OFFSET (-0.5) // en hPa
042 #define HUMI_OFFSET (-3) // en % 
043 #define ALTITUDE (487) // en m
044 
045 // Référence pour la bibliothèque des capteurs du SCD30
046 #include "SCD30.h"
047 
048 // Offset et paramètre pour le capteur (constantes)
049 #define AUTO_CALIBRATION false
050 #define CO2_OFFSET (0)
051 
052 // Pointeur vers la classe du capteur
053 SCD30 co2Sensor;

Les paramètres de l’application sont déclarés en tête du sketch via des directives de préprocesseur #define 1 . Ainsi, vous pourrez aisément modifier leurs valeurs par la suite si nécessaire. Rappelons que #define indique au préprocesseur, un agent logiciel qui intervient avant la compilation, qu’il doit effectuer un « rechercher / remplacer » sur l’ensemble du code.

Par exemple, lorsqu’il rencontre « #define MAIN_LOOP_DELAY (10000) », le préprocesseur recherche toutes les occurrences de la chaîne de caractères « MAIN_LOOP_DELAY » et les remplace par la chaîne de caractères « (1000) ». Au moment de la compilation cette valeur sera traduite dans le firmware en la constante numérique entière « 1000 ».

Notre application comportera une boucle principale sans clause de fin (dans la fonction loop) que l’on mettra en pause à chaque itération, pour une durée MAIN_LOOP_DELAY. Cette boucle devra fixer les fréquences (potentiellement différentes) auxquelles la station réalisera différentes opérations telles que mesurer, communiquer en Wi-Fi, enregistrer sur la carte SD, remettre à l’heure la RTC …

Les lignes de code 16 à 19 déclarent les variables et constantes pour réaliser cette gestion modulaire du temps pour une station qui ne réalise à ce stade que la lecture des capteurs environnementaux :

  • La boucle principale fera une pause de MAIN_LOOP_DELAY = 10000 ms entre deux itérations
  • Les mesures ne se feront qu’une fois toutes les SET_REC_MOD = 1 itérations de la boucle principale
  • Les variables rec_mod et main_loop_delay seront expliquées avec la suite du code.

Les mesures de température, humidité, pression et CO2 seront rassemblées dans une structure Measure définie aux lignes 19 à 25. Ensuite, en ligne 27, nous déclarons une variable measure de type Measure que nous allons renseigner dans la boucle principale. Rassembler plusieurs variables dans une struct présente au moins deux avantages : 1) cela vous aidera à mieux structurer votre code, à le rendre plus lisible et plus facile à maintenir et 2) ceci assurera potentiellement de meilleures performances au firmware2.

Les directives de préprocesseur #include donnent au sketch l’accès aux classes qui permettront de piloter les capteurs. Ces classes sont localisées dans les dossiers des bibliothèques Arduino que vous avez installées, à l’intérieur de fichiers « .h » et « .cpp ». Par exemple les lignes 32-33 donnent les références des classes pilotes des MEMS HTS221 et LPS22HB du shield X-NUCLEO IKS01A2, respectivement contenues dans les fichiers « HTS221Sensor.h » et « LPS22HBSensor.h ».

Les lignes 36-37 définissent des pointeurs vers ces classes3, qui permettront plus tard de les instancier, de lire les mesures des capteurs et de les récupérer dans la mémoire du microcontrôleur. Toutes ces notions (#defines, #includes, classes, instances, pointeurs, etc.) sont des fondamentaux de la programmation en c/c++, une littérature abondante est à votre disposition sur Internet si vous souhaitez les approfondir.

Rappelons quelques bonnes pratiques de codage. Lorsque vous écrirez un sketch, pour que sa maintenance soit aisée, veillez à donner aux paramètres, aux variables et aux constantes des noms explicites. Pour cette même raison, indentez correctement le code en abusant de la combinaison de touches + dans l’IDE Arduino et **commentez-le** au maximum. Utilisez la balise ouvrante « // » si le commentaire tient sur une seule ligne, placez-le entre les balises « /* » et « */ » autrement.

Le sketch se poursuit avec la fonction setup qui procède aux initialisations de variables et de périphériques :

058 void setup() {
059 
060   // Initialise le port série du ST-LINK
061   Serial.begin(STLK_BDRATE);
062   while (!Serial) delay(100);
063   
064   Serial.println("\nST-LINK serial baud rate set to " + String(STLK_BDRATE));
065 
066   // Initialise la LED utilisateur
067   pinMode(LED_BUILTIN, OUTPUT);
068 
069   // Modulo pour les mesures
070   rec_mod = SET_REC_MOD;
071 
072   // Initialise le bus I2C1 (par défaut sous Arduino)
073   Wire.begin();
074 
075   // Initialise les capteurs environnementaux
076   Initialize_Sensors(&Serial);
077 }

La fonction Initialize_Sensors contient le code de configuration et de démarrage des capteurs :

120 void Initialize_Sensors(HardwareSerial *serial) {
121 
122   HT = new HTS221Sensor (&Wire); // Création d'une instance de la classe "HTS221Sensor"
123   HT->Enable(); // Active le capteur HTS221
124   serial->println("HTS221 temperature sensor started.");
125 
126   PT = new LPS22HBSensor(&Wire); // Création d'une instance de la classe "LPS22HBSensor"
127   PT->Enable(); // Active le capteur LPS22HB
128   serial->println("LPS22HB temperature sensor started.");
129 
130   // Démarrage du capteur de CO2, calibration de celui-ci si requis
131   if (!co2Sensor.begin(Wire, AUTO_CALIBRATION)) { // Gestion d'erreur...
132     serial->println("CO2 sensor not detected. Please check wiring. Freezing...");
133     while (true); // Si le capteur est muet, bloque l'exécution du firmware ici
134   }
135 
136   // Paramétrage du capteur de CO2
137   co2Sensor.setMeasurementInterval(MAIN_LOOP_DELAY * SET_REC_MOD / 1000);
138   float offset = co2Sensor.getTemperatureOffset();
139 
140   co2Sensor.setTemperatureOffset(offset);
141   serial->println("SCD30 CO2 sensor started.");
142 }

Nous remarquons que cette fonction prend comme argument le pointeur *serial vers un objet de type HardwareSerial. Ceci nous permettra de changer si nécessaire le port série qui fait remonter les messages qu’elle renvoie sans devoir la modifier. Nous y reviendrons un peu plus loin.

Du fait que l’IDE Arduino ne propose pas encore l’auto-complétion, le seul moyen pour connaitre les membres des classes qui pilotent les capteurs ainsi que le type de leurs arguments consiste à aller lire leur code source dans les fichiers de leurs bibliothèques. Par exemple, la méthode getTemperatureOffset du capteur SCD30 est disponible dans les fichiers « SCD30.h » et « SCD30.cpp » qui précisent qu’elle ne prend pas d’argument et renvoie un float.

La fonction loop contient la boucle principale du futur firmware :

082 loop void () {
083 
084   // Allume la LED utilisateur
085   digitalWrite(LED_BUILTIN, HIGH);
086 
087   // Mesures effectuées toutes les SET_REC_MOD itérations
088   if (rec_mod == SET_REC_MOD) {
089 
090     // Acquisition de la temp., de la pression, de l'humidité et de la conc. en CO2
091     Read_Sensors(&Serial);
092 
093     // Affichage des mesures sur le port série du ST-LINK
094     Serial.print("Temperature(C): ");
095     Serial.print(measure.temperature, 1);
096     Serial.print(" | Pression(hPa): ");
097     Serial.print(measure.pressure);
098     Serial.print(" | Humidite relative(%): ");
099     Serial.print(measure.humidity);
100     Serial.print(" | CO2(ppm): ");
101     Serial.print(measure.co2);
102     Serial.print('\n');
103 
104     rec_mod = 0;
105 
106   }
107 
108   rec_mod++;
109 
110   // Eteint la LED utilisateur
111   digitalWrite(LED_BUILTIN, LOW);
112 
113   // Suspend l'exécution de la boucle pendant MAIN_LOOP_DELAY ms
114   delay(MAIN_LOOP_DELAY);
115 }

On note l’utilisation de la variable rec_mod : elle est testée (ligne 88) puis incrémentée de 1 (ligne 108) à chaque « tour de loop ». Lorsque sa valeur atteint SET_REC_MOD, elle est remise à zéro (ligne 104). Cette technique permet d’appeler la fonction Read_Sensors tous les SET_REC_MOD tours de boucle. On réalise ici l’équivalent de la fonction mathématique modulo, avec le compteur rec_mod et un test conditionnel if. Du fait que SET_REC_MOD = 1, elle peut vous sembler futile, mais nous lui attribuerons une valeur plus grande lorsque la boucle principale effectuera d’autres actions que Read_Sensors.

La temporisation de la boucle principale est réalisée grâce à l’instruction delay à la ligne 114. Lorsqu’elle est exécutée, le déroulement du firmware est suspendu pendant MAIN_LOOP_DELAY millisecondes. Cette pause est programmée de telle sorte que le microcontrôleur passera quasiment toute son énergie juste pour « attendre ». Vous l’aurez compris, l’instruction delay doit autant que possible rester inutilisée. Nous présenterons plus tard des alternatives plus efficaces.

Etudions à présent le code de la fonction Read_Sensors :

147 void Read_Sensors(HardwareSerial *serial) {
148 
149   float temperature, pressure, humidity;
150   HTS221StatusTypeDef errHTS221;
151   LPS22HBStatusTypeDef errLPS22HB;
152 
153   errHTS221 = HT->GetTemperature(&temperature); // Lecture de la température
154   if (errHTS221 != HTS221_STATUS_OK || !temperature) { // Gestion d'erreur...
155     serial->println("HTS221 temperature sensor failure. Freezing...");
156     while (true); // En cas d'erreur de lecture, bloque l'exécution du firmware ici
157   }
158 
159   errLPS22HB = PT->GetPressure(&pressure); // Lecture de la pression
160   if (errLPS22HB != LPS22HB_STATUS_OK || !pressure) { // Gestion d'erreur...
161     serial->println("LPS22HB pressure sensor failure. Freezing...");
162     while (true); // En cas d'erreur de lecture, bloque l'exécution du firmware ici
163   }
164 
165   errHTS221 = HT->GetHumidity(&humidity); // Lecture de l'humidité
166   if (errHTS221 != HTS221_STATUS_OK || !humidity) { // Gestion d'erreur...
167     serial->println("HTS221 humidity sensor failure. Freezing...");
168     while (true); // En cas d'erreur de lecture, bloque l'exécution du firmware ici
169   }
170 
171   // Mémorise les mesures dans les membres de la structure "measure"
172   measure.temperature = temperature + (float)TEMP_OFFSET;
173   measure.pressure = (uint16_t)(Corr_Pres(pressure + (float)PRES_OFFSET, ALTITUDE));
174   measure.humidity = (uint8_t)(humidity + (float)HUMI_OFFSET);
175 
176   // Paramétrage du capteur de CO2 (correction de pression absolue)
177   co2Sensor.setAmbientPressure((uint16_t)(pressure + (float)PRES_OFFSET));
178 
179   while (!co2Sensor.dataAvailable()); // Attend que le capteur renvoie une valeur
180 
181   uint16_t co2 = co2Sensor.getCO2(); // Lecture de la concentration de CO2
182   if (!co2) { // Gestion d'erreur : si la concentration lue de CO2 égale 0 ppm
183     serial->println("CO2 sensor failure. Freezing...");
184     while (true); // En cas d'erreur de lecture, bloque l'exécution du firmware ici
185   }
186 
187   // Mémorise la concentration de CO2 dans le membre "co2" de la structure "measure"
188   measure.co2 = co2 + (uint16_t)CO2_OFFSET;
189 }

Une bonne partie de celui-ci sert à décider que faire si un capteur est muet, ou bien s’il renvoie des mesures absurdes. Par exemple, les lignes 153 à 157 gèrent une potentielle erreur de lecture de la température. Le cas échéant un message est d’abord renvoyé sur le port série serial (ligne 155) : serial->println(…). Ensuite (ligne 156) l’exécution du firmware est bloquée par une boucle while(true) sans clause de sortie.

Il est important de prévoir un nombre raisonnable de messages explicites sur un port série pour contrôler que le déroulement du programme est bien celui que vous imaginiez et pour aider à comprendre ce qui ne va pas dans le cas contraire. Avec une IDE comme celle d’Arduino, qui n’autorise pas le débogage interactif, cette pratique vous fera gagner beaucoup de temps.

Nous avons fait le choix d’envoyer les messages sur le port série qui passe par le connecteur USB du ST-LINK démarré à la ligne 68. Ce port série par défaut est désigné par la classe Serial. Remarquez que lorsque nous l’utilisions aux lignes 101 à 109, l’appel aux méthodes de Serial se faisait avec un opérateur « . ». Par exemple à la ligne 97 : Serial.print(measure.pressure) pour afficher la mesure de pression en appelant la méthode « print ». En revanche, dans la fonction Read_Sensors, l’appel des méthodes se fait par l’opérateur « - > », par exemple à la ligne 155 : serial->println(“HTS221 temperature sensor failure. Freezing…”).

La convention d’appel change car le « serial » qui apparaît ici n’a rien à voir avec la classe Serial précédente. La ligne 156 montre que « serial » est en fait un argument qui est passé à la fonction Read_Sensors :

void Read_Sensors(HardwareSerial *serial)

HardwareSerial *serial désigne un pointeur (une adresse) vers un objet de type HardwareSerial. Lorsqu’une classe est référencée de cette façon (par son adresse) C++ impose que l’accès à ses membres soit réalisé avec « - > » et non avec « . ».

Pourquoi compliquer ainsi l’écriture ? Justement pour être en mesure de passer en argument le port série utilisé de sorte à pouvoir éventuellement le changer sans avoir à modifier le code. La ligne 76 confirme que c’est effectivement le port série du ST-LINK qui est passé en argument (par adresse, d’où le « & » devant) :

Read_Sensors(&Serial)

Nous pourrons par la suite rediriger les messages de Read_Sensors sur un autre UART, par exemple celui connecté au module Bluetooth HC-06, simplement en remplaçant &Serial par &Bluetooth_Serial.

Le reste du code, notamment le paramétrage des capteurs, est immédiatement inspiré des exemples disponibles avec leurs bibliothèques de pilotes Arduino / STM32duino. La mesure de pression est corrigée en intégrant la variation de pression due à l’altitude du lieu pour afficher la pression équivalente rapportée au niveau de la mer comme le font les stations météo professionnelles :

194 float Corr_Pres(float pressure, float altitude ) {
195   return pressure * pow(1.0 - (altitude * 2.255808707E-5), -5.255);
196 }
  1. Attention, dans votre sketch, il ne faudra jamais terminer une ligne de directive de préprocesseur (#define, #include, #…) par un « ; » ! 

  2. Sur une architecture ARM et avec C/C++ il faut autant que possible limiter à trois le nombre d’arguments que l’on passe aux fonctions. Jusqu’à trois, le Cortex mémorise les arguments dans ses registres. Au-delà il est contraint de recourir à sa pile, plus lente. Il est donc intéressant de passer plusieurs variables à l’intérieur de structures pour réduire le nombre d’arguments. 

  3. C’est-à-dire des adresses vers les emplacements où le code binaire des classes et localisé dans la mémoire du microcontrôleur.