1°/ Les protocoles de communication

Quand on connecte un composant/module aux GPIOs du µC pour acquérir des données, il existe principalement deux modes de communication entre les deux parties.

La première solution est "directe" : le µC lit directement l'état d'une ou plusieurs pins digitales ou analogique et obtient ainsi la donnée du capteur. Par exemple, on lit directement l'état d'un switch à 2 états en le connectant sur un pin dont on lit l'état haut ou bas. Parfois, on va également mesurer le temps d'une pin change d'état, comme c'est le cas avec le capteur ultrasonic. Certains capteur de température, de pression, ... se contentent d'émettre un voltage variable selon la mesure. Dans ce cas, on lit l'état sur une pin analogique, le voltage étant transformé en une valeur numérique entre 0 et un maximum dépendant du µC (1023 sur esp8266 et 4095 sur esp32). Il faut ensuite traduire cette valeur en un mesure, en fonction des spécification du composant.

Cette solution est donc limitée à transmettre une seule informations de type binaire ou numérique mais assez limitée en valeur. Par exemple, par de chiffre à virgule, de texte, de plusieurs informations à la fois, etc.

La deuxième solution permet cela. Elle se base sur des protocoles de communication digitaux, où la façon matérielle d'envoyer/recevoir les bits composant les informations est définie par le protocole. Selon le protocole, un certains nombre de GPIO vont être nécessaires pour communiquer.

Dans le monde des µC non-industriel, il y a 3 protocoles principalement utilisés :

  • le protocole série,
  • l'I2C
  • le SPI

1.1°/ protocole série

Ce protocole se base sur une paire de pins nommées RX et TX, utilisées par les 2 parties commmunicantes. Pour que la partie A envoie des informations à la partie B, la pin TX de A doit être branchée sur le pin RX de B. La figure ci-dessous montre un exemple avec 2 arduino, en connexion réciproque

RXTX

Techniquement, si une des deux parties n'a pas besoin d'envoyer et l'autre de recevoir, il y a juste besoin de brancher une paire TX/RX. En revanche, on remarque qu'il n'y a aucun lien qui permette aux deux partie de se synchroniser, comme avec un signal d'horloge. Cela vient du fait que les 2 parties doivent initialiser le protocole avec la même vitesse de lecture/écriture, exprimée en bauds (en gros bits par secondes). S'ils ne fonctionnent pas à la même vitesse, la communication sera impossible.

 A l'intérieur du µC, une partie du circuit, nommée UART, est capable de gérer le protocole série, en lisant/écrivant des données sur une paire de pins prédéfinies ou bien, selon le µC, que l'on peut choisir, sachant qu'un µC peut contenir plusieurs UARTs. Par exemple, l'esp32 possède 3 UARTs, qui utilisent par défaut les paires RX/TX [3,1], [9,10] et [16,17], mais que l'on peut affecter à d'autres pins.

D'un point de vue programmation arduino, ces UART sont représentées par des objets préxistants, généralement nommés Serial, Serial1, Serial2, ... respectivement associés à une des UART 0, 1, 2, ... du µC. 

La liaison série est également très utilisée par des capteurs qui envoient des volumes de données variables, moyennement longs (quelques dizaines/centaines d'octets), mais pas forcément très rapidement. Dans ce cas, les informations sont souvent sous forme textuelle. C'est le cas des capteurs GPS. 

Exemple : un capteur GPS est branché sur les pin 27 et 28 d'un esp32, mais en utilisant l'UART 2 de l'esp32 (donc Serial2)

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, 27, 28);
}

void loop() {
  while (Serial2.available()) {
    Serial.print(char(Serial2.read()));
  }
}

 

Attention, l'UART 0 (associé à Serial) est généralement utilisée dans les sketch pour communiquer avec un ordinateur. En effet, à l'extérieur du µC, les pins par défaut de l'UART 0 (par exemple 3,1) sont connectées à un composant qui convertit le protocole série vers le protocole USB, qui lui-même est connecté à une prise USB. On peut ainsi brancher la carte de dev. à un ordinateur via un câble USB, ce qui est nettement plus pratique qu'avec un câble série puisque les ordinateur actuels n'ont plus de port série. Dans l'exemple ci-dessus, la liaison est initialisée à 115200 bauds, ce qui implique que du côté ordinateur, il faut régler la lecture à la même vitesse. Sinon, on reçoit des informations erronées, comme on le constate dans le moniteur arduino si les bauds sont mal réglés.

