[[PageOutline]] {{{#!html

Programmation ESP32 / Arduino }}} {{{#!protected - Le [htdocs:files répertoire] contient package eps32.tgz et le json du package. - Vous pouvez tester le composant ESP32 en exécutant quelques exemples de tutoriaux (décrivez succinctement ce que vous avez fait dans le CR). Les exemples sont accessibles }}} = Objectif de la séance Le but de la séance est d'écrire une application multitâches Arduino utilisant plusieurs périphériques. Vous allez écrire une application affichant par exemple la luminosité chaque seconde sur l'écran OLED, tout en faisant clignoter la LED avec un ON/OFF par le bouton poussoir et contrôler le buzzer en faisant dépendre sa fréquence de la lumière (s'il y a un buzzer sur votre maquette) ou d'une action du bouton ou encore d'une valeur entrée au clavier du terminal. En conséquence, vous allez : - 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 (sur la broche 36) - Utiliser le buzzer (sur la broche 17) Vous pouvez écrire plusieurs sketchs, chacun gérant un seul périphérique, mais l'application finale contiendra toutes les tâches pour tous les périphériques (ceux que vous aurez traités). Vous rendrez une archive avec : 1. un compte rendu au format markdown (ou en pdf) expliquant en détail votre sketch Arduino, l'idée est de vous approprier cette méthode de programmation, alors les explications que vous faites sont utiles pour vous. Vous devez aussi insérez un graphe représentant les tâches et leur communication (avec graphviz). 1. le sketch Arduino avec des commentaires, celui contenant toutes les tâches. = Installation ESP32 / Arduino Si vous êtes sur votre ordinateur personnel, vous allez devoir installer l'IDE Arduino et installer les bibliothèques permettant d'utiliser l'ESP32. Vous devez vous rendre à l'adresse `https://www.arduino.cc/en/Main/Software` et suivre la procédure correspondant à votre OS (Linux, Windows ou MacOS). Pour les machines du laboratoire, vous devez utiliser la version d'arduino qui se trouve dans `/opt/arduino-1.8.8/`. Vous pouvez appeler arduino directement : {{{#!bash > /opt/arduino-1.8.8/arduino }}} Je vous conseille d'ajouter le chemin `/opt/arduino-1.8.8` dans la variable PATH dans le `.bashrc`. {{{#!bash Dans le .bashrc : export PATH=/opt/arduino-1.8.8:$PATH }}} **Installation des librairies ESP32** Que vous soyez sur votre ordinateur personnel ou sur une des machines du réseau, les modules ESP32 ne sont pas disponibles par défaut dans l'IDE Arduino, il faut les ajouter, voici la procédure à suivre pour ajouter les gestionnaires de cartes à base d'ESP32 : * Lancer `Arduino` * Cliquer sur `Fichier > Préférences` * Dans `URL de gestionnaire de cartes supplémentaires`, écrire :[[BR]] `https://dl.espressif.com/dl/package_esp32_index.json` * Dans l'onglet voisin (réseau ou Network), cliquez sur `Configuration manuelle...` et pour proxy: `asim.lip6.fr` et port `3128` (ne mettez pas votre login et password). * 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 et cela prend de la place dans le répertoire $HOME/.arduino, vous risquez de dépasser votre quota et donc vous devrez peut-être faire du ménage) * Cliquer sur `Fermer` **Les documents utiles se trouvent** - [https://randomnerdtutorials.com/installing-the-esp32-board-in-arduino-ide-windows-instructions/ Tutoriel pour l'installation des bibliothèque ESP32] - [https://github.com/adafruit/Adafruit_SSD1306 Repository API Ecran OLED] - [https://github.com/adafruit/Adafruit-GFX-Library Repository API Graphique] - [http://www.mon-club-elec.fr/pmwiki_reference_arduino/pmwiki.php?n=Main.ReferenceMaxi Langage Arduino] = Vérification de l'environnement 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 &` (ou directement `arduino&` si vous avez changer le `$PATH`) - Sélectionner : `Tools -> Boards -> TTGO-Lora-OLED V1` - Sélectionner : `Tools -> Ports -> /dev/ttyUSB0 ou /dev/ttyUSB1` (ça change parfois à chaque branchement) - 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 `✓` 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 - Vous pouvez changer la fréquence de clignotement dans la fonction `loop()` pour vous convaincre que c'est bien votre code qui s'exécute. = Exécution ''multi-tâches'' == Tâches standards Vous allez 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 fonction `loop()`. - 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 `Tache_t`. Cette structure est passée en argument de la tâche des fonctions `setup_Tache()` et `loop_Tache()`. Les fonctions `loop_Tache()` et `setup_Tache()` peuvent avoir des variables locales mais leur état n'est pas conservé entre deux instances d'exécutions. Elles peuvent aussi avoir des variables static mais ces variables ont une valeur unique même si la tâche est en plusieurs exemplaires et que donc elle utilise la même fonction `loop_Tache()`. Les variables static ne sont pas conseillées parce qu'elles ne peuvent être initialisées dans la fonction `setup_Tache()`. Expliquez pourquoi dans le CR pour vérifier que vous avez compris. La structure contexte ressemble à : {{{#!c struct Tache_s { unsigned int etat; int config; }; struct Tache_s T1, T2; // 2 contextes pour 2 instances de tâches ayant le même comportement }}} C'est la fonction `setup_Tache()`qui initialise le contexte avec des paramètres. {{{#!c void setup_Tache(struct Tache_s *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 (une famine pour les autres tâches). 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 : {{{#!c void loop_Tache(tache_t *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 dans les applications embarquées), 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 (c'est un identifiant, il en faudra autant que de tâches périodiques défini dans le `#define MAX_WAIT_FOR_TIMER`). Le second est une période en microsecondes. `wairFor()` peut être appelée aussi souvent que l'on veut, 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. Si vous écrivez `waitFor(12,100)`, vous avez au moins 13 tâches périodiques allant de 0 à 12 et donc vous avez `#define MAX_WAIT_FOR_TIMER 13` ou plus. == 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. Ici, les tâches ne communiquent pas entre elles. VOus pouvez modifier le programme initial pour afficher "Salut" en plus de "bonjour" toutes les 2 secondes sans changer le comportement existant. Vous aurez donc "Salut" et "bonjour" qui s'affiche avec une périodicité propre à chaque message. {{{#!c // -------------------------------------------------------------------------------------------------------------------- // 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ériodes écoulées 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_s { 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_s * 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_s * 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_s { int timer; // numéro de timer utilisé par WaitFor unsigned long period; // periode d'affichage char mess[20]; } ; void setup_Mess( struct Mess_s * 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_s *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_s Led1; struct Mess_s Mess1; //--------- Setup et Loop void setup() { setup_Led(&Led1, 0, 100000, LED_BUILTIN); // 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** Mettez ces réponses dans votre CR (avec les questions pour que le CR soit lisible) - 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 ? = Utilisation de l'écran OLED Nous allons utiliser un écran OLED connecté en I2C, 128x64 **ssd1306**. Vous devez ajouter les fonctions d'accès à cet écran, c'est en fait son **driver**, mais contrairement à UNIX, l'API des drivers n'est pas standard, chaque périphérique propose ces fonctions et ces codes sont proposées, soit par les constructeurs, soit par des membres de la communauté. - Allez dans `Outils/Gérer les bibliothèques` (`Tools/Manage Libraries`) - Tapez dans la barre de recherche à droite de la fenêtre : `ssd1306 Adafruit` (la 1re de la liste) et installez la version `1.3.0` (celle-là fonctionne pour moi). Vous devez prendre également la bibliothèque graphique GFX en Tapant `Adafruit GFX Library` (la 2me de la liste) et installez la version `1.10.14` - Vous devez aussi installer la library `BUSIO Adafruit` - 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 : 128x64 I2C. - Pour tester la librairie rendez-vous dans `File -> Exemples -> Adafruit SSD1306 -> ssd1306_128x64_i2c`. Il s'agit d'un programme qui teste les fonctionnalité de l'écran et de la bibliothèque graphique. > IMPORTANT (merci Shaithan)\\ > Le composant OLED utilise I2C + un signal de reset\\ > Les broches par défaut utilisé par de code de test correspond au module Arduino "officiel" et pas à l'ESP32\\ > En conséquence, vous devez modifier une ligne et en ajouter une : > * au début du fichier modifier (4 → 16) : `#define OLED_RESET 16 // Reset pin...`` > * Dans la fonction setup() ou setup_oled() (au début avant le begin de oled) : `Wire.begin(4, 15); // pins SDA , SCL` **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 nommé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. {{{#!c enum {EMPTY, FULL}; struct mailbox_s { int state; int val; }; struct mailbox_s mb = {.state = EMPTY}; void loop_T1(... mailbox_t * mb ...) { if (mb->state != EMPTY) return; // attend que la mailbox soit vide mb->val = 42; mb->state = FULL; } void loop_T2(... mailbox_t * 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 [...] (par `analogRead()`) 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âche `oled`. 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 de `lum`, l'une vers `oled` l'autre vers `led`. = 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 ce système, 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âche `led` par une nouvelle boîte à lettre. - Représenter le graphe de tâches final sur un dessin en utilisant le langage de [http://www.graphviz.org/ graphviz] (regarder [https://graphviz.gitlab.io/_pages/Gallery/directed/traffic_lights.html ce graphe bi-parti]. C'est un graphe biparti avec des ronds pour les tâches et des rectangles pour les boites à lettres. = Tout ensemble Le but est de faire une application avec le maximum de tâches afin de voir l'intérêt de ce système. Vous allez ajouter le buzzer et le bouton poussoir pour complexifier votre application. Le buzzer est branché sur la broche 17 et le BP sur la broche 23 (il n'y a pas de résistance de pullup mais il y en a une dans l'ESP32 que vous devez demander). Pour faire sonner le buzzer vous devez lui envoyer une tension périodique correspondant à la fréquence que vous voulez entendre, vous pouvez utiliser WaitFor() ou le module PWN (utilisable avec cette broche, je vous laisse chercher). {{{ #!comment Pour le moment, ça ne marche pas. = Utilisation du détecteur de geste APDS-9960 (optionnel) le détecteur de geste APDS-9960 permet de détecter des mouvement de la main par exemple gauche -> droite, droite -> gauche. C'est un composant I2C. Nous le verrons pour le micro-projet mais si vous voulez, vous pouvez le tester dès maintenant. Je vous propose d'ajouter une tâche qui arrête le clignotement de la LED si vous faite un déplacement droite -> gauche et qui le redémarre avec un déplacement gauche -> droite. Vous allez commencer par récupérer la library et le programme de test à partir du site : https://www.gotronic.fr/art-capteur-rgb-mouvement-apds9960-22744.htm Puis intégrer une nouvelle tâche dans votre application. }}}