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 }