IoT Grundlagen

Vor- und Nachteile von Finite-State-Maschinen: Switch-Cases, C/C++-Zeiger und Nachschlagetabellen (Teil II)

Jose García
· 7 Minuten Lesezeit
Per E-Mail versenden

Dies ist der zweite und letzte Teil unserer Finite State Machine (FSM)-Implementierung. hier auf den ersten Teil der Serie verweisen und weitere allgemeine Informationen zu endlichen Zustandsmaschinen erfahren .

Finite-State-Maschinen oder FSMs sind einfach eine mathematische Berechnung von Ursachen und Ereignissen. Basierend auf Zuständen berechnet ein FSM eine Reihe von Ereignissen basierend auf dem Zustand der Maschineneingänge. Für einen Status namens SENSOR_READ könnte ein FSM beispielsweise ein Relais (auch als Steuerereignis bezeichnet) auslösen oder eine externe Warnung senden, wenn ein Sensormesswert über einem Schwellenwert liegt. Zustände sind die DNA des FSM – sie diktieren internes Verhalten oder Interaktionen mit einer Umgebung, wie etwa die Annahme von Eingaben oder die Produktion von Ausgaben, die dazu führen können, dass ein System seinen Zustand ändert. Als Hardware-Ingenieure ist es unsere Aufgabe, die richtigen FSM-Zustände auszuwählen und Ereignisse auszulösen, um das gewünschte Verhalten zu erzielen, das den Anforderungen unseres Projekts entspricht.

Im ersten Teil dieses FSM-Tutorials haben wir ein FSM mit der klassischen Switch-Case-Implementierung erstellt. Jetzt untersuchen wir die Erstellung eines FSM mithilfe von C/C++-Zeigern, mit dem Sie eine robustere Anwendung mit einfacheren Firmware-Wartungserwartungen entwickeln können.

HINWEIS : Der in diesem Tutorial verwendete Code wurde auf dem Arduino Day 2018 in Bogotá von Jose Garcia, einem der Hardware-Ingenieure Ubidots Die vollständigen Codebeispiele und Sprechernotizen finden Sie hier .

Nachteile des Schaltergehäuses:

Im ersten Teil unseres FSM-Tutorials haben wir uns mit Switch-Fällen und der Implementierung einer einfachen Routine befasst. Jetzt erweitern wir diese Idee, indem wir „Zeiger“ vorstellen und zeigen, wie Sie sie anwenden können, um Ihre FSM-Routine zu vereinfachen.

Eine Switch-Case- Implementierung ist einer if-else- Routine sehr ähnlich; Unsere Firmware durchläuft jeden Fall und wertet ihn aus, um zu sehen, ob die Trigger-Case-Bedingung erreicht ist. Schauen wir uns unten eine Beispielroutine an:

switch(state) { case 1: /* ein paar Sachen für State 1 erstellen */ state = 2; brechen; Fall 2: /* ein paar Sachen für Status 2 erstellen */ state = 3; brechen; Fall 3: /* ein paar Dinge für Status 3 erstellen */ state = 1; brechen; default: /* einige Dinge standardmäßig erstellen */ state = 1; }

Im obigen Code finden Sie ein einfaches FSM mit drei Zuständen. In der Endlosschleife geht die Firmware zum ersten Fall über und prüft, ob die Statusvariable gleich Eins ist. Wenn ja, führt es seine Routine aus; Ist dies nicht der Fall, geht es zu Fall 2 über, wo der Statuswert erneut überprüft wird. Wenn Fall 2 nicht erfüllt ist, geht die Codeausführung zu Fall 3 über und so weiter, bis entweder der Status erreicht ist oder die Fälle erschöpft sind.

Bevor wir uns mit dem Code befassen, wollen wir uns etwas genauer mit einigen möglichen Nachteilen der Switch-Case- oder If-Else -Implementierungen befassen, damit wir sehen können, wie wir unsere Firmware-Entwicklung verbessern können.

Nehmen wir an, dass die Anfangszustandsvariable 3 ist: Unsere Firmware muss drei verschiedene Wertvalidierungen durchführen. Für ein kleines FSM stellt dies möglicherweise kein Problem dar, aber stellen Sie sich eine typische industrielle Produktionsmaschine mit Hunderten oder Tausenden von Zuständen vor. Die Routine muss mehrere nutzlose Wertprüfungen durchführen, was letztlich zu einer ineffizienten Ressourcennutzung führt. Diese Ineffizienz ist unser erster Nachteil – der Mikrocontroller verfügt nur über begrenzte Ressourcen und wird mit ineffizienten FSM-Routinen überlastet. Daher ist es unsere Pflicht als Ingenieure, so viele Rechenressourcen wie möglich auf dem Mikrocontroller einzusparen.

