Avantages et inconvénients des machines à états finis : cas de commutation, pointeurs C/C++ et tables de recherche (partie II)
Il s'agit de la deuxième et dernière partie de notre implémentation de Finite State Machine (FSM). Vous pouvez référencer la première partie de la série et en apprendre davantage sur les machines à états finis ici .
Les machines à états finis, ou FSM, sont simplement un calcul mathématique de causes et d'événements. Sur la base des états, un FSM calcule une série d'événements en fonction de l'état des entrées de la machine. Pour un état appelé SENSOR_READ par exemple, un FSM pourrait déclencher un relais (c'est-à-dire un événement de contrôle) ou envoyer une alerte externe si une lecture du capteur est supérieure à une valeur seuil. Les états sont l'ADN du FSM – ils dictent le comportement interne ou les interactions avec un environnement, comme l'acceptation d'entrées ou la production de sorties, qui peuvent amener un système à changer d'état. C'est notre travail en tant qu'ingénieurs matériels de choisir les bons états FSM et de déclencher des événements pour obtenir le comportement souhaité qui correspond aux besoins de notre projet.
Dans la première partie de ce didacticiel FSM, nous avons créé un FSM en utilisant l'implémentation classique du switch-case. Nous allons maintenant explorer la création d'un FSM à l'aide de pointeurs C/C++, ce qui vous permettra de développer une application plus robuste avec des attentes de maintenance du micrologiciel plus simples.
REMARQUE : Le code utilisé dans ce didacticiel a été présenté lors de la journée Arduino 2018 à Bogotá par Jose Garcia, l'un des ingénieurs matériels Ubidots Vous pouvez trouver les exemples de code complets et les notes du présentateur ici .
Inconvénients du boîtier de commutation :
Dans la première partie de notre tutoriel FSM , nous avons examiné les cas de commutation et comment implémenter une routine simple. Nous allons maintenant développer cette idée en présentant les « pointeurs » et comment les appliquer pour simplifier votre routine FSM.
Une de switch-case est très similaire à une routine if-else notre micrologiciel bouclera sur chaque cas, les évaluant pour voir si la condition du cas de déclenchement est atteinte. Regardons un exemple de routine ci-dessous :
switch(state) { case 1 : /* crée des trucs pour l'état 1 */ state = 2; casser; cas 2 : /* crée des trucs pour l'état 2 */ state = 3; casser; cas 3 : /* crée des trucs pour l'état 3 */ state = 1; casser; par défaut : /* crée des trucs par défaut */ state = 1; }
Dans le code ci-dessus, vous trouverez un FSM simple avec trois états. Dans la boucle infinie, le firmware ira au premier cas, vérifiant si la variable d'état est égale à un. Si oui, il exécute sa routine ; sinon, il passe au cas 2 où il vérifie à nouveau la valeur de l'état. Si le cas 2 n'est pas satisfait, l'exécution du code passera au cas 3, et ainsi de suite jusqu'à ce que l'état soit atteint ou que les cas soient épuisés.
Avant d'entrer dans le code, comprenons un peu plus certains inconvénients possibles des switch-case ou if-else afin que nous puissions voir comment améliorer le développement de notre firmware.
Supposons que la variable d'état initiale soit 3 : notre firmware devra faire 3 validations de valeurs différentes. Cela ne pose peut-être pas de problème pour un petit FSM, mais imaginez une machine de production industrielle typique comportant des centaines ou des milliers d'états. La routine devra effectuer plusieurs contrôles de valeurs inutiles, ce qui entraînera finalement une utilisation inefficace des ressources. Cette inefficacité devient notre premier inconvénient : le microcontrôleur est limité en ressources et sera surchargé de routines FSM inefficaces. En tant que tel, il est de notre devoir en tant qu'ingénieurs d'économiser autant de ressources informatiques que possible sur le microcontrôleur.
Imaginez maintenant un FSM avec des milliers d'états : si vous êtes un nouveau développeur et que vous devez implémenter un changement dans l'un de ces états, vous devrez examiner des milliers de lignes de code dans votre routine principale loop(). Cette routine inclut souvent beaucoup de code non lié à la machine elle-même, il peut donc être difficile de déboguer si vous centrez toute la logique FSM dans la boucle principale().
Et enfin, un code contenant des milliers d' if-else ou switch-case n'est ni élégant ni lisible pour la majorité des programmeurs embarqués.
Pointeurs C/C++
Voyons maintenant comment implémenter un FSM concis à l'aide de pointeurs C/C++. Un pointeur, comme son nom l’indique, pointe quelque part à l’intérieur du microcontrôleur. En C/C++, un pointeur pointe vers une adresse mémoire dans le but de récupérer des informations. Un pointeur est utilisé pour obtenir la valeur stockée d'une variable lors de l'exécution sans connaître l'adresse mémoire de la variable elle-même. Utilisés correctement, les pointeurs peuvent être un énorme avantage pour la structure de votre routine et la simplicité de la maintenance et de l'édition futures.
- Exemple de code de point :
int a = 1462 ; int monAdressePointer = &a; int maValeurAdresse = *monAdressePointer;
Analysons ce qui se passe dans le code ci-dessus. La variable myAddressPointer pointe vers l'adresse mémoire de la variable a (1462) , tandis que la variable myAddressValue récupère la valeur de l'adresse mémoire pointée par myAddressPointer. En conséquence, on peut s'attendre à obtenir une valeur de 874 pour myAddressPointer et de 1462 pour myAddressValue. Pourquoi est-ce utile ? Parce que nous ne stockons pas seulement les valeurs en mémoire, nous stockons également les fonctions et les comportements des méthodes. Par exemple, l'espace mémoire 874 stocke la valeur 1462, mais cette adresse de stockage peut également gérer des fonctions pour calculer une intensité de courant en kA. Les pointeurs nous donnent accès à cette fonctionnalité supplémentaire et à la convivialité des adresses mémoire sans avoir besoin de déclarer une instruction de fonction dans une autre partie du code. Un pointeur de fonction typique peut être implémenté comme ci-dessous :
void (*funcPtr) (vide);
Pouvez-vous imaginer utiliser cet outil dans notre FSM ? Nous pouvons créer un pointeur dynamique qui pointe vers les différentes fonctions ou états de notre FSM au lieu d'une variable. Si nous avons une seule variable qui stocke un pointeur qui change dynamiquement, nous pouvons modifier les états FSM en fonction des conditions d'entrée.
Tables de recherche
Passons en revue un autre concept important : les tables de recherche, ou LUT. Les LUT offrent un moyen ordonné de stocker des données, dans des structures de base qui stockent des valeurs prédéfinies. Ils nous seront utiles pour stocker des données dans nos valeurs FSM.
Le principal avantage des LUT est le suivant : si elles sont déclarées statiquement, leurs valeurs sont accessibles via des adresses mémoire, ce qui est un moyen d'accès aux valeurs très efficace en C/C++. Vous trouverez ci-dessous une déclaration typique pour une LUT FSM :
void (*const state_table [MAX_STATES][MAX_EVENTS]) (void) = { action_s1_e1, action_s1_e2 }, /* procédures pour l'état { action_s2_e1, action_s2_e2 }, /* procédures pour l'état { action_s3_e1, action_s3_e2 } /* procédures pour l'état } ;
C'est beaucoup à digérer, mais ces concepts jouent un rôle important dans la mise en œuvre de notre nouveau FSM efficace. Maintenant, codons-le pour que vous puissiez voir avec quelle facilité ce type de FSM peut se développer au fil du temps.
Remarque : Le code complet du FSM peut être trouvé ici – nous l'avons divisé en 5 parties pour plus de simplicité.
Codage
Nous allons créer un simple FSM pour implémenter une routine de LED clignotante. Vous pouvez ensuite adapter l'exemple à vos propres besoins. Le FSM aura 2 états : ledOn et ledOff, et la led s'éteindra et s'allumera toutes les secondes. Commençons !
/* CONFIGURATION DE LA MACHINE D'ÉTAT */ /* États valides de la machine à états */ typedef enum { LED_ON, LED_OFF, NUM_STATES } Type d'état ; /* Structure de la table de la machine à états */ typedef struct { StateType État ; // Crée le pointeur de fonction void (*fonction)(void); } StateMachineType;
Dans la première partie, nous implémentons notre LUT pour créer des états. Idéalement, nous utilisons la méthode enum() pour attribuer une valeur de 0 et 1 à nos états. Le nombre maximum d'états se voit également attribuer une valeur de 2, ce qui est logique dans notre architecture FSM. Ce typedef
sera étiqueté StatedType afin que nous puissions y faire référence plus tard dans notre code.
Ensuite, nous créons une structure pour stocker nos états. Nous déclarons également un pointeur intitulé function , qui sera notre pointeur de mémoire dynamique pour appeler les différents états FSM.
/* Déclaration initiale de l'état et des fonctions du SM */ StateType SmState = LED_ON; void Sm_LED_ON(); void Sm_LED_OFF(); /* Table LookUp avec états et fonctions à exécuter */ StateMachineType StateMachine[] = { {LED_ON, Sm_LED_ON}, {LED_OFF, Sm_LED_OFF} } ;
Ici, nous créons une instance avec l'état initial LED_ON, et nous déclarons nos deux états et enfin créons notre LUT. Les déclarations d'état et le comportement sont liés dans la LUT, nous pouvons donc accéder facilement aux valeurs via les index int . Pour accéder à la méthode sm_LED_ON(), par exemple, nous coderons quelque chose comme StateMachineInstance[0];
.
/* Routines de fonctions d'état personnalisées */ void Sm_LED_ON() { // Code de fonction personnalisé digitalWrite(LED_BUILTIN, HAUT); retard (1000); // Passer à l'état suivant SmState = LED_OFF ; } void Sm_LED_OFF() { // Code de fonction personnalisé digitalWrite(LED_BUILTIN, FAIBLE); retard (1000); // Passer à l'état suivant SmState = LED_ON ; }
Dans le code ci-dessus, la logique de nos méthodes est implémentée et n'inclut rien de spécial à part la mise à jour du numéro d'état à la fin de chaque fonction.
/* Routine de changement d'état de la fonction principale */ void Sm_Run(void) { // S'assure que l'état réel est valide if (SmState < NUM_STATES) { (*StateMachine[SmState].fonction) (); } sinon { // Code d'exception d'erreur Serial.println("[ERREUR] État non valide"); } }
La fonction Sm_Run()
est le cœur de notre FSM. Notez que nous utilisons un pointeur (*)
pour extraire la position mémoire de la fonction de notre LUT, puisque nous accéderons dynamiquement à une position mémoire dans la LUT pendant l'exécution. Le Sm_Run()
exécutera toujours plusieurs instructions, alias événements FSM, déjà stockées dans une adresse mémoire du microcontrôleur.
/* FONCTIONS PRINCIPALES ARDUINO */ void setup() { // mettez votre code d'installation ici, pour l'exécuter une fois : pinMode(LED_BUILTIN, SORTIE); } boucle vide() { // mettez votre code principal ici, pour l'exécuter à plusieurs reprises : Sm_Run(); }
Nos principales fonctions Arduino sont désormais très simples : la boucle infinie fonctionne toujours avec la routine de changement d'état définie précédemment. Cette fonction gérera l'événement pour déclencher et mettre à jour l'état réel du FSM.
Conclusions
Dans cette deuxième partie de notre série Machines à états finis et pointeurs C/C++, nous avons passé en revue les principaux inconvénients des routines FSM à commutation de cas et identifié les pointeurs comme une option appropriée et souhaitable pour économiser de la mémoire et augmenter les fonctionnalités du microcontrôleur.
En résumé, voici quelques-uns des avantages et des inconvénients de l’utilisation de pointeurs dans votre routine de machine à états finis :
Avantages :
- Pour ajouter plus d'états, déclarez simplement la nouvelle méthode de transition et mettez à jour la table de recherche ; la fonction principale sera la même.
- Vous n'êtes pas obligé d'exécuter toutes les instructions if-else : le pointeur permet au micrologiciel d'accéder à l'ensemble d'instructions souhaité dans la mémoire du microcontrôleur.
- Il s’agit d’une manière concise et professionnelle de mettre en œuvre FSM.
Inconvénients :
- Vous avez besoin de plus de mémoire statique pour stocker la table de recherche qui stocke les événements FSM.