Ces tableaux qui peuvent nous simplifier le développement Arduino

. Par : bobyAndCo. URL : http://www.locoduino.org/spip.php?article227

L’ambition de ce petit article n’est pas de faire une présentation exhaustive de ce que sont les tableaux dans le langage Arduino et C/C++ mais plus simplement d’apporter quelques « recettes » qui pourraient bien faciliter grandement vos développements.

Il est par exemple assez fréquent de voir des programmes où la déclaration et l’initialisation des broches sont faites de la manière suivante :

  1. const int Pin23 = 23;
  2. const int Pin24 = 24;
  3. const int Pin25 = 25;
  4. const int Pin26 = 26;
  5. const int Pin27 = 27;
  6. const int Pin28 = 28;
  7. ...

Et parfois, les listes sont bien plus longues.

Et puis, dans le setup on trouve :

  1. pinMode(Pin23, OUTPUT);
  2. pinMode(Pin24, OUTPUT);
  3. pinMode(Pin25, OUTPUT);
  4. pinMode(Pin26, OUTPUT);
  5. pinMode(Pin27, OUTPUT);
  6. pinMode(Pin28, OUTPUT);
  7. ...

et puis encore la commutation des broches (dont cette fois je me dispenserai de faire toute la liste)

  1. digitalWrite(Pin23, HIGH);
  2. digitalWrite(Pin24, HIGH);
  3. ...

Procéder ainsi est exact au regard de la programmation. Mais c’est long, fastidieux, source d’erreurs et cela rend la maintenance problématique. Par ailleurs, ça prend beaucoup de place dans l’IDE et rend difficile une vision globale de notre code nécessitant d’utiliser les ascenseurs de la page. Quand on voit le bas du code, on ne voit plus le début et quand on voit le début, on ne voit pas la fin.

Encore une fois, ce que je présente ici n’est pas un cours sur les tableaux. Les puristes pourront dire que l’on pouvait faire encore plus, encore mieux. Je m’adresse ici avant tout aux lecteurs qui n’ont pas beaucoup d’expérience mais sont (très) motivés pour faire des programmes Arduino efficaces pour leur réseau et qui peuvent parfois passer beaucoup de temps à résoudre des erreurs relevant d’une simple faute de frappe.

Comment savoir si je dois recourir à l’utilisation d’un tableau ?

D’une manière générale, dès que vous rédigez des lignes de code répetitives, il y a de grandes chances qu’un tableau puisse vous simplifier la vie. Mais plutôt que des discours, voyons un exemple. Voici comment, avec un tableau, il est possible d’écrire les lignes de code présentées ci-dessus :

  1. const byte pin[] = {22, 23, 24, 25, 26, 27, 28}; // Déclaration et initialisation d'un tableau

pin[] = {...,...}; est l’une des façons de déclarer un tableau qui portera le nom pin. Son type doit être le même que celui des valeurs qu’il contient. Ici ce sont des bytes puisqu’ils représentent des broches de la carte et que leur nombre n’atteindra jamais 256.. Et nous les déclarons comme constantes (const) puisqu’elles ne vont pas changer de valeur au cours de notre programme. N’hésitez pas à vous reporter à l’article de Jean-Luc qui met en lumière l’importance d’un bon typage.

Utiliser les tableaux.

Un tableau est une liste ordonnée de valeurs de même type. La taille d’un tableau correspond au nombre d’éléments du tableau. Ici, il y 7 éléments dans notre tableau de broches. On dit donc que la taille du tableau est 7.

Pour accéder à la valeur d’un élément du tableau, on utilise son index. Ici, le premier élément a la valeur 22, le second 23 et le dernier 28. Mais il y a une subtilité qui vous posera sans doute quelques déboires au début. Le premier élément du tableau a pour index 0 (zéro) et non 1. Le second a pour index 1 et ainsi de suite.

Si l’on voulait par exemple placer dans la variable test la valeur de la première broche, on écrirait ceci :

  1. byte test = pin[0]; // test == 22

Avant d’aller plus loin, il faut que je vous présente le meilleur allié du tableau, la boucle for. Utilisée conjointement aux tableaux, elle fera des merveilles dans vos programmes. La boucle for est elle aussi indispensable pour traiter des opérations répétitives. Regardez par exemple comment je déclare mes broches de tout à l’heure en OUTPUT et que je les commute ensuite à HIGH dans mon setup :

  1. void setup() {
  2. for (int i = 0; i < 7; i++) {
  3. pinMode(pin[i], OUTPUT); // Toutes les pins sont commutées en OUT
  4. digitalWrite(pin [i], HIGH); // Toutes les pins prennent pour valeur HIGH
  5. } // Fin de la boucle for
  6. } // Fin Setup

