Trucs, astuces et choses à ne pas faire !

... en programmation pour votre Arduino

. Par : Dominique. URL : https://www.locoduino.org/spip.php?article174

En programmation, personne ne peut se vanter de tout savoir. Il y a des formations, bien-sûr, mais dans notre domaine du modélisme, on a tendance à la négliger un peu pour s’occuper de nos trains avant tout.

Ce site est destiné à vous aider dans votre formation, non seulement par les articles sur la programmation, mais aussi les exemples qui se trouvent dans les réalisations.

Cet article procède différemment : il est destiné à vous faire connaitre une partie de l’expérience d’un grand expert australien de l’Arduino : Nick Gammon qui a rassemblé sur son site, des quantités d’informations utiles, mais en anglais. Il est très présent aussi sur le forum arduino.cc dont il est modérateur d’ailleurs, par la grande qualité de ses contributions.

Cet article se concentre sur une petite partie de son savoir faire : cette page que je vais tenter de traduire pour vous.

Tant qu’on ne les a pas commises, on peut faire des erreurs ! On apprend ainsi "sur le tas". Donc autant en connaitre un maximum pour les éviter.

J’espère que cet article vous fera gagner du temps !!!

Nous allons présenter 2 parties :

  • les erreurs classiques à éviter
  • les trucs et astuces à utiliser

Les erreurs classiques à éviter absolument

Lorsqu’on clique sur le bouton "vérifier", le compilateur vous signale les erreurs qui l’empêchent de produire le code pour votre Arduino.

Mais quand il ne signale pas d’erreur, c’est là qu’il faut se méfier !!

Les erreurs décrites ci-après ne sont pas signalées par le compilateur mais elles empêchent votre Arduino de fonctionner.

ERREUR #1 : Lire trop de données d’un port de communication.

Par port de communication, on entend ici soit le port série (le plus utilisé), soit un port Ethernet ou un port CAN ou I2C, etc...

exemple à ne pas faire :

if (Serial.available ())
  {
  char a = Serial.read ();
  char b = Serial.read ();
  }

L’instruction Serial.available () retourne une valeur non-nulle si au moins un caractère est disponible. Mais rien ne permet de savoir s’il y en a 2 ou plus.

Lire 2 caractères est une erreur. A 9600 bps, la transmission d’un caractère dure environ une milliseconde alors que l’instruction Serial.read() s’éxécute en à peine une microseconde. Le 2ème caractère n’est peut-être pas encore arrivé et le résultat est vraiment incertain.

Pour l’éviter il faut tester la disponibilité de chaque caractère dans une boucle while par exemple :

while (Serial.available () > 0)
    mon_traitement (Serial.read ());

ERREUR #2 : Tenter de tester un numéro de pin, plutôt que l’état de cette pin.

Cela parait bête, mais ça arrive, exemple :

const byte switchPin = 4;
...

if (switchPin == HIGH) 
  {
  // switch is pressed
  }

Le résultat du test sera toujours faux puisque la valeur 4 est différente de HIGH qui vaut 1 (LOW vaut 0).

il faut écrire :

const byte switchPin = 4;
...

if (digitalRead (switchPin) == HIGH) 
  {
  // switch is pressed
  }

ERREUR #3 : Confondre "=" (assignation) et "==" (comparaison).

Voilà l’erreur :
if (temperature = 10)

La variable temperature se verra assignée la valeur 10 et le résultat du if sera toujours vrai.

Il faudra écrire :
if (temperature == 10)   // comparaison

De façon similaire, ceci est mauvais :
counter == 5;

Il faut écrire :
counter = 5;   // assignation

ERREUR #4 : Faire un Serial.print ou un delay dans une routine d’interruption.

Ce code ne peut pas fonctionner :

ISR (TIMER0_OVF_vect)
  {
  Serial.print ("Timer overflow !");
  delay (100);
  }

Comme les interruptions sont désactivées pendant l’exécution de la routine ISR, ni la fonction Serial.print(), ni delay() ne peuvent s’exécuter car elle utilisent des interruptions.

D’une manière générale, dans une routine d’interruption, il faut respecter les règles suivantes :

  • y mettre le moins de code possible
  • ne pas utiliser delay ()
  • ne pas utiliser Serial.print ()
  • déclarer "volatile" les variables partagées avec le code principal
  • ne pas activer ou désactiver les interruptions

Vous trouverez plus de détails dans l’article Les interruptions.

ERREUR #5 : Utiliser trop de mémoire RAM.

