Stocker des données sur une carte micro SD. Code Arduino compatible ESP32, ESP8266 • Domotique et objets connectés à faire soi-même

Pour les projets d’objets connectés qui nécessitent le stockage d’une importante quantité de données, il peut s’avérer nécessaire d’avoir recours à un stockage sur carte micro SD. En effet, la plupart des cartes de développement Arduino ne disposent pas de mémoire Flash contrairement à l’ESP32 ou ESP8266.

Les cartes de développement ESP32 et ESP8266 embarquent un module de mémoire flash d’au moins 4Mo dont on peut utiliser au moins 3Mo pour stocker des données. Attention car la mémoire flash peut être écrite 10000 fois ce qui est faible pour un système d’acquisition de données.

Par contre, le stockage sur la mémoire flash embarquée est très bien adapté pour enregistrer le code source de l’interface HTML, feuille de style, fichiers de paramètres… Pour en savoir plus, voici quelques articles qui traitent du sujet

Repérage des broches du bus SPI des cartes de développement les plus courantes

Tous les modules lecteur de carte micro-SD utilisent le bus SPI pour communiquer avec le micro-contrôleur. Impossible de lister toutes les cartes de développement du marché.

Voici toutefois un tableau récapitulatif des cartes les plus courantes. Certaines cartes de développement disposent de plusieurs bus SPI.

Breakout (carte d’extension) lecteur de carte micro SD sur bus SPI

Les modules lecteur de carte micro SD utilisent le bus SPI pour communiquer avec le micro-contrôleur, Le bus SPI nécessitera de réserver 4 broches à votre projet. Cela ne pose pas de problème pour la plupart des cartes de développement à l’exception de l’ESP8266 plus limité.

Voir plus d’offres

Dans ce cas, vous pouvez opter pour un ESP32 dont la programmation est très similaire. Vous pouvez également ajouter une carte d’extension I2C qui permettra d’augmenter le nombre d’I/O ou d’entrées analogiques. Voici quelques solutions courantes.

Cartes de développement avec lecteur de carte SD intégré

Certaines cartes de développement embarquent un lecteur de carte. micro-SD. Voici quelques cartes de développement très courantes dans le grand public.

Malheureusement aucun carte Arduino (sauf erreur de ma part) n’est équipée d’un lecteur de carte SD, y compris les nouveaux modèles Pro. Idem pour l’ESP8266. Il faudra obligatoirement passer par un module ou un shield à empiler (au format d1 mini).

Connecter directement la carte micro-SD au micro-contrôleur !

Chaque carte micro SD embarque un contrôleur SPI, donc on peut très bien se passer d’un lecteur de carte micro-SD !

C’est beaucoup moins élégant mais vous pouvez vous dépanner avec quelques résistances en cas de panne ou en attendant de recevoir votre matériel. Pour en savoir plus, lisez ce tutoriel proposé par Renzo Mischianti.

3e28zljxwihyfvgrw6zc-1212570

Source : mischianti.org

Formater la carte SD en FAT16 ou FAT32

La librairie SD ne supporte que le formatage de type FAT16 ou FAT32. Les systèmes de fichier FAT16 et FAT32 sont supportés par tous les systèmes d’exploitation. Windows, macOS et Linux (y compris les versions ARM). Vous n’aurez aucun problème pour lire vos données.

Pour éviter tout problème de compatibilité, il est préférable d’utiliser l’utilitaire gratuit SD Card Formatter développé par l’association SD. SD Card Formatter est disponible pour toutes les plateformes ici.

Aucun réglage à faire, tout est automatique !

avyiwymimqnixjdz58hf-5448294

Fonctions proposées par la librairie SD.h, classes SD et File

La documentation officielle est disponible en ligne ici.

Classe SD, initialisation, opérations sur les dossiers

La classe SD fournit des fonctions pour accéder à la carte SD et manipuler les fichiers et répertoires quelle contient.

begin(broche_CS)

Initialise la bibliothèque SD et la carte. Renvoie true en cas de succès, false en cas d’échec.