En conclusion, si on utilise l'UART 0 pour communiquer avec l'ordinateur, on ne peut donc pas brancher en même temps  un composant sur les pins 3,1. D'où l'utilisation de l'UART 2.

 

Le problème principal de la liaison série est qu'elle ne permet pas de chaîner des composants utilisant tous le protocole série. Il faut utiliser une paire RX/TX pour chacun. Or, sur les µC n'ont qu'un nombre limité d'UARTs capable de gérer matériellement le protocole série. Par exemple, l'esp8266 a deux UARTs, mais la 0 est déjà utilisée et la deuxième ne peut qu'envoyer. Il faut donc émuler le protocole série par logiciel. C'est très facile en utilisant la bibliothèque SoftwareSerial, mais cela charge beaucoup le µC et il n'est pas rare d'avoir des problèmes dès qu'on veut gérer 2 composants de cette façon.

Exemple avec SoftwareSerial sur les pin D5, D6 de l'esp8266 :

#include <SoftwareSerial.h>
#define baudrate 9600

SoftwareSerial mySerial(D5, D6); // RX, TX

void setup() {
  Serial.begin(115200);
  mySerial.begin(baudrate);
}

 

Le deuxième problème est relié au premier, à savoir la vitesse de communication. L'UART d'un µC peut en théorie atteindre voire dépasser le mégabaud, mais en pratique on dépasse rarement les 115KBauds. C'est encore pire avec l'émulation logicielle où il vaut mieux reste dans des valeurs basses, genre 9600 bauds. De plus, il n'est pas rare que certains composants/module ne puissent communiquer qu'en 9600 bauds.

  

1.2°/ I2C

Ce protocole est très simple et se base sur un "bus" partagé entre des composants utilisant ce même protocole. On peut donc chaîner plusieurs composants, sous certaines conditions, comme on le voit sur la figure ci-dessous.

i2c

Le protocole I2C se base sur la notion de maître et d'esclaves, avec des communications seulement entre maître et esclaves, mais jamais entre esclaves. Pour fonctionner, il y a des contraintes :

  • chaque esclave doit avoir une adresse différente sur le bus
  • tous doivent être connectés à une paire de pins nommées SDA et SCL, SDA permettant de véhiculer les bits des informations à transmettre et SCL véhiculant un signal d'horloge pour synchroniser tous les participants.

En général, les µC ont des GPIOs par défaut pour le bus I2C, comme par exemple 21 et 22 sur l'esp32. Selon l'µC, on peut change ou non ce comportement par défaut. Sur l'esp32/8266, on peut utilise (presque) toutes celles que l'on veut.

A noter que ce bus est bidirectionnel, donc le maître peut envoyer aux esclaves et inversement. De plus, le maître peut envoyer soit à un esclave particulier, soit à tous (=broadcast).

La vitesse de base de l'I2C est de 100Kbits/s, mais on peut assez souvent aller bien plus vite (> 1Mbits/s), si les composants reliés le permettent. Par ailleurs, les µC n'ont pas forcément 

La limitation principale vient plutôt de l'adressage. En effet, les composants/modules ont une adresse fixée par leur constructeur. Pour certains composants, on peut choisir entre deux adresses. Par exemple, le BME280 utilisé en TP permet de choisir entre 0x76 et 0x77. Il est donc possible de mettre au maximum 2 BME280 sur un même bus I2C. En revanche, si un autre composant utilise ces même adresses, on ne pourra pas le brancher sur le même bus. Quand on a un conflit d'adresses, il est cependant possible d'utiliser plusieurs bus I2C ou bien un multiplexeur.

En arduino, on utilise la bibliothèque Wire pour utiliser l'I2C. Comme pour le protocole série, on doit initialiser le protocole avec une fonction begin(), qui permet de spécifier si besoin les pins SDA et SCL utilisées, ainsi que la fréquence d'horloge, qui va influer sur la vitesse de communication. L'API et des exemples se trouvent dans la documentation officielle : https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/i2c.html

Pour des exemples pratiques, notamment sur les problèmes d'utilisation avec d'autres bibliothèques, les conflits d'adresse, ... : https://randomnerdtutorials.com/esp32-i2c-communication-arduino-ide/

1.3°/ SPI

