Sauvegarder des paramètres dans la mémoire Flash

A plusieurs reprises nous avons déclaré dans nos sketchs des paramètres « en dur » avec la directive de préprocesseur #define. Par exemple : les informations de connexion au réseau Wi-Fi, le jeton pour les services de Thingsboard, la clef d’accès à l’API OpenWeather, la position géographique de la station … sont tous codés avec #define.

En programmant de la sorte, si l’utilisateur de la station souhaite changer ultérieurement l’une de ces informations (s’il décide d’installer sa station ailleurs qu’au sommet de la tour Eiffel, par exemple) il devra modifier le sketch STM32duino, le recompiler et mettre à jour le firmware de la station.

Cette approche n’est pas élégante et nécessite de surcroit que l’utilisateur dispose d’une IDE Arduino et qu’il sache s’en servir. Nous préfèrerions lui offrir la possibilité de communiquer des paramètres à la station après que celle-ci a démarré et de les enregistrer dans sa mémoire persistante, de sorte qu’il ne soit pas nécessaire de recommencer ces opérations de paramétrage après chaque reset.

Le sketch « record_parameters.ino » détaille comment programmer ces fonctionnalités, en prenant comme exemple l’enregistrement des paramètres de la connexion au Wi-Fi au démarrage :

  1. Juste après un reset de la station, nous allons tester si le bouton utilisateur est maintenu enfoncé.
  2. Si tel est le cas, la station demandera à l’utilisateur de saisir le SSID et le mot de passe du réseau Wi-Fi, sur le port série du module Bluetooth. L’utilisateur devra donc s’y être préalablement appairé avec un PC, un smartphone … et s’y être connecté avec un logiciel terminal RS232.
  3. Une fois le SSID et le mot de passe reçus par la station, le microcontrôleur les enregistrera dans sa mémoire flash, qui ne s’effacera pas lorsque son alimentation électrique sera interrompue.
  4. Au prochain redémarrage, si le bouton utilisateur n’est pas enfoncé, le microcontrôleur ira lire les données préalablement écrites en flash et chargera en mémoire vive le SSID et le mot de passe, puis se connectera au Wi-Fi si ces informations ont été correctement saisies initialement.

Dans les déclarations globales nous avons apporté les modifications suivantes :

  • Nous avons ajouté « bool setup_completed = false ; » en ligne 43. Cette variable servira à modifier le comportement de la fonction processSerial selon qu’elle sera appelée pour paramétrer le Wi-Fi au démarrage ou pour envoyer des commandes à la station une fois la fonction loop en cours d’exécution.
  • Nous avons ajouté la ligne 212 « *#include ;* » qui correspond à la bibliothèque d’émulation d’une EEPROM dans la mémoire flash.
  • Nous avons modifié les déclarations des tableaux pour les paramètres de connexion au Wi-Fi :
0099 char ssid[MAX_LEN] = {0}; // Tableau tampon pour le SSID
0100 char pass[MAX_LEN] = {0}; // Tableau tampon pour le mot de passe

La fonction setup modifiée a désormais le code (complet) suivant :