Si la broche n’est pas précisée, la librairie utilise la broche SS par défaut de la plateforme cible

exists(nom_de_fichier)

Teste si un fichier ou un répertoire existe sur la carte SD. Renvoie true si le fichier ou le répertoire existe, false dans le cas contraire.

end()

Ferme la communication avec le lecteur de carte avant éjection

mkdir(nom_de_dossier)

Créez un répertoire sur la carte SD, y compris l’arborescence si elle n’existe pas. Renvoie true si la création du répertoire a réussi, false dans le cas contraire.

Par exemple la commande mkdir(“data/today”) créera le dossier data puis le sous-dossier today.

open(chemin_fichier, mode)

Ouvre un fichier sur la carte SD.

Les méthodes pour réaliser des opérations sur les fichiers sont listés au prochain paragraphe.

Mode (paramètre optionnel), le mode dans lequel ouvrir le fichier

  • FILE_READ (mode par défaut) ouvre le fichier en lecture. Le pointeur est placé au début du fichier
  • FILE_WRITE ouvre le fichier en lecture et en écriture. Le pointeur est placé à la fin du fichier. Si le fichier n’existe pas, il est créé automatiquement.

remove(nom_de_fichier)

Supprime un fichier de la carte SD. Renvoie true si la suppression du fichier a réussi, false en cas d’échec. Aucune valeur de retour si le fichier n’existait pas sur la carte SD.

rmdir(nom_de_dossier)

Supprime un répertoire de la carte SD.  Renvoie true si la suppression du répertoire a réussi, false sinon. Aucune valeur de retour si le répertoire n’existait pas sur la carte SD.

Attention, le répertoire doit être vide pour pouvoir être supprimé

Classe File, opérations sur les fichiers

La classe File permet de réaliser toutes les opérations de lecture et l’écriture de fichiers individuels sur la carte SD.

name()
Renvoie le nom du fichier

available()

Vérifie si le fichier n’est pas vide. Renvoie le nombre d’octets utilisés par le fichier.

close()

Ferme le fichier ouvert précédemment avec la fonction open()

flush()

Garantit que tous les octets écrits dans le fichier sont physiquement enregistrés sur la carte SD. Cela se fait automatiquement lorsque le fichier est fermé.

peek()

Lit un octet du fichier sans passer au suivant. La pointeur n’est pas déplacé. Utilisé la méthode seek() pour déplacer le pointeur dans le fichier.

position()

Récupère la position actuelle du pointeur dans le fichier. Octet non signé.

fprint(data, base)

Imprime (enregistre) les données dans le fichier, qui doit avoir été ouvert au préalable à l’aide de la fonction open() dans le mode FILE_WRITE.

data  les données à imprimer. Types supportés : char, byte, int, long ou string.

base (optionnel) la base dans laquelle imprimer les nombres: BIN pour binaire (base 2), DEC pour décimal (base 10), OCT pour octal (base 8), HEX pour hexadécimal (base 16).

println(data, base)

Identique à la méthode print() mais ajoute un retour chariot (CR) et nouvelle ligne (LF). Pratique pour un enregistreur de données.

seek(pos)

Positionne le pointeur de fichier à la position indiquée (type unsigned long). La position doit être comprise entre 0 et la taille du fichier (inclus). Renvoie true en cas de succès, false en cas d’échec

size()

Récupère la taille du fichier en octets (non signé).

read() ou read(buf, len)

Lire dans le fichier du fichier à la position du pointeur.

Utiliser la fonction open() avant de pouvoir lire dans un fichier

En cas de doute, utiliser la méthode isDirectory() pour savoir si c’est bien un fichier.

Utiliser la fonction seek() pour placer le pointeur à l’endroit désiré. Renvoie l’octet (ou caractère) suivant, ou -1 si aucun n’est disponible.

Utiliser la méthode available() pour tester si le fichier n’est pas vide

N’oubliez pas de fermer le fichier avec close() dès qu’il n’y a plus rien à lire.

write(data) ou write(buf, len)

Ecrit des data dans le fichier. Renvoie le nombre d’octets écrits.

