Arduino sketch writing

De The Linux Craftsman
Aller à la navigation Aller à la recherche

Introduction

Un sketch est le nom donnée par Arduino aux programmes qui sont téléversés sur un ATMega (cerveau de la carte Arduino).

Comme un programme informatique, le sketch est découpé en plusieurs parties qui ont toutes leur importance.

Les différentes sections d'un sketch

Commentaires

La première partie d'un sketch est très certainement composée de commentaires. Dans cette introduction, on décrit l'objectif du sketch, l'auteur, sa date de création, le montage électronique qu'il implique, etc...

Ci-dessous un exemple de commentaire présent dans le sketch blink livré avec l'IDE Arduino.

/*
  Blink
  Turns on an LED on for one second, then off for one second, repeatedly.

  Most Arduinos have an on-board LED you can control. On the UNO, MEGA and ZERO 
  it is attached to digital pin 13, on MKR1000 on pin 6. LED_BUILTIN takes care 
  of use the correct LED pin whatever is the board used.
  If you want to know what pin the on-board LED is connected to on your Arduino model, check
  the Technical Specs of your board  at https://www.arduino.cc/en/Main/Products
  
  This example code is in the public domain.

  modified 8 May 2014
  by Scott Fitzgerald
  
  modified 2 Sep 2016
  by Arturo Guadalupi
*/

Les imports

Les imports, correspondent à des inclusions de bibliothèques ou librairies et ce font grâce au mot clé #include. En d'autres termes, lorsque vous faites une inclusion, vous ajouter toutes les lignes de code qui composent la bibliothèque en question.

Le problème avec les imports est que l'on à du mal à évaluer la complexité du code qui est appelé. La plus part du temps, on utilise qu'une infime partie des possibilités offertes par certaine bibliothèque très complète. Si c'est le cas et que vous n'avez plus de place pour votre sketch, il est peut-être intéressant d'isoler le code dont on a besoin plutôt que d'utiliser la bibliothèque en entier... la place est limitée sur un ATMega et chaque octet compte !

Ci-dessous un exemple d'inclusion de la bibliothèque Ethernet :

# include "Ethernet.h"

Cette inclusion fait 1482 lignes de code...

Variables globales

Les variables globales servent, à l'inverse de leurs homologues locales, à plusieurs endroit dans le sketch. Il peut aussi être intéressant, surtout pour les gros objets, de les faire instancier pendant la phase de démarrage de la puce plutôt que de le faire lors du premier appel. L’intérêt ici n'est pas une utilisation à plusieurs endroit mais un gain de temps.

Ci-dessous un exemple de variable globale accessible dans tout le sketch :

// Variable statique (portée sketch) et constante (sa valeur ne change pas)
static const char METHOD[] = "GET";

Fonction setup()

La fonction setup() est appelée une fois, au démarrage de la puce. C'est généralement ici qu'il faut positionner correctement les broches (en entrée ou en sortie) ainsi que leurs états (haut ou bas).

Lorsque l'on a des conditions qui doivent être satisfaites obligatoirement pour le bon fonctionnement du sketch, il ne faut pas hésiter à utiliser une LED pour montrer à l'utilisateur que le démarrage s'est effectué correctement, un peu comme avec un buzzer sur une carte mère.

Ci-dessous un exemple qui test le bon fonctionnement d'un module HC12 et qui utilise une LED pour signaler à l'utilisateur l'état du système :

void setup() {
  // Positionnement en sortie des pins
  pinMode(ledPin, OUTPUT);
  pinMode(setPin, OUTPUT);
  // On éteint la LED
  digitalWrite(ledPin, LOW);
  // passage en mode commande
  digitalWrite(setPin, LOW);
  // Démarrage de la communication avec le module
  hc12.begin(9600);
  // On demande au module un acquittement
  hc12.print(F("AT+"));
  // Délais pour que le module traite la commande
  delay(100);
  // On attend la réponse du module
  while(!hc12.available());
  // Le module doit répondre ''OK''
  if(strcmp(hc12.readString(), "OK") != 0){
    // Quelque chose s'est mal passé...
    while(true){
      // ... on fait clignoter la LED pour signaler l’erreur !
      digitalWrite(ledPin, HIGH);
      delay(500);
      digitalWrite(ledPin, LOW);
      delay(500);
    }
  }
  // passage en mode transparent
  digitalWrite(setPin, HIGH);
  // on allume la LED de manière fixe pour signaler la fin du setup()
  digitalWrite(ledPin, HIGH);
}