Déclarer une variable tableau :
int myReadings [2000];

va utiliser 4k octets de RAM (un entier occupe 2 octets).

Le Uno ne dispose que de 2k octets de Ram. Les Arduino à base de 328 sont dans le même cas.

Le symptôme est que le programme va probablement se resetter aussitôt démarré ou s’arrêter carrément.

Un bon moyen de s’assurer qu’il démarre correctement est d’afficher quelque chose sur le moniteur au démarrage, et de préférence le nom de votre programme, sa version et sa date :

// ... toutes les declarations ...
#define VERSION "MonTruc - V0.1 - 19/07/2016"

void setup ()
  {
  Serial.begin (115200);
  Serial.println (VERSION);
  }  // fin du setup

Si vous ne voyez pas s’afficher "MonTruc - V0.1 - 19/07/2016" au démarrage, il y a de bonnes chances que la mémoire RAM soit saturée.
N’oubliez pas que les bibliothèques aussi consomment de la RAM (SD, LCD, I2C, etc).

Vous trouverez plus de détails dans l’article consacré à la bibliothèque MemoryUsage

ERREUR #6 : Incrémenter une variable d’une manière incongrue.

Ceci ne marche pas :
i = i++;

Le résultat est incertain, et certainement pas i+1.

A la place il faut utiliser :
i++;
ou
i = i + 1;

ERREUR #7 : Appeler une fonction en oubliant les parenthèses.

L’erreur à éviter :

void fairebien ()
  {
  // les instructions de la routine
  }
  
void loop ()
  {
  fairebien;   // <-- pas de parentheses (c'est pas bien !)
  }

La compilation ne va pas le signaler mais le programme ne marchera pas.

Il faut bien écrire :

void fairebien ()
  {
  // les instructions de la routine
  }
  
void loop ()
  {
  fairebien ();   // <-- avec les parentheses (c'est bien !)
  }

ERREUR #8 : Faire plusieurs choses dans un If en oubliant les accolades.

C’est un grand classique :

 if (temperature > 40)
    digitalWrite (fourControl, LOW);
    digitalWrite (warningLight, HIGH);

L’indentation suggère que les 2 instructions qui suivent l’ if y sont attachées.
En réalité, seule la première sera soumise à la condition et la deuxième s’exécutera toujours.

Par précaution, il vaut mieux mettre des accolades, même pour une seule instruction :

 if (temperature > 40)
    {
    digitalWrite (fourControl, LOW);
    digitalWrite (warningLight, HIGH);
    }  // end if temperature > 40

ERREUR #9 : Déborder au delà de la taille maximale d’un tableau.

Un autre grand classique :

int table [5];
table [5] = 42;

Les 5 éléments de table sont indicés 0, 1, 2, 3, 4 mais pas 5 !
L’accès à table [5] se traduira par un écrasement de la mémoire juste après la variable table, avec des résultats inattendus. Cette erreur est difficilement détectable. Le mieux est de relire son code à tête reposée de temps en temps.

ERREUR #10 : Faire des calculs qui dépassent les capacités des variables.

Il ne faut pas croire que l’on peut initialiser un long avec des opérations sur des int :
unsigned long secondesparjour = 60 * 60 * 24;

Le valeurs 60, 60 et 24 sont traitées par le compilateur comme des entiers et le résultat restera toujours inférieur ou égal à 32767.

Il faut écrire :
unsigned long secondesparjour = 60UL * 60 * 24;

Le suffixe "UL" change le type du 60 pour en faire un unsigned long. Le résultat d’une opération arithmétique est toujours du type le plus encombrant de l’un de ses termes. utrement dit, puisqu’un unsigned long existe dans la formule, alors le résultat sera un unsigned long. ’60L’ aurait déclaré un long signé, ’60.0’ ou ’60.’ un double (réel double précision), et ’60.f’ un float (réel simple précision). Bien sûr, un cast (transtypage) direct aurait également fonctionné ’(unsigned long)60’ .

ERREUR #11 : Mettre des " ;" à la fin de toutes les lignes.

Après les if, for, while, switch, il ne faut pas mettre de point-virgule " ;"

 if (temperature > 40);    // <----- ce point-virgule est incorrect !
    {
    digitalWrite (fourControl, LOW);
    digitalWrite (warningLight, HIGH);
    }

  for (int i = 0; i < 10; i++);   // <----- ce point-virgule est incorrect !
    digitalWrite (i, HIGH);