Le protocole SPI est relativement similaire à l'I2C, à part le fait qu'il n'utilise pas de notion d'adresse pour référencer un composant sur le bus SPI et qu'il y a deux lignes séparées (MISO et MOSI) pour communiquer de maître vers esclave et inversement. En contrepartie, il faut que la maître ait une GPIO spéciale, nommée CS ou SS (chip select) reliée à chaque esclave. Cela permet de sélectionner avec quel esclave on veut communiquer. La figure ci-dessous illustre un cas avec 3 périphériques SPI, donc 6 GPIOs nécessaires du côté du maître.

SPI

Le bus SPI nécessite donc beaucoup plus de GPIOs que l'I2C, ce qui peut être problématique sur des petits µC avec peu de GPIOs.

Avec arduino, on utilise rarement le protocole SPI directement. Généralement, on installe des bibliothèques pour les composants qui vont masquer l'utilisation du protocole SPI. Attention cependant, comme avec l'I2C et le SPI, les µC ont généralement des GPIOs par défaut pour le SPI. Selon le µC on peut changer ce comportement par défaut ou pas. Sur l'esp32, tout est configurable à volonté, donc il vaut mieux choisir une bibliothèque de composant qui permette de choisir les pins pour le SPI.

A noter que certains composants savent communiquer en SPI et I2C, selon la façon de les brancher. C'est par exemple le cas du BME280.

 

2°/ créer des fichiers en mémoire flash

  • l'esp8266/eps32 ont la possibilité de créer une partition dans la mémoire flash afin de stocker des fichiers. Cette partition peut être formatée en utilisant différent types de système de fichiers, à savoir;
    • SPIFFS,
    • FatFS,
    • LittleFS
  • SPIFFS est celui "d'origine" mais il est plutôt basique. En effet, il n'est pas possible de créer des répertoires. De plus, les accès en lecture/écriture sont plutôt lents par rapport aux 2 autres : de 2x à 4x plus long
  • C'est donc plutôt fait pour stocker de petits volumes de données qui changent rarement, comme par exemple des informations de configuration de l'application, ou bien des données qu'il faut conserver entre boots.

 

  • Le système le plus rapide est LittleFS. De plus, il permet de créer des répertoire, comme FatFS. C'est pourquoi il a tendance à être de plus en plus utilisé.
  • Cependant, des bibliothèques tierces se basent sur SPIFFS pour stocker des informations. Si vous utilisez ces bibliothèques, alors vous n'avez pas trop le choix.

 

  • Quelque soit le système, il faut tout d'abord créer l'espace nécessaire en mémoire flash.
  • Pour cela, le plus simple est compter sur la fonction begin(), qui permet d'initialiser l'accès au système de fichier.
  • En effet, si on passe true en premier paramètre de cette fonction, elle va lancer le formatage si l'espace n'existe pas, ou bien s'il n'est pas du bon type.
  • Bien entendu, si un espace avec un autre système existait, ce dernier va être écrasé.

 

  • Il est également possible d'utiliser des outils pour créer la partition. Généralement, ces outils sont des plugins pour arduino, qu'il convient d'installer.
  • ATTENTION ! Ces plugins ne fonctionnent pas pour l'instant avec les version 2.X d'arduino, mais seulement les 1.8.X. 
  • Par exemple, le plugin pour l'esp32 est disponible ici : https://github.com/lorol/arduino-esp32fs-plugin/releases
  • Pour l'installer, il existe différent tutoriaux dont : https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/
  • Une fois le plugin installé, le menu Outils contient un item permettant de créer la partition et d'y déposer des fichiers.
  • Le processus consiste généralement à créer un répertoire data à côté du sketch et de mettre le fichiers dans data. Ensuite, le plugin formate puis copie le contenu de data dans la mémoire flash.

 

  • Une fois que la partition est créée et formatée, on peut y accéder par programmation de façon quasi identique avec les 3 système de fichiers.
  • En effet, les fonctions sont quasi identiques, à partir du moment où la partition est initialisée.
  • Par exemple, on peut créer une fonction générique pour lire un fichier, quelque soit le système de fichier.

Exemple avec SPIFFS, en supposant que la partition contienne un fichier myfile.txt :

#include "FS.h"
#include "SPIFFS.h"
...

