Table des matières

TD 3 Informatique Embarquée CESI

projet vscode pour le td: https://bvdp.inetdoc.net/files/cesi/td1/cesitd1.zip

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 :

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 :

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)==0xC
((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)

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:

Sur l'exemple:

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:

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 à completer 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__);
 
//// A COMPLETER!!!!
 
 
 
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());
  }
}

Solution 2026

cesitd3.cpp
/*! \file cesi1.ino
    \brief Premier TD pour le CESI.
    \author Bertrand Vandeportaele IUT GEII
    \date  28/10/2021
*/
#include "lib_io_tp.h"
 
/** Variable globale permettant de stocker la dernière valeur écrite sur le
    port de sortie, pour pouvoir en modifier uniquement certains bits */
unsigned char imageSortie = 0;
/** Variable globale indiquant la broche Arduino connectée à la LED */
const unsigned int LEDPIN = 3;
////////////////////////////////////////////////////////////////////
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;
};
////////////////////////////////////////////////////////////////////
CStateMachine ::CStateMachine()
{
 
    reset();
}
////////////////////////////////////////////////////////////////////
void CStateMachine ::reset()
{
    etat = 0;
}
////////////////////////////////////////////////////////////////////
void CStateMachine ::clock()
{
    // if (etat==0)   sortie=entree;
    // delay(20);
 
    // variables intérmédiaires pour les entrées
    unsigned char entree0 = (entree >> 0) & 1;
    unsigned char entree1 = (entree >> 1) & 1;
    unsigned char entree2 = (entree >> 2) & 1;
    unsigned char entree3 = (entree >> 3) & 1;
 
    // variables intérmédiaires pour les sorties
    // codage des sorties
    // Sortie0 est une AMZI
    unsigned char sortie0;
    if (etat == 3)
        sortie0 = entree1;
    else
        sortie0 = 0;
    // Sortie1 est une AMZI
    unsigned char sortie1;
    if (etat == 1)
        sortie1 = entree1;
    else if ((etat == 3) && (entree0 == 0))
        sortie1 = entree2;
    else
        sortie1 = 0;
    // Sortie2 est une AMZE
    unsigned char sortie2 = (sortie >> 2) & 1; // récupère la valeur précédente
    if ((etat == 0) && ((entree0 == 1) && (entree1 == 0)))
        sortie2 = entree2;
    // Sortie3 est une AMZE
    unsigned char sortie3 = (sortie >> 3) & 1; // récupère la valeur précédente
    if ((etat == 1) && (entree2 == 0))
        sortie3 = entree0;
    else if ((etat == 2) && (entree2 == 1) && (entree0 == 0))
        sortie3 = 0;
 
    // codage de l'évolution de l'état
    switch (etat)
    {
    default:
    case 0:
        if ((entree0 == 1) && (entree1 == 0))
            etat = 1;
        break;
    case 1:
        if (entree2 == 0)
            etat = 2;
        break;
    case 2:
        if (entree0 == 1)
            etat = 3;
        else if (entree2 == 1) // entree0==0 implicite par le else
            etat = 0;
        break;
    case 3:
        if (entree0 == 0)
            etat = 0;
        break;
    }
    // reconstruction de sortie à partir des variables intermédiaires
    sortie = (sortie3 << 3) | (sortie2 << 2) | (sortie1 << 1) | (sortie0 << 0);
}
////////////////////////////////////////////////////////////////////
 
