Outils pour utilisateurs

Outils du site


cesitd3

TD 3 Informatique Embarquée CESI

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

Objectifs du TD

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.

Cadencement de la MAE

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.

cadence.ino
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 >>>>>>>>>
  }
.....
}

Interaction de la MAE avec l’extérieur via ses Entrées/Sorties

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

Règles de nommage

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 :

  • EntreeC7 désigne le bit 7 de EntreeC.
  • Sortie2 désigne le bit 2 de l'unique bus de sortie.

Les valeurs des entrées et sorties sont fournies et récupérées de la MAE avec un code tel que :

echange.ino
....
mae.setEntree(e)
....
s=mae.getSortie() 
....

Si plusieurs attributs d'E/S sont nécessaires :

  • soit utiliser plusieurs accesseurs/manipulateurs ( ex: setEntreesA(), …, setEntreesN())
  • soit passer par des types sur plus de bits (ex: unsigned long int mae.getSortie(); )
  • soit passer les paramètres en E/S via pointeurs (ex: mae.getSorties(&sortiesA,&sortiesB….) )

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 :

  • de tester si (Entree3 et Entree2 et not Entree0) est Vrai en faisant
(Entree&0xD)==0xA
  • de tester si EntreeB6..4>2 en faisant
((EntreeB>>4)&0x7)>2
  • de piloter une sortie à partir d'une entrée, SortieC4=EntreeB2 s'écrit:
SortieC=(SortieC&0xEF) | ((EntreeB&0x4)<<2);
  • de piloter plusieurs sorties à partir de plusieurs entrées, SortieC7..4=EntreeB3..0 s'écrit:
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)

Types de sorties

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.

Actions A Mise à Zéro Implicite (AMZI)

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 !!!

Actions A Mise à Zéro Explicite (AMZE)

Exemple:

Le S,R ou M précédent l'action indique qu'il s'agit d'une AMZE:

  • S indique “Set”: mise à 1 de la sortie (éventuellement conditionnée)
  • R indique “Reset”: mise à 0 de la sortie (éventuellement conditionnée)
  • M indique “Mémorisation”: sauvegarde la valeur de quelque chose jusqu'à nouvel ordre

Sur l'exemple:

  • sur la transition de l'état 0 à 1, Sortie0 mise à 1 si entree3 est à 1, sinon Sortie1 est inchangée
  • sur l'état 2, Sortie0 est mise à 0
  • sur la transition de l'état 3 à 0, Sortie0 mémorise la valeur de entree4

Exercice !

Classe pour les MAE

Nous proposons ici une classe pour implémenter des MAE telles que décrites:

classemae.ino
////////////////////////////////////////////////////////////////////
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;    
};
////////////////////////////////////////////////////////////////////

Exemple d'implémentation d'une MAE

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.

Programme de test

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:

  • toutes les transitions entre états
  • tous les maintiens dans les états
  • toutes les combinaisons d'entrées possibles lorsque l'évolution ou les sorties en dépendent (Nous verrons deux moyens pour réaliser cela).

Afin de nous aider dans l'écriture du programme de test, je vous propose d'utiliser cette fonction:

debugMessage.ino
////////////////////////////////////////////////////////////////////
//! 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:

programmedetest.ino
////////////////////////////////////////////////////////////////////
//! 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() {
}

Utilisation de la MAE sur le composant cible

maesurcible.ino
#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());
  }
}
cesitd3.txt · Dernière modification : 2023/02/15 11:31 de bvandepo