Utiliser la fonction open() avant de pouvoir lire dans un fichier

Utiliser la fonction seek() pour placer le pointeur à l’endroit désiré. Renvoie l’octet (ou caractère) suivant, ou -1 si aucun n’est disponible.

N’oubliez pas de fermer le fichier avec close() dès qu’il n’y a plus rien à lire.

isDirectory()

Renvoie true si c’est un répertoire.

openNextFile()

Renvoie le fichier ou dossier suivant dans le chemin.

rewindDirectory()

Ramène au premier fichier du répertoire, utiliser conjointement avec openNextFile().

Disponibilité des fonctions sur Arduino, ESP32 et ESP8266

Les librairies SD.h et File.h sont disponibles sur les 3 plateformes. On pourrait croire que les fonctionnalités sont identiques quelque soit la plateforme mais il n’en est rien ce qui rend les développements un peu pénibles. C’est le choix d’Espressif qui a préféré utiliser les mêmes noms pour ces librairies.

Voici donc un tableau récapitulatif pour vous aider à mieux vous y retrouver. Espressif est revenu en arrière pour l’ESP32 en proposant les mêmes fonctions que sur Arduino. On dispose juste de 3 fonctions supplémentaires pour déterminer le type, l’espace occupé et l’espace disponible sur la carte micro SD.

Monter la carte SD. Code Arduino compatible ESP32, ESP8266

La première chose à faire avant de pouvoir lire ou enregistrer des fichiers sur la carte SD est de créer un objet SD à l’aide de la méthode begin().

Créer un nouveau croquis ou projet PlatformIO puis téléverser le code ci-dessous. Vous pouvez utiliser le fichier de configuration platformio.ini pour tester sur Arduino Uno, ESP32 ou ESP8266.

Code Arduino multi-plateforme Fichier Platformio.ini
#include 
#include 
#include 
#include 
#include 

#if defined(__AVR__)
  #define SD_CS    10
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  #define SD_CS     53
#elif defined(ESP8266)
  #define SD_CS    D8
#elif ESP32
  #define SD_CS    5   //SS
#endif     

void setup(){
  Serial.begin(115200);
  
  if(!SD.begin(SD_CS)){
      Serial.println("Card Mount Failed");
      return;
  } else {
    Serial.println("SD Card mounted with success");
  }
}

void loop(){
}
[env:lolin_d32]
platform = espressif32
board = lolin_d32
framework = arduino
monitor_speed = 115200

[env:d1_mini_lite]
platform = espressif8266
board = d1_mini_lite
framework = arduino
monitor_speed = 115200

[env:uno]
platform = atmelavr
board = uno
framework = arduino
monitor_speed = 115200
lib_deps = 161

Explication du code

La librairie SPI est nécessaire pour communiquer avec le lecteur carte SD. La librairie SD met à disposition des classes SD et File permettant de lire , écrire et manipuler les fichiers sur la cartes SD.

#include 
#include 

On utilise la directive #define pour spécifier au compilateur le code à inclure en fonction de la plateforme cible, ici la broche CS nécessaire pour la fonction begin().

#if defined(__AVR__) 
  #define SD_CS 10 
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) 
  #define SD_CS 53 
#elif defined(ESP8266)
  #define SD_CS D8 
#elif ESP32 
  #define SD_CS 5 //SS 
#endif

La méthode SD.begin(SD_CS) renvoi un booléen qui indique si l’objet SD a pu être initialisé, c’est à dire si une carte SD valide se trouve dans le lecteur de carte.

ESP32 uniquement, type de carte SD, espace occupé et espace disponible

Voici un petit programme qui permet de connaître l’espace occupé, l’espace disponible ainsi que le type de carte SD insérée dans le lecteur.

#include 
#include 
#include 

#if defined(__AVR__)
  #define SD_CS    10
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  #define SD_CS     53
#elif defined(ESP8266)
  #define SD_CS    D8
#elif ESP32
  #define SD_CS    5   //SS
#endif     