Télécharger

Quatre lignes de code alors qu’il m’en fallait dix huit précédemment.

Ici toutes mes broches se suivaient, mais cela aurait aussi fonctionné de la manière suivante :

  1. const byte pinIn[] = {22, 24, 26, 28, 30, 32, 34};
  2. const byte pinOut[] = {23, 25, 27, 29, 31, 33, 35};
  3.  
  4. void setup() {
  5.  
  6. for (int i = 0; i < 7; i++) {
  7. pinMode(pinIn[i], INPUT); // Toutes les pins in sont commutées en IN
  8. digitalWrite(pinIn[i], LOW); // Toutes les pins in ont pour valeur LOW
  9. pinMode(pinOut[i], OUTPUT); // Toutes les pins out sont commutées en OUT
  10. digitalWrite(pinOut[i], HIGH); // Toutes les pins out ont pour valeur HIGH
  11. } // Fin de la boucle for
  12.  
  13. } // Fin Setup

Télécharger

Prenons un autre exemple. Imaginons que mon programme soit destiné à piloter les 16 aiguillages de notre réseau. Je vais encore pouvoir simplifier un peu le code ci-dessus.

Voici une autre façon de déclarer un tableau que l’on va ici appeler aiguillage tout simplement et dont on va mettre le nombre d’éléments qu’il contiendra entre les [] :

  1. byte aiguillage[16]; // Déclaration d'un tableau ayant pour nom aiguillage dont la taille est de 16 éléments.

Notez bien qu’ici mon tableau est juste déclaré et pas encore initialisé car je ne lui ai donné aucune valeurs. Je vais le faire dans le setup d’une manière un peu similaire à toute à l’heure :

  1. const byte nbre_aiguillages = 16;
  2. byte aiguillage[nbre_aiguillages]; // Déclaration d'un tableau "aiguillage" de taille "nbre_aiguillages", soit 16 éléments.
  3. const byte first_pin = 22; // Valeur de la première pin qui va contrôler mes aiguilles.
  4.  
  5. void setup() {
  6. for (int i = 0; i < 16; i++) {
  7. byte pin = first_pin + i; // va prendre la valeur 22 quand i = 0, 23 quand i = 1 etc…
  8. aiguillage[i] = pin; // revient à écrire, si i = 0, aiguillage[0] = 22, si i = 1, aiguillage[1] = 23
  9. pinMode(aiguillage[i], OUTPUT); // Toutes les pins sont commutées en OUT
  10. digitalWrite(aiguillage[i], LOW); // Toutes les pins ont pour valeur LOW
  11. } // Fin de la boucle for
  12. } // Fin Setup

Télécharger

Allez, une dernière astuce pour rendre votre code plus facilement maintenable. En effet, si vous ajoutez une aiguille sur votre réseau, vous voyez bien que vous allez devoir parcourir le code pour remplacer 16 par 17.

Proposition -> ajouter au début de votre sketch :

#define NBRE_AIGUILLAGE 16

#define s’appelle une directive. Son comportement est assez simple. Quand vous lancez la compilation, le compilateur va commencer par rechercher partout dans votre code là où vous avez écrit NBRE_AIGUILLAGE et il va remplacer ce texte par la valeur 16. Un peu comme quand vous utilisez la fonction remplacer un mot par un autre dans un traitement de texte. Quel est l’intérêt ? Démonstration avec le code précédent :

  1. #define NBRE_AIGUILLAGE 16
  2. byte aiguillage[NBRE_AIGUILLAGE] ; // Déclaration d’un tableau dont la taille est NBRE_AIGUILLAGE, ici 16
  3. const byte first_pin = 22 ; // Valeur de la première pin qui va contrôler mes aiguilles.
  4.  
  5. void setup() {
  6. for (int i = 0; i < NBRE_AIGUILLAGE; i++) {
  7. byte pin = first_pin + i; // va prendre la valeur 22 quand i = 0, 23 quand i = 1 etc…
  8. aiguillage[i] = pin; // revient à écrire, si i = 0, aiguillage[0] = 22, si i = 1, aiguillage[1] = 23
  9. pinMode(aiguillage [i], OUTPUT); // Toutes les pins sont commutées en OUT
  10. digitalWrite(aiguillage [i], LOW); // Toutes les pins ont pour valeur LOW
  11. } // Fin de la boucle for
  12. } // Fin Setup

Vous voyez que si je change le nombre d’aiguillage, par exemple :

 

