Document de référence de Mr Nketsa à lire chez vous sur les machines à états: https://bvdp.inetdoc.net/files/cesi/td3/architecture_logicielle_mae_info_indust_avancee_2020_2021-p1-7.pdf
Nous avons vu précédemment un exemple de codage d'une 'petite' Machine A Etats. Cela nous a permis d'illustrer les concepts de Programmation Orientée Objet. Nous allons maintenant plus loin pour permettre de piloter des systèmes plus complexes. Nous allons privilégier la généricité (capacité à traiter un large panel de cas) à la performance (occupation mémoire et temps CPU). Pour gagner en performance, il serait possible d'intriquer le code de changement de l'état et celui du pilotage des sorties mais cela rendrait compliqué la gestion de certains cas, que la méthode proposée ici permet de traiter.
Dans ce TD nous aborderons également une notion importante: LE TEST UNITAIRE. Ceci consiste à écrire un programme dédié permettant de vérifier le bon fonctionnement d'un composant logiciel. Après avoir défini la procédure de test, nous montrerons comment faire en sorte que le composant logiciel puisse être testé sur une autre cible que celle sur laquelle il devra s'exécuter en exploitation.
Finalement, nous verrons comment il est aisé d'intégrer ce composant logiciel sur la cible (microcontrôleur Arduino) une fois que tout aura bien été préparé en amont.
Dans le TD précédent, le code de la MAE était exécuté lorsque le processeur avait le temps, c'est-à-dire que la cadence d’échantillonnage des entrées, d'évolution de l'état et de pilotage des sorties n'était pas garantie. On parle alors d'une MAE asynchrone.
Nous allons, dans ce TD, cadencer la MAE pour (tenter d') imposer une fréquence de fonctionnement. Ceci peut être réalisé soit à l'aide d'un timer par scrutation ou bien par le mécanisme d'interruption. Le code suivant illustre l'utilisation du timer de l'Arduino par scrutation pour le cadencement, avec une détection de retard trop important, qui permet au système de se rendre compte si la cadence souhaitée n'est pas obtenue.
void loop(){ ..... unsigned int periodicite=10; static unsigned long timer = millis(); if (millis() - timer >= (periodicite*1.2)) digitalWrite(LEDRETARD,HIGH); //allume une LED dès que la MAE est en //retard de plus de 20% d'une période d'horloge if (millis() - timer >= periodicite) { timer += periodicite; <<<<<<<<<<appel de la MAE ici >>>>>>>>> } ..... }
Contrairement au TD précédent, le code de la MAE de ce TD ne va contenir aucun code d'accès aux périphériques tels que les entrées/sorties. Au lieu de cela, ces E/S seront échangées avec la MAE via des méthodes manipulateurs et accesseurs, et le code de la MAE manipulera uniquement les attributs correspondants comme des variables. Le gros avantage de cette approche est que le code de la MAE ne contient pas de code dépendant du matériel : il est possible de le compiler et de le tester hors ligne, et même pour différentes cibles (PC, µc, SOC).
Nous allons donc utiliser des attributs pour les E/S. Il est décidé ici d'utiliser le suffixe du nom d'une entrée pour indiqué le numéro de bit correspondant. Dans le cas ou plusieurs bus d'entrées ou sorties sont nécessaires, il est décidé d'utiliser une lettre en fin de nom pour identifier le bus dans le cas ou plusieurs sont nécessaires.
Par exemple :
Les valeurs des entrées et sorties sont fournies et récupérées de la MAE avec un code tel que :
.... mae.setEntree(e) .... s=mae.getSortie() ....
Si plusieurs attributs d'E/S sont nécessaires :
Dans le code, l'exploitation des entrées et sorties se fait à l'aide des opérateurs de masquage et de décalage et avec des valeurs numériques. Par exemple, il est possible :
(Entree&0xD)==0xA
((EntreeB>>4)&0x7)>2
SortieC=(SortieC&0xEF) | ((EntreeB&0x4)<<2);
SortieC=(SortieC&0x0F) | ((EntreeB&0xF)<<4);
Si besoin (car la complexité du système le requière), la MAE peut utiliser et calculer des variables temporaires :
char EntreeC3=(EntreeC>>3)&1; SortieB= (SortieB7<<7) | .... | (SortieB0<<0);
Il est également possible d'utiliser des méthodes manipulateurs et accesseurs pour agir individuellement sur les bits des bus d'entrées et sorties, par exemple :
void setEntreeBit(unsigned char nbit,unsigned char entreeval) unsigned char getSortieBit(unsigned char nbit)
Les sorties d'un système type microprocesseur (que ce soit les variables ou même les sorties physiques GPIO) sont mémorisées par construction. Nous allons décrire des actions du modèle de MAE qui sont soit à mise à zéro implicite (c'est à dire non mémorisée) soit à mise à zéro explicite (c'est à dire mémorisée) et nous montrons ici comment coder ces comportements. Une sortie est soit AMZI soit AMZE et conserve son status dans toute la MAE.
Exemple :
Le I précédent l'action indique qu'il s'agit d'une AMZI.
Cet exemple montre la nécessité de décrire les sorties une par une et de mélanger le codage des actions sur états et sur transitions : exercice !!!
Exemple:
Le S,R ou M précédent l'action indique qu'il s'agit d'une AMZE:
Sur l'exemple:
Exercice !
Nous proposons ici une classe pour implémenter des MAE telles que décrites:
//////////////////////////////////////////////////////////////////// class CStateMachine //déclaration de la classe { //! membres accessibles depuis l'extérieur de la classe, il s'agit de l'interface d'interaction de la classe public: //! Constructeur CStateMachine(); //! méthode pour redémarrer la machine à état void reset(); //! méthode pour cadencer la machine à état (faire 1 coup d'horloge) void clock(); //! méthode manipulateur pour fournir les entrées void setEntree(unsigned char entreeval) { entree=entreeval;} //! méthode manipulateur pour fournir les entrées individuellement void setEntreeBit(unsigned char nbit,unsigned char entreeval) { entree= (entree&~(1<<nbit)) | ((entreeval&1)<<nbit); } //Serial.println(entree,HEX); //! méthode accesseur pour accéder aux sorties unsigned char getSortie() { return sortie;} //! méthode accesseur pour accéder à une sortie individuellement unsigned char getSortieBit(unsigned char nbit) { return (sortie>>nbit)&1;} //! méthode accesseur pour accéder à l'état courant unsigned char getEtat() { return etat;} //! membres privés pour réaliser l'encapsulation: ces attributs sont inacessibles directement depuis l'extérieur de la classe private: //! numéro de l'état actif unsigned char etat; //! valeur des entrées unsigned char entree; //! valeur des sorties unsigned char sortie; }; ////////////////////////////////////////////////////////////////////
Implémentons l'exemple de MAE suivant:
L'implémentation de cette MAE peut être : Exercice !
La déclaration de la classe et son implémentation peuvent être rangés dans 2 fichiers .h et .cpp comme nous l'avons fait au TD précédent afin de permette d'inclure cette MAE en tant que librairie dans un programme.
Nous allons maintenant procéder au test de notre composant MAE, comme vous l'avez fait pour des composants VHDL. Vous l'avez fait via des chronogrammes, mais la méthode habituelle consiste plutôt à écrire un programme de test appelé testbench. Nous allons ici écrire un programme de test qui va valider le bon fonctionnement (au moins au niveau logiciel) de notre MAE et permettre de reproduire des tests à l'identique pour pouvoir reproduire des erreurs et les traiter. Ces tests permettent notamment de s'assurer qu'un composant logiciel conserve son fonctionnement nominal après que des modifications aient été appliquées.
Pour faire ce test, nous allons générer des valeurs sur les entrées de la MAE via le programme pour faire évoluer l'état et les sorties, et vérifier que les valeurs obtenues sont bien conformes à celle attendues. Ce programme doit être le plus exhaustif possible et dans le cas du test d'une MAE cela se traduit par le fait qu'il doit faire apparaître:
Afin de nous aider dans l'écriture du programme de test, je vous propose d'utiliser cette fonction:
//////////////////////////////////////////////////////////////////// //! fonction d'affichage d'un message de debug bien pratique, //! qui affiche un message texte, le nom du fichier et le numéro de la ligne //! depuis où la fonction a été appelée void debugMessage(const char * chaineMsg,const char * chaineFile,const unsigned int line){ Serial.print("DEBUG "); Serial.print(chaineFile); Serial.print(" : l "); Serial.print(line); Serial.print(" : "); Serial.print(chaineMsg); Serial.println(); } ////////////////////////////////////////////////////////////////////
Cette fonction sera utilisée de la manière suivante pour afficher facilement un message et identifier automatiquement la ligne et le fichier dans lequel le problème s'est produit:
if ( Quelque chose n'est pas normal) debugMessage("Erreur: explication", __FILE__, __LINE__);
Voici le programme de test qui peut être utilisé pour tester la MAE:
//////////////////////////////////////////////////////////////////// //! Un programme pour tester notre composant (logiciel) void programmeDeTest1(){ //! Le composant à tester CStateMachine mae; Serial.print("Test lancé le "); Serial.print(__DATE__ ); Serial.print(" à "); Serial.println(__TIME__ ); Serial.print("Fonction de test: "); Serial.println(__func__); //boucle pour gérer les différents cas à tester for (unsigned int ntest=0;ntest<8;ntest++){ Serial.print("début du test numéro: "); Serial.println(ntest); mae.reset(); mae.setEntree(0); /* mae.setEntreeBit(0,1); mae.setEntreeBit(1,1); mae.setEntreeBit(2,1); mae.setEntreeBit(3,1); mae.setEntreeBit(0,0); mae.setEntreeBit(2,0); mae.setEntreeBit(1,0); mae.setEntreeBit(3,0); */ //test du maintien dans l'état 0 mae.clock(); //ici on teste la mae sans considération de timing, donc clock() n'est pas conditionné à un timer mae.clock(); //la MAE doit être dans l'état 0 if (mae.getEtat()!=0) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); mae.setEntreeBit(0,1); //équivalent ici à mae.setEntree(1); mae.clock(); //transition de 0 à 1 if (mae.getEtat()!=1) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); if (mae.getSortieBit(2)!=0) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); mae.reset(); //ici pour faire le test en fonction des 2 valeurs de entree2 je reset la mae //on verra plus bas une manière plus générale de tester 2 cas if (mae.getEtat()!=0) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); mae.setEntreeBit(2,1); //met entree2 à 1 pour tester la mémorisation d'un 1 sur sortie2 mae.clock(); //transition de 0 à 1 if (mae.getSortieBit(2)!=1) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //test du maintien dans l'état 1 //bloqué dans l'état 1 tant que entree2 est à 1 //verifie si sortie1 recopie bien entree1 mae.setEntreeBit(1,1); mae.clock(); if (mae.getSortieBit(1)!=1) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); mae.setEntreeBit(1,0); mae.clock(); if (mae.getSortieBit(1)!=0) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //transition de etat 1 vers état 2 // avec mémorisation de entree0 qui vaut 0 ou 1 dans sortie3, //il faut faire 2 tests, donc j'utilise le bit 0 de ntest pour cela mae.setEntreeBit(0,((ntest>>0)&1)); //pour régler la valeur à mémoriser mae.setEntreeBit(2,0); //pour déclencher la transition mae.clock(); if (mae.getSortieBit(3)!=((ntest>>0)&1)) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //test du maintien dans l'état 2 mae.setEntreeBit(2,0); mae.setEntreeBit(0,0); mae.clock(); mae.clock(); if (mae.getEtat()!=2) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); //transition de etat 2 vers etat3 mae.setEntreeBit(0,1); mae.clock(); if (mae.getEtat()!=3) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); //vérifie si sortie0 recopie bien entree1 //il faut faire 2 tests, donc j'utilise le bit 1 de ntest pour cela mae.setEntreeBit(1,((ntest>>1)&1)); mae.clock(); //test du maintien dans l'état 3 //la mae doit être maintenue dans l'état 3 et sortie0 doit être égale à entree1 if (mae.getEtat()!=3) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); if (mae.getSortieBit(0)!=((ntest>>1)&1)) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //transition de etat 3 vers etat 0, on teste les 2 cas de recopie de sortie1 , avec entree2= bit 2 de ntest mae.setEntreeBit(0,0); mae.setEntreeBit(2,((ntest>>2)&1)); mae.clock(); if (mae.getEtat()!=0) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); if (mae.getSortieBit(1)!=((ntest>>2)&1)) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); mae.clock(); //sortie1 est une AMZI, elle doit revenir à 0 if (mae.getSortieBit(1)!=0) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //revenons à l'état 2 pour tester la transition vers l'état 0, //durant cette transition, il faut remettre à 0 la sortie3, donc on va s'arranger pour la mémoriser à 1 dans la //transition de 1 vers 2 mae.setEntreeBit(0,1); mae.setEntreeBit(1,0); mae.clock(); mae.setEntreeBit(2,0); mae.setEntreeBit(0,1); //on mémorise un 1 dans sortie 3 mae.clock(); if (mae.getEtat()!=2) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); mae.setEntreeBit(0,0); mae.setEntreeBit(2,1); mae.clock(); if (mae.getEtat()!=0) debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__); if (mae.getSortieBit(3)!=0) debugMessage("Erreur: La sortie n'est pas à la valeur prévue", __FILE__, __LINE__); //Ce test n'est même pas exhaustif car nous n'avons pas testé si les AMZI reviennent bien toutes à 0 //et si les AMZE mémorisent bien //et si les entrées autres que celle écrites sur le modèle n'ont pas un impact (par exemple que fait l'entree1 dans l'état 2) } Serial.println("fin du test"); //vidage de la FIFO d'affichage: //flush(std::cout); } //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// void setup() { Serial.begin(115200); //Normalement le programme de test est un programme à part programmeDeTest1(); } //////////////////////////////////////////////////////////////////// void loop() { }
#include "fichierdelamae.h" //le code de la MAE #include "lib_io_tp.h" //le code de la MAE ne dépend pas des librairies d'entrées/sorties //fournit les fonctions unsigned char readPort(void); // et void writePort(unsigned char value) {} //////////////////////////////////////////////////////////////////// CStateMachine maeUtile; //instanciation de la MAE //////////////////////////////////////////////////////////////////// void setup() { Serial.begin(115200); } //////////////////////////////////////////////////////////////////// void loop() { unsigned int periodicite=10; static unsigned long timer = millis(); if (millis() - timer >= periodicite) { timer += periodicite; maeUtile.setEntree(readPort()); maeUtile.clock(); writePort(maeUtile.getSortie()); } }