Fonction loop()

La fonction loop() est le cœur du sketch et peut s'apparenter à un while(true) dans le sens ou le code à l'intérieur de cette fonction va s’exécuter indéfiniment.

Cette fonction permet de comprendre ce que le sketch est censé faire. Il ne faut pas la surcharger avec un code trop complexe. Il est considéré comme une bonne pratique de fragmenter le code en fonctions et de faire appel à ces fonctions dans loop(). C'est là tout l'intérêt des fonction annexes !

Ci-dessous un exemple de la fonction loop() de l'exemple blink :

// the loop function runs over and over again forever
void loop() {
  digitalWrite(ledPin, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(ledPin, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second
}

Fonction annexes

Les fonctions annexes sont la pour accueillir toute la complexité du code et chacune doit avoir un but précis.

Il ne faut pas hésiter à décortiquer un code complexe en plusieurs fonctions :

  • le code est plus simple ;
  • le code gagne en lisibilité ;
  • les bugs sont corrigés plus rapidement ;

Ci-dessous un exemple tronqué de fonctions annexes servant dans l'élaboration d'un serveur Web avec un module WizNet :

void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (readLine(c)) {
          if (readBytes == 0) {
            if (isUrl) {
              executeRequest(client);
            } else {
              // Error happened
              sendHeader(http_error, client);
              client.println();
            }
            // give the web browser time to receive the data
            delay(1);
            // close the connection:
            client.stop();
          } else {
            if (!isUrl && readUrl()) {
              isUrl = true;
            }
          }
        }
      }
    }
  }
}
/**
   Read a line in the HTTP request and put it in a buffer
*/
bool readLine(char c) {
  // code de readLine
}
/**
   Used to parse the URL and arguments
*/
bool readUrl() {
  // code de readUrl
}
/**
   Process the HTTP request
*/
bool executeRequest(EthernetClient client) {
  // Code de executeRequest
}
/**
   Used to send the HTTP header with the given code
*/
void sendHeader(int16_t code, EthernetClient client) {
 // code de sendHeader
}

Les fonctions annexes contiennent quelques 300 lignes qui auraient rendu illisible la fonction loop()...

Sketch complet

Ci-dessous un sketch qui reprend toutes les parties vu précédemment :

/**
 * Sketch d'exemple pour montrer 
 * l'utilité de chacunes des sections
 * d'un sketch
 * 
 * @author jcf
 */
#include "Arduino.h"

// Delais de clignotement en ms
static const uint8_t BLINK_DELAY=500;
// Pin sur laquelle est connectée l'anode de la LED
static const uint8_t LED_PIN = 13;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
}

void loop() {
  blink();
}
/**
 * Fait clignoter une led
 */
void blink(){
  digitalWrite(LED_PIN, HIGH);
  delay(BLINK_DELAY);
  digitalWrite(LED_PIN, LOW);
  delay(BLINK_DELAY);
}

Le langage C/C++

Les mots clés

static

Le mot clé static possède deux fonctions :

  • dans une fonction, il permet de garder la valeur de la variable entre deux invocations ;
  • pour une variable globale ou une fonction cela fixera la portée au niveau fichier (encapsulation).

Ci-dessous est illustré la première fonction :

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

void loop(){
  add();
  delay(100);
}

void add(){
  // instanciation et affectation
  static uint8_t i = 0;
  // incrémentation
  Serial.print(i++);
  if(i == 10){
    // Remise à zéro
    Serial.println(F("."));
    i = 0;
  }else{
    Serial.print(F(", "));
  }
}

