Enregistrer sur une carte SD

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

Le sketch « write_sd.ino », construit sur la base de « read_sensors.ino », montre comment gérer un module pour carte SD mais aussi l’interruption du bouton utilisateur et comment se débrasser de la fonction delay. Quelques lignes s’ajoutent aux déclarations globales :

062 // Pour une gestion du temps avec millis()
063 uint32_t currentTime = 0;
064 uint32_t previousTime = 0;
065 
066 // Déclarations pour le module carte SD
067 #include <SPI.h> // Pilote du contrôleur de bus SPI
068 #include <SD.h> // Pilote du module carte SD
069 #define SDCARD_SSEL (PC7) // Chip select pin
070 
071 File logfile; // Fichier pour enregistrer les mesures
072 char logfilename[] = "DATALOG.CSV"; // Nom du fichier
073 
074 // Tables tampon pour écriture sur la carte SD
075 #define RECORD_PAYLOAD_SIZE (165)
076 char Record_Payload[RECORD_PAYLOAD_SIZE] = {0};
077 
078 // Switch de gestion de l'interruption
079 volatile bool sd_card_listing = false;

Les lignes 62-63 déclarent deux variables pour réaliser un temps d’attente non bloquant remplaçant delay.

Le module pour carte SD est géré par le contrôleur de bus SPI d’où la déclaration de ses pilotes en ligne 68. On décide également de la broche qui fera office de chip select pour le module carte SD : PC7 d’après la ligne 69.

En ligne 79, la variable globale booléenne sd_card_listing est déclarée avec l’attribut volatile, très important en programmation embarquée, et dont vous devez bien comprendre la raison d’être.

La variable sd_card_listing sera par la suite modifiée par la routine de service de l’interruption (ISR) du bouton utilisateur, la fonction Button_Down_ISR1. Cette ISR ne sera donc jamais appelée par les fonctions setup ou loop du programme principal, c’est le NVIC qui se chargera de l’invoquer chaque fois que le bouton utilisateur sera pressé.

Il se trouve que le compilateur gcc analyse le code du sketch et, généralement, l’optimise avant de le traduire en code binaire pour le firmware. Entre autres optimisations, il identifie toutes les fonctions/variables qui ne sont pas explicitement appelées/modifiées par le programme principal et il les considère comme du code « mort », présent dans le sketch mais de son point de vue jamais utilisé. En conséquence, il ne traduit pas ces lignes de code dans le firmware afin de réduire la quantité de mémoire que celui-ci occupe.

Les ISR et les variables globales qu’elles modifient seront donc systématiquement considérées comme du code mort par le compilateur. De ce fait, pour empêcher qu’elles soient supprimées par celui-ci, il faut lui signaler explicitement qu’elles ne doivent pas faire l’objet d’optimisations. C’est exactement à cela que sert le mot clef volatile lorsqu’il est placé devant une variable modifiée par une ISR. Cette précaution préserve à la fois la variable concernée et les ISR qui la modifient.

La fonction setup gagne également quelques lignes :

103   // Initialise le bouton utilisateur
104   pinMode(USER_BTN, INPUT_PULLUP);
105 
106   // On attache une interruption au bouton USER pour gérer son appui
107   attachInterrupt(digitalPinToInterrupt(USER_BTN), Button_Down_ISR, LOW );
108 
109   // Initialise le module carte SD
110   Initialize_SD_card(&Serial);
111 
112   // Effacement d'un éventuel ancien fichier de logs sur la carte SD
113   LogFile_Delete(logfilename, &Serial);
114 
115   // Création d'un nouveau fichier de logs sur la carte SD
116   LogFile_CreateOpen(logfilename, &Serial);

Elle fait appel à quatre nouvelles fonctions :

  • Button_Down_ISR, la routine de service de l’interruption du bouton utilisateur. La ligne 107 active et configure l’interruption du bouton pour qu’elle survienne lorsqu’il est enfoncé (dans l’état LOW d’après la fiche technique de notre NUCLEO-L476RG) ;
  • Initialize_SD_card qui configurera le module carte SD et le fichier de logs sur lequel on écrira ;
  • LogFile_Delete qui effacera un éventuel fichier de logs déjà présent la carte SD ;
  • LogFile_CreateOpen qui créera un nouveau fichier de logs sur la carte SD et l’ouvrira en écriture.