CStateMachine mae;
////////////////////////////////////////////////////////////////////
//! 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();
}
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
//! 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 < 4; 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__);
 
        if (mae.getSortieBit(1) != 0)
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
 
            mae.setEntreeBit(0, 1); // équivalent ici à mae.setEntree(1);
                                // reglage de entree2 à mémoriser dans sortie2
        mae.setEntreeBit(2, ntest & 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) != (ntest & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
        mae.setEntreeBit(1, (ntest>>1) & 1);
 
        //// A COMPLETER!!!!
        // TEST DU MAINTIEN EN 1
        mae.setEntreeBit(2, 1);
        mae.clock();
        if (mae.getEtat() != 1)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        if (mae.getSortieBit(1) != ((ntest>>1) & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
            // TEST DU PASSAGE EN 2
        mae.setEntreeBit(2, 0);
        mae.clock();
        if (mae.getEtat() != 2)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
 
        if (mae.getSortieBit(2) != (ntest & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
        // TEST DU MAINTIEN EN 2
        mae.setEntreeBit(2, 0);
        mae.setEntreeBit(0, 0);
        mae.clock();
        if (mae.getEtat() != 2)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        // TEST DU PASSAGE EN 0
        mae.setEntreeBit(2, 1);
        mae.setEntreeBit(0, 0);
        mae.clock();
        if (mae.getEtat() != 0)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        if (mae.getSortieBit(2) != (ntest & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
        // on ramene la mae dans l'etat 2
        //  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__);
        //on reregle entree2 correctement
         mae.setEntreeBit(2, ntest & 1);
 
            mae.setEntreeBit(0, 1); // équivalent ici à mae.setEntree(1);
            mae.setEntreeBit(1, 0); // é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__);
 
        //// A COMPLETER!!!!
        // TEST DU MAINTIEN EN 1
        mae.setEntreeBit(2, 1);
        mae.clock();
        if (mae.getEtat() != 1)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        // TEST DU PASSAGE EN 2
        mae.setEntreeBit(2, 0);
        mae.clock();
        if (mae.getEtat() != 2)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        // passage vers 3
        mae.setEntreeBit(0, 1);
        mae.clock();
        if (mae.getEtat() != 3)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
 
        if (mae.getSortieBit(2) != (ntest & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
        // mantien en 3
        mae.setEntreeBit(0, 1);
        mae.clock();
        if (mae.getEtat() != 3)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
        // passage vers 0
        mae.setEntreeBit(0, 0);
        mae.clock();
        if (mae.getEtat() != 0)
            debugMessage("Erreur: La MAE n'est pas dans l\'état prévu", __FILE__, __LINE__);
 
        if (mae.getSortieBit(2) != (ntest & 1))
            debugMessage("Erreur: La SORTIE n'est pas à la valeur prévue", __FILE__, __LINE__);
 
        Serial.println("fin du test");
        // vidage de la FIFO d'affichage:
        // flush(std::cout);
    }
}
////////////////////////////////////////////////////////////////////
 
//-------------------------------------------------------------------
// prototypes de toutes les fonctions de ce fichier
//-------------------------------------------------------------------
/*! \fn void setup()
    \brief Initialisation des périphériques et des variables globales
*/
void setup()
{
    //-------------------------------------------------------------------
    //            CODE A COMPLETER PAR LES ETUDIANTS
    //-------------------------------------------------------------------
    SetupES();
    mae.reset();
    Serial.begin(115200);
    pinMode(LEDPIN, OUTPUT);
    digitalWrite(LEDPIN, HIGH);
    // Normalement le programme de test est un programme à part
    programmeDeTest1();
}
//-------------------------------------------------------------------
/*!
 * \fn void loop()
 * \brief La fonction loop doit appeler une seule fonction exo... à la
 * fois, vous devez conserver le code de tous les exercices mais n'en
 * utiliser qu'un à la fois
 */
void loop()
{
    //-------------------------------------------------------------------
    //            CODE A COMPLETER PAR LES ETUDIANTS
    //-------------------------------------------------------------------
 
    unsigned int periodicite = 10;
    static unsigned long timer = millis();
    if (millis() - timer >= (periodicite * 1.2))
        digitalWrite(LEDPIN, LOW); // 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;
        mae.setEntree(readPort());
        mae.clock();
        writePort(mae.getSortie());
        // Serial.println(mae.getEtat());
        // delay(20);
    }
}