IDE Arduino - Quelques bases

Un billet qui a la prétention de donner quelques solides idées sur les méthodes de programmation des Arduinos et autres Atmega dans le domaine de la robotique et de la domotique.

Un certain nombre des problèmes que l'on rencontre en programmation de l'Arduino sont récurrents. Je vous propose d'étudier ces sujets et d'en trouver une optimisation à ma sauce.

Pour commencer une règle à respecter comme avec tous les langages : "commentez largement chaque ligne de programme"

LES TYPES

Ce que j'aime dans l'IDE Arduino, dérivé du C++, c'est l'obligation de décrire toutes les constantes et variables utilisées dans notre programme. C'est pénible à faire, mais clarifie le fonctionnement.

Donc, commençons par bien maîtriser les types de variables. Il est intelligent de bien connaître l'utilisation de nos variables et d'utiliser le type le plus adapter. Il en va du bon fonctionnement et de l'optimisation de notre programme. Réfléchissez toujours aux limites des valeurs de vos variables. Il est facile de se faire piéger, une boucle, une multiplication de notre compteur de boucle et paf ! on dépasse la valeur maximum de notre variable, c'est le bug assuré. Constante ou variable ? La différence est évidente : Un constante ne varie pas, une variable si ! Mais utiliser une constante pour définir une valeur est plus lisible. Les types les plus utilisés :

  • bool : un booléen ne peut prendre que 2 valeurs : 1 ou 0, true ou false, HIGH ou LOW
  • byte: un byte ou octet, c'est 8 bits, donc une valeur maximum de 255, c'est économique, mais aie ! aie ! aie ! si ça déborde.
  • int : c'est 2 octets ou 16 bits, mais attention le bit de poids le plus fort sert de "signe" + ou -, donc la valeur max est de +/-32.767

pour disposer d'un entier positif allant jusqu'à 65.535, il faut déclarer un "unsigned int".

Utiliser un même type de variable dans le "cas où", n'est pas une bonne idée. C'est consommateur de mémoire et peut ne pas faciliter la programmation, comme dans le cas des tests que nous verrons plus loin, où le booléen est bien mieux adapté.

Il existe d'autres types de variables, je vous suggère de consulter la littérature ad hoc : Référence Arduino

LES TESTS

Je vois beaucoup de programmes comporter des suites de "if" à suivre ou imbriqués, c'est particulièrement difficile à lire et à déboguer. Bien souvent on peut remplacer ces suites de "if" en étant un petit peu astucieux.

1/ Il faut commencer par bien maîtriser ses tables de vérité. 007-1.png

2/ Il faut comprendre qu'un test :
"if (ma_variable == valeur)" ne fait que renvoyer un "vrai" / "faux" ou un "1" / "0", donc nous avons tout à fait le droit d'écrire : "mon_booleen = (ma_variable == valeur)" où "mon_booleen" prendra la valeur du test : "vrai" ou "faux".

D'autre part, puisqu'un booléen renvoie un "vrai" ou un "faux", il est tout à fait autoriser d'écrire :
"if (mon_booleen) { mes_actions; }" en se passant d'écrire "if (mon_booleen == true)", ce qui est un pléonasme informatique !

Si le test doit renvoyer "vrai" quand "mon_booleen" est "faux", il suffit de faire le test :
"if (!mon_booleen) { mes_actions; }" Remarquez le "!" qui indique l'opposé de "vrai" = "faux".

3/ fort de ces précédentes et précieuses remarques, vous pouvez noter que :
"digitalWrite(broche_moteur, (etatBouton && !etatErreur && (ma_variable < 10))" a du sens, et que nous avons affecté une valeur "vrai" ou "faux" à la sortie qui détermine le fonctionnement d'un moteur par exemple dans ce cas.
Et qu'effectivement je n'ai utilisé aucun "if" !

LES FONCTIONS

Une fonction, c'est simplement du code que l'on met ailleurs et qui sera appelé dans le programme par l'appel de la fonction. Une fonction peut recevoir des paramètres et elle peut aussi renvoyer une valeur, exemple :
"void maFonction() { code...; }"                                          // fonction simple sans paramètre
"void maFonction(byte par1, byte par2) { code...; }"         // avec paramètres "par1" et "par2" au format "byte"
"int maFonction(int par1, int par2) { code...; return val; }"// une fonction qui prend 2 paramètres et qui renvoie "val" comme "int"

L'usage des fonctions est un grand facilitateur de programmation, car il "suffit" de diviser son programme en fonctions et de coder les fonctions indépendamment.
C'est facile à dire, c'est moins facile à faire, car il faut réfléchir précisément aux actions à confier à chaque fonction et surtout penser aux échanges entre les fonctions et le programme principal.
Là, l'expérience parle !
En toute logique, le programme principal peut se réduire à une suite de 4 ou 5 fonctions. La technique d'approche de la programmation par fonctions, consiste à s'occuper des sous-problèmes individuellement sans s'occuper trop du problème global. et quand on travaille sur le programme global on tante d'oublier les détails des sous-programmes.
Une règle reste simple à appliquer : "si on se retrouve avec une répétition de codes... Faire une fonction !"

LE BOUTON POUSSOIR

Autant il est facile de positionner une broche en sortie, autant la lecture d'une entrée n'est pas si simple qu'il n'y paraît. Il faut s'affranchir des problèmes de rebonds et ou de la lecture en boucle d'un état.
Bref ! Le mieux est de détecter, non pas l'état du bouton, mais le changement d'état de celui-ci. Hors pour détecter le changement d'état il est nécessaire d'accompagner notre bouton d'une variable booléenne capable de rappeler à notre programme l'état précédant la lecture.
Créons donc avant de commencer, un booléen :
"bool etatBouton=true;"
Dans le code de lecture, je vous suggère d'utiliser une fonction :