void setup(){
  Serial.begin(115200);
  
  if(!SD.begin(SD_CS)){
      Serial.println("Card Mount Failed");
      return;
  } else {
    Serial.println("SD Card mounted with success");
    #if ESP32
      uint8_t cardType = SD.cardType();

      if(cardType == CARD_NONE){
          Serial.println("No SD card attached");
          return;
      }

      Serial.print("SD Card Type: ");
      if(cardType == CARD_MMC){
          Serial.println("MMC");
      } else if(cardType == CARD_SD){
          Serial.println("SDSC");
      } else if(cardType == CARD_SDHC){
          Serial.println("SDHC");
      } else {
          Serial.println("UNKNOWN");
      }

      uint64_t cardSize = SD.cardSize();
    int cardSizeInMB = cardSize/(1024 * 1024);
     
    Serial.printf("Card size: %u MB \n", cardSizeInMB);
 
    uint64_t bytesAvailable = SD.totalBytes(); 
    int spaceAvailableInMB = bytesAvailable/(1024 * 1024);
 
    Serial.printf("Space available: %u MB \n", spaceAvailableInMB);
 
    uint64_t spaceUsed = SD.usedBytes(); 
    Serial.printf("Space used: %u bytes", spaceUsed);

    #endif   
  }
}

void loop(){
}

Ecrire dans un fichier, version ESP32

Voici enfin un dernier exemple qui explique comment écrire des données dans un fichier. Nous allons récupérer la température et la pression atmosphérique sur un BMP180 connecté sur le bus I2C.

fr0lacy301aqzm0z1uwt-9739119

Avant de téléverser le projet, il faudra modifier les variables pour que cela convienne à votre carte ESP32 :

  • SD_CS broche CS
  • PIN_SDA broche SDA du bus I2C, par défaut 21
  • PIN_SCL broche SCL du bus I2C, par défaut 22

kgq7bspsx7km6fxbzq1q-5017853

Par défaut, une mesure est faite chaque minute (TIME_TO_SLEEP) avant de mettre en veille profonde l’ESP32.

#include 
#include 
#include 
#include 
#include 
#include 

#define SD_CS    5   
#define PIN_SDA 22
#define PIN_SCL 17 // la broche 21 n'est pas exposee sur le GPIO de la LoLin D32   

// Conversion factor for micro seconds to seconds
uint64_t uS_TO_S_FACTOR = 1000000;  
// Sleep for 1 minutes = 60 seconds
uint64_t TIME_TO_SLEEP = 60;

Adafruit_BMP085 bmp;
int BME_EXIST = false;   
float temp = 0;
float pressure = 0;

void recordNewData();
void storeDataToSDCard(fs::FS &fs, const char * path, const char * message);

void setup(){
  Serial.begin(115200);

  Wire.begin(PIN_SDA, PIN_SCL);

  if (!bmp.begin()) {
    Serial.println("Could not find BMP180 or BMP280 sensor");
  } else {
    BME_EXIST = true;
  }
  if(!SD.begin(SD_CS)){
    Serial.println("Card Mount Failed");
    return;
  } else {
    Serial.println("SD Card mounted with success");
  }
}

void loop(){
  if ( BME_EXIST ) {
    recordNewData();
  }
  
  // Enable Timer wake_up
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  esp_deep_sleep_start();
}

void recordNewData(){
  temp = bmp.readTemperature();
  pressure = bmp.readSealevelPressure();
  String message = String(temp) + "," + String(pressure);
  storeDataToSDCard(SD, "/wp-content.txt", message.c_str());
}

