Version 3 (modified by 3 years ago) (diff) | ,
---|
Programmation ESP32 / Arduino
Objectif de la séance
Le but de la séance est d'écrire une application multitâches Arduino utilisant plusieurs périphériques. L'application finale contiendra toutes les tâches.
Vous allez devoir installer l'IDE Arduino et installer les bibliothèques permettant d'utiliser l'ESP32.
Puis vous aller :
- Utiliser la LED présente sur le module.
- Utiliser le port série qui relie le module et le PC.
- Utiliser l'écran OLED
- Utiliser la photorésistance
- Utiliser le buzzer
- Ecrire une application affichant la luminosité chaque seconde sur l'écran OLED, tout en faisant clignoter la LED avec un ON/OFF par le bouton poussoir.
- Vous pouvez tester le composant ESP32 en exécutant quelques exemples de tutoriaux (décrivez ce que vous avez fait dans le CR)
Les documents utiles se trouvent :
- Tutoriel pour l'installation des bibliothèque ESP32
- Repository API Ecran OLED
- Repository API Graphique
- Langage Arduino
Pour les machine du laboratoire, vous devez utiliser la version d'arduino qui se trouve dans /opt/arduino-1.8.8/
. Je vous conseille d'ajouter le chemin /opt/arduino-1.8.8
dans la variable PATH dans le .bashrc
.
Dans le .bashrc : export PATH=/opt/arduino-1.8.8:$PATH
Sinon, vous pouvez appeler arduino directement :
> /opt/arduino-1.8.8/arduino
Installation ESP32 / Arduino sur votre ordinateur personnel
- Ouvrir un navigateur web et se rendre à l'adresse :
https://www.arduino.cc/en/Main/Software
- Cliquer sur l'archive qui correspond à votre système.
- Cliquer sur
JUST DOWNLOAD
- Cliquer sur
Télécharger
Installation sur Mac OS
- l'archive se décompresse et est enregistrée dans le répertoire
Downloads
(sur ma machine en tout cas). - Déplacer l'application
Arduino
qui apparaît dans le répertoirDownloads
dans le répertoireApplications
.
Installation sur Linux
à compléter...
Installation sur Windows
- Cliquer sur la fenêtre popup
Ouvrir Microsoft Store
- Cliquer sur
Installer
- A la demande
Utiliser sur l'ensemble de vos appareils
, répondreNon merci
- Attendre le téléchargement (ça peut être long...)
- Cliquer sur
lancer
Quand le bouton apparaît - Arduino se lance, et demande éventuellement une mise-à-jour, ne pas la faire
Installation des librairies ESP32
Procédure à suivre pour ajouter les gestionnaires de cartes à base d'ESP32 :
- Lancer
Arduino
(si ce n'est pas déjà fait à l'étape précédente) - Cliquer sur
Fichier > Préférences
- Dans
URL de gestionnaire de cartes supplémentaires
, écrire :
https://dl.espressif.com/dl/package_esp32_index.json
- Cliquer sur
ok
- Cliquer sur
Outils > Type de carte... > Gestionnaire de carte
- Écrire dans la fenêtre de recherche :
esp32
` - Cliquer sur
Installer
- Attendre l'installation (ça peut être long).
- Cliquer sur
Fermer
Démarrage (rappel)
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 :
/opt/arduino-1.8.8/arduino &
- Sélectionner :
Tools -> Boards -> TTGO-Lora-OLED V1
- Sélectionner :
Tools -> Ports -> /dev/ttyUSB0 ou /dev/ttyUSB1
- 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
Exécution multi-tâches
Tâches standards
Il est possible de programmer des applications multi-tâches coopératives dans l'environnement Arduino sans pour autant dispose des services d'un OS. Le principe a été volontairement simplifié à l'extrême. Ici, toute l'application sera dans un seul fichier et nous n'allons pas utiliser la programmation objet pour ne pas complexifier.
Chaque tâche est représentée par
- une fonction
loop_Tache()
qui code son comportement qui sera appelée dans la fonctionloop()
. - une seconde fonction
setup_Tache()
qui initialise les ressources de la tâche (périphériques) et l'état interne. - une structure contenant l'état interne et le contexte d'exécution représenté par une variable globale sous forme d'une structure. Cette structure est passée en argument de la tâche des fonctions
setup_Tache()
etloop_Tache
.
Les fonctions loop_Tache
et setup_Tache
peuvent avoir des variables locales mais leur état n'est pas conservé entre deux exécutions.
Elles peuvent aussi avoir des variables static mais ces variables ont une valeur unique même si la tâche est à plusieurs exemplaires.
La structure contexte ressemble à :
struct Tache_st { unsigned int etat; int config; }; struct Tache_st T1, T2; // deux contextes pour deux tâches.
C'est la fonction setup_Tache()
qui va pouvoir initialiser le contexte avec des paramètres.
void setup_Tache(struct Tache_st *ctx, params...) { // Initialisation du contexte} ctx->etat = etat_initial; // reçu dans les paramètres ... }
La fonction loop()
demande donc l'exécution des fonctions loop_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.
connectors
sont des pointeurs vers des variables globales utilisées pour la communications inter-tâches.
La structure générale d'une tâche est la suivante :
void loop_Tache(struct Tache_st *ctx, connectors....) { // test de la condition d'exécution, si absent on SORT if (evement_attendu_absent) return; // code de la tache .... }
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 timer interne du processeur qui s'incrémente chaque microseconde. Cette fonction nommée waitFor(int timer, unsigned long period)
prend deux paramètres timer
et period
. Le premier un numéro de timer (il en faudra autant que de tâches périodiques). Le second est une période en microsecondes.
wairFor()
peut être appelé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 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 mais efficace :-) // -------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------- // 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]; // il y a autant de timers que de tâches périodiques 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 + newTime; // 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 du timer pour cette tâche 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[]
et à quoi sert-il ? - Si on a deux tâches indépendantes avec la même période, pourquoi ne peut-on pas utiliser le même timer dans waitFor() ?
- 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. Vous aurez donc "Salut" et "bonjour" qui s'affiche avec une périodicité propre à chaque message.
Utilisation de l'écran OLED
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. Vous devez ajouter une tâche nnommée
oled
dans votre programme en conservant celles déjà dans votre sketch (programme Arduino). L'idée, c'est d'avoir plein de tâches ensemble.
Communications inter-tâches
Lorsqu'on écrit un programme multi-tâches, il est intéressant de les faire communiquer. Pour ce faire, nous allons simplement créer variables globales et les donner en arguments aux tâches communicantes.
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 boite. La boite peut être vide ou pleine. l'écrivain T1 ne peut écrire que lorsque la boite 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 boite, on pourrait avoir des pertes, c'est parfois nécessaire, si T2 n'a pas eu le temps d'utiliser la boite mais que T1 a une nouvelle valeur, il peut écraser la valeur présente.
enum {EMPTY, FULL}; struct mailbox { int state; int val; } mb0 = {.state = EMPTY}; void loop_T1(... struct mailbox * mb ...) { if (mb->state != EMPTY) return; // attend que la mailbox soit vide mb->val = 42; mb->state = FULL; } void loop_T2(... struct mailbox * mb ...) { if (mb->state != FULL) return; // attend que la mailbox soit pleine // usage de mb->val mb->state = EMPTY; }
Questions
- Dans le texte précédent, quel est le nom de la boîte à lettre et comment est-elle initialisée ?
- Ajouter une tâche nommée
lum
qui lit toutes les 0,5 seconde le port analogique [...] (paranalogRead()
) sur lequel se trouve la photo-résistance et qui sort sa valeur dans une boite à lettre. Cette boite à lettre sera connectée à la tâcheoled
. Vous afficher la valeur en pourcentage de 0% à 100% en utilisant la fonction map() - 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 avec la tâche
lum
avec une nouvelle boite à lettre. Il doit y avoir deux boites sortant delum
, l'une versoled
l'autre versled
.
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'ESP32 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.
Ici, il faut créer une fonction SerialEvent()
qui sera invoqué lors de la réception d'un caractère par le port série.
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. Vous devez ajouter une tâche ISR, et la connecter à la tâcheled
par une nouvelle boîte à lettre. - Représenter le graphe de tâches final sur un dessin en utilisant le langage de graphviz (regarder ce graphe bi-parti dont le code est là). C'est un graphe biparti avec des ronds pour les tâches et des rectangles pour les boites à lettres.
Compte rendu
Vous rendrez un seul sketch Arduino avec des commentaires, celui contenant toutes les tâches. Dans le le compte-rendu, en plus des questions, vous insérerez le graphe de votre application.