// fonction lecture de mon bouton ( je considère que l'appui sur le poussoir amène un 0V sur la broche)
void lecBouton() {   // c'est une fonction
   // j'ai 2 actions, je passe obligatoirement par un test
   if (!digitalRead(broche_bouton) && etatBouton) {     
      // appui sur le bouton pour la 1ère fois : détection du chg d'état
      mon_bouton = !mon_bouton                       
   // changement de l'état de mon indicateur
   etatBouton = false;                            
   }
   etatBouton = ( digitalRead(broche_bouton)  // le bouton est relâché
}
L'!HYSTÉRÉSIS

Changeons de sujet, tout en restant dans le domaine de l'optimisation de nos programmes.
Je veux parler ici des seuils de déclenchement. Là aussi, c'est un problème récurrent rencontré lors d'un changement d'état ordonné par la mesure d'une variation linéaire.
Soyons plus clair. Nous avons une variable : une température par exemple, et nous voulons déclencher la mise en route d'une appareil : un chauffage par exemple, en fonction de la diminution de cette température...
Voilà bien un cas très classique. Si nous n'y prenons garde, nous déclenchons la marche de notre appareil dès le passage au dessous d'une valeur déterminée, une consigne en un mot. Notre capteur n'étant pas parfait, il peut osciller autour de notre consigne et faire s'allumer et s'éteindre notre équipement en quelques secondes, ce qui n'est pas souhaitable pour la pérennité de notre matériel. Ceci s'appelle un "POMPAGE".
Il va falloir verrouiller le fonctionnement, une fois la décision prise de mettre en route notre chauffage. C'est là qu'intervient "l'HYSTÉRÉSIS". Celui-ci consiste tout simplement à modifier la consigne dès lors que notre seuil de déclenchement est atteint.
Un exemple vaut mieux qu'un long discours :

const byte hysteresis = 5;         // ,5°C
const byte MonChauffage = 7;  // n° de broche du chauffage
byte consigne = 200;                 // exprimé en 10ème de °C
bool etatChauffage = true;        // variable booléenne donnant l'état du chauffage "marche" ou "arrêt"
byte temperature = 0;               // valeur du capteur de température en 10ème de °C

void setup() {
    pinMode(MonChauffage, OUT);
}

void departChauffage(byte temp;) {    // encore une fonction
   bool chauffe = (temp < (consigne + etatChauffage * hysteresis));
// notez que je fais un test sur la valeur de la consigne augmentée OU NON de l'hystérésis
   digitalWrite(MonChauffage, chauffe);
   etatChauffage = chauffe;
}

void loop() {
   departChauffage(temperature());  // temperature() est une fonction qui lit la température d'un capteur
}

L'idée du code ci-dessus est d'ajouter l'hystérésis uniquement si le chauffage fonctionne (1*hysteresis =hysteresis). Donc avant mise en route du chauffage, la consigne vaut "consigne"(200), après démarrage la consigne passe à "consigne +hysteresis"(205). Le chauffage s'arrêtera de lui-même au delà de 20,5°C, et la consigne repassera alors à 200.
Mon chauffage devra bien fonctionner pour passer de 20°C à 20,5°C.

DÉCLENCHEMENT DES ÉVÉNEMENTS

Il s'agit maintenant et pour finir, de résoudre un autre probléme récurrent, à savoir : "Comment déclencher les actions dans un rythme cohérent ?"
Éffectivement, tous les programmes que nous écrivons, dès qu'ils dépassent quelques fonctions simples, nécessites de rythmer les actions intelligemment.
Par exemple, un afficheur nécessite d'être rafraîchit le plus rapidement possible, on va dire en temps réel (presque !), alors qu'une température doit être questionnée toutes les secondes et qu'un bouton lui, est interrogé toutes les 100ms pour être confortable, sans passer notre temps à ça.

J'apporte là une solution toute personnelle, certainement pas unique et pas forcément la meilleure.
Ma méthode consiste à faire des boucles imbriquées dans lesquelles j'appelle les fonctions principales. Ça veut dire, que les fonctions sont là encore importantes pour obtenir un code lisible... Démonstration :

// ################################################# //
void loop() {
  // ------------- boucle ~10" --------------------- //
    departChauffage(byte temp;)

  for (byte s=0; s<10; s++) {
    // -------------- boucle ~1" --------------------- //
    temp=lecTemp(); // acquisition des températures
   
    for (byte m=0; m<5; m++) {        
       // --------------- boucle ~200ms ---------------- //
      lecBouton();                                        // lecture de bouton

      for (byte n=0; n<25; n++) {
          // ------------- boucle temps réel -------------- //
          affichage();                                     // 8ms de temps d'activité

      } // fin de boucle temps réel
    } // fin de boucle 400ms
  } // fin de boucle 1"
} // fin de boucle 10" (loop)
// ################ THIS IS THE END ! ################## //

Le code ci-dessus montre un exemple de tâches effectuées à différents rythmes. Cela suppose de connaître à peu près le délai de fonctionnement de la tâche la plus fréquemment appelée, ici l'affichage qui dure ~8ms, puis la boucle compte 50 occurences soit 25*8ms=200ms, ce qui va bien pour une lecture de bouton poussoir, puis une boucle de 5*200ms=1", etc...

CONCLUSION

J'espère que ces quelques conseils vous faciliteront la vie dans le codage de vos programmes.