jeudi 27 février 2020

ARDUINO : la fragmentation mémoire




ARDUINO : la fragmentation mémoire


Lorsque l'on utilise des objets de la classe String sur ARDUINO, on voit souvent des commentaires visant à déconseiller son usage. L'utilisation de String pourrait provoquer apparemment une fragmentation de la mémoire.

On constate aussi que certaines personnes qui ont ré-écrit leur code en éliminant les objets String ont résolu leurs problèmes comme par magie :

https://forum.arduino.cc/t/softwareserial-not-reliable/879780/73

Si l'on jette un coup d’œil au début de ce sujet de discussion, le demandeur soupçonne un problème de fiabilité de SoftwareSerial. Ensuite au post #73, après avoir réécrit son code sans utiliser d'objets String, il conclut :

Avoid Strings like the Covid19. Don't let your software control your memory allocations and de-allocations.

Tout d'abord, qu'est ce que la fragmentation mémoire ?

1. La théorie

1.1. L'allocation dynamique

Il faut tout d'abord expliquer ce qu'est l'allocation dynamique de la mémoire. Voici la représentation graphique de la mémoire d'un ARDUINO :


On voit ici une zone appelée Free Memory, entre Stack (pile) et BSS (zone des variables globales).

Cette zone s'appelle Heap (le tas en français). On peut déjà remarquer une chose : si le logiciel comporte un grand nombre de variables globales la taille du tas sera réduite d'autant.

L'allocation dynamique de mémoire revient donc à réserver un bloc mémoire de la taille demandée dans cette zone appelée tas. Le demandeur est responsable de la libération de ce boc quand il sera devenu inutile.

1.2. La fragmentation

La fragmentation mémoire est un phénomène qui crée des trous dans le tas au fur et à mesure des allocations et libérations de blocs de mémoire :


On voit sur cette image que lorsque l'on alloue des objets dynamiquement, au fur et à mesure des allocations et libérations on peut très facilement aboutir à une impossibilité d'allouer de la mémoire pour un objet.

Ici l'objet LargeObj aurait pu être facilement alloué au départ, ou après avoir alloué Obj1, mais l'allocation et libération successives d'objets a créé des trous et ces trous ont une taille insuffisante pour accueillir un objet de taille moyenne : LargeObj.

1.3. La rencontre de la pile et du tas

Lors de l'exécution d'un logiciel, la pile est utilisée pour empiler les adresses de retour des fonctions ainsi que les variables locales.

La pile est gérée grâce à un registre spécial : le pointeur de pile.