Le programme précédent affichera indéfiniment la ligne suivante :

0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
...

Cette technique est intéressante car elle permet de ne pas polluer l'espace global avec des variables utilisées uniquement dans une fonction mais, il faut garder à l'esprit que le code est moins lisible.

const

Le mot clé const permet de préciser que la valeur d'une variable ne changera pas, à l'instar de son homologue, la variable.

Attention cependant, en C const fait partie du type et non de la variable. Ce qui veut dire que le code suivant ne fonctionnera pas :

  void add(uint8_t i);

  const uint8_t i = 0;
  /**
  * Impossible car i est de 
  * type "const uint8_t" et non "uint8_t"
  */
  add(i);

Cette particularité permet au programmeur de spécifier un contrat avec l'appelant. Si la fonction contient le mot clé const, la valeur des variables ou structures passées en paramètre n'est pas modifiée. Cela fait partie d'un sujet plus vaste qui s'appelle la correction programmatique.

volatile

Le mot clé volatile permet de spécifier si la valeur d'une variable est susceptible d'être modifier entre deux accès. Cela permet de préciser à un compilateur, qui voudrait trop optimiser le code en supprimant des lectures ou écritures en mémoire, de ne pas réutiliser la valeur mais de la relire.

La valeur peut être modifiée par un autre thread (processus) ou par une interruption. L'exemple sur le site Arduino, met en exergue ce cas de figure :

const byte ledPin = 13;
// Broche permettant une interruption (bouton, etc...)
const byte interruptPin = 2;
// État de la led à bas
volatile byte state = LOW;

void setup() {
  // On configure l'anode en sortie
  pinMode(ledPin, OUTPUT);
  // On tire la broche du bouton vers le haut (pullup)
  pinMode(interruptPin, INPUT_PULLUP);
  /**
  * On attache une interruption sur la broche du bouton
  * Lorsque l'on appuie sur le bouton, la fonction blink est appelée
  */
  attachInterrupt(digitalPinToInterrupt(interruptPin), blink, CHANGE);
}

void loop() {
  digitalWrite(ledPin, state);
}

void blink() {
  state = !state;
}

On voit bien dans l'exemple suivant que la valeur de la variable state ne change pas à l'intérieur de la fonction loop() et cette modification d'état est prise en compte uniquement grâce à volatile.

Il faut garder à l'esprit que le compilateur transforme le code !

  • sans volatile:
void loop() {
  digitalWrite(13, LOW);
}
  • avec volatile :
void loop() {
  digitalWrite(13, state);
}

Les types numériques

Quand on code en C, il faut utiliser les standard définit dans la norme C99 et qui spécifie plusieurs types numériques. Cette norme à été introduite pour palier les différences qu'il y a entre les compilateurs et les plateformes pour lesquels ils sont écrits (processeur 32bit, microcontrôleur 8bits, etc...) :

  • int8_t pour un nombre sur 8 bits (1 octet)
  • int16_t pour un nombre sur 8 bits (2 octet)
  • int32_t pour un nombre sur 8 bits (4 octet)
  • int64_t pour un nombre sur 8 bits (8 octet)

Le sketch suivant permet de s'en rendre compte :

void setup() {
  Serial.begin(9600);
  Serial.println(F("Types numerique a taille variable : "));
  int a = 0;
  Serial.print(F("int : \t"));
  Serial.println(sizeof(a));
  float b = 0;
  Serial.print(F("float : \t"));
  Serial.println(sizeof(b));
  double c = 0;
  Serial.print(F("double : \t"));
  Serial.println(sizeof(c));
  long d = 0;
  Serial.print(F("long : \t"));
  Serial.println(sizeof(d));
  long long e=0;
  Serial.print(F("long long : \t"));
  Serial.println(sizeof(d));
  Serial.println(F("Types numerique a taille fixe (C99): "));
  int8_t f = 0;
  Serial.print(F("uint8_t : \t"));
  Serial.println(sizeof(f));
  int16_t g = 0;
  Serial.print(F("uint16_t : \t"));
  Serial.println(sizeof(g));
  int32_t h = 0;
  Serial.print(F("uint32_t : \t"));
  Serial.println(sizeof(h));
  int64_t i = 0;
  Serial.print(F("uint64_t : \t"));
  Serial.println(sizeof(i));
}
void loop(){
}