0217 void setup() {
0218 
0219   // Initialise le port série du ST-LINK
0220   Serial.begin(STLK_BDRATE);
0221   while (!Serial) delay(100);
0222   Serial.println("\nST-LINK serial baud rate set to " + String(STLK_BDRATE));
0223 
0224   // Initialise la LED utilisateur
0225   pinMode(LED_BUILTIN, OUTPUT);
0226 
0227   // Initialise le bus I2C1 (par défaut sous Arduino)
0228   Wire.begin();
0229 
0230   // Démarre la RTC
0231   rtc.setClockSource(STM32RTC::LSE_CLOCK); // Choix de la source d'horloge
0232   rtc.begin();
0233 
0234   // Modulo pour les recalages de la RTC
0235   rtc_mod = SET_RTC_MOD;
0236 
0237   // Initialise les capteurs environnementaux
0238   Initialize_Sensors(&Serial);
0239 
0240   // Modulo pour les mesures des capteurs environnementaux
0241   rec_mod = SET_REC_MOD;
0242 
0243   // Initialise le bouton utilisateur
0244   pinMode(USER_BTN, INPUT_PULLUP);
0245 
0246   // Initialise le module carte SD
0247   Initialize_SD_card(&Serial);
0248 
0249   // Création d'un nouveau fichier de logs sur la carte SD
0250   LogFile_CreateOpen(logfilename, &Serial);
0251 
0252   // Initialise le port série du module HC-06
0253   BT_Serial.begin(BT_BDRATE);
0254   while (!BT_Serial) delay(100);
0255   Serial.println("Bluetooth serial baud rate set to " + String(STLK_BDRATE));
0256 
0257   // Autorise le module Bluetooth à interrompre le mode veille
0258   LowPower.enableWakeupFrom(&BT_Serial, BT_Serial_ISR);
0259   Serial.println("Bluetooth module ready to process commands");
0260   BT_Serial.println("Bluetooth module ready to process commands");
0261 
0262   // Initialise le module Wi-Fi
0263   Initialize_WiFi(&Serial);
0264 
0265   /* Mise à jour des informations de connexion WiFi via Bluetooth.
0266     Pour entrer les informations de connexion au WiFi il faut :
0267       1 - Maintenir le bouton user de la carte NUCLEO enfoncé au démarrage.
0268       2 - Se connecter au port série du module HC-06 avec Termite pour renseigner
0269       le SSID et le mot de passe du réseau WiFi sélectionné.*/
0270   Update_Wifi_Credentials(&Serial);
0271 
0272   // On attache une interruption au bouton USER pour gérer son appui
0273   attachInterrupt(digitalPinToInterrupt(USER_BTN), Button_Down_ISR, LOW );
0274 
0275   // Initialise le service MQTT
0276   Initialize_MQTT(&Serial);
0277 
0278   // Initialise le watchdog
0279   main_loop_delay = Initialize_Watchdog(&Serial);
0280 
0281   // Démarre le mode basse consommation
0282   LowPower.begin();
0283   Serial.println("Low power mode activated");
0284 
0285   // Signale que la fonction setup s'est déroulée jusqu'au bout
0286   setup_completed = true;
0287 }

Nous avons apporté à setup les modifications suivantes :

  • Nous avons ajouté à la ligne 270 l’appel à la fonction Update_Wifi_Credentials.

  • Nous avons ajouté à la ligne 286 « setup_completed = true ; ».

  • Nous avons supprimé l’appel à la fonction listWiFiNetworks qui se fera dans Update_Wifi_Credentials.

  • Nous avons pris soin de placer le code d’initialisation du port série du module Bluetooth (lignes 253 à 255) avant l’appel à Update_Wifi_Credentials. C’était indispensable puisque nous utiliserons la connexion Bluetooth pour saisir l’identifiant et le mot de passe du Wi-Fi.

  • Nous avons déplacée l’activation de l’interruption associée au bouton utilisateur à la ligne 273, après l’appel à Update_Wifi_Credentials pour éviter que cette interruption interfère avec l’appui sur le bouton lorsqu’il sera utilisé pour initier l’enregistrement en mémoire flash.

Enfin, aucune modification n’a été apportée à la fonction loop.

Voici le code complet de la fonction Update_Wifi_Credentials qui réalise toutes les nouvelles opérations :