Lorsque l'on appelle une fonction l'adresse de retour est placée sur la pile et le pointeur de pile est décrémenté de 2 octets (la taille d'une adresse).

Si la fonction appelée déclare une variable locale le pointeur de pile est décrémenté de la taille de la variable.

Si cette fonction appelle une autre fonction, le pointeur de pile est encore décrémenté, de la même manière.

Si la valeur du pointeur de pile descend trop bas il y a un risque d'aller corrompre les adresses hautes du tas, et cela peut avoir des conséquences imprévisibles.

Quelle est la taille du tas et de la pile ? Ces deux espaces ont environ la même taille. Ils se partagent la mémoire libre à parts égales.

Si aucune allocation dynamique de mémoire n'est utilisée, le tas n'existe pas et donc la pile dispose de toute la mémoire libre. Le risque de crash est réduit.

1.4. String

Pourquoi incriminer la classe String ?

La classe String utilise l'allocation dynamique de mémoire pour le stockage de la chaîne de caractères que l'objet contient.

Car String peut contenir des chaînes de caractères de tailles très diverses et donc, sans allocation dynamique, cela conduirait à fixer une taille maximale, et donc à consommer de la mémoire inutilement.

1.5. Mise en évidence

A la recherche d'un moyen de mettre en évidence la fragmentation de la mémoire je suis tombé sur cet article :

https://cpp4arduino.com/2018/11/06/what-is-heap-fragmentation.html

Je ne vais pas traduire les explications de l'auteur, qui sont grosso modo les mêmes que précédemment, en plus détaillé. Son article a l'avantage de comporter des images, et elles sont suffisamment parlantes.

2. Expérimentons

L'auteur propose un code que l'on peut trouver ici :

https://github.com/bblanchon/cpp4arduino/tree/master/HeapFragmentation

Un tableau de String est instancié statiquement (variable globale) :

String strings[NUMBER_OF_STRINGS];

Ensuite le tableau subit une série d'affectations en boucle infinie :

void loop()
{
  for (String &s : strings) {
    // Replace each string with a new random one
    s = generateRandomString();
  }

  Serial.print(getTotalAvailableMemory());
  Serial.print(' ');
  Serial.print(getLargestAvailableBlock());
  Serial.print(' ');
  Serial.print(getFragmentation());
  Serial.println();
}

La fonction generateRandomString() permet de créer une String de longueur aléatoire.

L'auteur affiche ensuite quelques valeurs :

  • la quantité de mémoire libre
  • la taille du plus grand bloc disponible
  • le pourcentage de fragmentation

Ce code a toutefois un problème : en cas de défaut d'allocation aucune erreur n'est signalée.

J'ai donc modifié la fonction afin d'afficher un message d'erreur :

String generateRandomString()
{
  static int counter = 0;
  String result;

  int len = random(SMALLEST_STRING, LARGEST_STRING);
  for (int i = 0 ; i < len ; i++) {
    result += '?';
    if (result.length() != i+1) {
      Serial.print("count :"); Serial.println(counter);
      Serial.print("wanted :"); Serial.println(len);
      Serial.print("got :"); Serial.println(i);
      Serial.println("TOO BAD");
      while (1);
    }

  }
  counter++;
  return result;
}

Un compteur est également affiché en cas d'erreur, permettant de savoir au bout de combien d'allocations l'erreur est survenue.

La quantité de mémoire désirée ainsi que celle réellement obtenue sont également affichées.

Voici le code complet :

// C++ for Arduino
// What is heap fragmentation?
// https://cpp4arduino.com/

// This program uses several String instances of random size.
// Over time, the Strings produces heap fragmentation.
// The program prints the value of the heap fragmentation.
// You can use the Serial Plotter to see the curve.

#include "MemoryInfo.h"

// Try to change these values and observe the evolution of the fragmentation.
// As you'll see, fragmentation disappears if SMALLEST_STRING == LARGEST_STRING,
// i.e. if the size of the strings is constant.
const size_t NUMBER_OF_STRINGS = 20;
const size_t MAX_MEM_USAGE = getTotalAvailableMemory() * 3 / 4;
const size_t LARGEST_STRING = MAX_MEM_USAGE / NUMBER_OF_STRINGS;
const size_t SMALLEST_STRING = LARGEST_STRING / 5; // varies from 1x to 5x

// The collection of string.
// They allocate and release memory from the heap. We could have called malloc()
// and free() manually but using String is simpler.
String strings[NUMBER_OF_STRINGS];

// At program startup, initialize the serial port.
void setup()
{
  Serial.begin(115200);
  Serial.print("LARGEST_STRING "); Serial.println(LARGEST_STRING);
  Serial.print("SMALLEST_STRING "); Serial.println(SMALLEST_STRING);
  randomSeed(analogRead(0));
}

// At each iteration, get new values for each string and print the value.
void loop()
{
  for (String &s : strings) {
    // Replace each string with a new random one
    s = generateRandomString();
  }

  Serial.print(getTotalAvailableMemory());
  Serial.print(' ');
  Serial.print(getLargestAvailableBlock());
  Serial.print(' ');
  Serial.print(getFragmentation());
  Serial.println();
}

// Generates a string whose length is picked randomly between SMALLEST_STRING
// and LARGEST_STRING
String generateRandomString()
{
  static int counter = 0;
  String result;

  int len = random(SMALLEST_STRING, LARGEST_STRING);
  for (int i = 0 ; i < len ; i++) {
    result += '?';
    if (result.length() != i+1) {
      Serial.print("count :"); Serial.println(counter);
      Serial.print("wanted :"); Serial.println(len);
      Serial.print("got :"); Serial.println(i);
      Serial.println("TOO BAD");
      while (1);
    }
  }
  counter++;
  return result;
}

Les conditions de test sont les suivantes :

  • le tableau contient 20 Strings
  • la mémoire est occupée au maximum à 75%

Il faudra ajouter ces quelques fichiers supplémentaires :

https://github.com/bblanchon/cpp4arduino/blob/master/HeapFragmentation/MemoryInfo.cpp

https://github.com/bblanchon/cpp4arduino/blob/master/HeapFragmentation/MemoryInfo.h

Dans un sous-répertoire Ports il faut ajouter :

3. Les tests

Essai sur ARDUINO NANO :

LARGEST_STRING 61
SMALLEST_STRING 12
834 809 3.00
666 542 18.62
612 481 21.41
588 362 38.44
count :97
wanted :52
got :50
TOO BAD

On rencontre un problème après 97 allocations mémoire.

Essai sur ARDUINO MEGA :

LARGEST_STRING 291
SMALLEST_STRING 58
4043 3963 1.98
3564 3055 14.28
3357 2313 31.10
3085 1777 42.40
2687 1487 44.66
2490 939 62.29
2391 667 72.10
2376 667 71.93
2363 667 71.77
count :195
wanted :286
got :245
TOO BAD

On rencontre un problème après 195 allocations mémoire.

Faisons un deuxième essai sur la MEGA en modifiant NUMBER_OF_STRINGS :

const size_t NUMBER_OF_STRINGS = 100;

LARGEST_STRING 54
SMALLEST_STRING 10
3737 3661 2.03
3291 2818 14.37
2869 2195 23.49
2670 1934 27.57
2424 1666 31.27
2287 1451 36.55
2212 1340 39.42
2141 1340 37.41
2086 1127 45.97
2045 911 55.45
2030 801 60.54
1995 801 59.85
1943 801 58.78
1920 801 58.28
1893 801 57.69
1873 637 65.99
1855 581 68.68
1827 526 71.21
1822 470 74.20
1816 416 77.09
1810 416 77.02
1797 416 76.85
1793 416 76.80
1790 416 76.76
1784 416 76.68
1777 416 76.59
1770 304 82.82
1768 248 85.97
1765 248 85.95
1760 248 85.91
1760 248 85.91
1759 248 85.90
1756 192 89.07
1754 192 89.05
1753 192 89.05
1753 192 89.05
1744 192 88.99
count :3705
wanted :53
got :52
TOO BAD

On rencontre un problème après 3705 allocations mémoire.

On se rend compte assez rapidement que si l'on fait varier les paramètres du test (NUMBER_OF_STRINGS, SMALLEST_STRING) on peut très bien ne rencontrer aucun problème, ou retarder son apparition.

Ce test peut sembler extrême. Allouer 20 strings ayant une taille de 12 à 61 octets sur une NANO n'arrive pas tous les jours.

Vus sous l'angle des variables globales, ce test est tout aussi extrême. A part le tableau de String il n'y en a pratiquement pas.

Or il est tout de même assez rare qu'un code ne comporte pas de variables globales. Le sketch laisse donc un maximum d'espace libre pour l'allocation, ce qui est très favorable.

Dans la vraie vie, cet espace sera forcément plus restreint et donc les problèmes d'allocation seront plus ou moins probables.

Le problème est qu'un logiciel simple a toujours tendance à évoluer et les ajouts successifs de fonctionnalités et de librairies peuvent entraîner à la longue des problèmes incompréhensibles.

4. Constat

La conclusion que l'on peut tirer de cette manipulation est que si l'espace de stockage d'un objet du type String ne peut être alloué, ou ré-alloué pour augmenter sa taille, un problème de manque de mémoire n'est pas détectable, à moins de tester la taille comme je l'ai fait dans la fonction generateRandomString().

Or les méthodes des librairies renvoyant des String ne font pas cette vérification.

Je donnerai ce simple exemple extrait de la librairie ARDUINO :

String Stream::readString()
{
  String ret;  int c = timedRead();
  while (c >= 0)
  {
    ret += (char)c;    c = timedRead();
  }
  return ret;
}

Il est extrêmement clair que si l'espace de stockage de la String ret ne peut être augmenté, le caractère c sera perdu.

Cette méthode Stream::readString() est celle qui est utilisée lorsque l'on appelle Serial.readString() pour lire les caractères sur la ligne série de l'ARDUINO.

Le deuxième constat que l'on peut faire est que si le nombre d'objets String est très réduit, et que leur taille est elle-même faible, il y a une chance sur X pour que l'on soit à l'abri d'un problème.

Mais là encore cela va dépendre fortement de l'espace mémoire disponible. Si le logiciel comporte beaucoup de variables globales, on ne sera pas du tout à l'abri.

Oui, mais combien vaut X ? Quelle est la limite à ne pas franchir ?

C'est une réponse impossible à donner avec certitude. Le logiciel peut très bien fonctionner pendant des heures et planter au bout de quelques jours.

5. Alternative

5.1. Les alternatives faciles

On peut déjà commencer par éliminer certaines utilisations inutiles et je dirais même néfastes de String. On voit beaucoup trop de codes écrits comme ceci :

  Serial.println(String("value: ") + String(10) + String(" volts"));

Cette écriture provoque des allocations mémoire totalement inutiles et elle est très inefficace en terme de temps d'exécution. Autant écrire ceci :

  Serial.print("value: "); Serial.print(10); Serial.println(" volts");

Ce n'est pas plus long à écrire. L'objet Serial possède toutes les méthodes permettant d'afficher une chaîne de caractères, un nombre entier, un nombre flottant, etc. Autant en profiter.

5.2. Reserver la mémoire

La classe String permet de réserver de la mémoire pour un objet. L'allocation des différents blocs de mémoire est donc faite une fois pour toutes au départ.

L'intérêt de cette technique est de conserver le côté pratique des méthodes de la classe String.

Ajoutons ces quelques lignes à la fonction setup() de notre code précédent :

  for (String &s : strings) {
    if (!s.reserve(LARGEST_STRING)) {
      Serial.println(F("Not enough memory for this test"));
      while (true);
    }
  }

Il est bien entendu impératif de tester la valeur retournée de la méthode reserve(). Dans le cas contraire rien n'indiquerait un problème éventuel de mémoire et le fonctionnement du logiciel pourrait poser des problèmes.

Après cette modification, l'exécution du code précédent ne pose plus de problème, mais c'est au prix d'une consommation mémoire maximale.

5.3. Les C strings

5.3.1. Le caractère

On peut difficielement parler de chaîne de caractère sans parler de caractère :

char c;

Cet unique caractère est composé de 8 bits, et il est signé, c'est à dire que sa valeur pourra être comprise entre -128 et 127.

Un caractère peut être imprimable ou non. Consultons une table ASCII :

Il s'agit ici de la tables ASCII standard, sans caractères accentués.

Remarque : Il existe d'autres tables, par exemple latin-1, comportant des codes allant de 128 à 255, dans laquelle nous trouverons des caractères accentués courament utilisés en Europe occidentale.

Dans cette table chaque caractère a un code. Jusqu'à 31 il s'agit de caractères de contrôle (non imprimables) utilisés en général dans les communications. Ensuite entre 32 (espace) et 126 (~) nous trouvons la liste des caractères imprimables.

L'erreur couramment comise par les débutants en C est de considérer qu'un caractère a besoin d'être converti si l'on désire obtenir son code ASCII.

C'est le cas dans beaucoup de langages, PYTHON par exemple.

En C ce n'est pas vrai. Le caractère 'A' est strictement égal à 65 (en décimal) ou 0x41 (en hexadécimal).

Cela implique qu'en C on peut affecter à un caractère une variable de type caractère ou une variable entière :

void setup()
{
  Serial.begin(115200);
  char c = 'A';
  Serial.print("c = "); Serial.println(c);
  c = '\x41';
  Serial.print("c = "); Serial.println(c);
  c = 0x41;
  Serial.print("c = "); Serial.println(c);
  c = 65;
  Serial.print("c = "); Serial.println(c);
  Serial.print("isprint(c) : "); Serial.println(isprint(c));
  c = 6;
  Serial.print("c = "); Serial.println(c);
  Serial.print("isprint(c) : "); Serial.println(isprint(c));
  Serial.print("(int)c = "); Serial.println((int)c);
  Serial.print("c == '\\x41' : "); Serial.println('A' == '\x41');
  Serial.print("c == 0x41 : "); Serial.println('A' == 0x41);
  Serial.print("c == 65 : "); Serial.println('A' == 65);
}

void loop()
{
}

Ce petit sketch affichera :

c = A
c = A
c = A
c = A
isprint(c) : 194
c =
isprint(c) : 0
(int)c = 6
c == '\x41' : 1
c == 0x41 : 1
c == 65 : 1

La fonction isprint() permet de savoir si un caractère est imprimable ou non. Elle retourne un nombre différent de ZERO si oui, et ZERO sinon.

La notation '\x41', couramment utilisée, désigne un caractère ayant le code hexadécimal 0x41.

  c = '\x41';
  c = 0x41;
  c = 65;

Ces trois affectations sont strictement équivalentes.

5.3.2. La chaîne de caractères

Ceci est une C string, c'est à dire une chaîne de caractère C :

  char buf[TAILLE];

Ce n'est ni plus ni moins qu'un tableau de caractères.

Une chaîne de caractères peut contenir des caractères littéraux (imprimables) ou non :

  char buf[] = "AZERTYUIOP\x06";

Cette chaîne se termine par un caractère 6. Pour pouvoir l'insérer dans une chaîne de caractères on le fait précéder d'un caractère d'échappement backslash suivi de x pour indiquer qu'il s'agit d'un code hexadécimal. Cela implique que si l'on veut insérer un backslash dans une chaîne on doit le doubler :

  char buf[] = "AZERTYUIOP\\";

La première chose à savoir est que l'index d'une chaîne de caractères C, comme de n'importe quel tableau, démarre à ZÉRO :

char s[] = "azertyuiop";

s[0] vaut 'a' et s[1] vaut 'z', etc.

La deuxième chose à savoir est qu'une chaîne de caractères C doit être terminée par un caractère nul : '\0' :

char s[] = "azertyuiop";

Dans la déclaration ci-dessus le caractère nul est automatiquement ajouté. Ce n'est pas la peine d'écrire :

char s[] = "azertyuiop\0";

Dans la déclaration suivante la chaîne de caractères comporte 10 caractères, mais elle a une occupation mémoire de 11 caractères :

char s[] = "azertyuiop";

La fonction strlen() permet de connaître sa longueur. La fonction retournera bien 10, le caractère '\0' n'étant pas comptabilisé.

Si la chaîne n'est pas terminée par ZERO la longueur retournée sera forcément fausse car strlen() s'arrêtera sur le premier caractère ZERO rencontré après le début de la chaîne. Il en rencontrera très probablement un, quelque part en mémoire.

  char buf[10];

Dans cette déclaration par contre la chaîne a une longueur de 10 caractères, y compris le terminateur '\0'. Si elle doit contenir 10 caractères effectifs il conviendra d'ajouter 1 à sa taille.

5.3.3. C string vs String

L'inconvénient d'une C string, contrairement à un objet String, est que l'on est obligé de lui donner une taille fixe, alors qu'un objet String est extensible à volonté.

Normalement si l'on conçoit un logiciel devant recevoir des commandes par une ligne série, on est censé connaître la longueur maximale de ces commandes.

5.3.3. C string et librairies

Les librairies acceptent-elles de travailler avec des C strings au lieu de String ? Pas forcément.

Dans le cas de la librairie ARDUINO, c'est majoritairement le cas.

size_t Stream::readBytes(char *buffer, size_t length);

Par exemple la méthode Stream::readBytes() permet de faire le même travail que la méthode Stream::readString().

Elle reçoit en paramètre l'adresse d'une C string et sa taille. Elle renvoie le nombre de caractères reçus.

Avec certaines librairies tierces cependant il est possible que cette alternative ne soit pas proposée. Il faudra donc implémenter nous-même une méthode.

5.3.4. Fonctions

Les fonctions permettant de manipuler les C strings sont nombreuses. La fonction strcmp() utilisée dans les 2 exemples suivants 5.2 et 5.3 permet de comparer deux chaînes.

On peut trouver la liste complète de ces fonctions dans le fichier string.h

On peut ajouter également stdlib.h

De nombreuses documentations et tutoriels existent sur le WEB.

Une petite table de correspondance est bien utile :

Méthode String Description Fonction C string
c_str Exporte une chaîne vers une C string implicite
charAt Recherche un caractère strchr
compareTo Compare deux chaînes strcmp
concat Concatène deux chaînes strcat
endsWith Teste si une chaîne se termine par le contenu d'une autrestrcmp (1)
equalsCompare deux chaînes strcmp
equalsIgnoreCase Compare deux chaînes sans tenir compte de la casse stricmp
strcmp Copie le contenu d'une chaîne dans une autre strcpy
indexOf Recherche la position d'un caractère strchr
lastIndexOf Recherche la position d'un caractère en commençant par la fin de la chaîne strrchr
length Retourne la longueur d'une chaîne strlen
remove Supprime un ou des caractères memcpy (2)
reserve Fixe la taille d'une chaîne implicite
setCharAt Modifie un caractère d'une chaîne str[i] = c;
startsWith Teste si une chaîne commence par le contenu d'une autre strncmp
substring Copie une portion de chaîne dans une autre strncpy
toCharArray Exporte une chaîne vers une C string strcpy
toDouble Exporte une chaîne vers un double float atof
toFloat Exporte une chaîne vers un float atof
toInt Exporte une chaîne vers un integer atoi
toLowerCase Transforme une chaîne en minuscules strlwr
toUpperCase Transforme une chaîne en majuscules strupr
trim Supprime les espaces en début et find de chaîne.

(1)  voici une fonction endsWith :

bool endsWith(const char *s, const char *s2)
{
  return strcmp(s + strlen(s) - strlen(s2), s2) == 0 ? true : false;
}

(2) voici une fonction remove :

void remove(const char *s, int index, int count)
{
  memcpy((void *)s+index, s+index+count, strlen(s)-index-count+1);
}

A cette longue liste on peut ajouter quelques fonctions stdlib ou stdio très utiles, sans équivalent dans String :

strtok : extrait des token (jetons) d'une chaîne de caractères.

void setup() {
  Serial.begin(115200);
  char s[] = "11 33 99";
  char *token;
  token = strtok(s, " ");
  while (token != NULL) {
    Serial.println(token);
    token = strtok(NULL, " ");
  }
}

void loop() {
}

Ce petit sketch affichera :

11
33
99

sscanf : extrait des données d'une chaîne de caractères à l'aide d'une chaîne de format.

void setup() {
  Serial.begin(115200);
  char s[] = "11 33 99";
  int a, b, c;
  sscanf(s, "%d %d %d", &a, &b, &c);
  Serial.println(a);
  Serial.println(b);
  Serial.println(c);
}

void loop() {
}

Ce petit sketch affichera :

11
33
99

5.3.5. Liens utiles

Rien de tel que quelques bons tutoriels pour se familiariser :

openclassrooms

mon-club-elec

electroniqueamateur

5.4. Les dangers

5.4.1. Le danger des variables locales

L'utilisation des C strings n'est pas dépourvue de danger, mais ce danger se situe au niveau de la déclaration des variables locales.

Prenons un exemple :

void loop(void)
{
  char buffer[1000];
// ...

C'est une variable locale à une fonction. Elle va donc être allouée sur la pile.

Imaginons que le logiciel complet, après compilation, consomme 1400 octets de mémoire RAM. L'espace libre restant est de 2048 - 1400 = 648 octets.

Déclarer une chaîne de caractères de 1000 octets sur la pile alors que la mémoire libre a une taille inférieure (648 octets) est donc potentiellement source de problème.

La zone des variables initialisées (DATA), si aucune variable de l'application n'est initialisée à la déclaration, occupe peu de place : 22 octets.

La zone des variables globales BSS démarrerait donc aux alentours de 0x116 et se terminerait en 0x116+1400 = 0x68E.


En effet si au départ le pointeur de pile est initialisé à la valeur RAMEND (0x8FF), après avoir déclaré ce buffer local le pointeur de pile sera décrémenté de 1000 octets. Il vaudra donc 0x8FF - 1000 = 0x517.

Or cette adresse 0x517 se trouve dans la zone des variables globales BSS (0x100 à 0x678).

La moindre utilisation de ce buffer provoquera un ravage des variables globales, et potentiellement un crash.

5.4.2. Le danger de débordement

Lorsque l'on écrit dans une chaîne de caractères, le débordement n'est pas contrôlé. Si l'on écrit en dehors de la chaîne, il y a de fortes chances que le logiciel aille écraser une autre variable, et modifier le comportement du logiciel de manière imprévisible, y compris en provoquant un crash.

#define SIZE 10
char s1[] = "azertyuiopqsdfghjklm";
char s2[SIZE];

Si l'on copie s1 dans s2 à l'aide de strcpy(), comme la taille de s2 est de dix caractères, la partie "qsdfghjklm" sera copié après la chaîne s2.

void setup() {
  Serial.begin(115200);
  strcpy(s2, s1);
  Serial.println(s2);
}

Le sketch n'affiche rien. Cela prouve bien qu'il plante. Si la taille de s2 est de 21 caractères, tout se passe bien.

Il est préférable d'utiliser strncpy().

Mais que dit le manuel ?

"Si la chaîne source a une taille supérieure à celle spécifiée en paramètre, alors la chaîne produite ne sera pas terminée par un code ASCII nul (caractère '\0'). Il sera alors de votre responsabilité de l'ajouter si vous souhaitez exploiter correctement la chaîne produite. En effet, n'oubliez pas qu'en C on manipule des chaînes AZT (A Zéro Terminal)."

Ce qu'il faut faire :

void setup() {
  Serial.begin(115200);
  strncpy(s2, s1, SIZE-1);
  s2[SIZE-1] = '\0';
  Serial.println(s2);
}

Les 9 premier caractères sont copiés, et un ZERO est ajouté dans la dizième case du tableau : s2[SIZE-1], donc s2[9].

Le sketch affichera "azertyuio", c'est à dire 9 caractères.

De la même manière on utilisera de préférence strncat() au lieu de strcat().

5.5. Les pointeurs

Les pointeurs sont la bête noire du débutant. C'est plutôt déroutant. Noua allons en parler.

Un pointeur est une variable contenant l'adresse d'une autre variable.

Dans le cas des chaînes de caractères on peut déclarer un pointeur comme ceci :

char *p;

On peut lui affecter une valeur à la déclaration :

char *p = "qsdfghjklm";

On peut également lui affecter l'adresse d'une chaîne de caractères :

char s[] = "azertyuiop";
char *p = s;

On peut également lui affecter l'adresse d'un caratère dans une chaîne de caractères :

char s[] = "azertyuiop";
char *p = s+5;

p contiendra donc l'adresse du sixième caractère de s (car on part de ZÉRO).

Cette écriture, plus compréhensible pour le débutant, est possible :

char s[] = "azertyuiop"; 
char *p = &s[5];

L'accès aux caractère d'une chaîne se fait comme s'il s'agissait d'une chaîne :

char s[] = "azertyuiop";
char *p = s;

p[0] sera égal à s[0]

Exercice :

void setup() {
  Serial.begin(115200);
  char s[] = "azertyuiop";
  char *p = "qsdfghjklm";
  Serial.print("s="); Serial.println(s);
  Serial.print("p="); Serial.println(p);
  Serial.println("#affectation p=s");
  p = s;
  Serial.print("strcmp(s, p)="); Serial.println(strcmp(s, p));
  Serial.print("p="); Serial.println(p);
  Serial.print("s[0]="); Serial.println(s[0]);
  Serial.print("p[0]="); Serial.println(p[0]);
  Serial.println("#affectation p=s+5");
  p = s+5;
  Serial.print("p="); Serial.println(p);
  Serial.println("#affectation p=&s[5]");
  p = &s[5];
  Serial.print("p="); Serial.println(p);
}

void loop() {
}

Ce petit sketch affichera :

s=azertyuiop
p=qsdfghjklm
#affectation p=s
strcmp(s, p)=0
p=azertyuiop
s[0]=a
p[0]=a
#affectation p=s+5
p=yuiop
#affectation p=&s[5]
p=yuiop

Quels sont les types des paramètre qu'attend une fonction de maniplation de C string de la librairie ?

Quand on voit ceci :

int strcmp(const char *, const char *);
char  *strcpy(char *, const char *);

On pourrait être tenté de croire qu'elles attendent des pointeurs.

Il n'en est rien. On peut leur passer en paramètres une chaîne de caractères ou un pointeur :

char s[] = "azertyuiop";
char *p = s;
p = s;
Serial.print("strcmp(s, p)="); Serial.println(strcmp(s, p));

Ce petit sketch affichera :

strcmp(s, p)=0

0 voulant dire : ZERO différences.

Dernier point concernant une erreur très courante :

char s1[] = "azertyuiop";
char s2[] = "azertyuiop";
Serial.print("s==s2 ? "); Serial.println(s == s2);


char s[] = "azertyuiop";
char s2[] = "qsdfghjklm";
Serial.print("s==s2 ? "); Serial.println(s == s2);

Ces deux codes afficheront :

s==s2 ? 0 

0 voulant dire : non égalité.

En effet l'opérateur == compare simplement les deux adresses, qui sont forcément différentes.

On a l'habitude de comparer deux objets de la classe String avec l'opérateur ==.

Mais l'opérateur  == de la classe String n'est pas l'opérateur == du C. Il a été surchargé (remplacé), et il compare les contenus.

Donc : ATTENTION.

5.6. Economiser la mémoire RAM

Certaines chaînes de caractères constantes ,des messages principalement, peuvent être déclarées en mémoire FLASH, ce qui libèrera de l'espace en mémoire RAM.

Exemple :

  Serial.print("AZERTYUIOPQSDFGHJKLM");

On utilise la macro F() :

  Serial.print(F("AZERTYUIOPQSDFGHJKLM"));

Quelle différence cela fait-il ?

Dans le premier cas :

Les variables globales utilisent 206 octets (2%) de mémoire dynamique, ce qui laisse 7986 octets pour les variables locales. Le maximum est de 8192 octets.

Dans le deuxième cas :

Les variables globales utilisent 184 octets (2%) de mémoire dynamique, ce qui laisse 8008 octets pour les variables locales. Le maximum est de 8192 octets.

Cela fait toute la différence.

6. Un exemple concret

Nous allons prendre comme exemple les méthodes Stream::readString() et Stream::readStringUntil(), souvent utilisées pour lire une chaîne de caractères sur la ligne série de l'ARDUINO.

Les situations dans lesquelles on utilise ces deux méthodes sont nombreuses :

  • communication avec un PC
  • communication avec un autre ARDUINO ou un ESP8266 ou un ESP32 
  • communication avec un module BlueTooth
  • etc.

Les classes HardwareSerial et SoftwareSerial héritent de ces méthodes, ainsi que toutes les classes susceptibles d'hériter de la classe Stream.

Dans la suite de cet article nous allons parler de ces deux méthodes Serial.readString() ou Serial.readStringUntil() car c'est souvent lors de l'utilisation de ces deux méthodes que les problèmes surviennent.

Mais il faut avoir à l'esprit que toutes les méthodes retournant une String sont concernées, quelque soit la librairie.

6.1. L'exemple

L'exemple que nous allons essayer de transformer est le suivant :

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

void loop()
{
  String buf;

  if (Serial.available()) {
    buf = Serial.readStringUntil('\r');
    // le caratère '\n' est éliminé
    buf.trim();
    if (buf.length() > 0) {
      Serial.println(buf);
      if (buf == "bonjour") {
        Serial.println("bienvenue");
      }
      else if (buf == "au revoir") {
        Serial.println("à bientôt");
      }
    }
  }
}

L'exemple répond "bienvenue" s'il reconnait la phrase "bonjour" et "à bientôt" s'il reconnait la phrase "au revoir".

Dans cet exemple un chaîne terminée par '\r' est attendue.

'\r' est le caractère retour charriot (CR) ayant pour valeur 13 ou 0x0D.

Pour nos essais il faudra donc paramétrer le terminal avec "CR" ou "NL et CR" comme caractères de fin de ligne.

'\r' est le caractère terminateur traditionnel d'un terminal, c'est pour cette raison qu'on l'utilise assez souvent, et cela offre l'avantage de pouvoir facilement réaliser des tests avec un terminal.

On voit également que le caractère '\n' (NL ou NewLine) est éliminé grâce à la méthode trim(). En effet si le terminal ARDUINO est paramétré avec "NL et CR" comme caractères de fin de ligne, le caractère NL va nous déranger et produire des lignes vides.

6.2. read()

La première méthode est d'utiliser une lecture caractère par caractère et de les stocker dans un buffer. Il faudra gérer un index :

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

char buf[256];
int i;  // index

void loop()
{
  if (Serial.available()) {
    int c = Serial.read();
    if (c == '\r') {
      Serial.println(buf);
      if (!strcmp(buf, "bonjour")) {
        Serial.println("bienvenue");
      }
      else if (!strcmp(buf, "au revoir")) {
        Serial.println("à bientôt");
      }
      i = 0;
    }

    // ne pas dépasser 255 pour laisser de la place pour le caractère NULL
    else if (i < 254) {
      // le caratère '\n' est ignoré
      if (c != '\n') {
        buf[i++] = c;

        // caratère nul de terminaison
        buf[i] = 0;
      }
    }
  }
}

La lecture se fait caractère par caractère. Chaque caractère reçu (sauf '\n') est ajouté au buffer et l'index i est incrémenté. Un caractère nul est ensuite ajouté.

Si le caractère '\r' est reçu la phrase est comparée à "bonjour" et "au revoir".

6.3. readBytesUntil()

Cette méthode bien pratique permet la lecture d'une chaîne de caractères ayant un caractère terminateur :

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

void loop()
{
  char buf[256];

  if (Serial.available()) {

    // ne pas dépasser 255 pour laisser de la place pour le caractère NULL
    int n = Serial.readBytesUntil('\r', buf, 255);
    // caratère NULL de terminaison
    buf[n] = '\0';
    // le caratère '\n' est ignoré
    if (n == 1 && buf[0] == '\n') {
      return;
    }
    Serial.println(buf);
    if (!strcmp(buf, "bonjour")) {
      Serial.println("bienvenue");
    }
    else if (!strcmp(buf, "au revoir")) {
      Serial.println("à bientôt");
    }
  }
}

readBytesUntil() accepte trois arguments :

  • le caractère terminateur
  • l'adresse du buffer
  • sa taille

L'implémentation de readBytesUntil() garantit que la chaîne devant recevoir les caractères ne contiendra pas plus de caractère que le maximum fixé par le dernier paramètre.

7. Un exemple de librairie tierce

Nous allons maintenant examiner le cas d'une librairie n'offrant pas de méthode alternative.

Imaginons une classe :

class Screen {
 public:
  Screen();
  int read(void);
  String listen(void);
};

La méthode listen n'a pas d'alternative utilisant une C string. Elle est implémentée comme suit :

String Screen:: listen(void)
{
  String buf;
  int c = read();
  while (c >= 0) {
    buf += (char)c;
    c = read();
  }
  return buf;
}

Il nous faut donc implémenter une méthode nous même et l'ajouter à la classe.

Il faut donc la dériver, car il est hors de question de modifier les fichiers de la librairie :

class MyScreen : public Screen
{
 public:
  MyScreen();
  size_t listen(char *buf, int size);
};

Et nous allons implémenter une autre méthode listen :

MyScreen::MyScreen()
{

  // constructeur
}
 

size_t MyScreen:: listen(char *buf, int size)
{
  int i = 0;

  int c = read();
  while (c >= 0) {
    buf[i++] = (char)c;
    buf[i] = '\0';
    c = read();
  }
  return i;
}

Notre classe dérivée est prête à être utilisée.

8. Conclusion

Le but de cet article était de prouver que la fragmentation de la mémoire n'est pas une illusion. Elle existe bel et bien et peut avoir des effets néfastes sur le comportement d'un logiciel.

Il est important bien sûr de se familiariser avec les C strings et un tutorial n'est pas inutile.

On pourra lire :

https://www.arduino.cc/reference/en/language/variables/data-types/string/

https://www.tutorialspoint.com/arduino/arduino_strings.htm

https://www.tutorialspoint.com/c_standard_library/string_h.htm


Cordialement

Henri


9. Mises à jour

28/02/2020 : 5.1. Les C strings
                     5.2. Les pointeurs
                     5.3. Economiser la mémoire RAM
29/02/2020 : 1.3. La rencontre de la pile et du tas
01/03/2020 : 5.1. Réserver la mémoire
                     5.3. Le danger des variables locales

lundi 24 février 2020

RASPBERRY PI : UPS DFRobot DFR0494 En Test







RASPBERRY PI : UPS DFRobot DFR0494

En Test



Je vous ai présenté la carte UPS DFR0494 en Novembre dans cet article :
https://riton-duino.blogspot.com/2019/10/raspberry-pi-ups-dfrobot-dfr0494.html

Il s'agit d'une carte UPS (alimentation avec batterie de secours) équipée d'une batterie LITHIUM-ION ou LIPO.

Nous allons aujourd'hui lui faire subir un test de mesures :
  • tension de la batterie
  • courant débité
  • courant consommé pendant la recharge
Trois cas peuvent se présenter :
  • la batterie subit une recharge permanente (peu recommandé)
  • la batterie débite un courant plutôt faible et elle est rechargée périodiquement (c'est mieux)
  • la batterie ne débite aucun courant - ou celui-ci est extrêmement faible - et elle est rechargée très rarement (c'est encore mieux)
En fonction des résultats cette carte sera donc validée ou non pour son utilisation sur une RASPBERRY PI, qui est utilisée depuis des années en tant que serveur DOMOTICZ.

Un classement sera effectué en tenant compte des résultats des tests de cet article :
https://riton-duino.blogspot.com/2020/01/cartes-dalimentation-batterie.html

1. Le montage

La mesure de la tension et du courant de la batterie sur le long terme nécessite un outil du type logger avec enregistrement des données. J'ai donc utilisé le logger analogique décrit le mois dernier :
https://riton-duino.blogspot.com/2020/01/un-logger-analogique-et-digital-version.html

Cet outil construit autour d'un ESP32 permettra l'envoi des mesures à un serveur installé sur la RASPBERRY PI et enregistrement des données sur plusieurs jours, ou plus si nécessaire.

1.1. Le serveur

Le logiciel serveur PYTHON est installé sur la RASPBERRY PI sur laquelle la carte UPS est enfichée.

Remarque : la RASPBERRY PI n'ayant ni écran ni clavier je me connecte à celle-ci à l'aide de SSH et les fichiers sont copiés avec la commande scp depuis un poste LINUX.
Sous Windows on peut utiliser PUTTY et WinSCP.

J'ai copié l'ensemble des fichiers dans un répertoire /home/pi/power-monitoring.

Il est configuré comme suit (web-power-monitor.conf) :

[global]
server.socket_host = "0.0.0.0"
server.socket_port = 8000
server.thread_pool = 10

L'adresse IP 0.0.0.0 permet l'accès depuis n'importe quel poste du réseau.
Le port 8000 est choisi car le 8080 est occupé par DOMOTICZ.

Il est ensuite lancé :

./index.py

1.2. Le logger

Les raccordements entre la carte et le bornier du logger sont réalisés comme suit :


Le logger est alimenté par un bloc secteur 5V / 1A.

A la mise sous tension il se connecte à la box et je récupère son adresse IP sur le terminal série (les trois pins à droite du bornier vert) : 192.268.1.33 dans mon cas.

Je me connecte avec le navigateur en entrant l'URL suivante : http://192.168.1.33/index.html

Il est ensuite configuré comme suit (voir dans l'article du logger : 8.1.2. La page de configuration) :
  • Host: 192.168.1.134
  • Port: 8000
  • Period: 10 Seconds
  • Looking for: Power Monitor
192.168.1.134 est l'adresse IP de la RASPBERRY PI. 8000 est le port d'écoute du serveur, le port qui a été choisi précédemment (voir 1.1. Le serveur).
La surveillance est effectuée avec une période de 10 secondes et seul le Power Monitor (surveillance de la tension et du courant par l'INA226) est activé.

Je laisse tourner pendant quelques dizaines de secondes puis je me connecte avec le navigateur en entrant l'URL suivante : http://192.168.1.134:8000/ :


On peut déjà constater que les mesures ont commencé :
  • tension de la batterie : 4.08V
  • courant débité : 0mA
Le courant est nul, en tous cas il est inférieur à la résolution de mesure de l'INA226 : 250µA. C'est un bon point.
Au début le logger a été configuré avec une période de 10 secondes afin de vérifier que tout fonctionnait bien.
Comme la carte UPS consomme très peu, la période est ensuite réglée sur 2 heures.

Il ne reste plus qu'à laisser le logger tourner pendant quelques jours.

1.2. Le résultat

Après trois mois de collecte de mesures voici ce que cela donne :

J'ai entre temps ajouté la date à l'affichage de l'axe des X du serveur.

Démarré le 24/01/2020 avec une tension de 4.08V, on voit bien que la tension a chuté à 4.05V. Pour atteindre une tension suffisamment basse pour déclencher la recharge il va falloir s'armer de patience.


1.3. Le classement

Parmi les cartes que j'ai testé dans l'article du mois de janvier on peut établir sans aucun mal un classement :

Chargeur UPS LI-M-1P-D
La batterie est en recharge permanente. C'est peu recommandé pour sa durée de vie.

Son prix est faible : < 3€.

Chargeur UPS LANTIANRC
La batterie est rechargée toutes les deux heures. C'est un peu mieux.

Son prix est faible : < 7€.

UPS RPI PowerPack
Le courant débité par la batterie est très faible, ce qui est un très bon point.

Par contre elle nécessite un condensateur de 3300µF soudé entre masse et +5V sur le connecteur USB afin d'éviter la micro-coupure et le redémarrage de la RASPBERRY PI. L'inconvénient est mineur.

Son prix est faible : < 8€.

UPS DFRobot DFR0494

Sans aucun doute la meilleure carte, qui préservera au mieux la durée de vie des batteries, et qui permet une adaptation parfaite à la RASPBERRY PI grâce à son connecteur.

Son prix est le plus élevé : 18,50€ (GoTronic).
Elle n'est pas disponible chez les revendeurs chinois.

Après ces quelques tests la DFRobot DFR0494 est adoptée à l'unanimité.


Cordialement
Henri

mercredi 12 février 2020

Un HUB 4 ports série


J'ai récemment publié un article  à propos des convertisseurs USB /série.
https://riton-duino.blogspot.com/2020/02/les-convertisseurs-usb-serie.html

Celui-ci m'a donné une idée. Ayant quelques mini-modules FT4232H dans mes tiroirs j'ai décidé d'en tirer parti et de me fabriquer une carte HUB 4 ports série.

En effet je travaille souvent sur des projets embarquant des cartes ARDUINO PRO MINI et il m'arrive parfois d'avoir besoin d'au moins deux ports (1  projet avec un émetteur et un récepteur radio par exemple).

Une autre application est envisagée :
Actuellement sur mon serveur DOMOTICZ (RASPBERRY PI) les 4 ports USB sont occupés :
  • 1 ARDUINO MEGA (passerelle RFLINK)
  • 1 ARDUINO NANO (télécommande infrarouge)
  • 1 ARDUINO NANO (passerelle NRF24L01)
  • un PRO MINI + convertisseur FT232R (passerelle RFM69)
Si je remplace les deux NANO par deux PRO MINI 8MHz il sera possible d'utiliser à la place de trois convertisseurs FT232R un seul FT4232H, ce qui libérera 2 ports USB.
Si je réserve un port du FT4232H en tant que port série 5V, je pourrai également y connecter la MEGA, et cela libérera 3 ports USB.

Le but est donc de remplacer 2, 3 ou 4 convertisseurs USB / série par un seul, pourvu de 4 connecteurs le raccordement de 4 cartes :
  • GND
  • 3.3V ou 5V
  • TX
  • RX
  • DTR
La carte convertisseur sera donc équipée de 4 connecteurs à 5 broches.

Pour d'autre applications il est bien entendu possible d'ajouter d'autres broches de contrôle. Le FT4232H dispose de la panoplie complète (RTS, CTS, DTR, DSR, DCD, RING).

1. Les mini-modules

Avant tout examinons ce mini-module FT4232H de plus près :
  • le FT4232H est un convertisseur USB / série 4 ports
  • le FT2232 existe aussi. Il offre 2 ports
  • deux modes d'alimentation : 
    • alimentation par le connecteur USB
    • alimentation externe 5V
  • le driver est le même que pour les FT232R :
Si l'alimentation se fait pas le connecteur USB, le régulateur 3.3V embarqué AIC1733 peut délivrer 500mA.

La tension d'alimentation du chip FT4232H est de 3.3V (maxi 3.6V).
Ces convertisseurs sont donc adaptés à des processeurs alimentés sous 3.3V :
  • ARDUINO PRO MINI 8MHz 3.3V
  • ESP8266, ESP32
  • STM32
  • etc.
Il est possible cependant d'envisager de l'utiliser avec des processeurs 5V moyennant deux modifications :
  • amener le 5V sur le connecteur série 5 broches
  • insérer un pont diviseur entre la broche RX du connecteur 5 broches et la pin RX correspondante du FT4232H
Sur les 4 ports disponibles, il est possible de réserver 1 ou plusieurs ports 5V.

2. L'offre

Examinons l'offre disponible.

On trouve ces modules chez les revendeurs officiels Farnell, Mouser, RS, etc.
Mais il sont assez chers : 26€ à 27€.
Il existe des clones sur AliExpress pour une dizaine d'euros, ce qui est plus abordable :
Ce module est disponible également sur AliExpress :

Il a des possibilités intéressantes :
  • commutateur d'alimentation (USB ou JACK)
  • commutateur 5V / 3.3V sur RX / TX
  • 4 ports RX / TX / GND / VCC sur connecteurs JST
Le même en version FT2232 :

3. La réalisation

Ces modules seraient directement utilisables tels quels avec des fils DUPONT mais cela risque d'être très fastidieux. L'idée est d'implanter le mini-module sur une carte et de mettre à disposition 4 connecteurs à 5 broches :
  • GND
  • 3.3V ou 5V
  • TX
  • RX
  • DTR
Les broches sont dans le même ordre que le connecteur de chargement d'une PRO MINI, ce qui permettra de relier la carte à l'aide d'un câble à 5 fils droit.

Le brochage du module est le suivant :


Il y a deux connecteurs 26 broches nommés CN2 et CN3, et dans la datasheet du mini-module les broches sont répérées AD0 à AD7, BD0 à BD7, CD0 à CD7 et DD0 à DD7.
Nous avons donc 4 ports A B C D avec :
  • 0 : TX
  • 1 : RX
  • 2 : RTS
  • 3 : CTS
  • 4 : DTR
  • 5 : DSR
  • 6 : DCD
  • 7 : RING
Le brochage des clones sera assez semblable. La sérigraphie sera même plus claire, car ces module sont plus grands.
Sur la photo du clone ci-dessus il y a 4 bus : ADBUS, BDBUS, CDBUS, DDBUS, et pour chacun des broches numérotées de 0 à 7.

4. Le câblage

Voici le câblage à réaliser :

Liaison FT4232 Connecteurs série
GND CN2.2 CN2.4 CN2.6
CN3.2 CN3.4
A relier ensemble
A1 B1 C1 D1
VBUS 5V CN3-1 CN3-3 (voir remarque 1)
A relier ensemble
(voir remarque 2)
3.3V VIO CN2-1 CN2-3 CN2-5
CN2-11 CN2-21
CN3-12 CN3-22
A relier ensemble
A2 B2 C2 D2
(voir remarque 2)
AD0 (TX1) CN2-7A3
AD1 (RX1)CN2-10 A4
AD4 (DTR1) CN2-14 A5
BD0 (TX2) CN2-18 B3
BD1 (RX2) CN2-17 B4
BD4 (DTR2) CN2-22 B5
CD0 (TX3) CN3-26 C3
CD1 (RX3) CN3-25 C4
CD4 (DTR3) CN3-21 C5
DD0 (TX4) CN3-17 D4
DD1 (RX4) CN3-16 D5
DD4 (DTR4) CN3-13 D6

1) Dans cette configuration (broche CN3-1 reliée à CN3-3) l'alimentation 5V provient du connecteur USB.
Il est possible d'alimenter le mini-module avec une alimentation 5V externe si les 500mA d'un port USB ne suffisent pas.
Dans ce cas il faudra alimenter par la broche CN3-3 et laisser la broche CN3-1 non connectée.

2) dans le cas où l'on envisage de disposer d'un ou plusieurs ports 5V il faudra relier VBUS (CN3-1) à A1, B1, C1 ou D1. Un pont diviseur devra être inséré entre l'entrée RX A4, B4, C4, ou D4 et la broche RX correspondante du FT4232H.
Exemple :

Voici le schéma d'un hub muni de cavaliers permettant de choisir pour chaque port la tension de travail 3.3V ou 5V :


Par exemple si l'on désire faire travailler le port A en 3.3V les deux cavaliers JP1 et JP5 seront enfichés sur les pins 1 et 2, sinon sur les pins 2 et 3 .

5. Photos

Quelques photos pour illustrer :
Le dessus
 La broche +3.3V est repérée au vernis à ongle.

Le dessous.
A gauche le mini-module avec le connecteur CN3 à gauche. A droite les 4 connecteurs DUPONT mâles avec de gauche à droite : GND, VCC, TX (jaune), RX (orange), DTR (marron).


Chargement d'une PRO MINI
Comme on le voit j'ai utilisé une plaquette à pastilles 70mm x 50mm.
Le câblage est réalisé comme suit :
  • des fils rigides nus 0.25mm² pour les lignes GND et 3.3V, au cas où il y ait besoin d'un peu de puissance.
  • du fil à wrapper pour les lignes TX, RX et DTR
Astuce : les fils rigides sont récupérés dans un morceau de câble Ethernet. Il sont dénudés et soudés directement sur les pastilles.

Le câble de liaison avec la PRO MINI est constitué de 5 fils et de 2 boîtiers DUPONT 5 broches (GND, VCC, TX, RX, DTR).

Si l'on envisage de brancher plusieurs cartes dans le but de les faire communiquer par radio, comme avec le RFM69 sur la photo, il serait plus sage de fabriquer des câbles plus longs, 50cm minimum, car les émetteurs / récepteurs radio n'aiment pas trop la promiscuité.

Voici une version complète avec toutes les lignes UART (TX, RX, RTS, CTS, DTR, DSR, DCD, RING) :


Un régulateur 3.3V et un condensateur sont implantés sur la carte, car j'ai grillé celui du mini-module à cause d'un court-circuit entre 3.3V et GND.
Apparemment le régulateur AIC1733 du mini-module est protégé en température et court-circui, mais qu'en est-il vraiment ?
Méfiance donc ...

6. Le test

Après avoir relié la carte au PC par un cordon mini-USB, nous devons donc disposer de 4 ports série supplémentaires. Et c'est bien le cas :

riton@alpha:/mnt/sdc1/riton$ ls /dev/ttyUSB*
/dev/ttyUSB0  /dev/ttyUSB1  /dev/ttyUSB2  /dev/ttyUSB3  /dev/ttyUSB4


Sous Windows il faudra ouvrir le Gestionnaire de Périphériques, après avoir installé le driver FTDI : https://www.ftdichip.com/Drivers/VCP.htm

Branchons maintenant une carte PRO MINI 8MHz sur un des 4 connecteurs.

Le chargement par l'IDE ARDUINO se passe bien :

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.01s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: reading input file "/tmp/arduino_build_496310/MotionSensor433MHz.ino.hex"
avrdude: writing flash (16074 bytes):

Writing | ################################################## | 100% 8.06s

avrdude: 16074 bytes of flash written
avrdude: verifying flash memory against /tmp/arduino_build_496310/MotionSensor433MHz.ino.hex:
avrdude: load data flash data from input file /tmp/arduino_build_496310/MotionSensor433MHz.ino.hex:
avrdude: input file /tmp/arduino_build_496310/MotionSensor433MHz.ino.hex contains 16074 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 6.05s

avrdude: verifying ...
avrdude: 16074 bytes of flash verified

avrdude done.  Thank you.


L'application chargée est un sketch pour un capteur de proximité MYSENSORS. Au démarrage le moniteur série doit afficher un tas d'informations à 115200 baud.
C'est bien le cas :
_  __       ____
|  \/  |_   _/ ___|  ___ _ __  ___  ___  _ __ ___
| |\/| | | | \___ \ / _ \ `_ \/ __|/ _ \| `__/ __|
| |  | | |_| |___| |  __/ | | \__ \  _  | |  \__ \
|_|  |_|\__, |____/ \___|_| |_|___/\___/|_|  |___/
        |___/                      2.3.2

16 MCO:BGN:INIT NODE,CP=RRNNA---,FQ=8,REL=255,VER=2.3.2
28 TSM:INIT
28 TSF:WUR:MS=0
32 TSM:INIT:TSP OK
34 TSF:SID:OK,ID=2
36 TSM:FPAR
40 ?TSF:MSG:SEND,2-2-255-255,s=255,c=3,t=7,pt=0,l=0,sg=0,ft=0,st=OK:
335 TSF:MSG:READ,0-0-2,s=255,c=3,t=8,pt=1,l=1,sg=0:0
342 TSF:MSG:FPAR OK,ID=0,D=1
2050 TSM:FPAR:OK
2050 TSM:ID
2052 TSM:ID:OK
2054 TSM:UPL
2064 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=24,pt=1,l=1,sg=0,ft=0,st=OK:1
2084 TSF:MSG:READ,0-0-2,s=255,c=3,t=25,pt=1,l=1,sg=0:1
2091 TSF:MSG:PONG RECV,HP=1
2093 TSM:UPL:OK
2095 TSM:READY:ID=2,PAR=0,DIS=1
2123 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=15,pt=6,l=2,sg=0,ft=0,st=OK:0100
2140 TSF:MSG:READ,0-0-2,s=255,c=3,t=15,pt=6,l=2,sg=0:0100
2359 TSF:MSG:SEND,2-2-0-0,s=255,c=0,t=17,pt=0,l=5,sg=0,ft=0,st=OK:2.3.2
2435 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=6,pt=1,l=1,sg=0,ft=0,st=OK:0
4454 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=11,pt=0,l=13,sg=0,ft=0,st=OK:Motion Sensor
4470 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=12,pt=0,l=3,sg=0,ft=0,st=OK:1.0
4501 TSF:MSG:SEND,2-2-0-0,s=1,c=0,t=1,pt=0,l=0,sg=0,ft=0,st=OK:
4507 MCO:REG:REQ
4524 TSF:MSG:SEND,2-2-0-0,s=255,c=3,t=26,pt=1,l=1,sg=0,ft=0,st=OK:2
4540 TSF:MSG:READ,0-0-2,s=255,c=3,t=27,pt=1,l=1,sg=0:1
4546 MCO:PIM:NODE REG=1
4550 MCO:BGN:STP
4552 MCO:BGN:INIT OK,TSP=1
0


J'ai fais le même test sur les quatre ports : tous OK.

7. PYTHON

On peut contrôler un FT4232 en bitbang. Voir l'article précédent :
https://riton-duino.blogspot.com/2020/02/les-convertisseurs-usb-serie.html#python

8. Conclusion

Voici donc un petit HUB à 4 ports série parfaitement fonctionnel, qui occupera donc seulement un port USB du PC.


Cordialement
Henri

9. Mises à jour
03/03/2020 : 7. PYTHON

lundi 10 février 2020

Les convertisseurs USB / série



Les convertisseurs USB / série


Lorsque l'on travaille avec des cartes ARDUINO PRO MINI ou des modules ESP8266 ou ESP32 dépourvus de convertisseur USB, on utilise généralement un convertisseur externe pour charger son application.

Tous les convertisseurs peuvent convenir, à partir du moment où l'on respecte bien la tension d'alimentation et la tension maximale des signaux que la carte peut supporter.

Pendant la phase de maquettage, il peut être pratique d'alimenter le montage complet à partir du convertisseur. Ceux-ci sont en général capables de fournir le courant nécessaire pour une carte PRO MINI et quelques modules peu consommateurs.

S'il s'agit d'une PRO MINI 16MHz, un convertisseur 5V est nécessaire. Une carte 16MHz ne tournerait pas sous 3.3V.

S'il s'agit d'une PRO MINI 8MHz 3.3V, on peut le faire avec un convertisseur 5V ou 3.3V.
Si des modules ne supportant pas 5V (NRF24L01 par exemple) sont connectés à l'ARDUINO, un convertisseur 3.3V est obligatoire.

Un ESP8266 ou ESP32 devra obligatoirement être chargé avec un convertisseur 3.3V, mais une alimentation délivrant au moins 500mA sera nécessaire.

Il faut bien distinguer tension d'alimentation et tension des signaux. En effet certains convertisseurs fournissant des signaux 3.3V ne fournissent pas forcément une tension d'alimentation de 3.3V (câble TTL-232R-3V3 par exemple).

Dans tous les cas, le câblage est le même :
  • GND du convertisseur sur GND de la carte
  • TX du convertisseur sur RX de la carte
  • RX du convertisseur sur TX de la carte
  • RTS ou DTR sur le DTR ou GRN de la carte (PRO MINI).
Dans certains cas la puissance fournie sur la broche 3.3V du convertisseur peut ne pas être suffisante (ESP8266 ou ESP32 par exemple). Il faudra donc alimenter le montage à l'aide d'une alimentation adaptée.

Nous allons examiner quelques modèles de convertisseurs.

Ensuite quelques paragraphes décrivent en détail certaines différences en fonction de l'OS utilisé (Linux, Windows).

1. Les convertisseurs 

Il faut prendre garde lors de l'achat de modules à la présence ou l'absence de broches telles que DTR ou RTS qui permettent le chargement automatique d'une carte ARDUINO PRO MINI. En effet bien que les chips proposent un certain nombre de lignes de contrôle, les modules convertisseurs ne les rendent pas forcément disponibles sur les connecteurs du module.

Sur une carte ARDUINO PRO MINI, la ligne DTR ou RTS du convertisseur doit être connectée sur la broche DTR ou GRN de la carte. En fait comme on le voit sur le schéma cette broche est reliée au RESET du microcontrôleur.
Si l'on utilise un convertisseur sans ligne DTR ni RTS, on peut appuyer sur le bouton RESET juste avant que le chargement ne commence.

Si l'on doit charger un ESP32 avec deux boutons EN et BOOT, RX et TX suffisent.

1.1. Le CH340 ou CH341

Ce chip est très commun. Il équipe souvent les cartes ARDUINO chinoises UNO, NANO, MEGA. Il a les caractéristiques suivantes :
  • interface : USB2
  • vitesse (baud) : 50bps à 2Mbps
  • tension 5V ou 3.3V
  • broche de sortie 3.3V
  • EEPROM interne (CH340B uniquement)
  • EEPROM externe (CH341)
On peut voir que seul le CH340B possède une EEPROM, bien pratique pour le stockage d'informations, le N° de série principalement.
Les autres modèles n'en possèdent pas, cela risque d'être handicapant. Nous verrons pourquoi au paragraphe 3. L'attribution des ports.

Le CH340 est disponibles sous forme de module :
CH340 5V / 3.3V
Ce modèle fournit une alimentation et des signaux 5V ou 3.3V en fonction de la position d'un cavalier.

En général ces modules ne proposent que les lignes RX et TX. Les lignes DTR ou RTS, bien pratiques pour charger une PRO MINI en automatique, sans appuyer sur le bouton RESET.

La plupart des modules sont équipé d'un CH340G. Je ne connais pas de module équipé d'un CH340B.

1.2. Le FT232R

Ce chip de la société FTDI a les caractéristiques suivantes :
  • USB2
  • vitesse (baud) : 300bps à 3Mbps
  • tension 5V ou 3.3V
  • broche de sortie 3.3V
  • EEPROM interne
  • tampon de réception et émission : 128 bytes et 256 bytes

La présence de deux tampons mémoire le rend plus performant dans les vitesses de transmission hautes.

Les modules sont très nombreux :



FT232R 5V / 3.3V


FT232R 5V / 3.3V

Ces deux modèle fournissent une alimentation et des signaux 5V ou 3.3V en fonction de la position d'un cavalier.
D'autres modèles proposent un interrupteur à bascule.

Je trouve le modèle rouge assez peu fiable. Les chargements échouent assez fréquemment.

En général ces modules proposent au moins une ligne DTR ou RTS.

Il est intéressant de fabriquer un câble directement enfichable sur la carte cible. Le câblage est dépendant du modèle de convertisseur utilisé :

Convertisseur avec connecteur enfichable sur ARDUINO PRO MINI


Câble FTDI TTL-232R
Ce câble existe aussi en version 3.3V : TTL-232R-3V3.

ATTENTION : la tension fournie par le fil VCC rouge est de 5V !
C'est indiqué dans la datasheet.

1.3. Le FT2232H et FT4232H

Pour les applications gourmandes FTDI propose également des convertisseurs à 2 et 4 ports.

Ils sont également disponibles sous forme de modules :
Les datasheets :
https://www.ftdichip.com/Support/Documents/DataSheets/Modules/DS_FT2232H_Mini_Module.pdf
https://www.ftdichip.com/Support/Documents/DataSheets/Modules/DS_FT4232H_Mini_Module.pdf

Une application JTAG :
http://www.avi-plus.com/images/fbfiles/files/HOWTO_JTAG_Interface_STMCLT_Clone_REV2.pdf

Une application personnelle (HUB 4 ports série) :
https://riton-duino.blogspot.com/2020/02/un-hub-4-ports-serie.html

On trouve également des modules chinois sur AliExpress :

FT4232 4 ports


FT4232 4 ports

Il a des possibilités intéressantes :
  • commutateur d'alimentation (USB ou JACK)
  • commutateur 5V / 3.3V sur RX / TX
  • 4 ports RX / TX / GND / VCC sur connecteurs JST
Le même en version FT2232 :
FT2232 2 ports

1.4. Le PL2303

Ce chip de la société PROLIFIC a les caractéristiques suivantes :
  • USB2
  • vitesse (baud) : 75bps à 12Mbps
  • tension 5V ou 3.3V
  • broche de sortie 3.3V
  • EEPROM interne
  • tampon de réception et émission : 128 bytes et 256 bytes
La présence de deux tampons mémoire le rend plus performant dans les vitesses de transmission hautes.

Les modules sont aussi très nombreux :
PL2303 5V / 3.3V
Ce modèle fournit une alimentation et des signaux 5V ou 3.3V en fonction de la position d'un cavalier.

Comme les modules CH340, en général ces modules ne proposent que les lignes RX et TX, pas de DTR ou RTS, bien pratiques pour charger une PRO MINI en automatique, sans appuyer sur le bouton RESET.

1.5. Le CP2102 ou CP2104

Ce chip de la société Silicon Labs a les caractéristiques suivantes :
  • USB2
  • vitesse (baud) : jusqu'à 921600Mbps
  • tension 5V ou 3.3V
  • broche de sortie 3.3V
  • EEPROM interne
  • tampon de réception et émission : 576 bytes et 640 bytes
La présence de deux tampons mémoire le rend plus performant dans les vitesses de transmission hautes.

Les modules sont aussi très nombreux :
CP2102 5V / 3.3V
Ce modèle fournit une alimentation et des signaux 5V ou 3.3V en fonction de la position d'un cavalier.

Apparemment si les FT232R avec commutateur 5V / 3.3V sont très répandus, il n'en va pas de même pour le CP2102 ou CP2104.
La majorité propose deux broches 5V et 3.3V mais qu'en est-il de la tension des signaux ? Cela demande à être approfondi.

En général ces modules proposent au moins une ligne DTR ou RTS.

2. Les drivers

Sous Windows il faudra les installer :
CH340 : http://www.wch.cn/download/CH341SER_EXE.html
FT232R : https://www.ftdichip.com/Drivers/VCP.htm
PL2303 : http://www.prolific.com.tw/US/ShowProduct.aspx?p_id=225&pcid=41
CP2102 & CP2104 : https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers

Attention : il semblerait que certaines versions de PL2303 (PL-2303HXA et PL-2303X) ne soient pas compatibles avec Windows 8 et 10.
Voir la page de téléchargement.

Sous Linux il n'y a rien à faire. Les drivers font partie de la distribution et sont directement utilisables.

3. L'attribution des ports

Lorsque l'on branche un convertisseur ou une carte UNO ou NANO sur un PC, le comportement diffère en fonction de l'OS.

3.1. Windows

Windows attribue un port COMX en fonction du driver et surtout du chip convertisseur utilisé.

Avec un FT232R ou un PL2303 le port de COM attribué sera différent pour chaque convertisseur, et en rebranchant un convertisseur précédemment utilisé celui-ci retrouvera le même port de COM que lors de sa précédente utilisation.

Ceci explique que sur certains postes de développeurs on peut avoir sous Windows 50 ports de COM déjà attribués.

Avec un CH340 Windows attribuera le premier port de COM libre lors du premier branchement. Ensuite ce port sera réutilisé, s'il est libre, pour tous les convertisseurs CH340.
Il est donc impossible de distinguer un CH340 d'un autre CH340 par son port de COM.

Ceci explique que si l'on branche une UNO sur un PC, qu'on la débranche et qu'on en branche une autre, elle se voit attribuer le même port.

Oui mais pourquoi ? tout simplement parce que le CH340 ne possède pas de mémoire EEPROM pour y stocker un numéro de série.

Un FT232R ou un PL2303 possède une EEPROM et un numéro de série y est inscrit à la fabrication.
Celui-ci est même modifiable par l'utilisateur. Chez FTDI par exemple :
FT_PROG EEPROM Programming Utility

Il est possible sous Windows de changer un numéro de port à l'aide du Gestionnaire de Périphériques :
https://plugable.com/2011/07/04/how-to-change-the-com-port-for-a-usb-serial-adapter-on-windows-7/

Encore une fois cette modification, si le convertisseur est un CH340, s'appliquera à tous les CH340.

3.2. Linux

Linux attribue le premier port TTY libre en partant de ttyUSB0 (ttyACM0 pour certaines cartes, MEGA officielle par exemple).

Si on débranche un convertisseur pour en rebrancher un autre, le même port sera attribué de nouveau.
Si on a laissé un logiciel terminal connecté au port TTY précédent, le port suivant sera attribué.

On pourrait être tenté de dire : Linux c'est le bazar !
Non, la gestion est plus simple et plus fiable.

Sous Linux il est possible également de modifier le nom d'un port TTY USB et de le rendre persistant. Il sera nécessaire de passer par udev :

Pour mon exemple concret sur RASPBERRY PI j'ai choisi un port USB sur lequel est branchée une carte PRO MINI avec un émetteur / récepteur 433MHz. Le convertisseur est un FT232R.
Normalement, au boot, le device ttyUSB2 lui est attibué. Je désire ajouter un device ttyUSB433, car je sais que DOMOTICS ne me proposera que des devices commençant par ttyUSB, ttyACM ou ttyAMA.

La commande lsusb permet de lister les ports USB. Ici je recherche un FT232 :

$ lsusb | grep FT232
Bus 001 Device 004: ID 0403:6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC


On voit ici l'identifiant constructeur (0403) et identifiant produit (6001).

Ensuite il faut connaître le port TTY l'aide de la commande dmesg. Si le convertisseur vient d'être branché on verra plus facilement ses informations.
Sinon il faudra les rechercher le mot FT232 dans la console avec CTRL-SHIFT-F :

$ dmesg
...
[    3.562108] ftdi_sio 1-1.2:1.0: FTDI USB Serial Device converter detected
[    3.562271] usb 1-1.2: Detected FT232RL
[    3.563289] usb 1-1.2: FTDI USB Serial Device converter now attached to ttyUSB2


Si plusieurs FT232R sont présents on pourra lever le doute en débranchant  et rebranchant celui que l'on désire modifier. Le informations se retrouveront en fin de liste.

On peut ensuite utiliser la commande udevadm pour avoir plus d'informations :

$  udevadm info -n /dev/ttyUSB2
P: /devices/platform/soc/3f980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB2/tty/ttyUSB2
N: ttyUSB2
S: serial/by-id/usb-FTDI_FT232R_USB_UART_A104JP07-if00-port0
S: serial/by-path/platform-3f980000.usb-usb-0:1.2:1.0-port0
S: ttyUSB433
E: DEVLINKS=/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A104JP07-if00-port0 /dev/serial/by-path/platform-3f980000.usb-usb-0:1.2:1.0-port0 /dev/ttyUSB433
E: DEVNAME=/dev/ttyUSB2
E: DEVPATH=/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.2/1-1.2:1.0/ttyUSB2/tty/ttyUSB2
E: ID_BUS=usb
E: ID_MODEL=FT232R_USB_UART
E: ID_MODEL_ENC=FT232R\x20USB\x20UART
E: ID_MODEL_FROM_DATABASE=FT232 USB-Serial (UART) IC
E: ID_MODEL_ID=6001
E: ID_PATH=platform-3f980000.usb-usb-0:1.2:1.0
E: ID_PATH_TAG=platform-3f980000_usb-usb-0_1_2_1_0
E: ID_REVISION=0600
E: ID_SERIAL=FTDI_FT232R_USB_UART_A104JP07
E: ID_SERIAL_SHORT=A104JP07
E: ID_TYPE=generic
E: ID_USB_DRIVER=ftdi_sio
E: ID_USB_INTERFACES=:ffffff:
E: ID_USB_INTERFACE_NUM=00
E: ID_VENDOR=FTDI
E: ID_VENDOR_ENC=FTDI
E: ID_VENDOR_FROM_DATABASE=Future Technology Devices International, Ltd
E: ID_VENDOR_ID=0403
E: MAJOR=188
E: MINOR=2
E: SUBSYSTEM=tty
E: TAGS=:systemd:
E: USEC_INITIALIZED=49791


FT232R, 0403, 6001 : c'est bien le bon convertisseur.

Il faut relever :
  • l'identifiant constructeur : 0403
  • l'identifiant produit : 6001
  • le N° de série : AL008YL2
  • le port TTY : ttyUSB1
Ensuite il suffit de créer un fichier nommé 99-usb-serial.rules par exemple dans le répertoire /etc/udev/rules.d. Ce fichier doit être créé avec les droits administrateur (sudo).

/etc/udev/rules.d/99-usb-serial.rules : 
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A104JP07", SYMLINK+="ttyUSB433".

Il faut ensuite redémarrer udev :
On trouve beaucoup de commandes du genre :

$ sudo /etc/init.d/udev restart
$ udevadm control --reload-rules && udevadm trigger

Mais ces commandes n'ont pas l'air d'avoir le moindre effet.
Donc je redémarre :

$ sudo reboot

A chaque fois que le kernel verra un device TTY USB ayant ces identifiants et ce numéro de série il l'appellera ttyFT232-1.
Bien entendu il est possible d'ajouter autant de lignes que nécessaire pour différents convertisseurs.

Au redémarrage nous voyons note nouveau port TTY :

$ ls /dev/ttyU*
/dev/ttyUSB0  /dev/ttyUSB1  /dev/ttyUSB2  /dev/ttyUSB433


On peut remarquer que le ttyUSB2 est toujours présent. ttyUSB433 est en fait un lien symbolique sur ttyUSB2 (un alias).

Ici aussi cette modification, si le convertisseur est un CH340, s'appliquera à tous les CH340 car on ne peut pas préciser de N° de série. Dommage.

L'avantage avec Linux est que l'on peut nommer le port TTY à sa convenance. Dans l'exemple je l'ai appelé ttyUSB433.

Ensuite comment distinguer physiquement plusieurs FT232R entre eux ? avec une étiquette, portant le même nom, collée sur le module par exemple.

4. L'attribution des ports au boot

Au démarrage d'une machine, si plusieurs convertisseurs ou plusieurs cartes ARDUINO sont déjà branchés, le N° des ports attribués vont dépendre bien sûr de l'OS et du type de convertisseur.

Cela concerne principalement les machines utilisées comme serveur domotique, avec plusieurs cartes ARDUINO branchées en permanence :
  • passerelle RFLINK
  • passerelle NRF24L01 ou RFM69
  • etc.
Ce sont souvent des machines à base de RASPBERRY PI, sous Linux, mais pas seulement. Windows 10 tourne aussi sur cette plateforme (RASPBERRY PI 4).

4.1. Windows

Sous Windows c'est facile : tous les convertisseurs retrouveront le petit nom qu'ils portaient précédemment.

Si plusieurs CH340 sont présents, il y aura incertitude, mais en général l'informatique est déterministe et normalement les N° de ports COM seront les même que précédemment, sauf peut être si l'on a interverti les ports USB de deux cartes, ou si la chance n'est pas de notre côté.

L'idéal est donc de n'avoir au maximum qu'un seul CH340 branché au démarrage sur un serveur si l'on veut pouvoir retrouver facilement leurs ports respectifs.
Les autres convertisseurs FT232R ou PL2303 se verront toujours attribuer le même port de COM.

4.2. Linux

Là encore les ports TTY attribués à chaque convertisseur a de fortes chances de rester le même, mais ce n'est pas garanti.
La bonne méthode est d'attribuer soi-même avec udev un petit nom reconnaissable comme vu plus haut (3.2. Linux).
Ici aussi si plusieurs CH340 sont présents, il y aura incertitude.

4.3. Exemple

Sur ma RAPBERRY PI qui héberge un serveur DOMOTICZ il y a quatre cartes ARDUINO branchées en USB :
  • une passerelle RFLINK (ARDUINO MEGA officielle)
  • une passerelle MYSENSORS 2.4GHz (ARDUINO NANO + CH340)
  • une passerelle MYSENSORS 433MHz (ARDUINO PRO MINI + FT232R)
  • un récepteur infrarouge (ARDUINO NANO + CH340)
Si je me contente de laisser telle quelle cette configuration il y aura incertitude sur les deux NANOs et la PRO MINI. En effet les trois ports seront nommés ttyUSB0, ttyUSB1 et ttyUSB2, un peu au hasard.

La MEGA porte le nom /dev/ttyACM0, car c'est une officielle. Si l'on avait affaire à une carte chinoise équipée d'un CH340 il y aurait une incertitude de plus.

Par chance la PRO MINI utilise un FT232R. On peut donc renommer son port série. Il a été renommé ttyUSB-433 à l'aide d'une règle udev.

En général cela se passe bien et les ports sont attribués comme suit :
  • récepteur infrarouge : /dev/ttyUSB0
  • passerelle RFLINK : /dev/ttyACM0
  • passerelle MYSENSORS 2.4GHz : /dev/ttyUSB1
  • passerelle MYSENSORS 433MHz : /dev/ttyUSB433
Il reste donc une incertitude entre ttyUSB0 et ttyUSB1 (deux NANOs CH340).

Comme je ne peux rien faire pour résoudre ce problème, à chaque redémarrage (ce n'est pas fréquent) si les ports des deux NANOS sont inversés, je débranche les deux, et je rebranche la carte "récepteur infrarouge" en premier, et la carte "passerelle MYSENSORS 2.4GHz" en second. Tout rentre dans l'ordre.
L'idéal serait de remplacer les deux NANOs par des PRO MINI et un FT232R dont le port série porterait un nom parlant : ttyUSB24 (pour 2.4GHz) ou ttyUSB38 (pour infrarouge 38 KHz) par exemple.

J'ai récemment développé un HUB 4 ports série :
https://riton-duino.blogspot.com/2020/02/un-hub-4-ports-serie.html
Bâti autour d'un FT4232H (quadruple FT232R), celui-ci me permettra de résoudre tous ces problèmes plus facilement, et accessoirement de libérer au moins deux ports USB.

5. Le bitbang

Qu'est ce que le bitbang ? c'est la possibilité qu'offrent certains convertisseurs de contrôler leurs broches par logiciel.
On peut ainsi les transformer en extension d'I/O pour PC, moyennant l'utilisation d'un driver approprié ou non.

FTDI propose D2XX :
https://www.ftdichip.com/Drivers/D2XX.htm
FTDI propose également libftdi et pylibftdi :
https://github.com/lipro/libftdi
https://pypi.org/project/pyftdi/
Une documentation : https://www.ftdichip.com/Support/Documents/AppNotes/AN_373 Bit-Bang Modes for the FT-X Series.pdf

Un exemple ici avec pyftdi :
https://riton-duino.blogspot.com/2020/02/un-hub-4-ports-serie.html#python 

Chez Silicon Labs : https://www.silabs.com/community/interface/knowledge-base.entry.html/2018/07/25/how_to_control_gpios-Wot7

Il est facile ensuite de contrôler les broches du convertisseur en PYTHON par exemple en utilisant la librairie libusb et le module pyusb.

5.1. PYTHON

Amusons-nous à piloter un FT4232H avec un script PYTHON.
Bien entendu on peut faire la même chose avec des modules FT232R ou FT2232H, et je l'ai d'ailleurs fait avec des modules chinois.

Pour cela il nous faut au préalable installer la librairie pylibftdi :

pip install pylibftdi

Ce script bitbang.py fait clignoter une LED (en série avec un résistance de 470KΩ) branchée sur le port AD0 (TX1) d'un FT4232H ou la broche TX d'un FT232R :

import time
from pylibftdi import Driver
from pylibftdi import BitBangDevice

def get_ftdi_device_list():
    """
    return a list of lines, each a colon-separated
    vendor:product:serial summary of detected devices
    """
    dev_list = []
    for device in Driver().list_devices():
        # list_devices returns bytes rather than strings
        dev_info = map(lambda x: x.decode('latin1'), device)
        # device must always be this triple
        vendor, product, serial = dev_info
        dev_list.append((vendor, product, serial))
    return dev_list

def blink_device(dev):
    """
    blink PORT0 of device
    """
    with BitBangDevice(device_id=dev) as bb:
        bb.direction = 0x01
        print bb.port

        while True:
            bb.port = 0x01
            time.sleep(1)
            bb.port = 0x00
            time.sleep(1)

if __name__ == '__main__':
    cnt = 0
    devices = get_ftdi_device_list()
    for device in devices:
        print "%d %s" % (cnt, device)
        cnt += 1
    dev = int(raw_input("select device: "))
    print dev
    if dev <= cnt:
        print "blink_device", dev
        blink_device(devices[dev][2])


Le script demande d'abord avec quel convertisseur il doit travailler, après avoir affiché ceux qu'il a trouvé :

$ python bitbang.py
0 (u'FTDI', u'FT4232H MiniModule', u'FTWEQGJE')
1 (u'FTDI', u'FT4232H MiniModule', u'FTWEQY84')
select device: 1
blink_device 1


Il trouve deux mini-modules, ce qui est vrai. Je choisis ne N° 1 et après cela la LED branchée sur le port AD0 clignote !

Cette manipulation peut être intéressante à plus d'un titre :

On peut par exemple écrire sur les entrées et consulter l'état des sorties d'un ARDUINO, afin de réaliser des tests, au lieu de faire ça avec des boutons et des LEDs.

Sous Linux, il faudra accorder les doits d'écriture sur le device USB :

$ cat /etc/udev/rules.d/98-ftdi.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", MODE:="0666" 
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", MODE:="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE:="0666"

Ce fichier de règles udev est OK pour FT2232H, FT4232H etFT232R (idProduct = 6010, 6011, 6001).

5.2. FT232 et I2C, SPI et autres

Une autre utilisation du bitbanging :
  • créer de toutes pièces un bus I2C ou SPI
  • générer un PWM
  • contrôler un PCF8574, un MCP23017, etc.
https://github.com/adafruit/Adafruit_Python_GPIO/tree/master/Adafruit_GPIO

Comme on le voit les possibilités sont nombreuses.

5.3. FT2232H et ESP32

Voici une dernière utilisation du bitbanging : un module FT2232H peut être utilisé pour réaliser une sonde JTAG pour ESP32. Il existe même un module ESP32-PROG spécialement conçu pour cette utilisation.
A tester ...

6. conclusion

J'espère avoir apporté des réponses à quelques questions que vous vous posiez peut-être à propos des convertisseurs USB.


Cordialement
Henri