Le sketch précédent donne le résultat suivant :

Types numerique a taille variable : 
int : 		2
float : 	4
double : 	4
long : 		4
long long : 	4
Types numerique a taille fixe (C99): 
uint8_t : 	1
uint16_t : 	2
uint32_t : 	4
uint64_t : 	8

Voici les valeurs aux limites des types fixes C99 :

int8_t uint8_t int16_t uint16_t int32_t uint32_t int64_t uint64_t
min -128 0 -32 678 0 -2 147 483 648 0 -922 337 203 685 477 0
max 127 255 32 677 65 535 2 147 483 648 4 294 967 295 922 337 203 685 477 18446744073709551616

En plus de rendre le programme portable sur plusieurs plateforme, l'utilisation des types fixes permet de minimiser l'espace occupé par les variables en mémoire vive. Cela force le programmeur à réfléchir à la taille des données qu'il va manipuler et, par la même, augmente la robustesse du programme (débordement de tampon, etc...).

Les fonctions spécifiques à Arduino

Numérique

pinMode()

Permet de configurer une broche en entrée ou en sortie.

  • pinMode(pin, mode):
    • pin: la broche à configurer ;
    • mode: la configuration à appliquer sur la broche. Peut prendre les valeurs INPUT en entrée, OUTPUT en sortie ou INPUT_PULLUP pour tirer l'entrée vers le haut.

Ci-dessous un exemple qui met la broche 13 en sortie :

void setup(){
  pinMode(13, OUTPUT);
}
void loop(){
}

digitalWrite() / digitalRead()

Permet de modifier ou de lire l'état d'une broche.

  • digitalWrite(pin, value):
    • pin correspond au numéro de la broche
    • value correspond soit à la valeur HIGH (3.3v ou 5v) soit à la valeur LOW (0v).
  • digitalRead(pin):
    • pin correspond au numéro de la broche

L'exemple ci-dessous fait clignoter la LED présente sur un Arduino UNO:

void setup(){
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);
}
void loop(){
  digitalWrite(13, !digitalRead(13));
  delay(1000);
}

pulseIn()

Permet de mesurer la durée d'une impulsion.

  • pulseIn(pin, value, timeout):
    • pin correspond à la broche sur laquelle lire l'impulsion ;
    • value correspond au type d'impulsion lue (HIGH ou LOW) ;
    • timeout correspond au temps au bout duquel pulseIn() rend la main si l'impulsion n'est pas observée.

Analogique

analogReference()

Permet de configurer le voltage de référence utilisé pour les entrées analogiques

  • analogReference(type): type correspond au type de référence et peut prendre les valeurs suivantes :
    • DEFAULT: référence par défaut (3.3v ou 5v)
    • INTERNAL: référence interne égale à 1.1v sur l'ATmega168 et ATmega328 et égale à 2.56v sur l'ATmega8
    • INTERNAL1V1: référence interne de 1.1V sur le Mega
    • INTERNAL2V56: référence interne de 2.56V sur le Mega
    • EXTERNAL: référence externe présente sur la broche AREF (entre 0v et 5V uniquement).

analogWrite() / analogRead()

Permet de modifier ou de lire l'état d'une broche analogique.

  • analogWrite(pin, value):
    • pin correspond au numéro de la broche ;
    • value correspond à la largeur de l'impulsion envoyée sur la broche (Modulation de Largeur d'Impulsion). La valeur varie entre 0 (toujours éteint) et 255 (toujours allumé) et permet de faire varier l'intensité d'une LED, la vitesse d'un moteur, etc...