1405 void Update_Wifi_Credentials(HardwareSerial *serial) {
1406 
1407   // Si le bouton utilisateur est enfoncé à ce moment, met à '\0' le premier
1408   // caractère dans l'EEPROM simulée
1409   if (!digitalRead(USER_BTN)) EEPROM.write(0, '\0');
1410 
1411   uint8_t ret, i;
1412 
1413   // Si ce caractère est zéro '\0', lance la demande de saisie du SSID et du PWD
1414   if (!EEPROM.read(0)) {
1415 
1416     BT_Serial.println("WiFi configuration mode set");
1417     serial->println("WiFi configuration mode set");
1418 
1419     // Liste des réseaux Wi-Fi disponibles
1420     listWiFiNetworks(serial);
1421 
1422     // Demande de saisie de l'identifiant du réseau Wi-Fi
1423     BT_Serial.println("Enter Wi-Fi SSID (" + String(MAX_LEN) + " char. max.) : ");
1424 
1425     // Acquisition du SSID sur le port série du module HC-06, dans BT_Buffer
1426     if (!processSerial(&BT_Serial, BT_Buffer)) {
1427       serial->println("Error while processing serial : null length SSID");
1428       while (true);
1429     }
1430 
1431     BT_Serial.println("Saved SSID : " + String(BT_Buffer));
1432 
1433     // Copie du buffer du port série dans le tableau ssid et, en cas d'erreur...
1434     ret = copy_arrays(BT_Buffer, ssid);
1435     if (ret) {
1436       serial->print("Error while copying SSID : " + String(ret));
1437       while (true);
1438     }
1439 
1440     serial->println("Saved SSID : " + String(ssid));
1441 
1442     // Demande de saisie du mot de passe du réseau Wi-Fi
1443     BT_Serial.println("Enter Wi-Fi password (" + String(MAX_LEN) + " char. max.) : ");
1444 
1445     // Acquisition du mot de passe sur le port série du module HC-06, dans BT_Buffer
1446     if (!processSerial(&BT_Serial, BT_Buffer)) {
1447       serial->println("Error while processing serial : null length password");
1448       while (true);
1449     }
1450 
1451     BT_Serial.println("Saved password : " + String(BT_Buffer));
1452 
1453     // Copie du buffer du port série dans le tableau ssid et, en cas d'erreur...
1454     ret = copy_arrays(BT_Buffer, pass);
1455     if (ret) {
1456       serial->print("Error while copying password : " + String(ret));
1457       while (true);
1458     }
1459 
1460     serial->println("Saved password : " + String(pass));
1461 
1462     // Ecrit le SSID l'EEPROM du STM32L476
1463     for (uint16_t i = 0; i < strlen(ssid); i++) EEPROM.write(i, (byte)ssid[i]);
1464 
1465     // Termine par le caractère zéro pour clôturer la chaîne
1466     EEPROM.write(strlen(ssid), '\0');
1467 
1468     // Ecrit le PWD l'EEPROM du STM32L476, MAX_LEN + 1 bytes plus loin
1469     for (uint16_t i = 0 ; i < strlen(pass); i++)
1470                                   EEPROM.write(i + MAX_LEN + 1, (byte)pass[i]);
1471 
1472     // Termine par le caractère zéro pour clôturer la chaîne
1473     EEPROM.write(strlen(pass) + MAX_LEN + 1, '\0');
1474 
1475     serial->println("WiFi credentials successfully saved inside EEPROM");
1476   }
1477 
1478   // Si le SSID et le PWD sont déjà écrits en EEPROM alors lis les et
1479   // charge les en mémoire
1480   else {
1481 
1482     serial->println("Reading WiFi credentials from EEPROM");
1483 
1484     // Lis jusqu'au prochain caractère zéro de terminaison de chaîne
1485     for (uint16_t i = 0; i < MAX_LEN; i++) {
1486       BT_Buffer[i] = (char)EEPROM.read(i);
1487       if (!BT_Buffer[i]) break;
1488     }
1489   
1490     // Copie le SSID dans le tableau ssid et, en cas d'erreur,...
1491     ret = copy_arrays(BT_Buffer, ssid);
1492     if (ret) {
1493       serial->println("SSID copy error code : " + String(ret));
1494       EEPROM.write(0, '\0');
1495       serial->println("Resetting WiFi credentials");
1496       delay(5000);
1497       HAL_NVIC_SystemReset();
1498     }
1499 
1500     serial->println("Loaded SSID : " + String(ssid));
1501 
1502     // Lis jusqu'au prochain caractère zéro de terminaison de chaîne
1503     for (uint16_t i = 0; i < MAX_LEN; i++) {
1504       BT_Buffer[i] =  (char)EEPROM.read(i + MAX_LEN + 1);
1505       if (!BT_Buffer[i]) break;
1506     }
1507     
1508     // Copie le PWD dans le tableau pass et, en cas d'erreur,...
1509     ret = copy_arrays(BT_Buffer, pass);
1510     if (ret) {
1511       serial->println("PWD copy error code : " + String(ret));
1512       EEPROM.write(0, '\0');
1513       serial->println("Resetting WiFi credentials");
1514       delay(5000);
1515       HAL_NVIC_SystemReset();
1516     }
1517 
1518     serial->println("Loaded password : " + String(pass));
1519   }
1520 }

