mardi 20 juillet 2021

Les Expressions Régulières



Les Expressions Régulières

 

Aujourd'hui nous allons aborder un sujet inconnu de la plupart des amateurs : les expressions régulières.

Qu'est ce qu'une expression régulière ? C'est un motif, ou modèle, décrivant une chaîne de caractères.

Le but des expressions régulières est :

  • reconnaître la syntaxe d'une phrase
  • extraire des mots d'une phrase
  • remplacer des mots dans une phrase

1. Exemple

Que peut-on espérer des expressions régulières dans le monde ARDUINO, attendu que l'on a rarement l'occasion d'examiner des phrases ? En fait, une phrase peut très bien provenir d'un capteur, d'un dispositif intelligent, et dans ce cas on appelle cette phrase une trame.

Partons d'un exemple concret. Imaginons que nous ayons à implémenter l'analyse lexicale d'une chaîne de caractères provenant d'un système de mesure de tension et courant qui envoie périodiquement sur une ligne série des trames du genre de celle-ci :

<20/07/2021 17:55:04> 10.31V, 1.32A

Notre intention est d'extraire les valeurs de cette chaîne :

  • date
  • heure
  • tension
  • courant

La première idée qui vient à l'esprit est de rechercher les séparateurs :

  • < et >
  • espace et virgule
Il faut également examiner les caractères pour déterminer leur nature :
  • chiffre
  • point décimal
  • lettre minuscule, majuscule

1.1. strtok()

Voici la manière habituelle de découper une phrase en mots avec strtok() :

    char str[] = "- Voici une phrase avec quelques séparateurs ! -";
    const char * separators = " ,.-!";

    char * strToken = strtok(str, separators);
    while (strToken != NULL) {
        Serial.println(strToken);
        strToken = strtok(NULL, separators);
    }

On peut rapidement constater que la méthode classique qui consiste à utiliser la fonction strtok() n'est pas tout à fait adaptée à notre cas. Son prototype est le suivant :

char *strtok(char *str, const char *delim);

strtok() a besoin de délimiteurs, or au sein des mots 10.31V et 1.32A il n'y a pas de séparateur entre la valeur et l'unité.

Il faudrait donc considérer que 10.31V et 1.32A sont deux valeurs à extraire. Il ne restera plus qu'à utiliser atof() pour récupérer les valeurs 10.31 et 1.32 :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  char voltage[10], current[10];
  float volt, amp;
 
  char *p = strtok(phrase, "</");
  if (p == NULL) {
    Serial.println("manque </");
    return;
  }
  day = atoi(p);
  p = strtok(NULL, "/");
  if (p == NULL) {
    Serial.println("manque /");
    return;
  }
  month = atoi(p);
  p = strtok(NULL, "/ ");
  if (p == NULL) {
    Serial.println("manque / et espace");
    return;
  }
  year = atoi(p);
  p = strtok(NULL, ":");
  if (p == NULL) {
    Serial.println("manque :");
    return;
  }
  hour = atoi(p);
  p = strtok(NULL, ":");
  if (p == NULL) {
    Serial.println("manque :");
    return;
  }
  minute = atoi(p);
  p = strtok(NULL, ":>");
  if (p == NULL) {
    Serial.println("manque :>");
    return;
  }
  second = atoi(p);
  p = strtok(NULL, " ,");
  if (p == NULL) {
    Serial.println("manque virgule");
    return;
  }
  strcpy(voltage, p);
  voltage[strlen(voltage)-1] = '\0';
  volt = atof(voltage);
  p = strtok(NULL, " ");
  if (p == NULL) {
    Serial.println("manque espace");
    return;
  }
  strcpy(current, p);
  current[strlen(voltage)-1] = '\0';
  amp = atof(current);

  Serial.println(day);
  Serial.println(month);
  Serial.println(year);
  Serial.println(hour);
  Serial.println(minute);
  Serial.println(second);
  Serial.println(volt);
  Serial.println(amp);
}

void loop() {
}

Voici le résultat :

20
07
2021
17
55
04
10.31
1.32 

On remarque que la syntaxe de la phrase est vérifiée. Si elle n'est pas exactement celle prévue, rien ne sera affiché.

1.2. sscanf()

Il serait possible d'utiliser sscanf() :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  char voltage[10], current[10];
  float volt, amp;
  int n = sscanf(phrase, "<%d/%d/%d %d:%d:%d> %s %s>", &day, &month, &year, &hour, &minute, &second, voltage, current);
  if (n == 8) {
    volt = atof(voltage);
    amp = atof(current);
    Serial.println(day);
    Serial.println(month);
    Serial.println(year);
    Serial.println(hour);
    Serial.println(minute);
    Serial.println(second);
    Serial.println(volt);
    Serial.println(amp);
  }
}

void loop() {
}

Voici le résultat :

20
7
2021
17
55
4
10.31
1.32

sscanf() est incapable d'interpréter de manière simple certains séparateurs, par exemple la virgule. Par contre le code est un peu plus concis.

sscanf() est également capable de travailler sur des jeux de caractères : %[set].

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  char day[3], month[3], year[5], hour[3], minute[3], second[3];
  char voltage[10], current[10];
  float volt, amp;
  int n = sscanf(phrase, "<%[0-9]/%[0-9]/%[0-9] %[0-9]:%[0-9]:%[0-9]> %[0-9.]V, %[0-9.]A>", day, month, year, hour, minute, second, voltage, current);
  Serial.println(n);
  if (n == 8) {
    volt = atof(voltage);
    amp = atof(current);
    Serial.println(day);
    Serial.println(month);
    Serial.println(year);
    Serial.println(hour);
    Serial.println(minute);
    Serial.println(second);
    Serial.println(volt);
    Serial.println(amp);
  }
}

void loop() {
}

Dans cet exemple :

  • %[0-9] représente un nombre entier composé de N chiffres de 0 à 9
  • %[0-9.] représente un nombre flottant composé de N chiffres de 0 à 9 avec éventuellement un point

Il y a un inconvénient : les données extraites (day, month, year, hour, minute, second) sont rangées dans des chaînes de caractères. Si l'on a besoin de leur valeur numérique il faudra les convertir avec atoi().

Dans les deux cas, la valeur retournée de sscanf() est comparée à 8, le nombre de mots à extraire. Si la syntaxe n'est pas celle prévue, rien ne sera affiché.

1.3. Analyseur sur mesure

Pour analyser cette chaîne, classiquement, nous ferions les opérations suivantes en déplaçant un pointeur ou un index dans la chaîne :

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";