Le point-virgule termine l’instruction, donc ce qui suit n’est plus soumis à la condition et le programme fonctionne mal.

Il faudrait bien écrire :

if (temperature > 40)
    {
    digitalWrite (fourControl, LOW);
    digitalWrite (warningLight, HIGH);
    }

  for (int i = 0; i < 10; i++)
    digitalWrite (i, HIGH);

De la même manière, ceci est une erreur :

#define LED_PIN 10;    // <--- pas de ";" 
#define LED_PIN = 10;  // <--- pas de ";", pas de "=" 

il faut écrire :
#define LED_PIN 10

ou, mieux :
const byte LED_PIN = 10;

ERREUR #12 : Faire trop joli dans les commentaires .

Ceci est une erreur :

// pour faire un effet de symétrie\\
int butTheCompilerNeverSeesThisDeclaration;

car les "\\" font que la ligne suivante fait partie du commentaire, donc elle n’est pas compilée.

Restons simple et standard :

// ceci est un commentaire standard
int maVariable;

ERREUR #13 : Initialiser plusieurs variables dans une seule ligne.

Exemple :
int x, y, z = 12;

Ici seule la variable z est initialisée à 12.

Il faudrait écrire :
int x = 1, y = 2, z = 12;

ou mieux :

int x = 1;
int y = 2;
int z = 12;

ERREUR #14 : Oublier d’initialiser une variable locale d’une fonction.

Ceci conduit à un problème :

