Programmation Arduino
Objectifs de la séance
Le but de la séance est d'écrire une application multitâches Arduino utilisant plusieurs périphériques.
Préambule
Les documents nécessaires se trouvent :
Lors de cette séance, nous allons programmer sur Arduino en utilisant :
- La LED présente sur le module.
- Le port série qui relie le module et le PC.
- L'écran OLED
- La photorésistance
Test du matériel
Pour s'assurer que le module Arduino et la chaîne de compilation sont fonctionnels, vous pouvez reprendre l'exemple blink
`
- Brancher le module Arduino avec le câble USB
- lancer arduino (il y a un raccourci sur le bureau)
- Sélectionner :
Tools -> Boards -> Arduino Duemilanov...
(même si c'est une nano) - Sélectionner :
Tools -> Processor -> ATmega328
- Sélectionner :
Tools -> Ports -> /dev/COMMx
(le numéro du port est créé à la connexion) - Charger le programme Blink :
File -> Exemples -> 0.1 Basics -> Blink
- Vous pouvez changer la fréquence en modifiant les délais
- Cliquer sur l'icône en forme de V pour Compiler
- Cliquer sur l'icône en forme de -> pour uploader
- En bas de la fenêtre un message vous indique la réussite de la compilation et de l'upload.
- La led doit clignoter sur le module
Principe d'exécution multi-tâches
Il est possible de programmer des applications multi-tâches coopératives dans l'environnement Arduino sans pour autant disposer des services d'un OS. Le principe est volontairement simplifié à l'extrême. Ici, toute l'application sera dans un seul fichier. Nous n'allons pas utiliser la programmation objet pour ne pas complexifier, mais ce serait possible pour une application plus ambitieuse pour une meilleure maintenance.
Une application est composée de plusieurs tâches. Chaque tâche peut être répliquée plusieurs fois dans l'application et chaque réplica de tâche est exécutée à tour de rôle en temps partagé. Les (réplica de) tâches communiquent entre elles par l'intermédiaire de variables globales.
Les ISR (Interrupt Service Routine) qui sont des fonctions exécutées lors de la survenue d'interruption sont considérée comme des tâches.
Chaque tâche est représentée deux fonctions loop_Tache()
et setup_Tache()
- La fonction
loop_Tache()
code le comportement de la tâche. Elle es appelée dans la fonctionloop()
. Dans cet exemple l'application a trois tâches : la tâche 1 est répliquée 2 fois et la tâche 2 n'a qu'un réplica.La fonctionloop() { loop_Tache1(args...); // 1er réplica de la tâche 1 loop_Tache1(args...); // 2nd réplica de la tache 1 loop_Tache2(args...); // unique réplica de la tâche 2 }
loop()
demande donc l'exécution des fonctionsloop_Tache()
à tour de rôle. Les tâches n'ont pas le droit de conserver le processeur sinon cela crée un blocage du système. Cela signifie qu'il est interdit de faire des boucles d'attente d'un événement. La fonction loop() est en fait un ordonnanceur de tâche suivant un algorithme FIFO statique.
La fonction loop() ignore si la tâche est en mesure d'avancer. Les tâches sont toujours "prêtes". C'est chaque tâche qui doit vérifier si elle peut avancer ou pas, par exemple si les données attendues sont disponible ou si assez de temps c'est écoulé depuis la dernière instance d'exécution. Si oui elle avance, sinon elle sort immédiatement et la tâche suivante dans la liste est exécutée.
- La seconde fonction
setup_Tache()
initialise les ressources matérielles assignées de la tâche (périphériques) et l'état interne (voir plus loin).setup() { setup_Tache1(args...); // setup du 1er réplica de la tâche 1 setup_Tache1(args...); // setup du 2nd réplica de la tache 1 setup_Tache2(args...); // setup de l'unique réplica de la tâche 2 }
Chaque tâche (sous-entendu réplica de tâche) dispose de trois types de variables internes
- Variables locales temporaires :
- Les fonctions
loop_Tache
etsetup_Tache
peuvent avoir des variables locales. L'état n'est pas conservé entre deux instances d'exécution de la tâche.
- Les fonctions
- Variables d'état de tâche :
- Elles peuvent aussi avoir des variables
static
. Ces variables ont une valeur unique pour toutes les réplicas d'une tâche. - Ces variables définissent donc un état interne partagé par tous les réplicas.
- Elles peuvent aussi avoir des variables
- Variables de contexte d'exécution :
- Enfin chaque réplica de tâche dispose d'une structure contenant son état interne propre,
lequel est conservé entre deux instances d'exécution du réplica de tâche.
Le contexte d'exécution représenté par une variable globale du programme sous forme d'une structure.
Une structure différente est passée en argument de chaque réplica de tâche dans les arguments
des fonctions
setup_Tache()
etloop_Tache
. - La structure contexte ressemble à :
struct Tache_st { unsigned int etat; int config; }; struct Tache_st T1, T2; // deux contextes pour deux réplicas d'une tâche.
- Enfin chaque réplica de tâche dispose d'une structure contenant son état interne propre,
lequel est conservé entre deux instances d'exécution du réplica de tâche.
Le contexte d'exécution représenté par une variable globale du programme sous forme d'une structure.
Une structure différente est passée en argument de chaque réplica de tâche dans les arguments
des fonctions
C'est la fonction setup_Tache()
qui initialise l'état interne des tâches
void setup_Tache(struct Tache_st *ctx, args...) { stat type etat_tache; // Initialisation de l'état interne commun a tous les réplica etat_tache = etat_initial; // reçu dans les args // Initialisation du contexte} ctx->etat = etat_initial; // reçu dans les args ... }
Communications inter-tâches
Lorsqu'on écrit un programme multi-tâches, les tâches communiquent entre elles. Pour ce faire, nous allons simplement créer variables globales et les donner en arguments aux tâches. Chaque tâche reçoit donc en argument les pointeurs vers les variables qu'elle utilise pour communiquer. Ces pointeurs sont nommés des ports.
La structure forme de la fonction loop d'une tâche est donc :
void loop_Tache(struct Tache_st *ctx, ports....) { // test de la condition d'exécution, si absent on SORT if (evement_attendu_absent) return; // code de la tache .... }
Supposons que nous voulions que la tâche T1 envoie un message à la tâche T2. Nous allons utiliser une boite à lettre. Le code suivant explique le principe qui est basé sur une variable d'état à 2 valeur indiquant l'état de la boîte. La boîte peut être vide ou pleine. l'écrivain T1 ne peut écrire que lorsque la boîte est vide. Lorsqu'elle est vide, il y écrit et il change l'état. Inversement, le lecteur attend qu'elle soit pleine. Lorsqu'elle est pleine, il la lit et change l'état.
Il s'agit d'une communication sans perte. Si T1 ne testait pas l'état de la boîte, on pourrait avoir des pertes, c'est parfois nécessaire, si T2 n'a pas eu le temps d'utiliser la boîte mais que T1 a une nouvelle valeur, il peut écraser la valeur présente.
struct mailbox { enum {EMPTY, FULL} state; // Création d'une boite vide int val; } mb0 = {.state = EMPTY}; struct T1_st {...} T1; // Création de 2 contextes d'exécution struct T2_st {...} T2; void setup_T1(struct T1_st *ctx, args...) {...} // Déclaration des fonctions setup de tâche void setup_T2(struct T2_st *ctx, args...) {...} void loop_T1(struct T1_st *ctx, struct mailbox *mb) { if (mb->state != EMPTY) return; // attend que la mailbox soit vide mb->val = 42; mb->state = FULL; } void loop_T2(struct T1_st *ctx, struct mailbox *mb) { if (mb->state != FULL) return; // attend que la mailbox soit pleine // usage de mb->val mb->state = EMPTY; } setup() { setup_T1(&T1, args...); setup_T2(&T2, args...); } loop() { loop_T1(&T1, &mb0); loop_T2(&T2, &mb0); }
Gestion des tâches standard périodiques
Pour les tâches périodiques (elles sont fréquentes), nous pouvons écrire une fonction qui exploite un unique timer interne matériel du processeur qui s'incrémente chaque microseconde. Cette fonction nommée :
waitFor(int timer, unsigned long period)
Elle prend deux paramètres timer
et period
. Le premier un numéro de timer logique (il en faudra autant que de tâches périodiques). Le second est une période en microsecondes.
wairFor()
peut être invoquée aussi souvent que nécessaire, elle rend la valeur 1 une seule fois par période (second paramètre).
Si elle n'est pas appelée pendant longtemps alors elle rend le nombre de périodes qui se sont écoulées.
Autrement dit, si dans une tâche vous écrivez waitFor(12,100)
parce c'est le timer logique n°12 et que la période est de 100us
et si vous n'exécutez pas la tâche pendant 500us
alors au premier appel après ce délai de 500us waitFor(12,100)
rendra 5.
Exemple
Dans l'application suivante nous avons deux tâches périodiques Led
et Mess
.
La première fait clignoter une led dont le numéro est passé en paramètre à 5Hz.
La seconde affiche bonjour à une fois par seconde.
// -------------------------------------------------------------------------------------------------------------------- // Multi-tâches cooperatives : solution basique // -------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------- // unsigned int waitFor(timer, period) // Timer pour taches périodiques // configuration : // - MAX_WAIT_FOR_TIMER : nombre maximum de timers utilisés // arguments : // - timer : numéro de timer entre 0 et MAX_WAIT_FOR_TIMER-1 // - period : période souhaitée // retour : // - nombre de période écoulée depuis le dernier appel // -------------------------------------------------------------------------------------------------------------------- #define MAX_WAIT_FOR_TIMER 2 unsigned int waitFor(int timer, unsigned long period){ static unsigned long waitForTimer[MAX_WAIT_FOR_TIMER]; unsigned long newTime = micros() / period; // numéro de la période modulo 2^32 int delta = newTime - waitForTimer[timer]; // delta entre la période courante et celle enregistrée if ( delta < 0 ) delta += 1 + (0xFFFFFFFF / period); // en cas de dépassement du nombre de périodes possibles sur 2^32 if ( delta ) waitForTimer[timer] = newTime; // enregistrement du nouveau numéro de période return delta; } //--------- définition de la tache Led struct Led_st { int timer; // numéro de timer utilisé par WaitFor unsigned long period; // periode de clignotement int pin; // numéro de la broche sur laquelle est la LED int etat; // etat interne de la led }; void setup_Led( struct Led_st * ctx, int timer, unsigned long period, byte pin) { ctx->timer = timer; ctx->period = period; ctx->pin = pin; ctx->etat = 0; pinMode(pin,OUTPUT); digitalWrite(pin, ctx->etat); } void loop_Led(struct Led_st * ctx) { if (!waitFor(ctx->timer, ctx->period)) return; // sort s'il y a moins d'une période écoulée digitalWrite(ctx->pin,ctx->etat); // ecriture ctx->etat = 1 - ctx->etat; // changement d'état } //--------- definition de la tache Mess struct Mess_st { int timer; // numéro de timer utilisé par WaitFor unsigned long period; // periode d'affichage char mess[20]; } Mess_t ; void setup_Mess(struct Mess_st * ctx, int timer, unsigned long period, const char * mess) { ctx->timer = timer; ctx->period = period; strcpy(ctx->mess, mess); Serial.begin(9600); // initialisation du débit de la liaison série } void loop_Mess(struct Mess_st *ctx) { if (!(waitFor(ctx->timer,ctx->period))) return; // sort s'il y a moins d'une période écoulée Serial.println(ctx->mess); // affichage du message } //--------- Déclaration des tâches struct Led_st Led1; struct Mess_st Mess1; //--------- Setup et Loop void setup() { setup_Led(&Led1, 0, 100000, 13); // Led est exécutée toutes les 100ms setup_Mess(&Mess1, 1, 1000000, "bonjour"); // Mess est exécutée toutes les secondes } void loop() { loop_Led(&Led1); loop_Mess(&Mess1); }
Questions
- Que contient le tableau
waitForTimer[]
` ? - Dans quel cas la fonction
waitFor()
peut rendre 2 ? - Modifier le programme initial pour afficher "Salut" en plus de "bonjour" toutes les 1.5 secondes sans changer le comportement existant.
Utilisation de l'écran
Nous allons utiliser un écran OLED connecté en I2C, 128x32 ssd1306
- La bibliothèque de l'écran se trouve en tapant la requête
ssd1306 arduino
à l'adresse https://github.com/adafruit/Adafruit_SSD1306. Vous devrez prendre également la bibliothèque GFX à l'adresse https://github.com/adafruit/Adafruit-GFX-Library qui est la bibliothèque graphique. - Vous pouvez exécuter l'exemple proposé dans la bibliothèque. Cette bibliothèque fonctionne pour plusieurs types modèles. Vous allez choisir le bon exemple : 128x32 I2C.
- Pour ajouter une bibliothèque Arduino, vous devez simplement télécharger le .zip et importer directement le
.zip en sélectionnant le menu
Sketch -> include Library -> Add ZIP Library
- Pour tester la librairie rendez-vous dans
File -> Exemples -> Adafruit SSD1306 -> ssd1306_128x32_i2c
. Il s'agit d'un programme qui teste les fonctionnalité de l'écran et de la bibliothèque graphique.
Questions
- Extraire de ce code, ce qui est nécessaire pour juste afficher un compteur qui s'incrémente toutes des 1 seconde sur l'écran OLED.
Questions
- Ajouter une tâche qui lit toutes les 0,5 seconde le port analogique 15 (par
analogRead()
) sur lequel se trouve la photo-résistance et qui sort sa valeur dans une boite à lettre. - Mofifier la tâche Led pour que la fréquence de clignotement soit inversement proportionnel à la lumière reçue (moins il y a de lumière plus elle clignote vite). La tâche Led devra donc se brancher sur la boite à lettre.
Gestion des interruptions
Les périphériques peuvent lever des signaux d'interruption pour informer d'un événement sur un périphérique.
Avec Arduino, il est très simple d'attacher une routine d'interruption (ISR) à un signal d'interruption.
C'est la fonction attachInterrupt(num, ISR, type)
. Pour l'Arduino nano num
est égal à 0
ou 1
, ce qui correspond aux pins 2
et 3
qui sont des entrées de signaux d'interruptions. Il existe d'autres sources d'interruption comme le changement d'état d'une pins ou la réception d'une données depuis un bus par exemple Serial.onReceive(ISR)
Dans notre contexte la fonction ISR sera comme une fonction loop_Tache.
Question
Ajouter une tâche qui arrête le clignotement de la LED si vous recevez un s
depuis le clavier.