Le code de Button_Down_ISR est minimaliste, conformément aux bonnes pratiques de programmation des routines de service des interruptions :

346 void Button_Down_ISR(void) {
347   sd_card_listing = true;
348 }

Cette ISR se limite à mettre à « true » la variable sd_card_listing. C’est ensuite la boucle principale loop qui appellera une autre fonction après un test de sd_card_listing. Les ISR doivent toujours être aussi concises que possible afin que le NVIC soit libéré au plus vite et disponible pour le cas où il aurait à traiter une nouvelle interruption une fraction de seconde après. Toujours dans le souci de préserver la réactivité du microcontrôleur, il est fortement déconseillé d’utiliser des instructions bloquantes telles que delay dans une ISR.

Le code de Initialize_SD_card montre comment réassigner une broche du contrôleur du bus SPI1 en utilisant l’API STM32duino :

262 void Initialize_SD_card(HardwareSerial * serial) {
263 
264   // Change la broche assignée à la ligne SPI CLK (horloge)
265   SPI.setSCLK(PB3); // Nb : par défaut la broche est PA5
266 
267   /* Démarre le bus SPI et initialise le module SD sur la broche SDCARD_SSEL
268     pour le chip select */
269   if (!SD.begin(SDCARD_SSEL)) {
270     serial->println("SD card failed, or not present");
271     while (true);
272   }
273   serial->println("SD card module initialized");
274 }

Cette opération est réalisée à la ligne 265 ; elle est documentée ici. Il est important qu’elle précède l’instruction qui démarre le contrôleur du bus SPI1, laquelle est « dissimulée » dans l’instruction SD.begin(SDCARD_SSEL) de la ligne 269.

Pourquoi avoir changé la broche de sortie du signal d’horloge du contrôleur du bus SPI1 ?
Pour ne pas mettre en panne la fonction première de la broche PA5 qui, d’après le mappage des broches de la NUCLEO-L475RG, pilote la LED utilisateur. Autrement dit, si nous avions démarré SP1 sans avoir pris cette précaution, nous n’aurions plus pu contrôler la LED ultérieurement dans la boucle principale.

Comment avons-nous déterminé que PB3 était éligible pour remplacer PA5 en tant que SPI1 SCLK ? Tout simplement, et encore, grâce aux schémas de mappage des broches de la NUCLEO-L475RG.

Les codes de LogFile_Delete et LogFile_CreateOpen sont des applications immédiates de la bibliothèque « SD Library » de l’API Arduino (voir ce tutoriel ici) :

334 void LogFile_Delete(char* logfilename, HardwareSerial * serial) {
335 
336   if (SD.exists(logfilename)) {
337     // Efface le fichier logfile
338     SD.remove(logfilename);
339     serial->println("Previous " + (String)logfilename + " deleted");
340   }
341 }

279 void LogFile_CreateOpen(char* logfilename, HardwareSerial * serial) {
280   // Ouvre le fichier logfile en écriture
281   logfile = SD.open(logfilename, FILE_WRITE);
282   serial->println("File " + (String)logfilename + " available for data recording");
283 }

La fonction loop est modifiée comme suit :

122 void loop() {
123 
124   // Relève le nombre de millisecondes écoulées depuis le dernier reset
125   currentTime = millis();
126 
127   // Si MAIN_LOOP_DELAY ms se sont écoulées depuis le dernier appel à millis()...
128   if ((currentTime - previousTime) > MAIN_LOOP_DELAY) {
129 
130     previousTime = currentTime; // Pour l'itération suivante
131     
132     digitalWrite(LED_BUILTIN, HIGH);// Allume la LED utilisateur
133     
134     Read_Sensors(&Serial);// Interrogation des capteurs environnementaux
135 
136     // Affichage des mesures sur le port série du ST-LINK
137     Serial.print("Temperature(C): ");
138     Serial.print(measure.temperature, 1);
139     Serial.print(" | Pression(hPa): ");
140     Serial.print(measure.pressure);
141     Serial.print(" | Humidite relative(%): ");
142     Serial.print(measure.humidity);
143     Serial.print(" | CO2(ppm): ");
144     Serial.print(measure.co2);
145     Serial.print('\n');
146 
147     // Enregistrements effectués toutes les SET_REC_MOD itérations
148 
149     if (rec_mod == SET_REC_MOD) {
150 
151       // Construit un objet "String" nommé "payload" à partir des mesures
152       String payload = String(measure.temperature) + ";" + String(measure.pressure) + ";" + String(measure.humidity);
153 
154       // Envoie le contenu de payload dans le tableau de caractères Record_Payload
155       payload.toCharArray( Record_Payload, RECORD_PAYLOAD_SIZE );
156 
157       // Ecris le contenu de Record_Payload comme nouvelle ligne dans logfilename
158       SD_Write_Record(logfilename, Record_Payload);
159 
160       rec_mod = 0;
161 
162     }
163 
164     rec_mod++;
165 
166     // Eteint la LED utilisateur
167     digitalWrite(LED_BUILTIN, LOW);
168     
169   }
170 
171   if (sd_card_listing) { // Liste des enregistrements SD
172     LogFile_List(logfilename, &Serial);
173     sd_card_listing = false;
174   }
175 }

