Ouvrir:
https://wokwi.com/projects/412530738901878785
et cliquer sur la petite flèche à droite de Save, puis “save a copy” pour obtenir une autre URL!
Nous allons continuer à utiliser Wokwi pour ce TD, les consignes données en cesitd1 et en cesitp1 sont donc toujours valables. Commencez par vous authentifier sur https://wokwi.com/ puis créez un nouveau projet pour la carte Arduino UNO R3. Vous démarrerez ce TD avec ce fichier de code que vous remplierez au fur et à mesure du TD.
Pour reproduire cette schématique dans votre projet, copier coller le contenu du fichier suivant dans l'onglet diagram.json en écrasant le contenu antérieur:
{ "version": 1, "author": "Bertrand Vandeportaele", "editor": "wokwi", "parts": [ { "type": "wokwi-arduino-uno", "id": "uno", "top": 128, "left": 20, "attrs": {} }, { "type": "wokwi-logic-analyzer", "id": "logic1", "top": 165.29, "left": 365.03, "attrs": {} }, { "type": "wokwi-led", "id": "leda", "top": 380.34, "left": 236.43, "attrs": { "color": "red", "label": "canal A" } }, { "type": "wokwi-slide-switch", "id": "swa", "top": 400.11, "left": 288.25, "rotate": 90, "attrs": {} }, { "type": "wokwi-led", "id": "ledb", "top": 430.34, "left": 236.43, "attrs": { "color": "red", "label": "canal B" } }, { "type": "wokwi-slide-switch", "id": "swb", "top": 450.11, "left": 288.25, "rotate": 90, "attrs": {} }, { "type": "wokwi-ky-040", "id": "encoder1", "top": 346.34, "left": 23.32, "attrs": {} } ], "connections": [ [ "logic1:GND", "uno:GND.2", "black", [ "h0" ] ], [ "swa:3", "leda:C", "black", [ "v10", "h0" ] ], [ "swa:2", "leda:A", "green", [ "h0" ] ], [ "uno:GND.3", "leda:C", "black", [ "v0" ] ], [ "swa:1", "uno:5V", "red", [ "h0" ] ], [ "swa:2", "uno:A4", "green", [ "h0" ] ], [ "swb:3", "ledb:C", "black", [ "v10", "h0" ] ], [ "swb:2", "ledb:A", "green", [ "h0" ] ], [ "uno:GND.3", "ledb:C", "black", [ "v0" ] ], [ "swb:1", "uno:5V", "red", [ "h0" ] ], [ "swb:2", "uno:A5", "green", [ "h0" ] ], [ "logic1:D0", "uno:A4", "green", [ "h0" ] ], [ "logic1:D1", "uno:A5", "green", [ "h0" ] ], [ "encoder1:CLK", "uno:A0", "green", [ "h0" ] ], [ "encoder1:DT", "uno:A1", "green", [ "h0" ] ], [ "encoder1:SW", "uno:A2", "green", [ "h0" ] ], [ "encoder1:VCC", "uno:5V", "red", [ "h0" ] ], [ "encoder1:GND", "uno:GND.3", "black", [ "h0" ] ], [ "logic1:D5", "uno:A0", "green", [ "h0" ] ], [ "logic1:D6", "uno:A1", "green", [ "h0" ] ], [ "logic1:D7", "uno:A2", "green", [ "h0" ] ] ] }
Ce schéma comporte une carte Arduino UNO R3 et un analyseur logique comme utilisé en TP. Il comporte également 2 interrupteurs (reliés aux broches A4 et A5 à des LEDs pour facilement voir leurs états) qui vont nous permettre de simuler finement un composant appelé Encodeur en quadrature. Le schéma comporte également un vrai composant encodeur (reliés aux broches A0 et A1) contenant également un bouton poussoir (relié à la broche A2).
Analysons les chronogrammes sur cette image tirée de la documentation du composant encodeur simulé sur Wokwi ( https://docs.wokwi.com/parts/wokwi-ky-040 ):
Dans la suite nous appellerons A la valeur du signal CLK et B la valeur du signal DT.
Nous proposons d'utiliser une “machine à états” (abrégée MAE) pour effectuer le traitement des signaux générés par l'encodeur afin de déterminer la position angulaire (par INCrémentations et DECrémentations d'une variable) de ce dernier (et plus tard la vitesse de rotation). Le modèle graphique de cette MAE est visible ci-dessous et nous allons dans le TD l'implémenter, d'abord en langage C, puis en C++ avec l'approche Orientée Objet, puis nous l'améliorerons. Vous devrez sauver les différentes versions pour pouvoir les comparer à posteriori.
Image tirée de https://hal.archives-ouvertes.fr/hal-02021357/document où vous pourrez trouver l'implémentation VHDL de ce modèle pour comparaison:
Dans cette première implémentation, nous utiliserons des variables globales pour stocker l'état en cours et les valeurs lues sur les entrées. Les périphériques ainsi que variables globales devront être initialisées dans la fonction setup(). L'implémentation de la MAE se fera directement dans la fonction loop() (au bon endroit vis à vis des commentaires fournis) et la valeur de position angulaire mesurée sera affichée à l'aide de Serial.println() vers la console série.
byte etat; //numéro de l'état actif byte entreeA; //valeur lue sur l'entrée A byte entreeB; //valeur lue sur l'entrée B int brocheA; //numéro de broche Arduino utilisée pour l'entrée A int brocheB; //numéro de broche Arduino utilisée pour l'entrée B int position; //position angulaire mesurée void setup() { Serial.begin(115200); //configurer et initialiser ce qui doit l'être } void loop() { //lecture et mémorisation des entrées //actions sur transition //évolution de l'état switch (etat){ case 0: if ... etat=...; else if .... etat=....; break; default: } //actions sur état //affichage de la valeur mesurée 10 fois par seconde unsigned int periodiciteTache1=100; static unsigned long timerTache1 = millis(); if (millis() - timerTache1 >= periodiciteTache1) { timerTache1 += periodiciteTache1; Serial.println(position); } }
Dessiner la vue composant du système et l'implémenter. Vérifier le bon fonctionnement à l'aide de wokwi2gtkwave sur le jeu de 2 interrupteurs puis sur le vrai encodeur. Analyser les différences et en tirer une conclusion sur la robustesse du système vis à vis des rebonds mécaniques.
Vous avez dû préalablement visionner la vidéo de cours présentant les concepts de la Programmation Orientée Objet: https://youtu.be/5Y9P4MIkTIs
Et lire le polycopié du cours correspondant pour comprendre les termes utilisés ici: https://bvdp.inetdoc.net/files/iut/cours_POO_intro_complet_2021_numbered.pdf
Sauvez le projet sous un nouveau nom. Nous allons maintenant adapter le code pour le transformer en une Classe et en faire une librairie Objet.
L'interface d'interaction de la classe permet de créer une instance de la classe et de lui demander de faire des actions ou bien d'informer de son état.
L'état interne du composant doit être protégé par le mécanisme de l'encapsulation afin qu'il ne puisse être modifié ou lu uniquement lors d'un usage prévu par le concepteur de la classe.
Discutons sur la classe suivante:
//////////////////////////////////////////////////////////////////// class CStateMachineRotary //déclaration de la classe { public: //membres accessibles depuis l'extérieur de la classe, il s'agit de l'interface d'interaction de la classe CStateMachineRotary(int brocheAinit, int brocheBinit); //Constructeur avec paramètres pour régler les 2 broches Arduino utilisées void clock(); //méthode pour cadencer la machine à état (faire 1 coup d'horloge) int getPosition()//méthode accesseur pour accéder à l'attribut privé position { return position;} //on parle de méthode inlinée car l'implémentation est faite dans la déclaration de la classe private: //membres privés pour réaliser l'encapsulation: ces attributs sont inacessibles directement depuis l'extérieur de la classe byte etat; //numéro de l'état actif byte entreeA; //valeur lue sur l'entrée A byte entreeB; //valeur lue sur l'entrée B int brocheA; //numéro de broche Arduino utilisée pour l'entrée A int brocheB; //numéro de broche Arduino utilisée pour l'entrée B virtual void frontDetecte(){}; //methode abstraite, sera implémentée dans la classe fille virtual void actionSurTousLesEtats(){}; //methode abstraite, sera implémentée dans la classe fille protected: //ces membres peuvent être accédés dans les classes filles int position; //position angulaire mesurée }; //ne pas oublier le ; //////////////////////////////////////////////////////////////////// //Implémentation de la méthode constructeur CStateMachineRotary::CStateMachineRotary(int brocheAinit, int brocheBinit){ // à compléter } //Implémentation de la méthode clock void CStateMachineRotary::clock(){ // à compléter }
Vous allez maintenant devoir implémenter le constructeur de la classe ainsi que la méthode clock(). Rassurez vous, le travail a été déjà réalisé à l'exercice précédent et il s'agit principalement de faire du couper/coller.
Vous allez également devoir construire une instance de cette classe de portée globale à l'aide d'une déclaration:
CStateMachineRotary encoder1(A4,A5);
et adapter le programme précédent pour qu'il utilise la classe. Pour rappel l'invocation des méthodes d'encoder1 se fait à l'aide de l'opérateur “.”
Nous illustrons ici une des notions de “ré-utilisabilité” permise par l'approche objet: l'instanciation multiple.
Après avoir testé le bon fonctionnement de l'objet encoder1 puis ajouté au programme une instance encoder2 utilisant les broches A0 et A1. Utiliser le code suivant pour afficher la position des 2 encodeurs:
Serial.print(encoder1.getPosition()); Serial.print(" "); Serial.print(encoder2.getPosition()); Serial.println();
Nous illustrons ici une autre notion de “ré-utilisabilité” permise par l'approche objet: l'héritage qui va permettre de créer une nouvelle classe (classe fille) bénéficiant des fonctionnalités de la classe dont elle hérite (classe mère).
//////////////////////////////////////////////////////////////////// class CStateMachineRotaryWithSpeed: public CStateMachineRotary { public: CStateMachineRotaryWithSpeed(int brocheAinit, int brocheBinit); float getSpeed(); //méthode accesseur pour accéder à l'attribut privé speed private: float speed; //vitesse mesurée unsigned long lastPulseTime; //horodatage du dernier front void frontDetecte(); //implémentation pour effectuer des traitements lorsqu'un front est detecté }; //////////////////////////////////////////////////////////////////// //Implémentation du constructeur de la classe fille, exécutée après avoir exécuté le constructeur de la classe mère CStateMachineRotaryWithSpeed::CStateMachineRotaryWithSpeed(int brocheAinit, int brocheBinit): CStateMachineRotary(brocheAinit, brocheBinit){ lastPulseTime=micros(); //initialisation de la date initiale pour l'horodatage des fronts speed=0; //initialise vitesse nulle au démarrage. Attention, en simu, l'encodeur génère des fronts parasites } //////////////////////////////////////////////////////////////////// //implémentation de la méthode frontDetecte() qui n'était pas implémentée dans la classe mère void CStateMachineRotaryWithSpeed::frontDetecte() { // à compléter } //implémentation de la mesure de vitesse float CStateMachineRotaryWithSpeed::getSpeed(){ //à faire: calculer la vitesse et l'affecter à l'attribut speed return speed; }
Il faut maintenant implémenter les méthodes frontDetecte() et getSpeed() . Vous allez également ajouter deux appels à frontDetecte(); dans l'implémentation de CStateMachineRotary.clock() lors des changements de position. Ceci n'affectera pas le fonctionnement de CStateMachineRotary car frontDetecte() est vide dans cette classe. Par contre, ceci permet d'ajouter facilement un traitement à chaque front détecté dans la classe fille.
Modifier la déclaration d'encoder2 pour qu'il devienne une instance de la classe CStateMachineRotaryWithSpeed. Utiliser le code suivant pour afficher la position des 2 encodeurs et la vitesse d'encoder2:
Serial.print(encoder1.getPosition()); Serial.print(" ") Serial.println(encoder2.getPosition()); Serial.print(" ") Serial.println(encoder2.getSpeed());
Nous allons à nouveau créer une classe fille CStateMachineRotaryWithSpeedAndReset à partir de la classe CStateMachineRotaryWithSpeed cette fois-ci pour ajouter la réinitialisation de la position angulaire lors de l'appui sur le bouton indépendamment de l'état courant de la machine à état. Débrouillez vous, puis changer le type d'encoder2 en CStateMachineRotaryWithSpeedAndReset en utilisant la broche A2 pour la réinitialisation de la position angulaire et tester la nouvelle fonctionnalité.
Nous souhaitons maintenant ajouter une nouvelle fonctionnalité à l'ensemble des classes, la capacité à régler la position angulaire en cours de fonctionnement. Pour cela, nous allons ajouter une méthode manipulateur void setPosition(int newposition) à la classe mère. Dans loop(), faire en sorte d'augmenter la position automatiquement de 4 toute les 10 secondes.
Il est temps d'organiser le code produit. Nous allons faire une paire de fichiers pour notre librairie afin de contenir les trois classes CStateMachineRotary, CStateMachineRotaryWithSpeed et CStateMachineRotaryWithSpeedAndReset.
Pour ajouter au projet les 2 fichiers de librairie, cliquer sur la flèche à droite de diagram.json puis sur “New file” et saisir libencodeur.h puis faire de même pour libencodeur.cpp
Pour éviter l'inclusion multiple (plusieurs #include du même fichier dans un projet, les macros suivantes sont utilisées pour encadrer les définitions dans le fichier .h:
#ifndef LIB_ENCODER_H #define LIB_ENCODER_H //inclusion des libraires nécessaires au fonctionnement de notre librairie: #include <Arduino.h> // COUPER COLLER ICI Les déclarations des 3 classes #endif
Le fichier cpp doit avoir cette forme:
//inclusion de l'entête de notre libraire pour que les classes soient connues #include "libencodeur.h" // COUPER COLLER ICI Les implémentations des méthodes des 3 classes
Finalement le programme Arduino devra également comprendre un #include “libencodeur.h” afin de pouvoir utiliser notre librairie. Cette librairie pourra dorénavant être utilisée dans d'autres projet simplement en important ces deux fichiers et en incluant le fichier .h
Testez votre projet, bravo si tout fonctionne. Vous pourrez noter que nous n'avons utilisé aucun pointeur dans tout l'exercice:) .