Stellen Sie sich nun einen FSM mit Tausenden von Zuständen vor: Wenn Sie ein neuer Entwickler sind und eine Änderung in einem dieser Zustände implementieren müssen, müssen Sie sich Tausende von Codezeilen in Ihrer Hauptroutine loop() ansehen. Diese Routine enthält oft viel Code, der nichts mit der Maschine selbst zu tun hat. Daher kann es schwierig sein, Fehler zu beheben, wenn Sie die gesamte FSM-Logik innerhalb der Hauptschleife () zentrieren.

Und schließlich ist ein Code mit Tausenden von if-else- oder switch-case -Anweisungen für die meisten eingebetteten Programmierer weder elegant noch lesbar.

C/C++-Zeiger

Sehen wir uns nun an, wie wir mithilfe von C/C++-Zeigern ein prägnantes FSM implementieren können. Ein Zeiger zeigt, wie der Name schon sagt, auf eine Stelle innerhalb des Mikrocontrollers. In C/C++ zeigt ein Zeiger auf eine Speicheradresse mit der Absicht, Informationen abzurufen. Ein Zeiger wird verwendet, um während der Ausführung den gespeicherten Wert einer Variablen abzurufen, ohne die Speicheradresse der Variablen selbst zu kennen. Bei richtiger Anwendung können Zeiger einen großen Vorteil für die Struktur Ihrer Routine und die Einfachheit zukünftiger Pflege und Bearbeitung darstellen.

  • Beispiel für einen Punktcode:
int a = 1462; int myAddressPointer = &a; int myAddressValue = *myAddressPointer;

Lassen Sie uns analysieren, was im obigen Code passiert. Die Variable myAddressPointer zeigt auf die Speicheradresse der Variablen a (1462) , während die Variable myAddressValue den Wert der Speicheradresse abruft, auf die myAddressPointer zeigt. Dementsprechend ist zu erwarten, dass von myAddressPointer und von myAddressValue ein Wert von 1462 erhalten wird. Warum ist das nützlich? Da wir nicht nur Werte im Speicher speichern, speichern wir auch Funktionen und Methodenverhalten. Beispielsweise speichert der Speicherplatz 874 den Wert 1462, aber diese Speicheradresse kann auch Funktionen zur Berechnung einer Stromstärke in kA verwalten. Zeiger geben uns Zugriff auf diese zusätzliche Funktionalität und die Nutzbarkeit der Speicheradresse, ohne dass eine Funktionsanweisung in einem anderen Teil des Codes deklariert werden muss. Ein typischer Funktionszeiger kann wie folgt implementiert werden:

void (*funcPtr) (void);

Können Sie sich vorstellen, dieses Tool in unserem FSM zu verwenden? Wir können anstelle einer Variablen einen dynamischen Zeiger erstellen, der auf die verschiedenen Funktionen oder Zustände unseres FSM zeigt. Wenn wir eine einzelne Variable haben, die einen Zeiger speichert, der sich dynamisch ändert, können wir die FSM-Zustände basierend auf den Eingabebedingungen ändern.

Nachschlagetabellen

Sehen wir uns ein weiteres wichtiges Konzept an: Nachschlagetabellen oder LUTs. LUTs bieten eine geordnete Möglichkeit, Daten in Grundstrukturen zu speichern, die vordefinierte Werte speichern. Sie werden für uns nützlich sein, um Daten in unseren FSM-Werten zu speichern.

Der Hauptvorteil von LUTs besteht darin: Wenn sie statisch deklariert sind, kann auf ihre Werte über Speicheradressen zugegriffen werden, was in C/C++ eine sehr effektive Möglichkeit für den Wertezugriff darstellt. Nachfolgend finden Sie eine typische Deklaration für eine FSM-LUT:

void (*const state_table [MAX_STATES][MAX_EVENTS]) (void) = { action_s1_e1, action_s1_e2 }, /* Verfahren für den Zustand { action_s2_e1, action_s2_e2 }, /* Verfahren für den Zustand { action_s3_e1, action_s3_e2 } /* Verfahren für den Zustand };

Es ist viel zu verdauen, aber diese Konzepte spielen eine große Rolle bei der Umsetzung unseres neuen und effizienten FSM. Lassen Sie uns es nun codieren, damit Sie sehen können, wie leicht diese Art von FSM mit der Zeit wachsen kann.

Hinweis: Den vollständigen Code des FSM finden Sie hier – wir haben ihn der Einfachheit halber in 5 Teile aufgeteilt.

Codierung

Wir erstellen ein einfaches FSM, um eine blinkende LED-Routine zu implementieren. Anschließend können Sie das Beispiel an Ihre eigenen Bedürfnisse anpassen. Das FSM hat zwei Zustände: ledOn und ledOff, und die LED schaltet sich jede Sekunde aus und wieder ein. Fangen wir an!

/*    STATUSMASCHINEN-SETUP   */ /*    Gültige Zustände der Zustandsmaschine */ typedef enum {   LED_ON,   LED_OFF,   NUM_STATES } StateType; /*    Struktur der Zustandsmaschinentabelle */ typedef struct {   StateType State;   // Funktionszeiger erstellen   void (*Funktion)(void); } StateMachineType;

Im ersten Teil implementieren wir unsere LUT, um Zustände zu erstellen. Praktischerweise verwenden wir die Methode enum(), um unseren Zuständen einen Wert von 0 und 1 zuzuweisen. Der maximalen Anzahl von Zuständen wird ebenfalls der Wert 2 zugewiesen, was in unserer FSM-Architektur sinnvoll ist. Diese Typdefinition wird als „StatedType“ , damit wir später in unserem Code darauf verweisen können.

Als nächstes erstellen wir eine Struktur zum Speichern unserer Zustände. Wir deklarieren außerdem einen Zeiger mit der Bezeichnung „ function“ , der unser dynamischer Speicherzeiger zum Aufrufen der verschiedenen FSM-Zustände sein wird.

/*     Anfängliche SM-Zustands- und Funktionsdeklaration */ StateType SmState = LED_ON; void Sm_LED_ON(); void Sm_LED_OFF(); /*    LookUp-Tabelle mit auszuführenden Zuständen und Funktionen */ StateMachineType StateMachine[] = {   {LED_ON, Sm_LED_ON},   {LED_OFF, Sm_LED_OFF} };

Hier erstellen wir eine Instanz mit dem Anfangszustand LED_ON, deklarieren unsere beiden Zustände und erstellen schließlich unsere LUT. Zustandsdeklarationen und Verhalten hängen in der LUT zusammen, sodass wir über int -Indizes . Um beispielsweise auf die Methode sm_LED_ON() zuzugreifen, codieren wir etwas wie StateMachineInstance[0]; .

/*    Routinen für benutzerdefinierte Statusfunktionen */ void Sm_LED_ON() {   // Benutzerdefinierter Funktionscode   digitalWrite(LED_BUILTIN, HIGH);   Verzögerung (1000);   // Zum nächsten Status wechseln   SmState = LED_OFF; } void Sm_LED_OFF() {   // Benutzerdefinierter Funktionscode   digitalWrite(LED_BUILTIN, LOW);   Verzögerung (1000);   // Zum nächsten Status wechseln   SmState = LED_ON; }

Im obigen Code ist unsere Methodenlogik implementiert und enthält außer der Aktualisierung der Statusnummer am Ende jeder Funktion nichts Besonderes.

/*    Hauptfunktionsstatusänderungsroutine */ void Sm_Run(void) {   // Stellt sicher, dass der tatsächliche Status gültig ist   if (SmState < NUM_STATES) {     (*StateMachine[SmState].function) ();   }   sonst {     // Fehlerausnahmecode     Serial.println("[ERROR] Ungültiger Status");   } }

Die Funktion Sm_Run() ist das Herzstück unseres FSM. Beachten Sie, dass wir einen Zeiger (*) , um die Speicherposition der Funktion aus unserer LUT zu extrahieren, da wir während der Ausführung dynamisch auf eine Speicherposition in der LUT zugreifen. Sm_Run () führt immer mehrere Anweisungen, auch FSM-Ereignisse genannt, aus, die bereits in einer Speicheradresse des Mikrocontrollers gespeichert sind.

/*    WICHTIGSTE ARDUINO-FUNKTIONEN   */ void setup() {   // Geben Sie Ihren Setup-Code hier ein, um ihn einmal auszuführen:   pinMode(LED_BUILTIN, OUTPUT); } void loop() {   // Geben Sie hier Ihren Hauptcode ein, um ihn wiederholt auszuführen:   Sm_Run(); }

Unsere wichtigsten Arduino-Funktionen sind jetzt sehr einfach – die Endlosschleife läuft immer mit der zuvor definierten Zustandsänderungsroutine. Diese Funktion verarbeitet das Ereignis, um den tatsächlichen FSM-Status auszulösen und zu aktualisieren.

Schlussfolgerungen

In diesem zweiten Teil unserer Serie „Finite State Machines und C/C++-Zeiger“ haben wir die Hauptnachteile von Switch-Case-FSM-Routinen untersucht und Zeiger als geeignete und wünschenswerte Option identifiziert, um Speicher zu sparen und die Funktionalität des Mikrocontrollers zu erhöhen.

Im Überblick sind hier einige der Vor- und Nachteile der Verwendung von Zeigern in Ihrer Finite-State-Machine-Routine aufgeführt:

Vorteile:

  • Um weitere Zustände hinzuzufügen, deklarieren Sie einfach die neue Übergangsmethode und aktualisieren Sie die Nachschlagetabelle. Die Hauptfunktion wird dieselbe sein.
  • Sie müssen nicht jede if-else-Anweisung ausführen – der Zeiger lässt die Firmware zum gewünschten Befehlssatz im Speicher des Mikrocontrollers „gehen“.
  • Dies ist eine prägnante und professionelle Möglichkeit, FSM umzusetzen.

Nachteile:

  • Sie benötigen mehr statischen Speicher, um die Nachschlagetabelle zu speichern, in der die FSM-Ereignisse gespeichert sind.