void calculateStuff ()
  {
  int a;

  a = a + 1;

Car a n’est pas initialisé et le résultat sera n’importe quoi !

ERREUR #15 : Tenter de positionner plusieurs pins dans une seule instruction.

Ceci est une erreur :
digitalWrite ((8, 9, 10), HIGH);

Car seule la pin 10 sera mise à HIGH.

Il faudrait écrire :

digitalWrite (8, HIGH);
digitalWrite (9, HIGH);
digitalWrite (10, HIGH);

ou

for (byte i = 8; i <= 10; i++)
  digitalWrite (i, HIGH);

ERREUR #16 : Oublier un " ;" après un "return".

Attention !

 if (a < 10)
    return     // <----- pas de ";"!
  a = a + 1;

Ce code sera compilé comme s’il était écrit :

 if (a < 10)
    return a = a + 1;

ERREUR #17 : Abuser de la récursivité.

Voilà une erreur spectaculaire :

void loop ()
  {
  // faire des tas de choses
 
  loop ();    //  <--- recursion!
  }

A chaque appel de loop (), la pile est utilisée. Elle sera pleine rapidement et ça plantera !

Ceci aussi est à proscrire, pour revenir à la loop sous certaine condition :

void loop ()
  {
  foo ();
  }

void foo ()
  {
  if (something)
    loop ();
  }

ERREUR #18 : Utiliser la simple quote ’abcd’ au lieu de la double quote "abcd".

Ceci ne va pas :
char foo = 'start';

il faut écrire :
char foo [] = "start";

Le simple quote ne peut s’utiliser que pour affecter un caractère unique :
char forwardCommand = 'F';

ERREUR #19 : Problème de téléversement sur Mega d’un code contenant " !!!".

Certains programmes d’amorçage (bootloader) sur l’Arduino Mega arrêtent le téléversement lorsqu’ils rencontrent une suite de 3 points d’exclamation " !!!" dans le code :
Serial.println ("Erreur !!!");

La solution est de ne jamais mettre de " !!!" dans votre code ou de mettre à jour le bootloader, notamment en suivant les instructions sur le site de Nick Gammon : bootloader

ERREUR #20 : Réutiliser un Arduino déjà programmé dans une nouvelle configuration matérielle.

Ceci n’est pas vraiment un problème de programmation, mais cela peut détruire un Arduino.
Lorsqu’un projet est fini et qu’un Arduino de test peut être réutilisé sur un autre projet, il faut se méfier du programme qui y est enregistré. En changeant la configuration matérielle autour de cet Arduino, à la prochaine mise sous tension (par l’USB et l’IDE), il peut y avoir des incompatibilités matérielles, en particulier avec les pins programmées en sortie.

Pour éviter cela, le mieux est de charger ce code avant d’installer l’Arduino dans sa nouvelle configuration :

void setup () { }
void loop  () { }

Toutes les pins se retrouvent programmées en entrée et aucune mauvaise surprise ne peut arriver.

ERREUR #21 : Ajouter "void" devant un appel de fonction.

Ce code ne fonctionne pas :

void foo ()
  {
  Serial.println ("Hello, world.");
  }
  
void loop() 
  {
  void foo ();  //  <---- cela n'appelle pas la fonction foo
  }

Le mot clé "void" définit une fonction prototype et ne s’utilise QUE à la déclaration de la fonction.

ERREUR #22 : Oublier le mot "case" dans un switch.

Ce code ne fait pas ce qui est attendu :

switch (action)
  {
  wateringOn:
    digitalWrite (wateringPin, ON);
    break;
    
  wateringOff:
    digitalWrite (wateringPin, OFF);
    break;
  }  // end of switch

Il manque les mots-clé "case" :

switch (action)
  {
  case wateringOn:
    digitalWrite (wateringPin, ON);
    break;
    
  case wateringOff:
    digitalWrite (wateringPin, OFF);
    break;
  }  // end of switch

ERREUR #23 : Retourner un pointeur sur une variable locale.

Ceci est une erreur :

char * getString ()
{
  char s [] = "abc";
  return s;
}

La variable locale s est située dans la pile et elle n’existe que quand la fonction est appelée. Elle n’est donc plus valide au retour de la fonction.

Il vaut mieux écrire :

char * getString ()
{
  static char s [] = "abc";
  return s;
}

A titre de variante, s peut être déclarée en globale.

ERREUR #24 : Faire un "sscanf" ou "sprintf" sur un float.

Ceci ne marche pas :

float foo;
char buf [] = "123.45";
sscanf (buf, "%f", &foo);

sscanf () ne supporte pas les float sur Arduino !

Ceci ne marche pas non plus :

float foo = 42.56;
char buf [20];
sprintf (buf, "%f", foo);

sprintf () ne supporte pas les float sur Arduino !
A la place on pourra utiliser dtostrf () :

 float f = 42.56;
  char buf [20];
  dtostrf (f, 10, 4, buf);  // number, width, decimal places, buffer

ERREUR #25 : Oublier "break" dans un "switch/case".

Exemple :

switch (i) {
  case 0: {j = 0;}
  case 1 : {j = 1;}
}
Serial.print(" j = "); 
Serial.println(j); //  <--j vaut toujours 1

En effet, le break ayant été oublié à la fin du case 0, l’exécution continue au cas suivant et on finit par avoir i=1 quelque que soit la valeur de i au départ. C’est bête !

Il faut écrire :

switch (i) {
  case 0: 
    j = 0;
    break;
  case 1 : 
    j = 1;
    break;
}
Serial.print(" j = ");
Serial.println(j); // là c'est bon

"break" sert aussi à sortir d’une boucle for ou while quand une condition de sortie se réalise.
"continue" permet de terminer le traitement d’une boucle et de sauter au tour suivant. On imagine bien les effets dévastateurs d’un oubli dans ces 2 cas !

ERREUR #26 : Une mauvaise utilisation de "continue".

"continue" permet de terminer le traitement d’une boucle et de revenir au début de la boucle. Dans cet exemple, voyez-vous l’erreur ?

i = 0;
while (i < 10)
{    
  if (i == 5) continue; // retour au début de la boucle
  Serial.print("i = ");
  Serial.println(i);
  i++;
}

L’erreur vient du fait qu’on retourne au début de la boucle sans avoir incrémenté i : cela devient une boucle infinie, le programme semble figé !

On aurait du écrire :

i = 0;
while (i < 10)
{    
  if (i++ == 5) continue; // retour au début de la boucle
  Serial.print("i = ");
  Serial.println(i-1);
}

Dans ce cas i est toujours incrémenté juste après le test mais avant le continue. Et là ça marche. On affiche bien la série de valeurs 0 à 9 sans le 5.

Les trucs et astuces à utiliser de préférence

Après le négatif, voici un peu de positif !

TRUC #1 : Utiliser une table pour une série d’objets similaires .

Par exemple :

int ledPin1 = 3;
int ledPin2 = 4;
int ledPin3 = 5;
int ledPin4 = 6;

Sera avantageusement remplacé par :
const byte ledPin [4] = { 3, 4, 5, 6 };

Comme il s’agit de pins dont les valeurs ne changeront pas on ajoute le mot-clé "const" dans ce cas.

TRUC #2 : Placez vos chaines de caractères en Flash plutôt qu’en RAM .

La déclaration :
Serial.print ("Le programme démarre.");

Se traduit par l’occupation de la RAM par la chaîne "Le programme démarre."

Comme la RAM est rare, on peut faire en sorte de stocker cette chaîne (statique évidemment) en Flash en la préfixant d’un "F" :

Serial.print (F("Program starting."));

TRUC #3 : Mettez vos principaux paramètres en début de programme.

Au lieu de mettre les valeurs en dur dans les instructions :

void setup ()
  {
  Serial.begin (115200);
  pinMode (5, OUTPUT);
  digitalWrite (5, LOW);
  pinMode (6, OUTPUT);
  digitalWrite (6, HIGH);
...

Vous devriez déclarer ces paramètres regroupés en début de programme pour les modifier plus facilement par la suite :

const unsigned long BAUD_RATE = 115200;
const byte LED_PIN = 5;
const byte MOTOR_PIN = 6;

void setup ()
  {
  Serial.begin (BAUD_RATE);
  pinMode (LED_PIN, OUTPUT);
  digitalWrite (LED_PIN, LOW);
  pinMode (MOTOR_PIN, OUTPUT);
  digitalWrite (MOTOR_PIN, HIGH);
...

Le code ne change pas même si les paramètres changent et vous ne risquez plus d’introduire un coquille !

TRUC #4 : N’utilisez jamais Goto .

Bien que ce soit possible, il faut perdre cette vielle habitude !

Il est toujours possible de s’en passer en utilisant intelligemment les for,  while, do, break, continue et return.

TRUC #5 : Ne cherchez pas à optimiser votre code .

En le rendant obscur et illisible pour les autres.

En revanche, vous devez bien choisir les types de vos variables pour optimiser la place qu’elle prennent en RAM.

Pour mémoire, les limites des variables en fonction des types sont :

char:                 -128  to         +127
byte:                    0  to         +255
int:                -32768  to       +32767
unsigned int:            0  to       +65535
long:          -2147483648  to  +2147483647
unsigned long:           0  to  +4294967295

TRUC #6 : Faites confiance au compilateur .

Au lieu de faire le calcul vous même :
const unsigned long secondsInDay = 86400;  // 60 * 60 * 24

Laissez-le au compilateur :
const unsigned long secondesparjour = 60UL * 60 * 24;

De même, au lieu de vous soucier de la limite dans votre code :

char buffer [20];

// plus loin ...

if (i < 20)
  {
  buffer [i] = c;
  }

Laissez le compilateur s’en occuper pour vous :

char buffer [20];

// plus loin ...

if (i < sizeof (buffer))
  {
  buffer [i] = c;
  }

Si vous changez plus tard la taille du buffer, vous n’aurez pas à retoucher tout votre code.

Dans un tableau, plutôt que d’indiquer la taille :
const byte pinNumbers [5] = { 5, 6, 7, 8, 9 };

Faites le faire par le compilateur :
const byte pinNumbers [] = { 5, 6, 7, 8, 9 };

Pour connaitre le nombre d’éléments d’un tableau, avec sizeof, vous pouvez définir une MACRO :
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

Et voilà comment s’en servir :

int myArray [20];

int numberOfItems = ARRAY_SIZE (myArray);   // <--- sera = 20 dans ce cas

Attention : il ne faut pas oublier que sizeof ne fonctionnera pas avec un tableau passé en argument dans une fonction (c’est le pointeur qui est passé, pas le tableau). La réponse serait toujours égale à 2 (taille d’un pointeur sur un processeur 8 bits).
Si vous avez besoin de la taille du tableau, il faut aussi la passer dans les arguments.

TRUC #7 : Utiliser Else .

Au lieu d’enchaîner les if :

if (a == 5)
  {
  // do something
  }
if (a != 5)
  {
  // do something else
  }

Il vaut mieux utiliser else :

if (a == 5)
  {
  // do something
  }
else
  {
  // do something else
  }

TRUC #8 : Utiliser switch/case, plutôt qu’une série de if .

A éviter :

if (command == 'A')
  {
  // do something
  }
else if (command == 'B')
  {
  // do something
  }
else if (command == 'C')
  {
  // do something
  }
else
  {
  // unexpected command
  }

Préférez :

switch (command)
  {
  case 'A':
        // do something
        break;

  case 'B':
        // do something
        break;

  case 'C':
        // do something
        break;

  default:
        // unexpected command
        break;
  }  // end of switch 

Pour ceux qui peuvent lire l’anglais, je recommande de regarder le reste de la page de Nick qui contient bon nombre d’autres conseils plus adaptés aux "confirmés".

De même, à partir de cette page, je vous conseille de suivre les nombreux liens qu’il donne.

Bonne programmation !