Seulement certaine broches permettent d'effectuer cette modulation:

  • ATMega168/328 : 3, 5, 6, 9, 10, et 11;
  • ATMega1280/2560 : de 2 à 13 et de 44 à 46

Ci-dessous un exemple qui fait briller une LED sur la broche PWM numéro 9:

uint8_t ledPin = 9;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // Entier correspondant à la largeur de l'impulsion
  static uint8_t brightness = 0;
  // boolean pour savoir si on incrémente ou décrémente la valeur
  static bool isFading = false;
  if (isFading) {
    brightness--;
    // Si on arrive à 0, on ne décrémente plus
    if (brightness == 0) {
      isFading = false;
    }
  } else {
    brightness++;
    // Si on arrive à 255, on n'incrémente plus
    if (brightness == 255) {
      isFading = true;
    }
  }
  // On modifie la largeur de l'impulsion
  analogWrite(ledPin, brightness);
  // On attend un peu sinon c'est trop rapide
  delay(5);
}

Temps

millis() / micros()

Indique le temps qui s'est écoulé depuis le démarrage du microcontrôleur.

  • millis(): résolution de 50 jours, après la valeur repart à zéro (overflow)
  • micros(): résolution de 70 minutes, après la valeur repart à zéro (overflow)

delay() / delayMicroseconds()

Permet de mettre l’exécution du programme en pause pendant le temps désiré.

  • delay(unsigned long ms): ms le temps en millisecondes pendant lequel le programme s'arrête.
  • delayMicroseconds(unsigned long us) : us le temps en micro secondes pendant lequel le programme s'arrête. Cette fonction est précise pour un maximum de 16383 micro secondes, pour des délais plus grand il faut utiliser la fonction delay().

Attention, delayMicroseconds() arrête les interruptions avant son travail et repositionne le SREG (Status REGister) après.

Interruptions

attachInterrupt() / detachInterrupt()

Permet d'attacher et détacher des interruptions externes. Les interruptions sont des éventements qui arrivent de manière non prédictive et dont on souhaite être prévenu.

Cela peut-être inintéressant pour déclencher du code à l'appuie d'un bouton ou encore pour réveiller l'ATMega.

  • attachInterrupt(interrupt, ISR, mode)
    • interrupt: correspond au numéro d'interruption, en relation avec une broche, généralement déterminé par la fonction digitalPinToInterrupt(pin). Cependant, il est possible d'utiliser directement le numéro d'interruption, par exemple, la broche deux sur un UNO correspond à l'interruption 0.
    • ISR: correspond à la routine (fonction) appelée lorsque l'interruption est levée.
    • mode: correspond au type d'interruption et peut prendre les valeurs suivantes : LOW (état bas), CHANGE (changement d'état), RISING (état bas vers haut) et FALLING (état haut vers bas).
  • detachInterrupt(interrupt):
    • interrupt: correspond au numéro d'interruption, en relation avec une broche, généralement déterminé par la fonction digitalPinToInterrupt(pin). Cependant, il est possible d'utiliser directement le numéro d'interruption, par exemple, la broche deux sur un UNO correspond à l'interruption 0.

Ci-dessous un code qui permet d'allumer une LED quand on appuie sur un bouton:

const uint8_t ledPin = 13;
const uint8_t interruptPin = 2;
 
void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), toggle, FALLING);
}
 
void loop() {}
 
// Change l'état de la LED
void toggle() { 
  uint8_t pinState = !digitalRead(ledPin);
  digitalWrite(ledPin, pinState);  
}

interrupts() / noInterrupts()

Permet de désactiver (noInterrupts) et réactiver (interrupts) les interruptions qui sont actives par défaut. Cela peut être intéressant pour une portion de code critique ou l'on ne souhaite pas être dérangé par un événement externe. Il faut garder à l'esprit que l'ATMega fonctionne correctement grâce aux interruptions, il ne faut pas les désactiver trop longtemps !

void setup() {}

void loop(){
  noInterrupts();
  // code sensible ici
  interrupts();
  // retour à la normale !
}

Communication

Serial

Stream