#define NBRE_AIGUILLAGE 18, toute la suite de mon code reste vraie sans que j’ai besoin de le modifier.

Je ne voudrais pas rendre les choses trop compliquées mais je souhaitais vous donner quelques autres avantages des tableaux par exemple dans le loop. Attention, soyez attentif, je vais volontairement introduire une difficulté qui nous a tous piégée un jour.

Pour commander mes 16 aiguilles, on va dire que j’ai 16 boutons numérotés de 1 à 16

Pour eux aussi, je vais me simplifier la vie en les plaçant dans un tableau :

  1. byte btn[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};

Voici donc un tableau contenant 16 boutons numérotés sur mon TCO de 1 à 16. Dans le programme, on peut écrire :

digitalWrite(aiguillage [btn[1]], LOW);

Ce qui revient à dire, faire un digitalWrite sur la broche aiguillage qui a pour index le bouton sélectionné. Ici le btn[1]. Ca fonctionne très bien et c’est même plutôt recommandé de programmer ainsi. Seulement, quelle ne sera pas votre surprise de constater que c’est l’aiguille 2 qui se mettra en mouvement et non la 1.

En effet, ne confondez pas l’index du tableau, et la valeur correspondant à cet index. L’index d’un tableau commence à 0 alors que dans notre cas, nous avons commencé la numérotation de nos boutons à 1.

Aussi :

  1. btn[0] = 1;
  2. btn[1] = 2;
  3. .....

Pas très logique me direz-vous ! Eh bien si ; vous avez le droit de numéroter vos boutons comme vous le voulez. Pourquoi pas :

byte btn[] = {1, 12, 34};

* Sur ce point, je vous invite à lire attentivement la petite mise en garde en fin d’article.

Enumérations et tableaux.

Il n’est pas toujours facile de s’y retrouver avec des syntaxes qui ne sont pas très explicites :

byte btn[] = {1, 12, 34}; ou btn[1] ou encore btn[14]

D’aucun préfèrera des formulations comme « entrée gare », « sortie gare », « entrée dépôt »

Cela est envisageable en ayant recours aux énumérations :

enum { ENTREE_GARE, SORTIE_GARE, ENTREE_DEPOT,… };

déclare les identificateurs ENTREE_GARE, SORTIE_GARE, ENTREE_DEPOT,… comme étant des constantes entières de valeur 0, 1, 2, 3 …

 

Dans le code, cela va se traduire ainsi : digitalWrite(aiguillage [btn[ENTREE_GARE]], LOW);
plus lisible que : digitalWrite(aiguillage [btn[0]], LOW);

 

Au sujet des énumérations, vous pouvez là encore vous reporter à l’article de Jean-Luc cité plus haut sur les constantes. http://www.locoduino.org/spip.php?article102

L’EEPROM est un tableau.

Pour finir, un exemple utilisant la mémoire EEPROM. On hésite souvent à enregistrer des données dans l’EEPROM simplement parce que l’on ne sait pas comment ça fonctionne. Là encore, mon objectif n’est pas de vous expliquer l’EEPROM mais de permettre que vous vous en serviez.

Eh bien l’EEPROM est un tableau de cases mémoire. Un UNO dispose ainsi de 1024 « cases » mémoire en EEPROM qui peuvent chacune stocker 1 octet (8 bits).

Pour éviter toute déconvenue, je vous invite à utiliser quand vous le pouvez le type byte pour votre tableau qui correspond exactement à la taille d’une case mémoire. Le byte est un nombre entier non signé d’une valeur de 0 à 255 ce qui est largement suffisant dans notre exemple à 16 aiguillages.

Comme cela est expliqué dans l’article de Locoduino sur la bibliothèque EEPROM vous pouvez utiliser la fonction EEPROM.write avec le numéro de la case mémoire puis la valeur que vous souhaitez enregistrer.

Pour vous y retrouver facilement, vous pouvez écrire :

  1. #define NBRE_AIGUILLAGE 16
  2. byte aiguillage[NBRE_AIGUILLAGE]; // Déclaration d’un tableau dont la taille est NBRE_AIGUILLAGE, ici 16
  3. byte aigPos[NBRE_AIGUILLAGE]; // Déclaration d’un tableau qui va stocker la position de chaque aiguille
  4. for (int i = 0; i < NBRE_AIGUILLAGE; i++) {
  5. aigPos[i] = HIGH;
  6. digitalWrite(aiguillage[i], aigPos[i]);
  7. EEPROM.write(i, aigPosit[i]);
  8. }