Les commentaires du listing précédent devraient répondre à la plupart de vos interrogations. Trois autres fonctions sont appelées par Update_Wifi_Credentials :

  • A la ligne 1420, listWiFiNetworks, déjà présentée ici, qui n’a pas été modifiée.
  • Aux lignes 1426 et 1446 la fonction processSerial que nous avons dû modifier (listing ci-après).
  • Aux lignes 1434, 1454, 1491, 1509, copy_arrays, utile pour transférer les caractères saisis sur le port série du Bluetooth depuis le buffer de celui-ci dans les tableaux ssid et pass (listing ci-après).

Le listing de processSerial modifié :

1349 uint16_t processSerial(HardwareSerial *serial, char* serial_buffer) {
1350 
1351   // Nombre de caractères reçus
1352   uint16_t strLen = 0;
1353 
1354   // Le premier caractère est '\0' (terminaison de chaîne)
1355   serial_buffer[0] = '\0';
1356 
1357   // Boucle sans clause de fin pour pour suspendre l'exécution
1358   // jusqu'à la saisie complète lorsque processSerial est appelée
1359   // depuis Update_Wifi_Credentials
1360   while (true) {
1361 
1362     // Si on reçoit des caractères
1363     if (serial->available()) {
1364 
1365       // Lis un caractère
1366       char inChar = (char)serial->read();
1367 
1368       // Si ce caractère est LF ('\n')...
1369       if (inChar == '\n') {
1370         //... ajoute un caractère '\0' à la fin du buffer
1371         serial_buffer[strLen - 1] = '\0';
1372         break; //... quitte la boucle while (fin de réception)
1373       }
1374 
1375       // Autrement, aussi longtemps qu'on ne dépasse pas la taille
1376       // du buffer
1377       else if ((strLen < (MAX_LEN - 1))) {
1378         // ajoute au buffer le dernier caractère reçu
1379         serial_buffer[strLen++] = inChar;
1380       }
1381     }
1382     else if (setup_completed) {
1383       break;
1384     }
1385   }
1386 
1387   return strLen; // Renvoie le nombre de caractères lus.
1388 }

Pour finir, vous le listing de copy_arrays :

1525 uint8_t copy_arrays(char* input, char* output) {
1526 
1527   int retval = 0;
1528   int input_len = strlen(input);
1529   int output_len = MAX_LEN;
1530 
1531   // Si on est certain que le tableau d'entrée tiendra dans celui
1532   // de sortie
1533   if (input_len < output_len + 1 ) {
1534     // Fais la copie
1535     for (uint8_t i = 0; i < input_len; i++) {
1536       output[i] = input[i];
1537     }
1538   }
1539   // Si l'un des deux tableaux au moins est de longueur nulle....
1540   else if (!input_len || !output_len) {
1541     retval = 1; // retourne le code d'erreur "1"
1542   }
1543   // Si le tableau d'entrée est trop long pour être copié dans celui
1544   // de sortie
1545   else if (input_len > output_len) {
1546     retval = 2; // retourne le code d'erreur "2"
1547   }
1548   return retval;
1549 }