Différences entre versions de « Arduino sketch writing »
(42 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 5 : | Ligne 5 : | ||
=Les différentes sections d'un sketch= | =Les différentes sections d'un sketch= | ||
== Commentaires == | == Commentaires == | ||
+ | {| | ||
+ | |valign="top"| | ||
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... | 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- | + | Ci-contre un exemple de commentaire présent dans le sketch ''blink'' livré avec l'IDE Arduino. |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
/* | /* | ||
Ligne 28 : | Ligne 31 : | ||
*/ | */ | ||
</source> | </source> | ||
+ | |} | ||
+ | |||
== Les imports == | == Les imports == | ||
+ | {| | ||
+ | |valign="top"| | ||
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. | 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 ! | 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- | + | Ci-contre un exemple d'inclusion de la bibliothèque ''Ethernet''. |
+ | |||
+ | |valign="top"| | ||
+ | Cette inclusion fait 1482 lignes de code... | ||
<source lang="c"> | <source lang="c"> | ||
# include "Ethernet.h" | # include "Ethernet.h" | ||
</source> | </source> | ||
− | + | |} | |
− | |||
== Variables globales == | == Variables globales == | ||
+ | {| | ||
+ | |valign="top"| | ||
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. | 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- | + | Ci-contre un exemple de variable globale accessible dans tout le sketch : |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
// Variable statique (portée sketch) et constante (sa valeur ne change pas) | // Variable statique (portée sketch) et constante (sa valeur ne change pas) | ||
static const char METHOD[] = "GET"; | static const char METHOD[] = "GET"; | ||
</source> | </source> | ||
+ | |} | ||
== Fonction setup() == | == Fonction setup() == | ||
+ | {| | ||
+ | |valign="top"| | ||
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). | 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. | 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- | + | Ci-contre 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 : |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void setup() { | void setup() { | ||
Ligne 88 : | Ligne 104 : | ||
} | } | ||
</source> | </source> | ||
− | + | |} | |
== Fonction loop() == | == Fonction loop() == | ||
+ | {| | ||
+ | |valign="top"| | ||
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. | 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. | ||
Ligne 95 : | Ligne 113 : | ||
C'est là tout l'intérêt des fonction annexes ! | C'est là tout l'intérêt des fonction annexes ! | ||
− | Ci- | + | Ci-contre un exemple de la fonction ''loop()'' de l'exemple ''blink'' : |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
// the loop function runs over and over again forever | // the loop function runs over and over again forever | ||
Ligne 105 : | Ligne 124 : | ||
} | } | ||
</source> | </source> | ||
− | + | |} | |
== Fonction annexes == | == Fonction annexes == | ||
+ | {| | ||
+ | |valign="top"| | ||
Les fonctions annexes sont la pour accueillir toute la complexité du code et chacune doit avoir un but précis. | Les fonctions annexes sont la pour accueillir toute la complexité du code et chacune doit avoir un but précis. | ||
Ligne 114 : | Ligne 135 : | ||
* les bugs sont corrigés plus rapidement ; | * les bugs sont corrigés plus rapidement ; | ||
− | Ci- | + | Ci-contre un exemple tronqué de fonctions annexes servant dans l'élaboration d'un serveur Web avec un module WizNet. |
+ | |||
+ | Les fonctions annexes contiennent quelques 300 lignes qui auraient rendu illisible la fonction ''loop()''... | ||
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void loop() { | void loop() { | ||
Ligne 171 : | Ligne 195 : | ||
} | } | ||
</source> | </source> | ||
− | + | |} | |
== Sketch complet == | == Sketch complet == | ||
− | Ci-dessous un sketch qui reprend toutes les | + | Ci-dessous un sketch qui reprend toutes les section précédentes : |
<source lang="c"> | <source lang="c"> | ||
/** | /** | ||
* Sketch d'exemple pour montrer | * Sketch d'exemple pour montrer | ||
− | * l'utilité de | + | * l'utilité de chacune des sections |
* d'un sketch | * d'un sketch | ||
* | * | ||
Ligne 207 : | Ligne 231 : | ||
} | } | ||
</source> | </source> | ||
+ | |||
= Le langage C/C++ = | = Le langage C/C++ = | ||
==Les mots clés== | ==Les mots clés== | ||
===static=== | ===static=== | ||
− | Le mot clé [https://en.wikipedia.org/wiki/Static_(keyword) ''static''] possède deux | + | {| |
− | * dans une fonction, il permet de garder la valeur de la variable entre deux invocations ; | + | |valign="top" width="40%"| |
− | * pour une variable globale ou une fonction cela fixera la portée au niveau fichier (encapsulation). | + | Le mot clé [https://en.wikipedia.org/wiki/Static_(keyword) ''static''] possède deux utilités : |
+ | * dans une fonction, il permet de garder la valeur de la variable entre deux invocations. 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 ; | ||
+ | * pour une variable globale ou une fonction cela fixera la portée au niveau fichier (encapsulation). | ||
− | Ci-dessous est illustré | + | |valign="top" width="20%"| |
+ | Ci-dessous est illustré le premier cas de figure : | ||
<source lang="c"> | <source lang="c"> | ||
void setup(){ | void setup(){ | ||
Ligne 239 : | Ligne 267 : | ||
} | } | ||
</source> | </source> | ||
+ | |valign="top" width="10%"| | ||
Le programme précédent affichera indéfiniment la ligne suivante : | Le programme précédent affichera indéfiniment la ligne suivante : | ||
<pre> | <pre> | ||
Ligne 245 : | Ligne 274 : | ||
... | ... | ||
</pre> | </pre> | ||
− | + | |} | |
===const=== | ===const=== | ||
+ | {| | ||
+ | |valign="top" width="55%"| | ||
Le mot clé [https://en.wikipedia.org/wiki/Const_(computer_programming) ''const''] permet de préciser que la valeur d'une variable ne changera pas, à l'instar de son homologue, la variable. | Le mot clé [https://en.wikipedia.org/wiki/Const_(computer_programming) ''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 | + | Attention cependant, en ''C'' ''const'' fait partie du type et non de la variable. Ce qui veut dire que le code ci-contre ne fonctionnera pas. |
+ | |||
+ | 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 [https://en.wikipedia.org/wiki/Correctness_(computer_science) ''correction programmatique'']. | ||
+ | |valign="top" width="15%"| | ||
<source lang="c"> | <source lang="c"> | ||
void add(uint8_t i); | void add(uint8_t i); | ||
Ligne 261 : | Ligne 297 : | ||
add(i); | add(i); | ||
</source> | </source> | ||
− | + | |} | |
===volatile=== | ===volatile=== | ||
+ | {| | ||
+ | |valign="top"| | ||
Le mot clé [https://en.wikipedia.org/wiki/Volatile_(computer_programming) ''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. | Le mot clé [https://en.wikipedia.org/wiki/Volatile_(computer_programming) ''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. | ||
Ligne 294 : | Ligne 332 : | ||
} | } | ||
</source> | </source> | ||
− | On voit bien dans l'exemple | + | |valign="top"| |
+ | On voit bien dans l'exemple précédent 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 ! | Il faut garder à l'esprit que le compilateur transforme le code ! | ||
Ligne 309 : | Ligne 348 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
==Les types numériques== | ==Les types numériques== | ||
+ | {| | ||
+ | |valign="top" width="40%"| | ||
Quand on code en C, il est bon d'utiliser la norme [https://en.wikipedia.org/wiki/C99 C99] qui spécifie plusieurs types numériques de taille fixe. 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...) : | Quand on code en C, il est bon d'utiliser la norme [https://en.wikipedia.org/wiki/C99 C99] qui spécifie plusieurs types numériques de taille fixe. 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) ; | *''int8_t'' pour un nombre sur 8 bits (1 octet) ; | ||
− | *''int16_t'' pour un nombre sur 16 bits (2 | + | *''int16_t'' pour un nombre sur 16 bits (2 octets) ; |
− | *''int32_t'' pour un nombre sur 32 bits (4 | + | *''int32_t'' pour un nombre sur 32 bits (4 octets) ; |
− | *''int64_t'' pour un nombre sur 64 bits (8 | + | *''int64_t'' pour un nombre sur 64 bits (8 octets). |
Les types de tailles variables restent disponible : | Les types de tailles variables restent disponible : | ||
Ligne 328 : | Ligne 370 : | ||
*short = int ; | *short = int ; | ||
*float = double; | *float = double; | ||
− | + | |valign="top"| | |
− | Le sketch | + | Le sketch ci-dessous permet de s'en rendre compte. |
<source lang="c"> | <source lang="c"> | ||
void setup() { | void setup() { | ||
Serial.begin(9600); | Serial.begin(9600); | ||
− | Serial.println(F("Types | + | Serial.println(F("Types numeriques à taille variable : ")); |
byte b = 0; | byte b = 0; | ||
Serial.print(F("byte\t : ")); | Serial.print(F("byte\t : ")); | ||
Ligne 349 : | Ligne 391 : | ||
Serial.print(F("long\t : ")); | Serial.print(F("long\t : ")); | ||
Serial.println(sizeof(l)); | Serial.println(sizeof(l)); | ||
− | Serial.println(F("Types | + | Serial.println(F("Types numeriques à taille fixe (C99): ")); |
int8_t i8 = 0; | int8_t i8 = 0; | ||
Serial.print(F("uint8_t : ")); | Serial.print(F("uint8_t : ")); | ||
Ligne 366 : | Ligne 408 : | ||
} | } | ||
</source> | </source> | ||
− | + | |valign="top"| | |
+ | Il donne le résultat suivant : | ||
<pre> | <pre> | ||
− | Types | + | Types numeriques a taille variable : |
int : 2 | int : 2 | ||
float : 4 | float : 4 | ||
Ligne 374 : | Ligne 417 : | ||
long : 4 | long : 4 | ||
long long : 4 | long long : 4 | ||
− | Types | + | Types numeriques a taille fixe (C99): |
uint8_t : 1 | uint8_t : 1 | ||
uint16_t : 2 | uint16_t : 2 | ||
Ligne 380 : | Ligne 423 : | ||
uint64_t : 8 | uint64_t : 8 | ||
</pre> | </pre> | ||
+ | |} | ||
Voici les valeurs aux limites des types fixes C99 : | Voici les valeurs aux limites des types fixes C99 : | ||
− | {|border=1 | + | {|border=1 |
|- | |- | ||
| | | | ||
Ligne 414 : | Ligne 458 : | ||
|} | |} | ||
Voici les valeurs aux limites des types variables : | Voici les valeurs aux limites des types variables : | ||
− | {|border=1 | + | {|border=1 |
|- | |- | ||
| | | | ||
Ligne 442 : | Ligne 486 : | ||
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...). | 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...). | ||
+ | |||
+ | ==Compilation conditionnelle== | ||
+ | La compilation conditionnelle permet de choisir les lignes de codes qui vont faire partie du programme final en fonction d'assertions. | ||
+ | Celle est particulièrement utile pour modifier le code, par exemple, en fonction du microcontrôleur utilisée (328p-pu, mega2560, etc...). | ||
+ | |||
+ | <source lang="c"> | ||
+ | #if defined(__AVR_ATmega328P__) | ||
+ | // Code du UNO | ||
+ | #elif defined(__AVR_ATmega1280__) | ||
+ | // Code du Mega 1280 | ||
+ | #elif defined(__AVR_ATmega2560__) | ||
+ | // Code du Mega 2560 | ||
+ | #endif | ||
+ | </source> | ||
=Les fonctions spécifiques à Arduino= | =Les fonctions spécifiques à Arduino= | ||
==Numérique== | ==Numérique== | ||
===pinMode()=== | ===pinMode()=== | ||
+ | {| | ||
+ | |valign="top"| | ||
Permet de configurer une broche en entrée ou en sortie. | Permet de configurer une broche en entrée ou en sortie. | ||
*[https://www.arduino.cc/en/Reference/PinMode pinMode(pin, mode)]: | *[https://www.arduino.cc/en/Reference/PinMode pinMode(pin, mode)]: | ||
Ligne 451 : | Ligne 511 : | ||
**''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. | **''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- | + | Ci-contre un exemple qui met la broche 13 en sortie : |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void setup(){ | void setup(){ | ||
Ligne 459 : | Ligne 520 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
===digitalWrite() / digitalRead()=== | ===digitalWrite() / digitalRead()=== | ||
+ | {| | ||
+ | |valign="top"| | ||
Permet de modifier ou de lire l'état d'une broche. | Permet de modifier ou de lire l'état d'une broche. | ||
*[https://www.arduino.cc/en/Reference/DigitalWrite digitalWrite(pin, value)]: | *[https://www.arduino.cc/en/Reference/DigitalWrite digitalWrite(pin, value)]: | ||
Ligne 468 : | Ligne 532 : | ||
**''pin'' correspond au numéro de la broche | **''pin'' correspond au numéro de la broche | ||
− | L'exemple ci- | + | L'exemple ci-contre fait clignoter la LED présente sur un ''Arduino UNO'': |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void setup(){ | void setup(){ | ||
Ligne 479 : | Ligne 544 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
===pulseIn()=== | ===pulseIn()=== | ||
Ligne 487 : | Ligne 553 : | ||
**''timeout'' correspond au temps au bout duquel ''pulseIn()'' rend la main si l'impulsion n'est pas observée. | **''timeout'' correspond au temps au bout duquel ''pulseIn()'' rend la main si l'impulsion n'est pas observée. | ||
==Analogique== | ==Analogique== | ||
+ | {| | ||
+ | |valign="top"| | ||
===analogReference()=== | ===analogReference()=== | ||
Permet de configurer le voltage de référence utilisé pour les entrées analogiques | Permet de configurer le voltage de référence utilisé pour les entrées analogiques | ||
Ligne 507 : | Ligne 575 : | ||
**pin correspond au numéro de la broche ; | **pin correspond au numéro de la broche ; | ||
− | Ci- | + | Ci-contre, un exemple qui fait briller une LED sur la broche [https://www.arduino.cc/en/Tutorial/PWM PWM] numéro 9: |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
uint8_t ledPin = 9; | uint8_t ledPin = 9; | ||
Ligne 539 : | Ligne 608 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
==Temps== | ==Temps== | ||
Ligne 554 : | Ligne 624 : | ||
==Interruptions== | ==Interruptions== | ||
===attachInterrupt() / detachInterrupt()=== | ===attachInterrupt() / detachInterrupt()=== | ||
+ | {| | ||
+ | |valign="top" width="60%"| | ||
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. | 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 | + | Cela peut-être intéressant pour déclencher du code à l'appuie d'un bouton ou encore pour ''réveiller'' l'ATMega. |
*[https://www.arduino.cc/en/Reference/AttachInterrupt attachInterrupt(interrupt, ISR, mode)] | *[https://www.arduino.cc/en/Reference/AttachInterrupt 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. | **''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. | ||
Ligne 564 : | Ligne 636 : | ||
**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. | **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- | + | Ci-contre, un code qui permet d'allumer une LED quand on appuie sur un bouton: |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
const uint8_t ledPin = 13; | const uint8_t ledPin = 13; | ||
Ligne 583 : | Ligne 656 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
===interrupts() / noInterrupts()=== | ===interrupts() / noInterrupts()=== | ||
+ | {| | ||
+ | |valign="top" width="50%"| | ||
Permet de désactiver ([https://www.arduino.cc/en/Reference/NoInterrupts noInterrupts]) et réactiver ([https://www.arduino.cc/en/Reference/Interrupts 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 ! | Permet de désactiver ([https://www.arduino.cc/en/Reference/NoInterrupts noInterrupts]) et réactiver ([https://www.arduino.cc/en/Reference/Interrupts 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 ! | ||
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void setup() {} | void setup() {} | ||
Ligne 596 : | Ligne 673 : | ||
} | } | ||
</source> | </source> | ||
+ | |} | ||
+ | |||
+ | == Cas de l'ESP8266 == | ||
+ | {| | ||
+ | |valign="top" width="50%"| | ||
+ | Pour la carte ESP8266 il faut préciser, pour les fonctions qui servent de vecteurs d'interruption, qu'elles doivent être stockées dans la mémoire RAM et non la flash grâce au spécificateur ''ICACHE_RAM_ATTR'' | ||
+ | |valign="top"| | ||
+ | <source lang="c"> | ||
+ | void ICACHE_RAM_ATTR toggle() { | ||
+ | ... | ||
+ | } | ||
+ | </source> | ||
+ | |} | ||
==Communication== | ==Communication== | ||
===Serial=== | ===Serial=== | ||
+ | {| | ||
+ | |valign="top"| | ||
Les communications sur les broches ''Rx'' et ''Tx'' utilise les niveaux logiques [https://fr.wikipedia.org/wiki/Transistor-Transistor_logic TTL] (3.3v ou 5v). Touts les ''Arduino'' possèdent au moins un contrôleur [https://fr.wikipedia.org/wiki/UART UART] qui permet de communiquer avec un périphérique tier (PC, autre Arduino, module radio, etc...). | Les communications sur les broches ''Rx'' et ''Tx'' utilise les niveaux logiques [https://fr.wikipedia.org/wiki/Transistor-Transistor_logic TTL] (3.3v ou 5v). Touts les ''Arduino'' possèdent au moins un contrôleur [https://fr.wikipedia.org/wiki/UART UART] qui permet de communiquer avec un périphérique tier (PC, autre Arduino, module radio, etc...). | ||
* [https://www.arduino.cc/en/Reference/Serial Serial(speed)] : ''speed'' correspond à la vitesse du port série et peut prendre différentes valeurs comme ''9600'' ou ''115200''. | * [https://www.arduino.cc/en/Reference/Serial Serial(speed)] : ''speed'' correspond à la vitesse du port série et peut prendre différentes valeurs comme ''9600'' ou ''115200''. | ||
− | Ci- | + | Ci-contre un exemple qui utilise le port série : |
+ | |valign="top"| | ||
<source lang="c"> | <source lang="c"> | ||
void setup(){ | void setup(){ | ||
Ligne 611 : | Ligne 704 : | ||
} | } | ||
</source> | </source> | ||
− | = Gestion de la mémoire = | + | |} |
− | ==Les | + | |
− | == | + | =Gestion mémoire sur Arduino= |
+ | ==Les différents types de mémoires== | ||
+ | {| | ||
+ | |valign="top"| | ||
+ | ===La flash=== | ||
+ | La mémoire flash est utilisée pour stocker le programme qui va s'exécuter sur l'Arduino. Cette mémoire permet l'exécution du code mais pour pouvoir le modifier il faut d'abord le copier en SRAM. La mémoire flash est non volatile et permet la conservation du programme même en l'absence de source d’énergie mais possède un nombre de cycle d'écriture limité à 100.000. | ||
+ | |||
+ | ===La SRAM=== | ||
+ | La ''Static Random Access Memory'' ou SRAM, peut être lue et modifiée par le programme en cours d'exécution dans plusieurs buts : | ||
+ | *Les données statiques – Il existe un bloc réservé dans la SRAM pour toutes les données globales et statiques. Pour les variables avec une valeur initiale, le système copie les données de la flash vers la SRAM au démarrage du programme. | ||
+ | *''Heap'' – La ''heap'' sert pour les données dynamiquement allouée et commence à partir des données statique et s’étend au fur et à mesure que des données dynamiques sont allouée. | ||
+ | *''Stack'' – La ''stack'' sert pour les variables locales et pour mémoriser les interruptions et les appels de fonction. Elle grossie du haut de la mémoire vers la ''stack''. Chaque interruption, appel de fonction, allocation de variable locales fait grossir la ''stack'' vers la ''heap''. A la fin de l'interruption ou de l'exécution d'une fonction, l'espace alloué redevient libre. | ||
+ | |||
+ | La plupart des problèmes mémoire surviennent quand la ''heap'' recontre la ''stack''. Lorsque cela survient, les deux mémoires deviennent corrompues ce qui, dans la majorité des cas, se conclu par un crash. Si la collision n'est pas détectée et que l'ATMega ne redémarre pas, le comportement du programme devient erratique. | ||
+ | |||
+ | ===l'EEPROM=== | ||
+ | L'EEPROM est une autre mémoire non-volatile avec un nombre de cycle d'écriture limité à 100.000. Cette mémoire se lit bit par bit, ce qui peut rendre son utilisation un peu fastidieuse. Elle sert principalement à enregistrer des données de configuration ensuite utilisées par le programme pour modifier son comportement. | ||
+ | | | ||
+ | [[Fichier:Arduino sram diagram.jpg|centré|500px]] | ||
+ | |} | ||
+ | |||
+ | ==Architectures mémoire== | ||
+ | ===Deux concepts=== | ||
+ | Au début de l'ère de l'électronique ont émergé deux types d'architectures mémoire : Harvard et Princeton. | ||
+ | {|align="center" cellspacing="0" style="width:300px border-collapse:collapse" | ||
+ | |- | ||
+ | | | ||
+ | |align="center" style="border:1px solid black"|Harvard | ||
+ | |align="center" style="border:1px solid black"|Princeton (Von Neumann) | ||
+ | |- | ||
+ | |align="center" style="border:1px solid black"|Principe | ||
+ | |style="border:1px solid black"|Utilise deux types d'espaces mémoire : un pour le programme et un pour les données dynamiques. | ||
+ | |style="border:1px solid black"|Utilise un seul espace mémoire pour les programmes et les données dynamiques. | ||
+ | |- | ||
+ | |align="center" style="border:1px solid black"|Avantage | ||
+ | |align="center" style="border:1px solid black"|Rapidité | ||
+ | |align="center" style="border:1px solid black"|Fléxibilité | ||
+ | |} | ||
+ | |||
+ | ===Les microcontrôleurs=== | ||
+ | Les microcontrôleurs comme l'ATMega sont étudiés pour les applications embarquées et, à l'inverse des ordinateurs de bureau, ont une tâche bien définie qu'ils doivent effectuer de manière sûre et efficace. Le design des microcontrôleur est plutôt spartiate, bien éloigné des mise en cache multicouche ou encore des disques virtuels en mémoire, il se concentre sur ce qui est essentiel à son fonctionnement. | ||
+ | |||
+ | Le modèle Harvard convient tout à fait pour une utilisation dans le domaine de l'embarqué et d’ailleurs, sur un ATMega, on a bien le programme qui est stocké en mémoire flash et les données qui se trouvent en ''SRAM''. | ||
+ | |||
+ | La plupart du temps, le compilateur et le système gèrent ces choses automatiquement mais, quand l'espace commence à devenir exiguë, il est bon de savoir comment les choses se passent ''sous le capot''. | ||
+ | ===Différence avec un ordinateur classique=== | ||
+ | Les ordinateurs classique, MAC et PC, sont de conception hybride et profitent du meilleur des deux architecture. Au cœur du processeur ont est sur une concpetion Harvard qui compartimente les caches pour les instructions et les données pour maximiser les performance. Cependant ces instructions et données sont chargées d'un espace mémoire commum. Du point de vue du programmeur, cela apparaît comme une architecture ''Von Neumann'' avec plusieurs giga d'espace de stockage mémoire. | ||
+ | |||
+ | La différence la plus flagrante qui existe entre les ordinateurs et les microcontrôleurs est la quantité de mémoire disponible. L'Arduino Uno ne dispose que de 32Ko de mémoire flash et de 2Ko de SRAM ce qui est environ 100.000 fois plus petit qu'un PC bas de gamme ! | ||
+ | |||
+ | En travaillant dans un environnement minimaliste, il faut utiliser intelligemment les ressources disponible ! | ||
+ | |||
+ | ==Mesurer la consommation de SRAM== | ||
+ | {| | ||
+ | |- | ||
+ | |valign="top"| | ||
+ | Lorsqu'on écrit un programme destiné à tourner sur un microcontrôleur comme l'ATMega, l'utilisation de la mémoire est très importante et surtout la détection de fuites ! | ||
+ | Le programme peut très bien s'exécuter pendant 1, 2 voire 3 jours puis, d'un coup, plus rien... | ||
+ | Très souvent, les fuites viennent de l'utilisation de fonction d'allocation mémoire comme ''malloc'' ou ''new'' et qui doivent être suivie de ''free'' ou ''delete''. | ||
+ | Le temps de correction d'un bug de ce type est très rapide, encore faut-il s'en rendre compte ! | ||
+ | |||
+ | La fonction ci-contre permet de mesurer la quantité de mémoire disponible, c'est à dire, l'espace qui sépare la ''heap'' de la ''stack''. | ||
+ | |valign="top"| | ||
+ | <source lang="c"> | ||
+ | int freeRam () { | ||
+ | extern int __heap_start, *__brkval; | ||
+ | int v; | ||
+ | return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); | ||
+ | } | ||
+ | </source> | ||
+ | |- | ||
+ | |valign="top"| | ||
+ | {|width="90%" | ||
+ | | | ||
+ | Pour suivre l'évolution de la consommation mémoire il faut appeler, de manière régulière, la fonction précédente à des endroits choisi dans le code (là où on pense que la fuite se situe). | ||
+ | |} | ||
+ | Prenons l'exemple suivant : | ||
+ | <source lang="c"> | ||
+ | // Pointeur accessible de manière globale | ||
+ | char* str; | ||
+ | void setup() { | ||
+ | Serial.begin(9600); | ||
+ | printFreeRam(); | ||
+ | } | ||
+ | void loop() { | ||
+ | // Affectation d'un tableau de taille 50 | ||
+ | str = (char*) malloc(50); | ||
+ | printFreeRam(); | ||
+ | delay(200); | ||
+ | } | ||
+ | |||
+ | void printFreeRam () { | ||
+ | extern int __heap_start, *__brkval; | ||
+ | int v; | ||
+ | v = (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); | ||
+ | Serial.print(F("Free ram : ")); | ||
+ | Serial.println(v); | ||
+ | } | ||
+ | </source> | ||
+ | |valign="top"| | ||
+ | |||
+ | On voit clairement la quantité de mémoire diminuer... | ||
+ | <pre> | ||
+ | Free ram : 1836 | ||
+ | Free ram : 1784 | ||
+ | Free ram : 1732 | ||
+ | Free ram : 1680 | ||
+ | Free ram : 1628 | ||
+ | </pre> | ||
+ | |||
+ | Si on ajoute la ligne suivante après le ''malloc'' : | ||
+ | <source lang="c"> | ||
+ | free(str); | ||
+ | </source> | ||
+ | Plus aucun problème. | ||
+ | |||
+ | Il ne faut pas oublier de libérer l'espace réservè en mémoire ou, encore mieux, de reposer sur les des-allocation naturelle du système (en n'utilisant pas ''malloc'' ou ''new''). | ||
+ | |||
+ | De plus, il ne faut pas oublier qu'utiliser trop l'allocation dynamique conduit à la fragmentation de la ''heap''... | ||
+ | |} | ||
+ | |||
+ | ==Limiter l'utilisation de la mémoire== | ||
+ | ===Mémoire Flash=== | ||
+ | {| | ||
+ | |- | ||
+ | |valign="top"| | ||
+ | Lorsque le programme est conséquent, par exemple à cause de l'import de libraires, la mémoire flash commence à manquer cruellement. Il n'y a pas vraiment de miracle mais, suivre les préceptes suivants peut aider : | ||
+ | *Utiliser des fonctions : lorsque c'est possible, il faut essayer de factoriser le code qui se répète dans des fonctions. En plus d'augmenter la lisibilité, cela permet de faciliter la correction de bug. | ||
+ | *Supprimer le code mort : le plus compliqué, dans des programmes de plusieurs millier de lignes, c'est de s’apercevoir du code qui ne sert plus. | ||
+ | *Supprimer le ''bootloader'' : quand on à atteint les limites et que plus aucune optimisation n'est possible, il faut penser à supprimer le ''bootloader''. Cela va permettre à votre programme de s'exécuter plus rapidement mais il n'est plus possible d'utiliser l'Arduino tel quel, il faut utiliser un Arduino ISP (In-System Programer) comme celui ci-contre. | ||
+ | | | ||
+ | [[Fichier:Arduino isp.jpg|centré|250px]] | ||
+ | |} | ||
+ | |||
+ | ===Mémoire flash et PROGMEM=== |
Version actuelle datée du 10 août 2024 à 11:00
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-contre 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-contre un exemple d'inclusion de la bibliothèque Ethernet. |
Cette inclusion fait 1482 lignes de code... # include "Ethernet.h"
|
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-contre 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-contre 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-contre 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 :
Ci-contre un exemple tronqué de fonctions annexes servant dans l'élaboration d'un serveur Web avec un module WizNet. Les fonctions annexes contiennent quelques 300 lignes qui auraient rendu illisible la fonction loop()... |
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
}
|
Sketch complet
Ci-dessous un sketch qui reprend toutes les section précédentes :
/**
* Sketch d'exemple pour montrer
* l'utilité de chacune 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 utilités :
|
Ci-dessous est illustré le premier cas de figure : 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. ... |
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 ci-contre ne fonctionnera pas. 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. |
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);
|
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 précédent 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 !
void loop() {
digitalWrite(13, LOW);
}
void loop() {
digitalWrite(13, state);
}
|
Les types numériques
Quand on code en C, il est bon d'utiliser la norme C99 qui spécifie plusieurs types numériques de taille fixe. 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...) :
Les types de tailles variables restent disponible :
On en arrive à la conclusion suivante, sur Arduino :
|
Le sketch ci-dessous permet de s'en rendre compte. void setup() {
Serial.begin(9600);
Serial.println(F("Types numeriques à taille variable : "));
byte b = 0;
Serial.print(F("byte\t : "));
Serial.println(sizeof(b));
int i = 0;
Serial.print(F("int\t : "));
Serial.println(sizeof(i));
float f = 0;
Serial.print(F("float\t : "));
Serial.println(sizeof(f));
double d = 0;
Serial.print(F("double\t : "));
Serial.println(sizeof(d));
long l = 0;
Serial.print(F("long\t : "));
Serial.println(sizeof(l));
Serial.println(F("Types numeriques à taille fixe (C99): "));
int8_t i8 = 0;
Serial.print(F("uint8_t : "));
Serial.println(sizeof(i8));
int16_t i16 = 0;
Serial.print(F("uint16_t : "));
Serial.println(sizeof(i16));
int32_t i32 = 0;
Serial.print(F("uint32_t : "));
Serial.println(sizeof(i32));
int64_t i64 = 0;
Serial.print(F("uint64_t : "));
Serial.println(sizeof(i64));
}
void loop(){
}
|
Il donne le résultat suivant : Types numeriques a taille variable : int : 2 float : 4 double : 4 long : 4 long long : 4 Types numeriques 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 |
Voici les valeurs aux limites des types variables :
byte | short/int | unsigned short/int | float/double | long | unsigned long | |
min | 0 | -32,768 | 0 | -3.4028235E+38 | -2 147 483 647 | 0 |
max | 255 | 32 677 | 65 535 | 3.4028235E+38 | 2 147 483 648 | 4 294 967 295 |
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...).
Compilation conditionnelle
La compilation conditionnelle permet de choisir les lignes de codes qui vont faire partie du programme final en fonction d'assertions. Celle est particulièrement utile pour modifier le code, par exemple, en fonction du microcontrôleur utilisée (328p-pu, mega2560, etc...).
#if defined(__AVR_ATmega328P__)
// Code du UNO
#elif defined(__AVR_ATmega1280__)
// Code du Mega 1280
#elif defined(__AVR_ATmega2560__)
// Code du Mega 2560
#endif
Les fonctions spécifiques à Arduino
Numérique
pinMode()
Permet de configurer une broche en entrée ou en sortie.
Ci-contre 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.
L'exemple ci-contre 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
analogWrite() / analogRead()Permet de modifier ou de lire l'état d'une broche analogique.
Seulement certaine broches permettent d'effectuer cette modulation:
Ci-contre, 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 intéressant pour déclencher du code à l'appuie d'un bouton ou encore pour réveiller l'ATMega.
Ci-contre, 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 !
}
|
Cas de l'ESP8266
Pour la carte ESP8266 il faut préciser, pour les fonctions qui servent de vecteurs d'interruption, qu'elles doivent être stockées dans la mémoire RAM et non la flash grâce au spécificateur ICACHE_RAM_ATTR |
void ICACHE_RAM_ATTR toggle() {
...
}
|
Communication
Serial
Les communications sur les broches Rx et Tx utilise les niveaux logiques TTL (3.3v ou 5v). Touts les Arduino possèdent au moins un contrôleur UART qui permet de communiquer avec un périphérique tier (PC, autre Arduino, module radio, etc...).
Ci-contre un exemple qui utilise le port série : |
void setup(){
Serial.begin(9600);
Serial.println(F("Hello World"));
}
void loop(){
}
|
Gestion mémoire sur Arduino
Les différents types de mémoires
La flashLa mémoire flash est utilisée pour stocker le programme qui va s'exécuter sur l'Arduino. Cette mémoire permet l'exécution du code mais pour pouvoir le modifier il faut d'abord le copier en SRAM. La mémoire flash est non volatile et permet la conservation du programme même en l'absence de source d’énergie mais possède un nombre de cycle d'écriture limité à 100.000. La SRAMLa Static Random Access Memory ou SRAM, peut être lue et modifiée par le programme en cours d'exécution dans plusieurs buts :
La plupart des problèmes mémoire surviennent quand la heap recontre la stack. Lorsque cela survient, les deux mémoires deviennent corrompues ce qui, dans la majorité des cas, se conclu par un crash. Si la collision n'est pas détectée et que l'ATMega ne redémarre pas, le comportement du programme devient erratique. l'EEPROML'EEPROM est une autre mémoire non-volatile avec un nombre de cycle d'écriture limité à 100.000. Cette mémoire se lit bit par bit, ce qui peut rendre son utilisation un peu fastidieuse. Elle sert principalement à enregistrer des données de configuration ensuite utilisées par le programme pour modifier son comportement. |
Architectures mémoire
Deux concepts
Au début de l'ère de l'électronique ont émergé deux types d'architectures mémoire : Harvard et Princeton.
Harvard | Princeton (Von Neumann) | |
Principe | Utilise deux types d'espaces mémoire : un pour le programme et un pour les données dynamiques. | Utilise un seul espace mémoire pour les programmes et les données dynamiques. |
Avantage | Rapidité | Fléxibilité |
Les microcontrôleurs
Les microcontrôleurs comme l'ATMega sont étudiés pour les applications embarquées et, à l'inverse des ordinateurs de bureau, ont une tâche bien définie qu'ils doivent effectuer de manière sûre et efficace. Le design des microcontrôleur est plutôt spartiate, bien éloigné des mise en cache multicouche ou encore des disques virtuels en mémoire, il se concentre sur ce qui est essentiel à son fonctionnement.
Le modèle Harvard convient tout à fait pour une utilisation dans le domaine de l'embarqué et d’ailleurs, sur un ATMega, on a bien le programme qui est stocké en mémoire flash et les données qui se trouvent en SRAM.
La plupart du temps, le compilateur et le système gèrent ces choses automatiquement mais, quand l'espace commence à devenir exiguë, il est bon de savoir comment les choses se passent sous le capot.
Différence avec un ordinateur classique
Les ordinateurs classique, MAC et PC, sont de conception hybride et profitent du meilleur des deux architecture. Au cœur du processeur ont est sur une concpetion Harvard qui compartimente les caches pour les instructions et les données pour maximiser les performance. Cependant ces instructions et données sont chargées d'un espace mémoire commum. Du point de vue du programmeur, cela apparaît comme une architecture Von Neumann avec plusieurs giga d'espace de stockage mémoire.
La différence la plus flagrante qui existe entre les ordinateurs et les microcontrôleurs est la quantité de mémoire disponible. L'Arduino Uno ne dispose que de 32Ko de mémoire flash et de 2Ko de SRAM ce qui est environ 100.000 fois plus petit qu'un PC bas de gamme !
En travaillant dans un environnement minimaliste, il faut utiliser intelligemment les ressources disponible !
Mesurer la consommation de SRAM
Lorsqu'on écrit un programme destiné à tourner sur un microcontrôleur comme l'ATMega, l'utilisation de la mémoire est très importante et surtout la détection de fuites ! Le programme peut très bien s'exécuter pendant 1, 2 voire 3 jours puis, d'un coup, plus rien... Très souvent, les fuites viennent de l'utilisation de fonction d'allocation mémoire comme malloc ou new et qui doivent être suivie de free ou delete. Le temps de correction d'un bug de ce type est très rapide, encore faut-il s'en rendre compte ! La fonction ci-contre permet de mesurer la quantité de mémoire disponible, c'est à dire, l'espace qui sépare la heap de la stack. |
int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
| |
Prenons l'exemple suivant : // Pointeur accessible de manière globale
char* str;
void setup() {
Serial.begin(9600);
printFreeRam();
}
void loop() {
// Affectation d'un tableau de taille 50
str = (char*) malloc(50);
printFreeRam();
delay(200);
}
void printFreeRam () {
extern int __heap_start, *__brkval;
int v;
v = (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
Serial.print(F("Free ram : "));
Serial.println(v);
}
|
On voit clairement la quantité de mémoire diminuer... Free ram : 1836 Free ram : 1784 Free ram : 1732 Free ram : 1680 Free ram : 1628 Si on ajoute la ligne suivante après le malloc : free(str);
Plus aucun problème. Il ne faut pas oublier de libérer l'espace réservè en mémoire ou, encore mieux, de reposer sur les des-allocation naturelle du système (en n'utilisant pas malloc ou new). De plus, il ne faut pas oublier qu'utiliser trop l'allocation dynamique conduit à la fragmentation de la heap... |
Limiter l'utilisation de la mémoire
Mémoire Flash
Lorsque le programme est conséquent, par exemple à cause de l'import de libraires, la mémoire flash commence à manquer cruellement. Il n'y a pas vraiment de miracle mais, suivre les préceptes suivants peut aider :
|