void setup() {
  Serial.begin(115200);
  int day, month, year, hour, minute, second;
  float voltage, current;
  char *p = phrase;
  if (*p == '<') {
    p++;
    day = atoi(p);
    while (isdigit(*p)) p++;
    if (*p == '/') {
      p++;
      month = atoi(p);
      while (isdigit(*p)) p++;
      if (*p == '/') {
        p++;
        year = atoi(p);
        while (isdigit(*p)) p++;
        if (*p == ' ') {
          p++;
          hour = atoi(p);
          while (isdigit(*p)) p++;
          if (*p == ':') {
            p++;
            minute = atoi(p);
            while (isdigit(*p)) p++;
            if (*p == ':') {
              p++;
              second = atoi(p);
              while (isdigit(*p)) p++;
              if (*p == '>') {
                p++;
                if (*p == ' ') {
                  p++;
                  voltage = atof(p);
                  while (isdigit(*p) || *p == '.') p++;
                  if (*p == 'V') {
                    p++;
                    if (*p == ',') {
                      p++;
                      if (*p == ' ') {
                        p++;
                        current = atof(p);
                        while (isdigit(*p)) p++;

                        Serial.println(day);
                        Serial.println(month);
                        Serial.println(year);
                        Serial.println(hour);
                        Serial.println(minute);
                        Serial.println(second);
                        Serial.println(voltage);
                        Serial.println(current);
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

void loop() {
}

Voici le résultat :

20
07
2021
17
55
04
10.31
1.32 

Cette fois-ci nous obtenons un contrôle parfait sur chaque caractère, mais quelle longueur de code !

On remarque aussi que la syntaxe de la phrase est vérifiée. Si elle n'est pas exactement celle prévue, rien ne sera affiché.

1.4. Expression régulière

N' y a t'il pas plus simple ?

Si, bien sûr. En utilisant une analyse lexicale on pourrait faire cette opération en une seule fois.

Une description de cette chaîne pourrait être celle-ci :

<chiffres,/,chiffres,/,chiffres,espace,chiffres,:,chiffres,:,chiffres>

chiffres&point,V,virgule,espace,chiffres&point,A

Ensuite il faudrait donner à manger cette description et la phrase à analyser à un bout de logiciel qui nous dirait si la phrase correspond à la description, et si oui, extraire les données.

2. La librairie

La librairie de Nick Gammon (un personnage connu), installable depuis le gestionnaire de bibliothèques de l'IDE ARDUINO, est celle-ci :

https://github.com/nickgammon/Regexp

C'est une librairie qui se rapproche fortement de celles que l'on a l'habitude d'utiliser sur PC, en C, C++, PYTHON, ou d'autres langages.

2.1. Les exemples

Les exemples de la librairie sont assez pauvres. A part extraire tous les mots d'une phrase comme "The quick brown fox jumps over the lazy wolf", ils ne sont pas d'une aide suffisante. Cela justifie cet article.

2.2. Allons un peu plus loin

2.2.1. Présentation

Ce qui suit est une explication de cette page :

http://www.gammon.com.au/scripts/doc.php?lua=string.find 

Tout d'abord présentons les caractères spéciaux utilisés, ceux qui ont une signification particulière dans la description de la grammaire :

  • ^ : le début de la phrase
  • $ : la fin de la phrase
  • ( ) : encadre une capture (une extraction de données)
  •  % : introduit une classe de caractères
    • %a : des lettres
    • %d : des chiffres
  • . : représente n'importe quel caractère
  • [ ] : encadre un jeu de caractères
    • [a-z] : une minuscule
    • [A-Z] : une majuscule
    • [0-9] : un chiffre
  • * : exprime une répétition
    • [a-z]* : plusieurs minuscules (y compris aucune)
    • [A-Z]* : plusieurs majuscules (y compris aucune) 
    • [0-9]* : plusieurs chiffres (y compris aucun)
  • + : exprime une répétition
    • [a-z]+ : plusieurs minuscules (au moins une)
    • [A-Z]+ : plusieurs majuscules (au moins une)
    • [0-9]+ : plusieurs chiffres (au moins un) 
  • - : exprime une répétition, comme + mais en extrayant le moins possible de caractères
  • ? : exprime une répétition (zéro ou une)

Le caractère % est utilisé également pour échapper un caractère spécial

  • . : représente n'importe quel caractère
  • %. : représente un point
  • %( : représente une parenthèse ouvrante

Comment pourrions-nous décrire notre phrase précédente ?

<20/07/2021 17:55:04> 10.31V, 1.32A

Comme ceci :

<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A

Examinons chaque élément de cette description :

  • < : nous attendons un caractère <
  • ([0-9]+/[0-9]+/[0-9]+) : nous voulons extraire un mot composé de 3 groupes de plusieurs nombres séparés par un slash (/)
  • ([0-9]+:[0-9]+:[0-9]+) : nous voulons extraire un mot composé de 3 groupes de plusieurs nombres séparés par deux points (:)
  • > : nous attendons un caractère >
  • nous attendons un espace
  • ([0-9%.]+) : nous voulons extraire un mot composé de plusieurs chiffres et éventuellement d'un point, donc un nombre flottant
  • V : nous attendons une lettre V
  • , : nous attendons une virgule suivie d'un espace
  • ([0-9%.]+) : nous voulons extraire un mot composé de plusieurs chiffres et éventuellement d'un point, donc un nombre flottant
  • A : nous attendons une lettre A

2.2.2. Le code exemple

Le code présenté est inspiré de celui-ci :

https://github.com/nickgammon/Regexp/blob/master/examples/GlobalMatch/GlobalMatch.pde

Le voici :

#include <Regexp.h>

char phrase[] = "<20/07/2021 17:55:04> 10.31V, 1.32A";
char regexp[] = "<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

void match_callback  (const char * match, const unsigned int length, const MatchState & ms)
{
  char cap [10];   // must be large enough to hold captures

  Serial.print ("Matched: ");
  Serial.write ((byte *) match, length);
  Serial.println ();
  for (byte i = 0; i < ms.level; i++) {
    Serial.print ("Capture");
    Serial.print (i);
    Serial.print ("=");
    ms.GetCapture (cap, i);
    Serial.println (cap);
  }
}

void setup() {
  MatchState ms (phrase);
  unsigned long count;

  Serial.begin(115200);
  count = ms.GlobalMatch(regexp, match_callback);
  Serial.print("Found ");
  Serial.print(count);
  Serial.println(" matches.");
}

void loop() {
}

La variable phrase représente la phrase à analyser. La variable regexp représente l'expression régulière.

Exécutons ce code :

Matched: <20/07/2021 17:55:04> 10.31V, 1.32A
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=1.32
Found 1 matches.

1 correspondance a été trouvée (Found 1 matches), et 4 captures ont été réalisées :

  • la date
  • l'heure
  • la tension
  • le courant

La date est l'heure sont extraites en deux mots. Si l'on a besoin d'extraire le jour, le mois, l'année, l'heure, les minutes et les secondes séparément, il suffit d'encadrer chaque élément [0-9]+ par des parenthèses :

char regexp[] = "<([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

Le résultat sera :

Matched: <20/07/2021 17:55:04> 10.31V, 1.32A
Capture0=20
Capture1=07
Capture2=2021
Capture3=17
Capture4=55
Capture5=04
Capture6=10.31
Capture7=1.32
Found 1 matches. 

On pourrait corser la chose. Si le système de mesure est capable de travailler avec des unités autres que des Volts ou des Ampères, des millivolts ou de milliampères par exemple :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";
char regexp[] = "<([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)> ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

A la place de A ou V nous attendons plusieurs lettres, minuscule ou majuscule, et les parenthèses permettent de les extraire (capture) :

Matched: <20/07/2021 17:55:04> 10.31mV, 1.32mA
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=mV
Capture4=1.32
Capture5=mA
Found 1 matches.

Notre résultat pourra facilement être exploité car nous avons à disposition toutes les informations nécessaires, mesures et unités.

La grammaire peut être rendue plus tolérante. dans l'exemple qui suit, les caractères < et > peuvent être remplacés par [ et ] :

char phrase[] = "[20/07/2021 17:55:04] 10.31mV, 1.32mA";
char regexp[] = "[<%[]([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)[>%]] ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

explication de [<%[] et [>%]] :

On voit que les caractères [ et ] sont échapés à l'aide du caractère %, sinon ils seraient pris pour des caractères spéciaux.

Avec cette description, la date et l'heure peuvent être encadrées soit par < et > soit par [ et ].

Donc ces deux phrases donneront le même résultat :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";
char phrase[] = "[20/07/2021 17:55:04] 10.31mV, 1.32mA";

Matched: [20/07/2021 17:55:04] 10.31mV, 1.32mA
Capture0=20/07/2021
Capture1=17:55:04
Capture2=10.31
Capture3=mV
Capture4=1.32
Capture5=mA
Found 1 matches.

Si l'information <> ou [] a une signification, et qu'elle doit être prise en compte il suffit d'encadrer par des parenthèses :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Matched: [20/07/2021 17:55:04] 10.31mV, 1.32mA
Capture0=[
Capture1=20/07/2021
Capture2=17:55:04
Capture3=]
Capture4=10.31
Capture5=mV
Capture6=1.32
Capture7=mA
Found 1 matches.

Un dernier mot à propos des caractères spéciaux :

  • %a : représente une lettre, minuscule ou majuscule (équivalent à [a-zA-Z])
  • %d : représente un chiffre (équivalent à [0-9])
  • %l : représente une lettre minuscule (équivalent à [a-z])
  • %u : représente une lettre majuscule (équivalent à [A-Z])
  • %w : représente une lettre ou un chiffre (équivalent à [a-zA-Z0-9])
  • etc.

La liste complète ici : http://www.gammon.com.au/scripts/doc.php?lua=string.find 

Notre dernière expression peut donc être réduite :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Est équivalent à :

char regexp[] = "([<%[])([%d]+\/[%d]+\/[%d]+) ([%d]+:[%d]+:[%d]+)([>%]]) ([%d%.]+)([%a]+), ([%d%.]+)([%a]+)";

Elle donnera exactement le même résultat.

2.2.3. Mise au point

Si la phrase à analyser est longue et complexe, il est difficile d'écrire l'expression régulière d'une seule traite sans se tromper. On peut le faire étape par étape. On peut commencer par la date seule :

char phrase[] = "<20/07/2021 17:55:04> 10.31mV, 1.32mA";

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+)";

Matched: [20/07/2021
Capture0=[
Capture1=20/07/2021
Found 1 matches.

Ensuite, si tout va bien, ajoutons l'heure :

char regexp[] = "([<%[])([0-9]+/[0-9]+/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]])";

Matched: [20/07/2021 17:55:04]
Capture0=[
Capture1=20/07/2021
Capture2=17:55:04
Capture3=]
Found 1 matches.

Et ainsi de suite ...

2.2.4. Mise au point en ligne

Des sites WEB permettent de tester une expression régulière, par exemple regex101.com.

Mais il faut savoir que les expressions régulières traditionnelles sont légèrement différentes des expressions régulières de Nick Gammon :

  • les caractères spéciaux sont plus nombreux (slash / en est un)
  • le caractère spécial % est remplacé par backslash : \

Pour notre exemple j'ai simplement remplacé :

  • %[ par \[ et %] par \]
  • %. par \.
  • / par \/

La version de test sur regex101 devient :

([<\[])([0-9]+\/[0-9]+\/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>\]]) ([0-9\.]+)([a-zA-Z]+), ([0-9\.]+)([a-zA-Z]+)

Dans la fenêtre en bas à droite, on constate que la phrase est comprise et que les différents éléments sont extraits.

Après avoir testé la version WEB, Dans notre skecth il faut copier / coller l'expression, et faire les remplacements inverses :

char regexp[] = "([<%[])([0-9]+\/[0-9]+\/[0-9]+) ([0-9]+:[0-9]+:[0-9]+)([>%]]) ([0-9%.]+)([a-zA-Z]+), ([0-9%.]+)([a-zA-Z]+)";

Ce n'est pas une opération énorme, et avec un peu d'habitude on y parvient facilement.

2.2.5. Test avec un terminal

Si l'on désire faire des tests sur ARDUINO avec différentes phrases entrées dans un terminal série ou le moniteur serie, il suffit d'adapter un peu. Voici le premier exemple modifié :

#include <Regexp.h>

// enter a string like : <20/07/2021 17:55:04> 10.31V, 1.32A
char phrase[40];
char regexp[] = "<([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+)> ([0-9%.]+)V, ([0-9%.]+)A";

void match_callback  (const char * match, const unsigned int length, const MatchState & ms)
{
  char cap [10];   // must be large enough to hold captures

  Serial.print ("Matched: ");
  Serial.write ((byte *) match, length);
  Serial.println ();
  for (byte i = 0; i < ms.level; i++) {
    Serial.print ("Capture");
    Serial.print (i);
    Serial.print ("=");
    ms.GetCapture (cap, i);
    Serial.println (cap);
  }
}

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

void loop() {
  if (Serial.available()) {
    Serial.readBytesUntil('\n', phrase, 40);
    MatchState ms (phrase);
    unsigned long count;

    count = ms.GlobalMatch(regexp, match_callback);
    Serial.print("Found ");
    Serial.print(count);
    Serial.println(" matches.");
  }
}

3. Conclusion

En matière de développement informatique on arrive rarement à réaliser de grandes chose en restant cantonné dans son petit code personnel et ses petites habitudes, sans apprendre des autres.

L'utilisation des librairies ARDUINO simplifient grandement la tâche d'écriture de pilotes matériels lorsque l'on doit exploiter des composants et des modules.

Il en va de même pour le code pur. Plutôt que de s'enfermer dans une solution personnelle, ce qui revient souvent à s'engager dans une voie sans issue lorsque la complexité est importante, il est souvent préférable d'utiliser le travail des autres, surtout lorsque ce travail existe depuis des dizaines d'années.

C'est le cas pour les expressions régulières : elles sont nées en 1940.

Les expressions régulières peuvent sembler rébarbatives, mais le gain qu'elles apportent en terme de temps de développement est inestimable par rapport au temps passé en apprentissage.

En deux mots : la curiosité est un excellent défaut. Soyez curieux !


Cordialement

Henri



vendredi 16 juillet 2021

ESP32 & ESP8266 : les Fichiers de Paramètres

 

 

ESP32 & ESP8266 : les Fichiers de Paramètres

 

Nous allons examiner dans cet article une technique de paramétrage d'un logiciel sur ESP32 ou ESP8266.

Tout d'abord qu'est ce que le paramétrage ?

C'est la possibilité de pouvoir modifier certaines variables, et donc le comportement du logiciel, sans être obligé modifier, recompiler, et recharger le code :

  • à l'aide d'un fichier de paramètres
  • à l'aide d'une page WEB, avec sauvegarde

Dans cet article nous allons aborder différents sujets :

  • chargement d'un fichier dans la FLASH
  • lecture des paramètres
  • la fonction magique strtok(), ou mieux,  strtok_r()
  • un peu de C++ : classes, héritage, variables et méthodes de classe, etc.

1. Les possibilités

1.1. EEPROM

La première solution qui vient à l'esprit est : l'EEPROM. Pour rappel, sur un ESP8266 ou un ESP32 l'espace de stockage EEPROM est émulé en mémoire FLASH.

J'avais déjà présente un projet ESP8266 où certains paramètres étaient modifiables par une page WEB :

https://riton-duino.blogspot.com/2020/04/un-afficheur-tft-pour-domoticz-ou.html

Les paramètres sont stockés en FLASH à l'aide de la librairie EEPROM. Les valeurs par défaut, utilisées au premier démarrage, sont stockés sous forme de constantes en dur dans le code.

Stocker des paramètre à l'aide d'un espace en EEPROM n'est pas très souple. Cette méthode impose de créer une structure figée. Les paramètres ne sont pas nommés, et occupent chacun un emplacement précis en EEPROM.

1.2. Preferences

La librairie EEPROM est actuellement laissée de côté au profit de la librairie Preferences. Cette librairie permet de créer plusieurs espaces de noms, et pour chaque espace un ensemble de paramètres nommés.

L'exemple StartCounter est suffisamment parlant :

Un espace de noms my-app est ouvert :

preferences.begin("my-app", false);

On récupère la valeur d'un compteur :

unsigned int counter = preferences.getUInt("counter", 0);

On change la valeur du compteur :

preferences.putUInt("counter", counter);

On ferme l'espace de noms :

preferences.end();

Comme précédemment, les valeurs par défaut, utilisées au premier démarrage, sont stockés sous forme de constantes en dur dans le code.

Cette librairie peut convenir pour des cas de paramétrage simple. Pour l'usage que j'envisage, je préférerais opter pour un paramétrage du type fichier .INI, avec des sections :

[section1]
param1=valeur1
param2=valeur2

[section2]
param1=valeur1
param2=valeur2

Avec la librairie Preferences, cela imposerait de créer autant d'espaces de noms que de sections, ce qui serait assez vite fastidieux.

1.3. Fichier

Il est possible également de stocker les paramètres dans un fichier. Au départ, un fichier avec des valeurs par défaut est enregistré dans le système de fichiers SPIFFS. Ces valeurs peuvent être ensuite modifiées à l'aide d'une page WEB et réécrites dans le fichier, ou dans l'EEPROM.

Il devient donc possible, avec un code identique, de paramétrer plusieurs cartes avec des fichiers différents.

Partons d'un exemple simple :

Lorsqu'on travaille avec un ESP32 ou un ESP8266 la méthode que l'on utilise pour se connecter à un point d'accès est la suivante :

On déclare deux variables :

// remplacer par les vrais ssid et password
const char *ssid = "SSID";
const char *password = "PASSWORD"; 

Ensuite on appelle WiFi.begin() dans la fonction setup() :

  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

Et si l'on essayait de paramétrer ces variables ssid et password à l'aide d'un fichier ?

Ce fichier pourrait ressembler à ceci :

[WIFI]
ssid=
SSID
pwd=
PASSWORD

Ou ceci :

[WIFI]
access-point=SSID:
PASSWORD

Vu comme cela l'intérêt peut paraître faible, mais ce fichier pourrait contenir également d'autre paramètres de fonctionnement, plus complexes :

[server]
url=user:password@http://my-server.com:8080

Cette ligne pourrait servir à paramétrer l'accès à un serveur sur le port 8080, avec identifiant utilisateur et mot de passe.

Et cela peut aller beaucoup plus loin.

2. La librairie

Il existe une librairie permettant de lire des fichiers de paramètres, directement installable depuis l'IDE ARDUINO :

https://github.com/yurilopes/SPIFFSIniFile

L'auteur a commis une petite erreur : la compilation se passe mal en version ESP8266. Dans le fichier SPIFFSIniFile.h il faut remplacer :

#include <SPIFFS.h>

Par :

#ifdef ESP32
#include <SPIFFS.h>
#endif

3. Le chargement dans SPIFFS

Après avoir remplacé les valeurs de ssid et pasword, il faut placer le fichier dans un sous répertoire du projet, nommé data, et le charger à l'aide du menu "Outils / ESP8266 Sketch Data Upload" ou "Outils / ESP32 Sketch Data Upload".

Bien entendu on choisira auparavant la carte adéquate à l'aide du menu "Outils / Type de carte". Pour ma part j'ai testé ce qui suit avec "ESP32 dev module" et "Wemos D1 R1".

Avant cela, si ce n'est déjà fait, il faut installer un plugin.

3.1. ESP32

Installer le plugin :

https://github.com/me-no-dev/arduino-esp32fs-plugin

Il faut choisir le partitionnement de la FLASH à l'aide du menu "Outils / Flash Size". Le choix par défaut "Outils / Partition scheme" "Default 4MB with SPIFFS" suffit pour un essai.

Lancer le chargement à l'aide du menu "Outils / ESP32 Sketch Data Upload".

3.2. ESP8266

Installer le plugin :

https://github.com/esp8266/arduino-esp8266fs-plugin 

Il faut aussi choisir le partitionnement de la FLASH à l'aide du menu "Outils / Flash Size" : 4M (1M SPIFFS).

Lancer le chargement à l'aide du menu "Outils / ESP8266 Sketch Data Upload".

Sur ESP8266 j'ai eu des difficultés à utiliser le plugin. Le chargement ne se fait pas. L'IDE affiche simplement :

[SPIFFS] data    : /home/user/projects/esp-config/esp8266-config/data
[SPIFFS] size    : 1004
[SPIFFS] page    : 256
[SPIFFS] block   : 8192
/config.ini
[SPIFFS] upload  : /tmp/arduino_build_111554/esp8266-config.spiffs.bin
[SPIFFS] address  : 0x300000
[SPIFFS] reset    : nodemcu
[SPIFFS] port     : /dev/ttyUSB1
[SPIFFS] speed    : 921600
[SPIFFS] python   : python3
[SPIFFS] uploader : /home/
user/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/tools/upload.py

Mais le sketch exemple (que l'on verra ensuite) ne trouve pas le fichier.

Si l'on rencontre le même problème on peut charger le fichier en ligne de commande avec esptool.

Le nom du fichier à charger est indiqué plus haut en gras (upload).

Le chemin du logiciel de chargement est indiqué à la fin (uploader). il suffira de remplacer upload.py par esptool/esptool.py ou esptool\esptool.py sous Windows.

Sous Linux il suffit de construire une ligne de commande comme ceci :

python /home/user/.arduino15/packages/esp8266/hardware/esp8266/2.5.2/tools/esptool/esptool.py  --baud 115200 --port /dev/ttyUSB1 write_flash 0x300000 /tmp/arduino_build_111554/esp8266-config.spiffs.bin

Sous Windows ou Mac la ligne sera assez peu différente :

--port /dev/ttyUSB1 est à remplacer par le port de communication USB à utiliser.

Voici ce que cela donne :

config.spiffs.bin
esptool.py v2.6
Serial port /dev/ttyUSB1
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
MAC: 5c:cf:7f:14:3a:da
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 1028096 bytes to 1417...
Wrote 1028096 bytes (1417 compressed) at 0x00300000 in 0.1 seconds (effective 63914.4 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

4. Exemples 

La repository https://bitbucket.org/henri_bachetti/esp-config.git contient 3 exemples :

  • esp8266-config
  • esp32-config
  • esp32-relay-config

Les deux exemples esp8266-config et esp32-config sont équivalents et permettent simplement de connecter un ESP8266 ou un ESP32 au réseau WIFI, ssid et password étant stockés dans un fichier config.ini.

L'exemple esp32-relay-config permet de récupérer les paramètres d'un certain nombre de relais connectés à un ESP32.

4.1. Exemple simple

Nous allons utiliser ce fichier de paramètres (config.ini):

[WIFI]
access-point=SSID:
ABCDEFGHIJ

Bien entendu les identifiants SSID et ABCDEFGHIJ sont des exemples, à remplacer par les identifiants véritables du réseau WIFI dans le fichier config.ini

Le ssid et le password sont séparés par le caractère ':'. Pourquoi compliquer les choses alors que deux paramètres seraient plus simples à lire ?

Parce que dans un fichier de paramètres plus complexe une écriture de plusieurs paramètres sur une seule ligne aura l'avantage d'être plus concise et plus claire. Dans l'exemple suivant cela paraîtra plus évident.

4.1.1. La classe config

Pour lire notre ssid et notre password j'ai écrit une petite classe nommée config :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/config.h 

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/config.cpp

#define MAX_LINE        100

class config : public SPIFFSIniFile
{
  public:
    config(const char *fileName);
    bool read(void);
    void print(void);
    char *getSsid(void) {return m_ssid;}
    char *getPassword(void)  {return m_password;}
  private:
    const char *m_fileName;
    char m_ssid[MAX_LINE/2];
    char m_password[MAX_LINE/2];
};

Cette classe hérite de SPIFFSIniFile. Elle pourra donc appeler directement les méthodes publiques de SPIFFSIniFile (open(), getValue(), etc.).

Elle possède un constructeur qui se contente d'appeler celui de SPIFFSIniFile :

config::config(const char *fileName) :
  SPIFFSIniFile(fileName)
{
  m_fileName = fileName;
}

Elle possède également une méthode read() permettant de lire et stocker les paramètres :

bool config::read(void)
{
  char buffer[MAX_LINE];
  const char *p;
  char *s;

  if (!open()) {
    Serial.printf("%s: not found\n", m_fileName);
    return false;
  }
  if (getValue("WIFI", "access-point", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:WIFI:access-point not found (error %d)\n", m_fileName, getError());
    return false;
  }
  p = strtok_r(buffer, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing ':'\n", buffer);
    return false;
  }
  strcpy(m_ssid, p);
  p = strtok_r(NULL, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: missing password\n", buffer);
    return false;
  }
  strcpy(m_password, p);
  return true;
}

Tout d'abord le fichier est ouvert, puis le paramètre [WIFI] access-point est lu dans un buffer de taille suffisante (MAX_LINE, qui vaut ici 100) .

Comme on le voit les erreurs sont gérées et des messages sont affichés si le fichier ou le paramètre est introuvable, ou que le format est incorrect. Les erreurs possibles sont listées dans SPIFFSIniFile.h.

Nous voyons ici une utilisation de la fonction strtok_r(), qui permet de découper une chaîne de caractères en mots, appelés jetons (token).

Il existe deux fonctions : strtok(), et strtok_r(), qui est récursive.

L'utilisation de strtok_r() est préférable car dans notre code on n'est jamais certain qu'une fonction utilisant strtok() n'appellera pas une autre fonction qui appelle également strtok(). Dans ce cas, le contexte de travail de la première fonction sera détruit par la seconde.

On voit également que les fonctions C de manipulation de C strings sont utilisées (strcpy(), et strchr() et strcmp() dans l'exemple suivant). Pourquoi ne pas utiliser les objets String du C++ ? Premièrement parce que la librairie SPIFFSIniFile ne les utilise pas, deuxièmement parce qu'il n'existe pas d'équivalent de strtok_r() en C++. Il serait dommage de se passer de cette fonction si pratique.

Examinons la suite du code.

Cette ligne demande à strtok_r() de retourner le premier jeton de la chaîne buffer, en tenant compte du séparateur ':', situé entre ssid et password :

  p = strtok_r(buffer, ":", &s);

Dans le deuxième appel à strtok_r() la chaîne à traiter est remplacée par NULL. Cela indique à strtok_r() qu'elle va devoir chercher le jeton suivant :

  p = strtok_r(NULL, ":", &s);

Au bout du compte deux variables m_ssid et m_password se voient affecter les deux jetons :

  strcpy(m_ssid, p);
  ...
  strcpy(m_password, p);

Ces deux valeurs pourront être demandées par l'appelant quand il le désirera, à l'ide de deux méthodes getSsid() et getPassword().

4.1.2. Le sketch

Voyons maintenant ce qui se passe au niveau du sketch :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp8266-config/esp8266-config.ino

On déclare deux variables vide :

const char *ssid;
const char *password;

On objet de la classe config est instancié :

config config("/config.ini");

Ensuite dans la fonction setup() le système de fichiers SPIFFS est démarré, puis la configuration est lue et affichée :

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS.begin() failed");
  }
  config.read();
  config.print();

Il n'y a plus qu'à se connecter :

  WiFi.mode(WIFI_STA);
  ssid = config.getSsid();
  password = config.getPassword();
  WiFi.begin(ssid, password);


Et le moniteur série affiche la connexion :

ssid: SSID
password:
ABCDEFGHIJ
..
Connected to SSID

IP address: 192.168.1.42
MDNS responder started
HTTP server started

4.2. Exemple plus complexe

Tout d'abord pour essayer cet exemple il faudra installer la librairie MCP23017 :

https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

On peut l'installer à partir du gestionnaire de bibliothèques de l'IDE ARDUINO.

Le fichier de paramètres de l'exemple esp32-relay-config est celui-ci :

[WIFI]
access-point=SSID:
ABCDEFGHIJ

[relays]
main=GPIO-H(4, 5)
secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

On retrouve les paramètres de connexion au réseau WIFI.

Une section relays a été ajoutée. Elle permet de paramétrer l'utilisation d'un certain nombre de relais :

Un relais principal bistable à deux bobines est relié aux GPIOS 4 et 5. Son niveau de commande est HIGH (GPIO-H) :

main=GPIO-H(4, 5)

8 relais secondaires sont reliés à un expander du type MCP23017 :

  • les 4 premiers sont des relais activables par un niveau bas (I2C-L).
  • les 4 autres sont des relais activables par un niveau haut (I2C-H).

On peut imaginer que deux modules à 4 relais sont branchés sur la sortie du MCP23017, le premier activable par un niveau bas, l'autre par un niveau haut.

Et on peut aussi imaginer que par la suite, deux autres modules pourront être branchés et configurés, pour arriver à un total de 16 relais.

secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

Ici on voit bien l'avantage d'un paramétrage sur une seule ligne, que l'on aurait pu écrire comme suit, ce qui est nettement moins concis :

secondary1=I2C-L(0)
secondary2=I2C-L(1)
secondary3=I2C-L(2)
etc.

On a donc affaire à une grammaire permettant de configurer différents type de relais :
  • commande directe : GPIO-H ou GPIO-L
  • commande par I2C : I2C-H ou I2C-L
  • relais classique : 1 seule broche de commande
  • relais bistable : 2 broches de commande

Le but de la manoeuvre est d'écrire deux classes :

  • une classe config comme précédemment
  • une classe relay intelligente

4.2.1. La classe config

Parlons d'abord de la classe config

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/config.h

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/config.cpp

Elle a exactement la même interface que la précédente (config.h). par contre la méthode config::read() est plus complexe :

bool config::read(void)
{
  char buffer[MAX_LINE];
  const char *p;
  char *s;
  int n;
  relay *relay;

  if (!open()) {
    Serial.printf("%s: not found\n", m_fileName);
    return false;
  }
  if (getValue("WIFI", "access-point", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:WIFI:access-point not found (error %d)\n", m_fileName, getError());
    return false;
  }
  p = strtok_r(buffer, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing ':'\n", buffer);
    return false;
  }
  strcpy(m_ssid, p);
  p = strtok_r(NULL, ":", &s);
  if (p == NULL) {
    Serial.printf("%s: missing password\n", buffer);
    return false;
  }
  strcpy(m_password, p);
  if (getValue("relays", "main", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:main not found (error %d)\n", m_fileName, getError());
    return false;
  }
  relay = relay::getRelay(0);
  if (relay->create(buffer) != true) {
    return false;
  }
  if (getValue("relays", "secondary", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:secondary not found (error %d)\n", m_fileName, getError());
    return false;
  }
  n = 1;
  p = strtok_r(buffer, ", ", &s);
  while (p != NULL) {
    relay = relay::getRelay(n);
    if (relay == 0) {
      Serial.printf("relay %d: not found\n", n);
      return false;
    }
    if (relay->create(p) != true) {
      return false;
    }
    p = strtok_r(NULL, ", ", &s);
    n++;
  }
  return true;
}

Le début (la récupération de ssid et de password) est identique à la version précédente.

Ensuite le paramètre [relays] main est lu, et un premier relais principal est créé :

  if (getValue("relays", "main", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:main not found (error %d)\n", m_fileName, getError());
    return false;
  }
  relay = relay::getRelay(0);
  if (relay->create(buffer) != true) {
    return false;
  }

Ensuite le paramètre [relays] secondary est lu, et un certain nombre de relais secondaires sont créés :

  if (getValue("relays", "secondary", buffer, sizeof(buffer)) != true) {
    Serial.printf("%s:relays:secondary not found (error %d)\n", m_fileName, getError());
    return false;
  }
  n = 1;
  p = strtok_r(buffer, ", ", &s);
  while (p != NULL) {
    relay = relay::getRelay(n);
    if (relay == 0) {
      Serial.printf("relay %d: not found\n", n);
      return false;
    }
    if (relay->create(p) != true) {
      return false;
    }
    p = strtok_r(NULL, ", ", &s);
    n++;
  }

On voit ici un découpage à l'aide de strtok_r() du paramètre [relays] secondary en un certain nombre de jetons séparés par des virgules :

secondary=I2C-L(0),I2C-L(1),I2C-L(2),I2C-L(3),I2C-H(4),I2C-H(5),I2C-H(6),I2C-H(7)

Mais le format de ces différents jetons est le même que pour le relais principal. 

On remarque que si l'on a configuré trop de relais ( > MAX_RELAY), la fonction getRelay() retourne ZERO et la configuration s'arrête (la partie en vert).

La méthode relay::create() reçoit donc en paramètre une chaine représentant une description de relais :

  • "GPIO-H(4, 5)" pour le relais 0
  • "I2C-L(0)" pour le relais 1
  • "I2C-L(1)" pour le relais 2
  • ...
  • "I2C-H(7)" pour le relais 8 

A noter : la méthode relay::create() utilise strtok_r(), et elle est appelée par config::read() qui utilise aussi strtok_r(). Si l'on utilisait strtok() cela ne fonctionnerait pas.

4.2.2. La classe relay

Ensuite, voici la classe relay :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/relay.h 

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/relay.cpp

#define MAX_DEF         20
#define MAX_RELAY       32

enum accessType {IO, I2C};

class relay
{
  public:
    static bool begin(Adafruit_MCP23017 *mcp, uint8_t address=0);
    static int getCount();
    static relay *getRelay(int n);
    bool create(const char *def);
    relay() {m_onPin = m_offPin = -1; m_level = HIGH;}
    bool isPresent(void);
    bool isLatch(void);
    void print(void);
    void on(void);
    void off(void);
  private:
    static relay m_relay[MAX_RELAY];
    static Adafruit_MCP23017 *m_mcp;
    static bool m_mcpFound;
    accessType m_access;
    int m_onPin;
    int m_offPin;
    int m_level;
};

On voit tout d'abord deux constantes et une énumération :

  • MAX_DEF : la longueur maximale d'une description de relais (GPIO-H(4, 5) par exemple)
  • MAX_RELAY : le nombre maximal de relais configurables
  • accessType : définit la méthode d'accès au relais (GPIO ou I2C)

Les variables importantes sont les suivantes :

  • m_relay : le tableau de relais (32 au maximum)
  • m_mcp : l'adresse de l'objet MCP23017
  • m_access : la méthode d'accès du relais (GPIO ou I2C)
  • m_onPin : la pin d'activation du relais (ON)
  • m_offPin : la pin de désactivation du relais (OFF)
  • m_level : le niveau d'activation du relais (LOW ou HIGH)

m_relay, m_mcp et m_mcpFound sont des variables membres de la classe (static). Cela veut dire qu'elle n'appartient pas à une instance particulière, mais à la classe elle-même. Elles existent donc en exemplaire unique. Elle doivent être déclarées dans le code (relay.cpp).

La classe possède de nombreuses méthodes :

  • begin : initialiser la classe et surtout le composant MCP23017
  • getCount : retourne le nombre de relais configurés
  • getRelay : retourne l'adresse de l'objet relay N°n
  • create : crée un relais
  • relay : le constructeur
  • isPresent : indique que le relais est présent et configuré
  • isLatch : indique qu'il s'agit un relais bistable
  • print : affiche les informations du relais
  • on : active le relais
  • off : désactive le relais

Tout d'abord, voici la méthode begin()

bool relay::begin(Adafruit_MCP23017 *mcp, uint8_t address)
{
  m_mcp = mcp;
  mcp->begin(address);
  Wire.beginTransmission(MCP23017_ADDRESS+address);
  byte error = Wire.endTransmission();
  if (error == 0)  {
    Serial.printf("I2C device found at %02x\n", address);
    m_mcpFound = true;
    return true;
  }
  else if (error == 4) {
    Serial.printf("I2C error at %02x\n", address);
  }
  return false;
}
 

Elle appelle la méthode begin() de l'objet Adafruit_MCP23017. Ensuite elle vérifie que le MCP23017 est présent, car malheureusement la librairie AdaFruit ne le fait pas.

La méthode begin() est une méthode de classe (static). Cela veut dire qu'elle n'appartient pas à une instance particulière, mais à la classe elle-même. Elle devra être appelée ainsi :

Adafruit_MCP23017 mcp;
relay::begin(&mcp);

Nous verrons cela plus tard au niveau du sketch.

Le constructeur est simple :

    relay() {m_onPin = m_offPin = -1; m_level = HIGH;}

Il se contente d'initialiser quelques variables. 

La méthode getRelay() retourne l'adresse d'une instance du tableau m_relay :

relay *relay::getRelay(int n)
{
  if (n < MAX_RELAY) {
    return &m_relay[n];
  }
  return 0;
}

Attention: elle retourne 0 si le N° du relais est supérieur ou égal à MAX_RELAY.

Voyons la méthode create() :

bool relay::create(const char *def)
{
  char tmp[MAX_DEF];
  const char *p;
  char *s;

  strcpy(tmp, def);
  p = strtok_r(tmp, "(", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing parenthesis\n", def);
    return false;
  }
  if (!strcmp(p, "GPIO-H")) {
    m_access = IO;
    m_level = HIGH;
  }
  else if (!strcmp(p, "GPIO-L")) {
    m_access = IO;
    m_level = LOW;
  }
  else if (!strcmp(p, "I2C-H")) {
    m_access = I2C;
    m_level = HIGH;
  }
  else if (!strcmp(p, "I2C-L")) {
    m_access = I2C;
    m_level = LOW;
  }
  else {
    Serial.printf("%s: bad value\n", def);
    return false;
  }
  p = strtok_r(NULL, ")", &s);
  if (p == NULL) {
    Serial.printf("%s: bad format, missing parenthesis\n", def);
  }
  if (strchr(p, ',')) {
    strcpy(tmp, p);
    p = strtok_r(tmp, ", ", &s);
    m_onPin = atoi(p);
    p = strtok_r(NULL, ", ", &s);
    m_offPin = atoi(p);
  }
  else {
    m_onPin = atoi(p);
  }
  return true;
}

Rappel : cette méthode est appelée par la méthode read() de la classe config. Elle reçoit en paramètre une chaine représentant une description de relais :

  • GPIO-H(4, 5) pour le relais 0
  • I2C-L(0) pour le relais 1
  • I2C-L(1) pour le relais 2
  • ...
  • I2C-H(7) pour le relais 8

Le premier jeton est un mot suivi d'une parenthèse. Il est évalué pour savoir si c'est un relais sur GPIO (GPIO-H ou GPIO-L) ou sur bus I2C (I2C-L ou I2C-H).

L'indicateur L ou H permet de savoir si c'est un relais activable sur niveau bas (souvent le cas) ou haut (plus rarement).

Le résultat est stocké dans deux variables :

  • m_access : qui vaudra IO ou I2C
  • m_level qui vaudra LOW ou HIGH

Le deuxième jeton est un entier (ou deux) suivi d'une parenthèse fermante. Il est évalué pour savoir si c'est un relais classique ou bistable. On cherche d'abors si le jeton contient une virgule avec strchr(). Si oui, et que deux valeurs sont présentes, séparées par une virgule, c'est un relais bistable (à deux broches de commande), sinon, c'est un relais classique.

Le résultat est stocké dans deux variables :

  • m_onPin : la pin de commande ON
  • m_offPin : la pin de commande OFF

Quand la méthode relay::on() ou relay::off() sera appelée, l'objet aura donc toutes les informations nécessaires pour savoir comment commander le relais :

  • à l'aide du bus I2C ou non
  • quel niveau de commande appliquer (LOW ou HIGH)
  • sur quel N° de pin il doit envoyer la commande

Voici la méthode relay::on() :

void relay::on(void)
{
  if (!isPresent()) {
    return;
  }
  if (m_access == I2C) {
    // RELAIS sur bus I2C
    if (!m_mcpFound) {
      Serial.printf("MCP not found !!!\n");
      //return;
    }
    if (isLatch()) {
      // RELAIS bistable
      Serial.printf("MCP%d: %d\n", m_onPin, m_level);
      m_mcp->digitalWrite(m_onPin, m_level);
      delay(50);
      Serial.printf("MCP%d: %d\n", m_onPin, !m_level);
      m_mcp->digitalWrite(m_onPin, !m_level);
    }
    else {
      // RELAIS classique
      Serial.printf("I2C%d: %d\n", m_onPin, m_level);
      m_mcp->digitalWrite(m_onPin, m_level);
    }
  }
  else {
    // RELAIS sur GPIO
    if (isLatch()) {
      // RELAIS bistable
      Serial.printf("
GPIO%d: %d\n", m_onPin, m_level);
      digitalWrite(m_onPin, m_level);
      delay(50);
      Serial.printf("GPIO%d: %d\n", m_onPin, !m_level);
      digitalWrite(m_onPin, !m_level);
    }
    else {
      // RELAIS classique
      Serial.printf("GPIO%d: %d\n", m_onPin, m_level);
      digitalWrite(m_onPin, m_level);
    }
  }
}

Un relais bistable se commande avec une simple impulsion de 50ms sur sa broche SET ou RESET.

Si la fonction doit piloter un relais I2C, et que le MCP23017 est absent, elle envoie tout de même les commandes et affiche les informations correspondantes.

Si l'on désire qu'elle ne fasse rien, il suffit de décommenter la ligne en vert.

4.2.3. Le sketch

Voyons maintenant ce qui se passe au niveau du sketch :

https://bitbucket.org/henri_bachetti/esp-config/src/master/esp32-relay-config/esp32-relay-config.ino

Il est identique au précédent, mis à part quelques ajouts :

Un objet Adafruit_MCP23017 est instancié :

Adafruit_MCP23017 mcp;

La classe relay est initialisée dans la fonction setup() :

  if (relay::begin(&mcp) == true) {
    Serial.println("\nMCP23017 found");
  }
  else {
    Serial.println("\nMCP23017 not found !!!");
  }
  config.read();
  config.print();

Les relais sont désactivés à la fin de la fonction setup() :

  Serial.println("RESET all relays");
  for (int n = 0 ; n < relay::getCount() ; n++) {
    relay *relay = relay::getRelay(n);
    relay->off();
  }

Voici le résultat sur le moniteur série :

MCP23017 not found !!!
ssid: SSID
password: ABCDEFGHIJ
LATCH, GPIO, HIGH-LEVEL(4,5)
NORMAL, I2C, LOW-LEVEL(0)
NORMAL, I2C, LOW-LEVEL(1)
NORMAL, I2C, LOW-LEVEL(2)
NORMAL, I2C, LOW-LEVEL(3)
NORMAL, I2C, HIGH-LEVEL(4)
NORMAL, I2C, HIGH-LEVEL(5)
NORMAL, I2C, HIGH-LEVEL(6)
NORMAL, I2C, HIGH-LEVEL(7)
 
Connecting to
SSID
HTTP server started
RESET all relays
GPIO5: 1
GPIO5: 0
MCP not found !!!
MCP0: 1
MCP not found !!!
MCP1: 1
MCP not found !!!
MCP2: 1
MCP not found !!!
MCP3: 1
MCP not found !!!
MCP4: 0
MCP not found !!!
MCP5: 0
MCP not found !!!
MCP6: 0
MCP not found !!!
MCP7: 0
Connected to AP successfully!
WiFi connected to
SSID
IP address:  
192.168.1.37

Le MCP23017 n'est pas branché, ce qui est normal, je ne l'ai pas fait.

Les 9 relais sont configurés (la partie en vert).

Les bonnes commandes sont envoyées aux relais (la partie en orange), en particulier :

  • une commande sur le GPIO5 du MCP23017 à HIGH puis LOW (c'est un relais bistable)
  • les 4 premiers relais secondaires sont désactivés grâce à une commande sur leur GPIO à HIGH (ce sont des relais actifs au niveau LOW)
  • les 4 derniers relais secondaires sont désactivés grâce à une commande sur leur GPIO à LOW (ce sont des relais actifs au niveau HIGH)

Cela se passe très bien. C'est une bonne raison pour continuer dans cette voie.

Pourquoi un tel bazar pour configurer quelques relais ?

Il sera possible ensuite d'utiliser ces informations pour bâtir une interface homme machine avec des pages WEB dynamiques, qui permettront en quelques clicks :

  • d'ajouter ou de retirer des relais
  • d'assigner un rôle à chaque relais

Et tout cela sans avoir à recompiler le code !

5. Modification des paramètres en live

Ce sujet fera l'objet d'un autre article. Mais dans la pratique c'est relativement simple si l'on maîtrise déjà les formulaires HTML.

Une fois le formulaire validé, il suffit, dans la fonction de traitement de l'URL, d'ouvrir le fichier en écriture, de réécrire les nouveaux paramètres, et de le fermer.

Dans ce sketch, examiner la fonction handleFormData :

https://bitbucket.org/henri_bachetti/domoticz-esp8266-tft-display/src/master/arduino/esp8266-tft-display/esp8266-tft-display.ino

Remarque : dans le cadre de l'écriture d'un WebServer, si l'on a utilisé le système de fichiers SPIFFS pour stocker les ressources (pages HTML, images, etc.), à chaque fois que l'on rechargera les ressources le fichier de paramètre sera écrasé.

On peut conrourner ce problème en écrivant les nouveaux paramètres en EEPROM. Ainsi, à chaque démarrage, si les paramètres existent en EEPROM, le logiciel les lit et les utilise, sinon, il récupère ceux du fichier de paramètres.

On peut également imaginer une solution plus simple, sans page WEB, en transférant directement (upload) un nouveau fichier de paramètres, à l'aide du protocole HTTP, ou même FTP.

6. Téléchargement

Le projet se trouve ici :

https://bitbucket.org/henri_bachetti/esp-config.git

7. Conclusion

Cette pré-étude a été réalisée dans le but de créer un projet d'irrigation automatisée à base d'ESP32 (à venir), pour lequel j'envisage un paramètrege très souple, afin de laisser à l'utilisateur final le choix de ses interfaces de sortie (des électrovannes), à l'aide de simples pages WEB.


Cordialement

Henri


vendredi 9 juillet 2021

Piloter des Relais ou des MOSFEts à l'aide d'un Module MCP23008 ou MCP23017

 



Piloter des Relais ou des MOSFEts

à l'aide d'un Module MCP23008 ou MCP23017


Qu'il s'agisse de piloter un certain nombre de relais ou de MOSFETs, le nombre de sorties d'un ARDUINO est un facteur plutôt limitant. Jusqu'à 4 ou 8 voies c'est envisageable, au delà cela devient problématique.

Nos microcontrôleurs habituels disposent rarement de plus de 16 sorties utilisables, et l'on a pas forcément envie de s'encombrer d'une carte ARDUINO MEGA.

La solution est d'employer un expander I2C du genre MCP23017. Le nombre de sorties nécessaires au niveau du microcontrôleur sera donc réduit à seulement 2 :

  • ARDUINO : A4 & A5
  • ESP8266 : GPIO4 & GPIO5
  • ESP32 : GPIO21 & GPIO22

On emploie souvent un module expander I2C PCF8574 lorsque l'on veut piloter un écran LCD alphanumérique. Il suffit de souder le module à l'arrière du LCD, et le tour est joué :

Alors pourquoi ne pas adopter la même solution pour un module à relais ou à MOSFETs ?

Pour mes réalisations j'ai choisi le MCP23008 et le MCP23017, plus universels et plus modernes que le PCF8574 et le PCF8575.

Le PCF8574 et le PCF8575 ont des performances limitées en courant de sortie (50mA à l'état bas et 100µA à l'état haut. Ils seraient donc incapables de piloter un module à relais.

C'est pour cette raison que je leur préfère largement les MCP23008 et MCP23017, qui offrent de plus la possibilité de configurer leurs entrées avec des résistances de PULLUP internes, et possèdent une broche d'interruption.

Un MCP23008 ou un MCP23017 peut être alimenté sous 5V, mais également sous 3.3V.

Voici un module MCP23017 à 16 entrées / sorties :

Vue de dessous

Ce module ne dispose pas de trous de fixation. Il sera donc câblé en l'air, ou collé. Ce n'est pas une solution très esthétique et la fiabilité ne sera pas optimale.

Ces modules existent sous d'autres formes, plus facile à fixer :

Il faut choisir le modèle qui simplifiera au mieux le câblage et la mécanique.

Dans la suite de cet article, nous allons appeler ces modules "le module noir" et "le module vert".

1. Câblage

Je présente ici deux solutions :

  • relier un module MCP23017 du commerce avec le module relais à l'aide d'un câble maison
  • enficher un module MCP23008 ou MCP23017 maison sur le module relais

1.1. Fabriquer un câble DUPONT multipoints

Tout d'abord, pour une réalisation fiable, je déconseille totalement d'utiliser des fils DUPONT du commerce pour relier deux modules (MCP23017 et relais) entre eux. C'est la meilleure source de faux contacts qui soit. Il est préférable de fabriquer un câble à l'aide de boîtiers DUPONT multipoints :

boîtiers DUPONT 8 points


On en trouve différents modèles, jusqu'à 8 pins, y compris à double rangée.

On aura besoin de contacts à sertir :

On aura aussi besoin d'une pince. Des kits sont disponibles :

Fabriquer des câbles multi-conducteurs avec des connecteurs DUPONT n'est pas une mince affaire, mais ce n'est pas insurmontable : 


On voit ici deux cartes reliées par un câble à 17 conducteurs réalisé a l'aide de boîtiers DUPONT à 6 et 5 broches. Ce n'est pas très esthétique mais c'est une solution qui fonctionne.

Dans la suite de ce document, j'utilise des symboles schématiques de connecteurs DUPONT à 10 broches, ou même 17 broches. On trouve facilement des barrettes à souder pour PCB, mâles ou femelles, jusqu'à 40 points, mais on ne trouve pas de boîtiers DUPONT possédant plus de 8 points. On pourra utiliser plusieurs boîtiers pour réaliser un câble de plus de 8 points :

  • 10 points : 2 x 5 points
  • 17 points : 2 x 6 points + 5 points

1.2. Module expander DIY

Une autre solution, qui simplifie grandement le câblage et dont la fiabilité sera probablement supérieure, est de réaliser un module expander maison, avec un MCP23008 ou un MCP23017, et un connecteur femelle adapté qui vient s'enficher directement sur le connecteur mâle du module relais.

J'en ai développé un pour mon module à 16 MOSFETs : voir 3.3. Module MCP23017 DIY :

Module MCP23017 DIY

Pour une réalisation en exemplaire unique, il est facile de réaliser ce genre de module sur une plaquette à pastilles :

Voir ici : Développement électronique ARDUINO

Paragraphe 8.7. Le montage sur plaquette à pastilles

2. Module à relais

En général les modules relais possèdent un connecteur d'alimentation. Parfois, un cavalier est présent sur ce connecteur (photo ci-dessous). Il permet d'alimenter les bobines des relais à l'aide de la tension VCC présente sur le connecteur d'entrée, sans utiliser le connecteur d'alimentation. Or cette tension VCC provient souvent de la carte ARDUINO elle-même. Si l'ARDUINO est alimenté par sa broche VIN ou par le connecteur USB, le courant disponible sera insuffisant pour alimenter 8 ou 16 relais simultanément.

Il vaut mieux prévoir un câble d'alimentation séparé, provenant directement d'une alimentation 5V ou 12V. Pour cette alimentation, prévoir 375mW par relais, donc environ 3W pour 8 relais, 6W pour 16 relais.

Si le module est équipé de relais 12V, retirer le cavalier est indispensable, car il faudra appliquer du 12V entre les broches JD-VCC et GND.

Les modules relais ont évolué, grâce je pense à des optocoupleurs plus sensibles. Le dernier que j'ai acheté (4 relais) demande un courant de commande de seulement 1.4mA. Un module 16 relais aura donc besoin de 22mA au total, ce qu'un ARDUINO est parfaitement capable de fournir.

Les modèles plus anciens demandaient souvent 10mA.

2.1. Module à 8 relais

Le connecteur d'entrée possède 10 broches :

  • GND
  • 8 entrées
  • VCC

2.1.1. Expander du commerce

Pour commander ce module relais à l'aide d'un expander il nous faudrait un module équipé d'un MCP23008, malheureusement introuvable, à moins de le réaliser soi-même. Nous pouvons nous rabattre sur un MCP23017 dont on n'utilisera que 8 sorties.

Avec le module MCP23017 vert, le câble sera simple à réaliser :


4 boîtier DUPONT à 5 broches seront nécessaires.

Voici une petite vidéo:

J'ai utilisé le module vert, et deux câbles réalisés à l'aide de quatre boîtiers DUPONT à 5 broches.

Les relais sont alimentés en 12V par les broches JD-VCC et GND (à droite), car ce sont des modèles 12V.

Et voici le code correspondant :

#include <Wire.h>
#include "Adafruit_MCP23017.h"

Adafruit_MCP23017 mcp;

void setup() {
  mcp.begin();      // use default address 0
  for (int n = 0 ; n < 8 ; n++) {
    mcp.pinMode(n, OUTPUT);
    mcp.digitalWrite(n, HIGH);

  }
}

void loop() {
  // activer relais 1 à 8
  for (int n = 0 ; n < 8 ; n++) {
    mcp.digitalWrite(n, LOW);
    delay(200);
    mcp.digitalWrite(n, HIGH);
    delay(200);
  }
}

Comme ce sont des relais activables par un niveau bas, on les désactive dans la fonction setup() :

    mcp.digitalWrite(n, HIGH);

Avec le module MCP23017 noir, VCC sera relié par un fil séparé, car VCC et GND sont situés sur deux rangées différentes. Ce n'est pas une excellente solution. Les risques de faux contacts sur un connecteur DUPONT à 1 broche sont plus importants :

2.1.2. Expander DIY

J'ai réalisé un module expander à l'aide d'un MCP23008 :

Il possède les deux connectiques, pour un module à 4 relais (6 broches) et pour un module à 8 relais (10 broches).

l'adresse I2C (de 0x20 à 0x27) est sélectionnable à l'aide d'un DIP-SWITCH :


Il a été réalisé sur une plaquette à pastilles. La réalisation souffre d'une petite erreur  : contrairement à ce qui était prévu, les connecteurs de sortie ont été soudés côté composants. Une fois en place sur le module à relais la carte se retrouve à l'envers. Ce n'est pas très esthétique, mais cela ne gène pas le fonctionnement :



Je me console en me disant que l'ensemble n'est pas plus épais que le module relais seul.

Le module MCP23008 déborde d'environ 1cm du module relais. Pour une réalisation plus compacte, il faudrait utiliser des composants CMS sur un PCB maison.

2.1. Module à 16 relais

Examinons une carte à 16 relais :

Le connecteur d'entrée possède 20 broches :

  • 2 x GND
  • 16 entrées
  • 2 x 5V

Il possède en outre un bornier d'alimentation (12V car ce module est équipé de relais 12V).

Par contre, comme on peut le voir sur la photo, les entrées du module relais sont réparties sur deux connecteurs (entrées paires et entrées impaires) :

  • rangée du haut : 5V, 2, 4, 6, 8, 10, 12, 14, 16, GND
  • rangée du bas : 5V, 1, 3, 5, 7, 9, 11, 13, 15, GND
Le moins que l'on puisse dire est que le fabricant n'a pas pensé au câblage autrement qu'avec des fils DUPONT.

2.2.1. Expander du commerce

Celui-ci sera assez fastidieux à réaliser, que l'on utilise le module MCP23017 vert :

Ou que l'on utilise le module MCP23017 noir :

On peut se demander s'il ne vaut pas mieux utiliser 2 modules à 8 relais. Avec le module vert par exemple, qui dispose de broches VCC et GND pour chaque port A0-A7 et B0-B7. Pour ma part je n'hésiterais pas.

2.2.2. Expander DIY

Il ne serait pas bien compliqué de réaliser un module DIY à base de MCP23017, enfichable sur le module relais, comme précédemment.

3. Module à MOSFETs

Lorsque l'on commute des charges alimentées en tension continue (5V, 12V ou 24V), un relais n'est pas forcément le composant le plus adapté. Un MOSFET fera ce travail en silence, et avec une fiabilité supérieure.

Sa grille ne consomme aucun courant. Seule la résistance de PULLDOWN que l'on installe entre grille et source consomme : 50µA pour une résistance de 100KΩ sous 5V, à comparer avec les 70mA ou 150mA d'un relais..

J'avais présenté il y a un an deux modules à 8 et 16 MOSFETs :

Un Module à 8 ou 16 Mosfets

3.1. Module à 8 MOSFETs IRLZ44N

Dans cet article cité précédemment je décrivais également un module à 8 MOSFETs IRLZ44N, réalisé sur plaquette à pastilles :


On peut également le piloter à l'aide d'un module MCP23017.

Sur cette photo il est équipé d'un connecteur DUPONT femelle, mais on peut également souder un connecteur mâle.

Avec le module MCP23017 vert :

Avec le module MCP23017 noir :

3.2. Module à 16 MOSFETs


Le module à 16 MOSFETs AO3400 était à l'époque destiné à commander 16 petits rubans de LEDs. Il était piloté par une carte ARDUINO PRO MINI et un expander SX1509 à 16 sorties PWM :

Un éclairage d'escalier à LEDs

Ce module va être à nouveau utilisé dans un autre projet pour commander des électrovannes 12V à l'aide d'un ESP32.

Problème : l'ESP32 ne dispose pas d'assez de sorties pour piloter 16 électrovannes et divers capteurs et LEDs.

Il est possible d'utiliser un module expander du commerce, comme s'il s'agissait d'un module à relais.

Pour rappel le brochage du module à 16 MOSFETs est le suivant :

  • 1 : GND
  • 2 à 17 : entrée N°1 à 16
Contrairement à un module relais, un module à MOSFETs n'a pas besoin d'alimentation (VCC) sur ses entrées.

3.2.1. Expander du commerce

Avec le module MCP23017 vert, le câblage est le suivant :

Avec le module MCP23017 noir, le câblage sera très peu différent :

3.2.2. Expander DIY

Pour mes propres besoins, plutôt que d'utiliser un module MCP23017 du commerce, j'ai développé un module pouvant s'enficher sur le module à 16 MOSFETs AO3400.

Il s'agit en quelque sorte d'un shield, comme on dit dans le monde ARDUINO.

Les sorties sont assignées comme ceci :

  • 2 à 9 : port B0 - B7
  • 10 à 17 : port A0 - A7

Les deux ports sont permutés. Cela m'a permis d'avoir un schéma plus clair est surtout un routage du PCB permettant de réduire la largeur de la carte, qui doit être de moins de 30mm pour pouvoir être enfichée à l'horizontale sur le module à MOSFETs, sans gêner l'accès au connecteur de sortie de celui-ci.

Il s'agit d'un MCP23017 DIL 28 pattes. Ce circuit peut être facilement essayé sur une breadboard, et sa réalisation sur une plaquette à pastilles ne poserait aucun problème.

Le groupe de 3 jumpers JP1 permet de changer l'adresse I2C (de 0x20 à 0x27) :

A0, A1 et A2 correspondent aux broches 15, 16 et 17 sur le schéma.


L'adresse est sélectionnée à l'aide de points de soudure :

Pour obtenir l'adresse 0x20 il faudra réunir les pastilles 1-2, 4-5, 7-8
Pour obtenir l'adresse 0x21 il faudra réunir les pastilles 2-3, 4-5, 7-8
Pour obtenir l'adresse 0x22 il faudra réunir les pastilles 1-2, 5-6, 7-8
Pour obtenir l'adresse 0x23 il faudra réunir les pastilles 1-2, 4-5, 8-9
Pour obtenir l'adresse 0x24 il faudra réunir les pastilles 2-3, 5-6, 8-9
etc.

Le dossier se trouve ici :

https://bitbucket.org/henri_bachetti/arduino-prototyping/src/master/mosfet-module/sot23-16-mcp23017/

3.3. Module à MOSFETs tout en un

Pour en terminer avec cet exposé : si l'on est motivé, il est possible de réaliser un module tout en un :


Ce module à I2C à 8 MOSFETs IRLU2905 est l'équivalent du module à 8 MOSFETs IRLZ44N, en version PCB avec des MOSFETs de taille plus réduite (boîtier IPAK ou TO251AA). La commande se fait par le bus I2C grâce à un MCP23008 (8 sorties).

Bien entendu on peut remplacer les IRLU2905 par des MOSFETs de son choix (IRLU024, IRLU8743, etc.). Le schéma restera identique, seule l'implantation sur la carte sera éventuellement à modifier, si l'on désire utiliser des IRLZ44N en boîtier TO220 par exemple.

Quelques référence sont données ici :

MOSFETS de puissance

Choisir de préférence un modèle ayant une tension VGSth la plus faible possible.

La carte est équipée d'un connecteur d'entrée JST XH à 4 broches (J1) :

  • GND
  • SDA et SCL
  • VDD pour l'alimentation du MCP23008 (5v ou 3.3V)
Elle est également équipée de borniers :

  • connecteur 10A d'alimentation de puissance (J2)
  • 8 borniers 7A pour 8 charges

La broche VDD du connecteur d'entrée est limitée à 3A. Si les besoins sont supérieurs, utiliser le bornier 10A d'alimentation de puissance.

Un cavalier peut être mis en place sur J2 (bornes 2&3) si :

  • les charges fonctionnent sous la même tension que VDD
  • le courant est inférieur à 3A

Contrairement au module précédent, comme il y a de l'espace inoccupé sur la carte, l'adresse I2C (de 0x20 à 0x27) est sélectionnable à l'aide d'un DIP-SWITCH :

On remarque la présence de 3 résistances de PULLUP de 100KΩ qui polarisent les entrées A0 à A2 à VDD lorsque le DIP-SWITCH est ouvert. Ces résistances sont indispensables. Elles entraînent une consommation minime : 150µA sous 5V. On peut adopter une valeur de 470KΩ ou 1MΩ si l'on désire réduire cette consommation, mais le montage sera plus sensible aux parasites électromagnétiques.

Le dossier se trouve ici :

https://bitbucket.org/henri_bachetti/arduino-prototyping/src/master/mosfet-module/mcp23008-eight-mosfet/

4. Librairies

Voici les librairies pour le MCP23008 et MCP23017 :

MCP23008 : https://github.com/adafruit/Adafruit-MCP23008-library.git

MCP23017 : https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library.git

Elles sont installables à partir du gestionnaire de bibliothèques de l'IDE ARDUINO.

Dans ses dernières version (2.0.0 et suivantes) la librairie Adafruit-MCP23017-Arduino-Library gère le MCP23008 et le MCP23017, y compris les versions SPI MCP23S08 et le MCP23S17.

Si l'on utilise cette dernière version les codes proposés dans les paragraphes suivants seront légèrement différents :

#include "Adafruit_MCP23017.h"
// à remplacer par
#include <Adafruit_MCP23X17.h>
 
#include "Adafruit_MCP23008.h"
// à remplacer par
#include <Adafruit_MCP23X08.h>
 
Adafruit_MCP23017 mcp;
// à remplacer par
Adafruit_MCP23X17 mcp;
 
Adafruit_MCP23008 mcp;
// à remplacer par
Adafruit_MCP23X08 mcp;
 
  mcp.begin();
  // à remplacer par
  mcp.begin_I2C();

D'autre part, la méthode begin() des versions 1.XXX accepte un argument allant de 0 à 7. Elle ajoute elle-même 0x20 pour obtenir l'adresse I2C, tandis que la méthode begin_I2C() doit recevoir l'adresse I2C (0x20 à 0x27).

5. Essais

Après avoir réalisé le câblage et l'avoir vérifié, un petit test avec une carte ARDUINO chargé avec le sketch I2C-Scanner permettra de vérifier que le MCP23017 ou le MCP23008 est bien vu sur le bus I2C à l'adresse voulue.

5.1. Essais du module MCP23008 DIY

Le module MCP23008 DIY

Je ne possède pas de module à 8 relais, mais voici le module PCP23008 enfiché sur un module à 4 relais :

Le câblage de la plaquette à pastilles (80mm x 30mm) est réalisé à l'aide de fil à wrapper.

Un premier essai avec une UNO chargée avec le sketch I2C-Scanner montre que la carte fonctionne :

Scanning...
Appareil I2C trouve a cette adresse 0x20  !
Fin

Testons d'abord le module MCP23008 seul. Ce petit sketch permet de faire clignoter une LED branchée entre les pins 1 (GND) et 2 du connecteur de sortie (on doit toujours placer une résistance en série avec une LED  : 220Ω à 1KΩ) :

#include <Wire.h>
#include "Adafruit_MCP23008.h"

#define RELAY       0

Adafruit_MCP23008 mcp;
  
void setup() {  
  mcp.begin();
  mcp.pinMode(RELAY, OUTPUT);
}

void loop() {
  mcp.digitalWrite(RELAY, HIGH);
  delay(500);
  mcp.digitalWrite(RELAY, LOW);
  delay(500);
}

Testons ensuite le module MCP23008 enfiché sur un module à 4 relais. Voici un petit code pour activer les relais un à un :

#include <Wire.h>
#include "Adafruit_MCP23008.h"

Adafruit_MCP23008 mcp;

void setup() {
  mcp.begin();      // use default address 0
  for (int n = 0 ; n < 4 ; n++) {
    mcp.pinMode(n, OUTPUT);
    mcp.digitalWrite(n, HIGH);
  }
}

void loop() {
  for (int n = 0 ; n < 4 ; n++) {
    mcp.digitalWrite(n, LOW);
    delay(1000);
    mcp.digitalWrite(n, HIGH);
  }
}

Voici le résultat :


5.2. Essais du module MCP23017 DIY

Le module MCP23017 DIY

Il s'agit d'un prototype sur PCB simple face, avec des straps (fils rouges). 

Le sélecteur d'addresse I2C

Les trois broches d'adresse sont reliées à GND par un pâté de soudure. L'adresse sera 0x20.

Le module en place

J'ai utilisé un connecteur femelle coudé car c'est un prototype simple face, sans trous métallisés. La carte est donc enfichée verticalement, ce qui est assez encombrant.

Souder un connecteur droit côté soudures est plutôt déconseillé. Le risque est d'arracher les pastilles en retirant le module.

Si l'on fait fabriquer le PCB par JLCPCB par exemple, on pourra souder un connecteur droit, sous la carte, avec soudures côté composants, et celle-ci sera à plat, au dessus de la carte à MOSFETs.

Le PCB du module à MOSFETs, lui, a été réalisé par JLCPCB.

Un premier essai avec une UNO chargée avec le sketch I2C-Scanner montre que la carte fonctionne :

Scanning...
Appareil I2C trouve a cette adresse 0x20  !
Fin

Testons d'abord le module MCP23017 seul. Ce petit sketch permet de faire clignoter une LED branchée entre les pins 1 (GND) et 2 du connecteur de sortie (on doit toujours placer une résistance en série avec une LED  : 220Ω à 1KΩ) :

#include <Wire.h>
#include "Adafruit_MCP23017.h"

#define LED       8

Adafruit_MCP23017 mcp;
  
void setup() {  
  mcp.begin();
  mcp.pinMode(LED, OUTPUT);
}

void loop() {
  mcp.digitalWrite(LED, HIGH);
  delay(500);
  mcp.digitalWrite(LED, LOW);
  delay(500);
}

Attention : le MCP23017 est limité à 25mA par sortie et 150mA au total. Il ne s'agit pas de le surcharger avec 16 LEDs consommant 20mA chacune, allumées simultanément. Il n'apprécierait probablement pas.

Testons ensuite le module MCP23017 enfiché sur la carte à MOSFETs équipée d'une LED (avec résistance de 1KΩ) sur chaque sortie. Voici un petit code pour allumer les LEDs en chenillard :

#include <Wire.h>
#include "Adafruit_MCP23017.h"

Adafruit_MCP23017 mcp;

void setup() {
  mcp.begin();      // use default address 0
  for (int n = 0 ; n < 16 ; n++) {
    mcp.pinMode(n, OUTPUT);
  }
}

void loop() {
  // allumer LEDs 1 à 8
  for (int n = 8 ; n < 16 ; n++) {
    mcp.digitalWrite(n, HIGH);
    delay(20);
    mcp.digitalWrite(n, LOW);
    delay(20);
  }
  // allumer LEDs 9 à 16
  for (int n = 0 ; n < 8 ; n++) {
    mcp.digitalWrite(n, HIGH);
    delay(20);
    mcp.digitalWrite(n, LOW);
    delay(20);
  }
}
 
Ce petit test m'a permis de trouver une mauvaise soudure sur la résistance R12 du module à MOSFETs. La soudure manuelle n'est pas fiable à 100%, surtout avec des composants CMS, d'où l'intérêt de tester la carte avant de l'utiliser dans un montage.

Voici le résultat :


6. Conclusion

Qu'il s'agisse de commander un écran LCD, des relais ou des MOSFETs, l'expander I2C est une solution que j'ai de plus en plus tendance à systématiser, car cela permet de réduire fortement le nombre de sorties à monopoliser sur le microcontrôleur, et aussi de minimiser la quantité de fils pour le câblage.

7. Liens utiles

D'autres articles à propos de relais :

Piloter un relais. Transistor unipolaire ou bipolaire ?

Modules à relais DIY

Relais Retardé sans Microcontrôleur

Télécommande de Relais par Infra-Rouge


Cordialement

Henri

8. Mises à jour

10/07/2021 : 2.1.2. Expander DIY
                      5.1. Essais du module MCP23008 DIY