void readFile(fs::FS &fs, const char * path){
   Serial.printf("Reading file: %s\r\n", path);
   File file = fs.open(path);
   if(!file || file.isDirectory()){
       Serial.println("− failed to open file for reading");
       return;
   }
   Serial.println("− read from file:");
   while(file.available()){
      Serial.write(file.read());
   }
}

void setup(){
   Serial.begin(115200);
   if(!SPIFFS.begin(true)){ // true -> créer la partition si elle n'existe pas
      Serial.println("SPIFFS Mount Failed");
      return;
   }
   readFile(SPIFFS,"myfile.txt");
   ...
}

 

3°/ Lire/écrire sur une carte SD

  • Le TTGO T8 est fourni directement avec un emplacement pour carte micro-sd, dont les borches sont reliées directement à des GPIOs
  • Si on regarde au dos de la carte, il y a indiqué :
    • CS : IO13
    • MOSI : IO15
    • SCK : IO14
    • MISO : IO2
  • Cela permet de comprendre que les pins sont à priori faites pour utiliser un protocole de type SPI pour accéder à la carte.
  • Cela dit, si on fait une recherche sur le net avec sd card et esp32, on finit par tomber sur : https://github.com/espressif/arduino-esp32/tree/master/libraries/SD_MMC
  • Cette page, relativement technique, décrit l'API arduino pour ESP32. Elle nous indique que ce µC contient un circuit dédié à l'accès à des cartes SD et MMC.
  • De plus, il est indiqué que ce circuit est relié aux pins 2, 4, 12, 13, 14, 15 du µC.
  • La TTGO T8 est donc directement bien câblée, excepté le fait que les pins 4 et 12 ne soient pas visiblement connectées au lecteur de carte.
  • Cette documentation nous apprend également qu'il existe deux modes d'accès, appelés 1-line ou 4-line. Le deuxième est plus rapide car il utilise plus de bits à chaque accès.
  • Cependant, la TTGO T8 ne permet que d'utiliser le mode 1-line puisque le lecteur de carte n'est pas connecté aux pins 4 et 12.

 

  • Accéder à la SD ressemble fortement à l'accès à la partition flash présentée en section 1°.
  • En effet, on peut réutiliser toutes les fonctions d'accès aux fichiers, pourvu que l'accès à la carte soit initialisé.
  • Cela se voit dans l'exemple donné sur le github indiqué ci-dessus : https://github.com/espressif/arduino-esp32/blob/master/libraries/SD_MMC/examples/SDMMC_Test/SDMMC_Test.ino
  • On retrouve des fonctions pour lire/écrire un fichier sur n'importre quel type de système de fihciers, mais aussi pour lire/parcourir les répertoires.
  • Globalement, la seule chose qui change est le montage de la carte, ainsi que la présence de fonctions dédiées, comme celle qui permet de tester le type de carte.
  • Malheureusement, cet exemple ne fonctionne pas tel quel sur la TTGO T8 et il convient de le modifier. Le pourquoi et comment sera l'objet d'un exercice du TP5.

 

4°/ PSRAM

  • Les esp8266/32 ont une RAM interne utilisée principalement pour stocker les données utilisateurs, mais aussi une plus ou moins grande partie des instructions du sketch compilé.
  • Cette RAM est limitée en taille : 96Ko sur l'esp8266 et 520Ko sur l'esp32. Il est donc impossible de stocker de gros volumes de données, par exemple du son, images, vidéo.
  • Cependant, sur certaines cartes de développement, l'esp32 peut être relié à une RAM externe d'en général 8Mo, nommé PSRAM (Pseudo Static RAM)
  • ATTENTION ! Bien qu'elle soit physiquement de 8Mo, seuls 4Mo sont accessibles.
  • Pour pouvoir accéder à cette mémoire, il faut "activer" son utilisation avant de compiler le sketch.
  • Cela se fait dans le menu "Outils" d'Arduino, avec un item "PSRAM" qui permet d'activer ou non l'accès.
  • Ensuite, il suffit d'utiliser les fonctions d'allocation et libération mémoire. Les deux principales sont : ps_malloc() et free().

Exemple :

byte* psramBuffer = (byte*)ps_malloc(10); // alloue 10 octets
for(int i=0;i<10;i++) psramBuffer[i] = 100+i; // écriture
free(psramBuffer); // libération
  •  Le principe est donc exactement le même que la réservation de mémoire en C, avec des noms de fonction un peu différents.