mais vous pouvez aussi écrire sous forme de tableau :

  1. #define NBRE_AIGUILLAGE 16
  2. byte aiguillage[NBRE_AIGUILLAGE]; // Déclaration d’un tableau dont la taille est NBRE_AIGUILLAGE, ici 16
  3. byte aigPos[NBRE_AIGUILLAGE]; // Déclaration d’un tableau qui va stocker la position de chaque aiguille
  4. for (int i = 0; i < NBRE_AIGUILLAGE; i++) {
  5. aigPos[i] = HIGH;
  6. digitalWrite(aiguillage[i], aigPos[i]);
  7. EEPROM[i] = aigPosit[i] ;
  8. }

De même, pour charger dans le setup par exemple les valeurs contenues dans l’EEPROM :

  1. #include <EEPROM.h>
  2. #define NBRE_AIGUILLAGE 16
  3. byte aiguillage[NBRE_AIGUILLAGE]; // Déclaration d’un tableau dont la taille est NBRE_AIGUILLAGE, ici 16
  4. const byte first_pin = 22; // Valeur de la première pin qui va contrôler mes aiguilles.
  5.  
  6. void setup() {
  7. for (int i = 0; i < NBRE_AIGUILLAGE; i++) {
  8. byte pin = first_pin + i; // va prendre la valeur 22 quand i = 0, 23 quand i = 1 etc…
  9. aiguillage[i] = pin; // revient à écrire, si i = 0, aiguillage[0] = 22, si i = 1, aiguillage[1] = 23
  10. pinMode(aiguillage[i], OUTPUT); // Toutes les pins sont commutées en OUT
  11. if (NULL == EEPROM[i]) // Pas de valeur dans l'EEPROM à cette adresse
  12. digitalWrite(aiguillage[i], HIGH); // Alors valeur par defaut : HIGH
  13. else // Il y a une valeur dans l'EEPROM à cette adresse
  14. digitalWrite(aiguillage[i], EEPROM[i]);
  15. } // Fin de la boucle for
  16. } // Fin Setup

Télécharger

En conclusion :

L’utilisation des tableaux nécessite une petite gymnastique intellectuelle mais permet de simplifier grandement le code, de le sécuriser et le rendre plus facilement maintenable.

A chaque fois que vous commencez à manipuler des données répétitives et en grand nombre, dites vous que les tableaux pouront sans doute vous aider. Quand vous serez convaincu de leur utilité il ne vous faudra pas longtemps pour les maîtriser.

Enfin, quand vous aurez franchi cette première étape, vous serez entré dans l’univers des tableaux qui recèle nombre d’autres possibilités très puissantes. Car les tableaux, c’est bien plus encore que ce que je vous présente ici.

Comme d’habitude, n’hésitez pas à poser toutes vos questions pour vous permettre de maitriser cette « brique » importante de la programmation et nous essayerons avec tous ceux qui connaissent ce sujet de vous répondre au plus vite et au mieux.

Et pour finir, je souhaite remercier l’un de nos sympathiques Locoduinistes qui a inspiré cet article (il se reconnaitra) et que je remercie pour le travail fait ensemble.

Beaucoup de plaisir avec vos trains et vos Arduino.

 

* Mise en garde concernant index et taille de tableaux :

Nous avons vu plus haut le cas d’un tableau contenant 16 éléments. Nous avons vu également que pour accéder au premier élément du tableau, nous devions appeler l’index 0 du tableau :

  1. byte test = tableau[0];

Pour appeler la valeur du dernier élément du tableau, on devait donc écrire :

  1. byte test = tableau[15]; // Qui a pour valeur 16

Ecrire ceci ne vous donnera tout au plus qu’une valeur incohérente car l’index 16 du tableau n’existe pas, mais le compilateur laissera faire, il ne vous signalera aucune erreur, aucun warning :

  1. byte test = tableau[16];

Vous allez tout simplement lire la valeur contenue dans la case mémoire contigüe à tableau[15], ça peut-être rien, ça peut être quelque chose mais très probablement incohérent dans la logique de votre programme et cela risque de fausser son déroulement.

Plus grave, écrire ceci va effacer la valeur contenue dans la case mémoire contigüe à tableau[15], ça peut changer la cohérence de votre programme et croyez moi, vous risquez de chercher longtemps la cause :

  1. tableau[16] = test;

Bon, tout au plus, sur un Arduino, vous risquez de changer une valeur par une autre non désirées. Vous pouvez aussi risquer le plantage. Mais par contre, dans un programme C/C++ exécuté sur un PC, si vous effacez des données de vos programmes ou de votre système, là ça fait très mal. Et "ctrl + Z" ne fonctionnera pas !