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