Premier changement : nous avons remplacé delay par les instructions des lignes 125, 128, 130. On commence par mémoriser le temps écoulé depuis le démarrage du microcontrôleur avec millis (ligne 125). Ensuite on lui soustrait le relevé de temps de l’itération précédente (ligne 128). Si le temps écoulé est supérieur à MAIN_LOOP_DELAY alors les lignes 130 à 168 sont exécutées. A la ligne 130, on copie le relevé de temps dans previousTime pour les besoins de la prochaine itération.

Deuxième changement : nous avons ajouté la gestion de l’interruption du bouton utilisateur aux lignes 171 à 174. Lorsqu’on appuie dessus, le NVIC suspend temporairement l’exécution de loop, lance Button_Down_ISR qui réalise sd_card_listing = true puis laisseloop poursuivre. Le test de la ligne 171 est alors validé et les instructions 172-173 sont exécutées. Le microcontrôleur appelle LogFile_List puis désarme le test (sd_card_listing = false) afin que LogFile_List ne soit réexécutée que si le bouton est enfoncé de nouveau.

Que ce serait-il passé si nous n’avions pas fait le premier changement et conservé l’instruction delay ? A chaque itération loop aurait été bloquée pendant MAIN_LOOP_DELAY millisecondes. Si un utilisateur avait décidé d’appuyer sur le bouton pendant cette pause, rien ne se serait produit avant qu’elle ait pris fin.

Cette gestion avec millis garantit donc la réactivité de notre station mais elle n’est pas plus efficace que l’instruction delay sur le plan de la consommation d’énergie. En effet le microcontrôleur reste occupé 100% du temps sur loop alors que la station n’a besoin de mesurer qu’une fois toutes les MAIN_LOOP_DELAY millisecondes et de réagir à l’appui sur le bouton utilisateur à de rares moments, impossibles à anticiper. Nous apporterons juste après ce paragraphe une solution à ces problèmes grâce aux mode « Low Power ».

Finissons par le code de la fonction LogFile_List. Comme LogFile_Delete et LogFile_CreateOpen il est directement inspiré des exemples fournis avec la bibliothèque « SD Library » et il n’appelle aucun commentaire particulier :

296 void LogFile_List(char* logfilename, HardwareSerial * serial) {
297 
298   if (logfile.size()) { // si le fichier n’est pas vide
299 
300     String buffer;
301 
302     // Ferme le fichier logfile initialement ouvert en écriture
303     logfile.close();
304 
305     // Ouvre le fichier logfilename en lecture
306     logfile = SD.open(logfilename, FILE_READ);
307 
308     // Lis le fichier ligne par ligne et les imprime sur serial
309     if (logfile) {
310       while (logfile.available()) {
311         buffer = logfile.readStringUntil('\n');
312         serial->println(buffer);
313         delay(5);
314       }
315     } else {
316       serial->println("error opening " + (String)logfilename);
317       while (true);
318     }
319 
320     // Ferme le ficher logfile, ouvert en lecture
321     logfile.close();
322 
323     // Ouvre le fichier logfile, en écriture
324     logfile = SD.open(logfilename, FILE_WRITE);
325   }
326   else {
327     serial->println((String)logfilename + " is empty");
328   }
329 }
  1. Nous avons déjà expliqué le principe de fonctionnement des interruptions ici