En vue de l'utilisation de cartes Arduino
Nous abordons ici les bases de la programmation en C/C++. En fait, le langage n'importe pas, ce sont les principes généraux, les mécanismes, qu'il faut retenir.
Le choix du C/C++ est du au fait que nous utilisons des cartes Arduino, avec l'environnement de développement éponyme, qui propose une API en C/C++.
Les puristes ne manqueront pas de faire remarquer que le C et le C++ sont deux langages totalement différents. En effet, c'est le cas lorsque ce dernier est utilisé par des experts. Ici ce n'est pas le cas et la frontière entre les deux est extrêmement mince.
À la fin de cette présentation vous saurez structurer un programme et faire afficher des informations, émises depuis l'Arduino, sur votre ordinateur.
Arduino n'est pas qu'une petite carte électronique open source à base de micro-contrôleur ATMega. C'est également un environnement de développement, hérité de Processing, qui se veut simple et qui permet d'écrire, de compiler et de téléverser↓ un programme sur le micro-contrôleur.
Processing se programme en Java, Arduino se programme en C/C++. C'est donc ce dernier langage que nous aborderons ici.
L'environnement de développement (IDE) permet d'écrire son programme en faisant un peu de coloration syntaxique sur les mots-clés, les noms de fonctions, ... spécifiques à Arduino. Mais plus intéressant, il permet de compiler (c'est à dire, transformer le code écrit par un humain en une version compréhensible par le micro-contrôleur) notre programme en s'adaptant au type de carte Arduino que l'on possède. Pour cela, il faut aller renseigner cette information dans le menu .
Si des erreurs de programmation sont détectées, elles sont affichées en bas de la fenêtre, dans la zone noire. Il faut toujours commencer par corriger la première erreur avant même de tenir compte des autres. Souvent, cette première erreur en engendre d'autres qui ne sont pas réellement des erreurs. Il faut donc la corriger et recompiler. Bien entendu, une relecture attentive de votre code vous permettra d'éviter ces erreurs à la compilation.
Une fois le programme mis au point et compilant sans erreur, il est temps de le téléverser. Pour cela, il faut commencer par sélectionner le port série qui a été affecté à notre carte Arduino lorsqu'elle a été branchée à un port USB de notre ordinateur. Cette affectation se fait par le système d'exploitation et le nom, l'ordre, ou tout autre considération à propos de ce port, va dépendre du dit système. Sous Windows, vous aurez des ports COM, sous MacOS et Linux, ils auront des noms un peu plus compliqués comme tty.usbxxxxxxx ou /dev/usbxxxx. Le plus délicat est de choisir le bon. Pour ce faire, le mieux est de regarder la liste avant de brancher la carte Arduino sur le port USB et de choisir celui qui est apparu une fois la carte branchée.
En plus de proposer un environnement pour écrire, compiler et téléverser les programmes, l'environnement Arduino dispose d'une aide en ligne (que vous soyez connecté à l'Internet ou pas) pour chacune des fonctions qu'il propose. Elle est accessible soit via le menu
, soit en sélectionnant le nom d'une fonction et en faisant un clic droit dessus.L'avantage de la documentation trouvée via les moyens décrits ci-dessus est que vous obtenez des explications pour les fonctions installées sur votre système. Aller chercher celles disponible sur le site Arduino est également possible, mais vous n'aurez les informations que pour la toute dernière version des bibliothèques.
À présent que nous avons vu les grandes lignes de l'interface de programmation, attachons nous à la programmation en elle-même.
Un programme est une suite d'instructions, écrites dans un langage de programmation, qui s'exécuteront dans l'ordre dans lequel elles se présentent dans le fichier source. Ces instructions suivent un formalisme particulier dont les principaux sont détaillés dans le reste de ce document.
setup()
et loop()
Tout d'abord, les passages obligés. Vous allez écrire dans un langage de programmation spécifique qui, comme tout langage de programmation, a des règles strictes d'écriture.
Dans le cadre d'Arduino, ces règles se limitent à l'utilisation de deux fonctions : setup()
et loop()
. La première n'est exécutée qu'une seule fois à chaque démarrage du micro-contrôleur et a pour vocation de configurer les différents ports d'entrée/sortie, d'initialiser les différentes variables que l'on va utiliser. Ensuite, la seconde est exécutée en boucle, à l'infini, tant que le micro-contrôleur n'est pas mis hors tension. C'est le cœur de notre programme, là où tout va se faire.
Ci-dessous, vous est présenté un code Arduino minimal.
Vous remarquerez que nos deux fonctions sont précédées d'un mon clé particulier (void
), qui signifie que cette fonction ne produit pas de valeur de retour. Nous verrons ça plus en détail dans la section suivante.
Vous remarquerez également que chacune de ces fonctions est suivi d'un couple de parenthèses. C'est ce qui défini que ce sont effectivement des fonctions. Parfois ces parenthèses sont vides, parfois elles contiennent des noms qui sont les arguments de la fonction (les données sur lesquelles notre fonction va travailler).
Enfin, en plus du couple de parenthèses, il y a un couple d'accolades. Elles aussi sont obligatoires. Elles définissent les limites des instructions qui font la fonction. On appelle ça un bloc d'instruction, c'est à dire l'ensemble des instructions qui vont être exécutées à chaque fois que ma fonction sera appelée.
main()
?Les personnes ayant déjà programmées en C/C++ se demanderont pourquoi il n'y a pas la seule fonction obligatoire dans ces langages, c'est à dire la fonction main()
.
En réalité, cette fonction est présente, mais elle est entièrement prise en charge par l'environnement Arduino. Le réel code qui sera compilé est en fait le suivant :
Il est possible d'ajouter des commentaires dans le code. C'est à dire des phrases, des paragraphes, des bouts de texte, qui ne seront jamais pris en compte par le compilateur, mais qui servent de pense-bête, de moyen de communication entre différents programmeurs.
Ces commentaires, pour être différenciés du code environnant, doivent avoir une syntaxe particulière. Ici, il y a deux façons de faire.
Soit le commentaire concerne une ligne et une seule, alors il suffit de le faire commencer par le couple de caractères //
. Tout ce qui est entre ces caractères et la fin de la ligne n'est pas pris en compte.
Soit ils concernent plusieurs lignes et il faudra marquer le début et la fin du bloc de commentaire, respectivement par /*
et */
. Tout ce qui se trouve entre ces deux couples de caractères sera ignoré.
Il est possible de mettre autant de commentaire que l'on désire, où on le désire, dans la langue que l'on désire. C'est une aide précieuse et une mémoire à ne pas négliger quand on devra revenir sur un programme quelques mois plus tard.
Un programme tel que celui proposé à la section précédente n'a aucun intérêt et ne produit rien. Ce qui est intéressant c'est de pouvoir traiter des données, faire de calculs, lire des valeurs depuis les ports d'entrée, actionner des effecteurs branchés aux ports de sortie. Le support à tout ces traitement est l'utilisation de variables, décrites ci-dessous.
Une variable, dans un programme informatique, c'est une zones dans la mémoire de l'ordinateur, qui est nommée et dans lesquelles il est possible de lire et d'écrire des valeurs. Ces zones nommées sont crées par le programmeur en définissant de nouvelles variables qui lui permettront de résoudre son problème.
Le langage que nous utilisons est un langage typé, c'est à dire qu'il est obligatoire de donner un type aux variables que nous définissons.
Une variable a donc, pour être correctement formée, un type et un nom.
Les nom d'une variable peut être presque n'importe quoi à partir du moment où :
Le type dépend de l'utilisation que l'on va faire de la variable et il est à choisir dans la liste suivante :
void
, le type qui représente le fait qu'il n'y a pas de type. Rarement utilisé tel quel pour des variables, il sera plus utile pour les fonctions.byte
, c'est une valeur entière représentée sur un octet, soit sur 8 bits. Les valeur possibles varient entre 0 et 255.char
, c'est une valeur entière représentée sur un octet, soit sur 8 bits. Les valeur possibles varient entre -128 et 127.int
, c'est une valeur entière représentée sur deux octets, soit sur 16 bits (sur la plupart des Arduino). Les valeur possibles varient entre -32 768 et 32 767.long
, c'est une valeur entière représentée sur quatre octets, soit sur 32 bits. Les valeur possibles varient entre -2 147 483 648 et 2 147 483 647.float
, double
, ce sont des valeurs décimales représentées sur quatre octets, soit 32 bits. Les valeurs représentables varient de -3,4028235E+38 à 3,4028235E+38. Mais attention, la représentation informatique des nombres à virgule flottante est complexe et ne permet pas de représenter tous les nombres dans cet intervalle (il serait magique de pouvoir représenter un nombre infini de valeur sur un nombre fini de bits). Les calculs faits avec des flottants sont rarement justes, mais ne le sont qu'à une décimale près. La précision est de 6 à 7 nombres, partie entière et partie décimale comprise.Les types entiers (char
,int
, long
) peuvent être qualifiés par le mot-clé unsigned
. Il ne pourront alors plus prendre de valeur négative, mais en compensation, la valeur maximale représentable dans les positifs sera doublée (respectivement 255, 65 535 et 4 295 967 295).
D'autres types existent, hérités du C++ ou crées pour Arduino. Vous les retrouverez sur l'aide en ligne et nous ne les détaillerons pas ici. En effet, les types précédents suffisent pour résoudre les problèmes courants que l'on pourra rencontrer.
Par contre, il existe d'autres types, hérité de l'environnement wiring duquel est tiré le langage utilisé par l'Arduino, qui n'ont rien de nouveau, mais qui sont pratiques en ce sens qu'ils explicitent leur occupation mémoire.
int8_t
, uint8_t
, un entier sur 8 bits, signé et non signé.int16_t
, uint16_t
, un entier sur 16 bits, signé et non signé.int32_t
, uint32_t
, un entier sur 16 bits, signé et non signé.La figure suivante donne un exemple de déclaration de variables. Vous remarquerez également à cette occasion la présence du caractère ; à la fin de cette ligne. Le caractère ; est obligatoire à la fin de toute instruction en C/C++. Déclarer une variable est une instruction.
Les variables déclarées à l'extérieur de toute fonction sont appelées variables globales. C'est à dire qu'elles sont accessibles depuis n'importe quel endroit du code. Elles sont les pendantes des variables globales qui, a contrario, sont définies à l'intérieur d'un bloc et qui ne seront accessibles que dans ce bloc. Par exemple, une variable définie dans le bloc d'une fonction a une portée, une durée de vie, qui est égale au bloc définissant cette fonction.
En général il est fortement déconseillé d'utiliser des variables globales. Elles peuvent se révéler sources d'erreurs. En effet, rien n'interdit de définir des variables locales qui ont le même nom que des variables globales, et ceci sans aller à l'encontre de la première règle sur la façon de nommer les variables, puisqu'elles ne sont pas dans le même contexte. Mais elles existent pourtant en même temps et le programmeur peut très bien penser modifier la variable locale alors qu'en fait c'est sur la variable globale qu'il travaille. C'est ce qu'on appelle le masquage de variable (shadowing en anglais).
Mais, ceci dit, dans le contexte de la programmation embarquée, il est plus efficace d'utiliser les variables globales. C'est un peu technique, mais cela permet d'économiser de la mémoire au moment des appels de fonctions. Il faut simplement être conscient du problème de masquage évoqué ci-dessus et prendre soin de ne pas créer de problèmes.
Il est souvent nécessaire de manipuler un ensemble de valeurs comme étant une seule entité. Les tableaux pemettent de faire cela simplement. Pour créer un tableau en C/C++, il suffit de créer une variable dont le nom est suivi des caractères [ et ], avec une valeur numérique entre les deux, donnant la taille, en nombre d'éléments du tableau.
Un exemple est donné ci-dessous :
Il est important de noter que pour accéder à un élément du tableau, on utilise son indice et que les indices commencent à 0 et se terminent à taille-1.
En plus des variables, il est possible de définir des constantes, c'est à dire des entités dont la valeur ne pourra pas être modifiée.
#define
Un moyen simple de définir des constantes est l'utilisation de directives particulières (directive du pré-processeur, pour ceux qui veulent savoir de quoi il s'agit). Ce n'est pas du code C/C++ à proprement parler. Ce n'est pas nom plus une déclaration au sens d'une déclaration de variable. C'est juste un moyen de nommer quelque chose pour pouvoir le réutiliser plus tard facilement.
Vous remarquerez qu'il n'y a pas de ; à la fin de la ligne. Ce n'est pas une instruction du langage, il n'est donc pas nécessaire. Il peut même poser problème s'il est présent. En effet, ce mécanisme de déclaration de constante peut se voir comme du rechercher/remplacer. Le pré-processeur (qui intervient avant le compilateur, pour traiter le programme qu'on a écrit et le transformer en code exécutable par le micro-contrôleur) va recherche le mot qui vient juste après le #define
(dans l'exemple, le mot LED
) et le remplacer par tout le reste de la ligne (dans l'exemple, le mot 11
).
Une constante définie comme ça n'a pas de type. Son type sera déduit par le compilateur au moment de son utilisation, mais de manière grossière.
L'utilisation de la directive #define
ne se limite pas à la création de constantes, cependant c'est tout ce que nous verrons dans le cadre de cette introduction.
const
Il existe en C/C++ un moyen pour rendre une variable constante, c'est de lui rajouter le préfixe const
.
L'avantage c'est que c'est une grandeur typée, nommées, qui peut-être utilisée comme une variable classique sauf qu'on ne peut pas lui affecter de valeur à un autre moment que celui de sa déclaration. Lors de la compilation, il peut y avoir des messages d'erreurs qui sont émis si l'on ne respecte pas l'utilisation de types. C'est pratique pour le programmeur.
Vous remarquerez la présence d'un ; à la fin de la ligne de déclaration de la constante. C'est le même mécanisme que la déclaration d'une variable classique, c'est dont une instruction à part entière.
Programmer une carte à micro-contrôleur simple comme l'Arduino est sensiblement différent de la programmation classique. Entre autre, il n'est pas possible d'afficher quoi que ce soit directement sur un écran car ... il n'y a pas d'écran par défaut. Qu'à cela ne tienne, nous allons nous servir de notre ordinateur comme moyen de visualisation. C'est une solution de mise au point de programmes ou une solution de communication entre la carte et un ordinateur.
Cette solution a l'avantage d'être extrêmement simple à mettre en œuvre. Cette simplicité a pourtant un coût : votre programme sera plus volumineux et il prendra plus de temps à s'exécuter. Ces inconvénients peuvent devenir problématiques si la place est limitée (cas de gros programmes) ou si le respect strict du temps est nécessaire (cas d'une application temps réel par exemple). Il faut donc garder ça à l'esprit et être conscient du fait que le déroulement normal de votre programme est modifié lorsque vous utilisez ces instructions.
Pour la faire fonctionner il faut utiliser l'objet Serial
qui va utiliser la liaison série du port USB pour faire transiter des données. Ce transfert de données se fait à une certaine vitesse, qui est configurable, et qui s'exprime en bauds. Le baud est une unité qui, en première approximation, peut être assimilé au bit par seconde. La page Wikipedia vous donnera plus de détails si vous êtes curieux. La valeur par défaut est 9600 bauds.
Ensuite, il suffit d'écire les informations les unes à la suite des autres. Il faut s'attendre à des fonctions très basiques ne disposant pas des fonctionnalités plus avancées que l'on peut trouver dans les langages de programmation habituels.
Ci-dessous, un programme illustrant l'utilisation de la ligne série pour tracer l'exécution d'un programme.
Si vous faites exécuter le code ci-dessus par votre Arduino, vous remarquerez sans doute, au bout d'un certain temps, un changement radical de la valeur affichée. C'est un dépassement de capacité. Essayez de changer le type de la variable compteur
et voyez ce que ça change.
Un programme informatique c'est des calculs, un peu d'affichage, mais ce sont également beaucoup de tests et de boucles. Dans cette section nous abordons les tests et dans la suivante, les boucles.
Un test revient à se poser la question « est-ce qu'une valeur est inférieure, égale, ou supérieure à une autre valeur ? ». Il faut pour cela disposer des opérateur adéquats et il se trouve que l'environnement Arduino nous propose les principaux qui sont <
, <=
, ==
, !=
, >=
et >
. Soit respectivement, inférieur, inférieur ou égal, égal, différent, supérieur ou égal et supérieur.
Ici il y a un point important à noter, c'est le double ==
pour tester l'égalité. C'est pour pouvoir différentier de l'affectation qui se note avec un unique signe =
.
Ces opérateurs retournent tous une valeur en fonction du résultat du test. Si l'expression est fausse, une valeur nulle (0) est retournée. C'est donc le code pour dire « c'est faux ». Toute autre valeur est considérée comme étant vraie, ou un synonyme de « c'est vrai ».
Enfin, il est possible de faire des groupes de tests en reliant chacun des sous-tests par des opérateurs logiques. Il existe principalement trois opérateurs logiques utilisables pour cascader les tests :
&&
) qui veut que le test soit vrai si tous les sous-tests de l'expression sont vrais ;||
) qui veut que le test soit vrai si au moins un des sous-tests de l'expression est vrai ;!
) qui inverse le résultat d'un test.Vous pouvez voir un exemple d'utilisation des groupes de tests dans l'exemple complet, plus bas.
Une fois que l'on a une condition, il faut pouvoir réagir en fonction du résultat. Pour cela, il existe un mot clé réservé, qui est la structure de test if
. Un exemple d'utilisation est donné ci-dessous.
Ce qu'il faut retenir c'est la syntaxe du if
dont le test est obligatoirement entre parenthèses et qui est éventuellement suivi d'un else
qui indique la suite d'instructions à exécuter dans le cas où le test serait faut.
Des constructions plus complexes peuvent exister, du type
if ( test ) { instructions } else if ( test ) { instructions } else { instructions }
où il peut y avoir autant de blocs else if
que nécessaire et éventuellement (il n'est pas obligatoire) un unique bloc else
pour terminer la structure.
Les tests permettent donc d'altérer le comportement séquentiel d'un programme en sautant éventuellement des blocs d'instructions.
Faire des tests permet d'orienter la direction que va prendre notre code. C'est nécessaire. Mais parfois il est tout aussi utile de pouvoir faire un ensemble d'actions plusieurs fois d'affilée. C'est le principe des boucles que nous décrivons ici.
Il existe principalement trois types de boucles : for
, while
et do
... while
. Toutes sont basées sur un test qui mettra fin à l'exécution de la boucle et toutes fonctionnent globalement de la même manière. C'est uniquement la syntaxe qui va changer et l'ordre entre le premier test et l'exécution des commandes qui va être modifié pour la structure do
... while
.
Ci-dessous, quelques exemples donnant des cas d'utilisation typique de ces structures :
Le test utilisé sur les trois exemples précédent est une simple inégalité. Il est cependant possible d'utiliser n'importe quel test ou n'importe quelle combinaison de tests entre les parenthèses.
Les nombres que l'on manipule sont en général représentés en base 10 (nous avons 10 doigts aux mains...) Quand on compte, on passe de 9 à 10, puis à 11, ...
Dans le monde des ordinateurs, d'autres bases sont employées, les plus courantes étant la base 2 et la base 16. Nous expliquons ci-dessous les rudiments de fonctionnement et d'écriture de ces représentations.
La base 2 ne comprend que deux valeurs pour représenter ses nombres : 0 et 1. Pourtant, grâce à cela il est possible de représenter tous les nombres entier que l'on désire. Par exemple :
Base 10 | Base 2 |
---|---|
0 | 0 |
1 | 1 |
2 | 10 |
3 | 11 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
Le nombre en base 2 est représenté comme un polynôme dont les facteurs sont les valeurs binaires et dont les termes sont les puissances de 2 consécutives. Par exemple, pour représenter 6base 2, on a :
110base 2 = 1 × 22 + 1 × 21 + 0 × 20 = 6base 10.
La représentation binaire est celle réellement utilisée dans le cœur des ordinateurs. Ils calculent comme ça et tous les nombres que nous écrivons sont automatiquement transformés en base 2.
Il est bien entendu possible de représenter les nombres négatifs. Pour cela, un mécanisme appelé le complément à deux est utilisé.
La base hexadécimale fonctionne sur le même principe que la base 2, mais nous disposons à présent de 16 signes (base 16) pour représenter les nombres : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F.
Là encore, il est possible de représenter tous les nombres entier grâce à la base 16. Par exemple :
Base 10 | Base 16 |
---|---|
0 | 0 |
5 | 5 |
10 | A |
15 | F |
20 | 14 |
25 | 19 |
30 | 1E |
35 | 23 |
40 | 28 |
Le nombre en base 16 est représenté comme un polynôme dont les facteurs sont les valeurs de la représentation hexadécimale et dont les termes sont les puissances de 16 consécutives. Par exemple, pour représenter 12345base 10, on a :
3039base 16 = 3 × 163 + 0 × 162 + 3 × 161 + 9 × 160 = 12345base 10.
On peut remarquer que la représentation de 255base 10 est 11111111base 2 mais aussi FFbase 16. C'est la valeur maximale que peut prendre 1 octet (un groupe de 8 bits).
Autant la notation en base 2 est rarement utilisée directement dans les programmes, autant il est fréquent de devoir écrire des nombres en base 16. Pour que notre compilateur s'y retrouve, nous devons lui préciser que la base est différente. Les nombres en base 16 seront pour cela, toujours préfixés des deux caractères 0x
. Le bout de code suivant donne des exemples d'initialisation de variables entières.