Table des matières

Solution du début du TD promo 1 2024-2025

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!

TD 2 Informatique Embarquée CESI

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.

Schéma du montage utilisé dans le 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:

diagram.json
{  "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).

Analyse des chronogrammes et présentation du modèle de machine à états

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:

Implémentation en C

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.

maec.ino
 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.

Implémentation en C++ avec l'approche objet

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.

Interface d'interaction de la classe

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.

Attributs internes

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.

Classe CStateMachineRotary

Discutons sur la classe suivante:

classe.ino
////////////////////////////////////////////////////////////////////
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 “.”

Instanciation multiple de CStateMachineRotary

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:

affi.ino
Serial.print(encoder1.getPosition());
Serial.print(" ");
Serial.print(encoder2.getPosition());
Serial.println();

Extension pour réaliser la mesure de vitesse angulaire

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).

classespeed.ino
////////////////////////////////////////////////////////////////////
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.

Utilisation de 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:

affi2.ino
Serial.print(encoder1.getPosition());
Serial.print(" ")
Serial.println(encoder2.getPosition());
Serial.print(" ")
Serial.println(encoder2.getSpeed());

Ajout de la fonction de gestion du bouton de l'encodeur

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é.

Ajout d'une fonctionnalité à l'ensemble des classes

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.

Organisation des fichiers pour faire une librairie

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:

libencodeur.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:

libencodeur.cpp
//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:) .