Imprimer
Catégorie : Programmation avancée
Affichages : 485

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 :

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 :

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

 

 

 

 

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

 

 

4°/ PSRAM

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