void storeDataToSDCard(fs::FS &fs, const char * path, const char * message) {
  Serial.printf("Appending data to file: %s\n", path);

  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if(file.println(message)) {
    Serial.println("Data appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();
}

Explication du code

En fonction de la carte de développement, les broches I2C peuvent ne pas être exposées sur le GPIO. La librairie Wire.h pour ESP32 et ESP8266 permet d’attribuer d’autres broches en exécutant la fonction.

Wire.begin(PIN_SDA, PIN_SCL);

Pour en savoir plus sur la librairie Wire.h, lisez cet article

La librairie Adafruit_BMP085 a la fâcheuse tendance à faire planter l’ESP32 si le BMP180 n’a pas été correctement initialisé. IL est facile de contourner le problème en mettant un flag à True si le BMP180 a été correctement initialisé.

if (!bmp.begin()) {
    Serial.println("Could not find BMP180 or BMP280 sensor");
} else {
    BME_EXIST = true;
}

On l’ESP32 en sommeil entre deux enregistrements

esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
esp_deep_sleep_start();

Pour en savoir plus sur la mise en veille de l’ESP32, lisez ce tutoriel détaillé

On prépare l’enregistrement sous la forme d’une String. On prendra soin de convertir en chaîne les données numériques.

On va utiliser la méthode println() pour ajouter un enregistrement au fichier de données. Pour renvoyer à la ligne entre chaque enregistrement avec la fonction print(), vous devez ajouter la chaine “\r\n”.

Les méthodes print() et print() acceptent uniquement des chaînes au format C++. On devra donc convertir les strings dans ce format à l’aide de la méthode c_str().

String message = String(temp) + "," + String(pressure);
storeDataToSDCard(SD, "/wp-content.txt", message.c_str());

Toutes les fonctions sur les chaînes sont expliquées dans ce tutoriel

Pour enregistrer des données à un fichier existant, la méthode open() pour ESP32 dispose de l’option FILE_APPEND. Sur Arduino ou ESP8266, on ouvrira simplement avec l’option FILE_WRITE.

Si le fichier est correctement ouvert, on ajoute l’enregistrement. Comme le pointeur de fichier est placé automatiquement à la fin du fichier, les données sont ajoutées. Ici on utilise la méthode println() ce qui permet de passer automatiquement à la ligne.

Pour éviter de détériorer la carte SD et libérer celle-ci pour une autre fonction, on ferme le fichier avec la méthode close().

  File file = fs.open(path, FILE_APPEND);
  if(!file) {
    Serial.println("Failed to open file for appending");
    return;
  }
  if(file.println(message)) {
    Serial.println("Data appended");
  } else {
    Serial.println("Append failed");
  }
  file.close();

Exemple du fichier de données créé

21.60,98569.00
21.60,98578.00
21.60,98570.00
21.60,98576.00
21.60,98570.00
21.60,98572.00
21.60,98572.00
21.60,98571.00
21.60,98572.00
21.60,98567.00
21.60,98569.00
21.60,98568.00
21.60,98571.00

Ecrire dans un fichier, version ESP8266 ou Arduino

Le code adapté à l’Arduino ou l’ESP8266

#include 
#include 
#include 
#include 
#include 
#include   

#define TIME_TO_WAIT 5000
#define PATH_DATA_FILE "/wp-content.txt"

Adafruit_BMP085 bmp;
int BME_EXIST = false;   
float temp = 0;
float pressure = 0;

#if defined(__AVR__)
  #define SD_CS    10
#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  #define SD_CS    53
#elif defined(ESP8266)
  #define SD_CS    D8
#endif     

void setup(){
  Serial.begin(115200);

  if (!bmp.begin()) {
    Serial.println("Could not find BMP180 or BMP085 sensor at 0x77");
  } else {
    BME_EXIST = true;
  }

  if(!SD.begin(SD_CS)){
      Serial.println("Card Mount Failed");
      return;
  } else {
    Serial.println("SD Card mounted with success");
  } 
}

void loop(){
  if ( BME_EXIST ) {
    temp = bmp.readTemperature();
    pressure = bmp.readSealevelPressure();
    String message = String(temp) + "," + String(pressure) ;

    File file = SD.open(PATH_DATA_FILE, FILE_WRITE);
    if(!file) {
      Serial.println("Failed to open file for appending");
      return;
    }
    if(file.println(message)) {
      Serial.println("Data appended");
    } else {
      Serial.println("Append failed");
    }
    file.close();
  }  

  delay(TIME_TO_WAIT);
}

Mises à jour

12/10/2020 Publication de l’article

English version

Merci pour votre lecture.

Avez-vous aimé cet article ?