Finite State Machine Umgang mit Timern?

Ich habe hauptsächlich mit 8-Bit-MCUs gearbeitet, wo die meisten RTOSs zu viel Overhead haben.

Die meisten Anwendungen, an denen ich gearbeitet habe, waren nur ein periodischer Interrupt mit if/else-Ketten für die gesamte Verarbeitungslogik, und dann geht die MCU wieder in den Ruhezustand.

Dies hat für viele Dinge gut funktioniert und hat einen wirklich minimalen Overhead. Aber für ein System komme ich an den Punkt, an dem es so viele Steuerflags gibt, dass ich bereit bin, mein eigenes System "Spaghetti" zu nennen. Es wäre entsetzlich für jemand neuen, dieses System aufzugreifen und einige neue Funktionen zu implementieren.

(Ich habe eine zweifarbige LED, die ungefähr 8 verschiedene Zustände und zeitabhängige Blinkmuster haben muss, je nachdem, in welchem ​​​​Zustand sich der Rest des Systems befindet. Es ist eine schreckliche Übung, denn was so einfach sein sollte ...)

Ich habe mir überlegt, vielleicht eine endliche Zustandsmaschine zu machen und zu versuchen, so viele Kontrollflags auszusortieren.

Ein konzeptionelles Problem, das ich sehe, ist die Verwendung von Timern in einer Zustandsmaschine. Derzeit habe ich einen Hardware-Timer und dann eine Reihe von variabel definierten Timer-Zählern, die inkrementieren / dekrementieren, eine Steuerflag-Variable geht auf 0/1, und so gehen wir die if/else-Kette durch.

Würden Sie in meiner Planungsphase für eine strengere Zustandsmaschine einfach mehr Hardware-Timer verwenden und die externen Interrupts als Ereignisse auslösen, um zur Zustandsmaschine zurückzukehren?

Meine Bauchreaktion (ob richtig oder nicht) besteht darin, so viele externe Interrupts wie möglich für die Zustandsmaschine zu verwenden Die Steuerlogik ist einfach verwirrend und 2) Sie verwenden mehr Strom, um eine Reihe von Timern auszuführen, anstatt nur die Timer-Logik als Variablen zu behandeln.

Ich sehe, wie Sie variable Timer in Ihrer Zustandsmaschine immer noch inkrementieren / deinkrementieren könnten, aber ist das nicht unethisch gegenüber dem Zustandsmaschinenmuster?

Ich bin ziemlich zufrieden mit der Debatte zwischen Funktionszeigern und Schalteranweisungen, wie Sie die Zustandsmaschine codieren oder ob Sie eine Übergangstabelle usw. verwenden möchten.

Ich frage mich speziell, wie die Leute den Zeitverwaltungsaspekt ihrer Zustandsmaschinen auf elegante Weise gehandhabt haben.

+1 für eine schön formulierte Frage, aber es ist ein bisschen grenzwertig, Meinungen einzuholen. Wenn Sie sie also verbessern können, erhalten Sie möglicherweise mehr Antworten?
Bearbeiten? Ich weiß buchstäblich nicht, wie ich es zu einer weniger Meinung machen soll, es ist von Natur aus eine Designfrage.
Jede Zustandsmaschine hat einen Code, der den nächsten Zustand bestimmt, richtig? Wenn Sie in einen Zustand mit Timeout eintreten, erstellen Sie eine Momentaufnahme des Systemzeitgebers und speichern ihn. Wenn Sie sich dann für den nächsten Status entscheiden, erhalten Sie die aktuelle Zeit, subtrahieren den Snapshot und vergleichen ihn mit dem Timeout-Wert. Scheint nicht super kompliziert zu sein. Ich nehme an, es könnte ein bisschen Speicherplatz verbrauchen, der knapp sein könnte. Wenn Sie das Ethos der Zustandsmaschine retten möchten, können Sie einen Zustand dem Speichern der aktuellen Zeit zum Zwecke der späteren Berechnung der verstrichenen Zeit zuweisen.
@mkeith - Ja, die Zustandsmaschinenfunktionen (ich denke an Zeigerfunktionen, Annäherung an die Zustandsmaschine) würden auf den nächsten Zustand verweisen. Ich werde diesen Ansatz noodlen.
Wenn der Zeitgeber ein vorzeichenloser 16-Bit-Wert ist, dann ergibt das Subtrahieren über die Überlaufgrenze immer noch die richtige verstrichene Zeit.
"Es wäre schrecklich für jemand neuen, dieses System aufzugreifen und einige neue Funktionen zu implementieren." -> Implementieren Sie Ihre Zustandsmaschine mit einem Tool wie IBM Rational Rhapsody. Es generiert Code basierend auf Ihrem Zustandsmaschinendiagramm und wird daher auch parallel dokumentiert

Antworten (3)

Eine übliche Methode wäre, eine maximale Ausführungszeit für jeden Zustand festzulegen und dann jeden von ihnen zu bewerten (mit 100 % Codeabdeckung) und sicherzustellen, dass sie die maximale Ausführungszeit nie überschreiten. Mit etwas Glück kann man dafür sogar den On-Chip-Watchdog verwenden, wenn dieser mit ausreichend niedrigen Timeouts laufen kann.

Was Sie jetzt wahrscheinlich suchen, ist nicht eine, sondern mehrere Zustandsmaschinen. Das heißt, Sie können eine universelle Zustandsmaschine wie z

STATE_MACHINE[state++](); 
if(state == STATES_N) 
{ /* reset state machine */ 
} 

was nichts anderes tut, als die verschiedenen Softwaremodule zu durchlaufen und ihnen jeweils eine "Zeitscheibe" zu geben. Entweder können Sie alle verschiedenen Hardwaretreiber auf einmal durchlaufen und wieder schlafen gehen, oder Sie können nur einen einzigen davon ausführen. Dies hängt natürlich von den Echtzeitanforderungen ab.

Ein solcher Zustand könnte sein led_execute(), was die LED-Routine wäre, die verfolgt, was gerade auf den LEDs passiert. Diese Routine befindet sich im LED-Treiber und kann wiederum jeden LED-Zustand verfolgen, sodass es in etwa so aussieht:

typedef enum
{
  LED_OFF,
  LED_RED_LIT, // whatever names make sense
  LED_RED_BLUE_LIT,
  ...
  LED_DONE,
  LED_N
} led_state_t;
...    
static led_state_t led_state = LED_OFF;
...

void led_execute (void)
{
  led_state = LED_STATE_MACHINE[led_state]();
}

Wenn die Zustände von externen Eingaben abhängen, überspringen Sie möglicherweise den Rückgabezustandsteil und lassen Sie den Zustand nur durch Setter/Getter aktualisieren.

Dies sollte die Notwendigkeit von Flags vollständig eliminieren – insbesondere nicht verwandte Flags, die sich im selben Bereich befinden, was ein Alptraum sein kann. Der wichtigste Teil hier ist, die LED-Komplexität nicht mit der Komplexität anderer Hardware zu verwechseln.

Nehmen wir an, Sie entprellen gleichzeitig eine Taste. Angenommen, Sie müssen das Entprellen beenden, bevor die LEDs leuchten können - das bedeutet nicht, dass die Tasten über LEDs Bescheid wissen müssen oder dass LEDs über Tasten Bescheid wissen müssen. Der Anrufercode sollte diese Dinge verfolgen. Das heißt, Sie benötigen möglicherweise eine Abstraktionsschicht zwischen der äußersten Zustandsmaschine und den Treibern selbst. Wenn der LED-Treiber nur einen Eingang bekommt, "mach das!" vom Anrufer, dann sind ihm die Gründe dafür völlig egal.

Lundin, da stimme ich vollkommen zu. Ich lag letzte Nacht im Bett und stellte mir mehrere Zustandsmaschinen vor, als ich darüber nachdachte. Wenn die LED eine separate Zustandsmaschine hatte, nahm diese Zustände von einer anderen Maschine an. Wenn Sie diesen Weg gehen, reduzieren Sie Ihrer Meinung nach die Komplexität so weit, dass es sich lohnt? In meiner if/else-Routine komme ich an den Punkt, an dem es 6 Flags gibt, wenn die meisten IFs für die logische Steuerung von Zustandsrandfällen zu steuern sind. Ich stimme vollkommen zu, dass die LED grundsätzlich von allem anderen getrennt ist, aber die Flaggen werden überall verstreut, um das Schiff über Wasser zu halten.
Wenn Sie also sagen, dass jeder Zustand bewertet wird, meinen Sie im Grunde, dass jeder Zustand endet und den Hund treten muss, bevor der Timer abgelaufen ist? Das ist eine großartige Idee. In meinem Fall denke ich, dass mein Watchdog nicht so schnell laufen kann, wie ich es brauchen würde, aber in anderen Fällen würde es großartig funktionieren.
@Leroy105 Als ich in der Vergangenheit mit chaotischen Codebasen in Form von "Flaghetti" zu tun hatte, habe ich genau dieses Design verwendet, um sie zu entwirren. Ich habe ein spezielles Beispiel, bei dem ein Programm unter allen möglichen zeitweiligen Fehlern litt, aber als die Flags durch Zustandsmaschinen ersetzt wurden, verschwanden alle Fehler einfach, obwohl ich die eigentliche Anwendungslogik nicht berührt hatte. Also ja, ich weiß aus Erfahrung, dass dies ein vernünftiger Weg ist.
@ Leroy105 Was das Benchmarking betrifft, müssen Sie sicherstellen, dass ein Zustand nicht durcheinander gerät und die gesamte Echtzeitleistung ruiniert. Das Obige ist eine Art grobe Form eines RTOS, wenn es richtig implementiert ist. Sie können auch jeden Status aus dem Anrufercode takten und protokollieren - das ist etwas, das ich in unternehmenskritischerem Code verwende, aber dann auch zusammen mit einer Art Watchdog. Bemerkenswert ist auch, dass es keine brillante Idee ist, den Wdog aus einem ISR heraus zu treten. Wenn dies also alles ein großer ISR ist, suchen Sie vielleicht nach einem alternativen Design.
„Flaghetti“ – Bingo.
Lundin, haben Sie Ihre Meinung zur Verwendung von Funktionszeigern im Vergleich zu Schaltern geändert? Ich bin nicht religiös in Bezug auf die kompilierten Opcode-Argumente (dh Case-Anweisungen, um Tabellen zu springen usw.) - ich mache mir am meisten Sorgen über Tippfehler und Wartbarkeit. Ich codiere nicht nach MISRA-Standards, aber ich habe definitiv Fehler verursacht, indem ich in meinem Leben keine Array-Grenzen überprüft habe. Sogar die Verwendung von Arrays gibt mir eine Pause! Es kann ein Bär sein, einen Array-Fehler usw. abzufangen. Ich mag die saubere Natur der Zeiger. Scheint eine Wäsche zu sein, wahrscheinlich in Bezug auf die Ausführung. Schätzen Sie Ihre Meinung, da Sie diese Systeme von Anfang bis Ende gesehen haben.
Sie wissen, was ich wirklich nicht mag, ist, dass ich Zustandsmaschinen gesehen habe, bei denen die Zustände nicht in den Funktionen zurückgegeben werden. Die gesamte Logik verwendet eine fest typisierte Zustandsmatrix. Mein Gehirn scheint diese Herangehensweise nicht zu mögen. Scheint schwer zu debuggen? Ich werde auf jeden Fall eine Grenzüberprüfung in der Zustandsmaschine durchführen. Das habe ich auf die harte Tour gelernt (dh warum wird ein bisschen auf seltsame Weise umgeschaltet, aber der Rest des Systems funktioniert und es wird nur in der 3. Schleife der ISR umgeschaltet. Ich musste einen alten Timer anrufen in meiner Office-Suite, um mir etwas über Arrays in C beizubringen ... wir sind nicht mehr in der Java-Welt!).
Vier Stunden später kichere ich in mich hinein, als ich einen schwerwiegenden, bösen, zeitabhängigen Flaghetti-Fehler erwische, der dazu führen kann, dass eine farbige LED durchgehend leuchtet, aber in eine blinkende Routine übergeht. Hängt davon ab, wann der Übergangszeitpunkt. Cripes, was für ein Scheißeimer, den ich hier ohne Zustandsmaschine codiert habe. ;)
@ Leroy105 Der wichtigste Teil von Zustandsmaschinen ist, dass es vollkommen klar ist, wo die Zustandsänderungen stattfinden. Am häufigsten am Ende jeder Zustandsfunktion. Es ist aber auch möglich, einen externen „Scheduler“ zu haben, der jeder Hardware eine Zeitscheibe zum Ausführen gibt. Das hatte ich im Sinn, als ich diese Antwort schrieb, aber ich denke, solche Designs sollten eigentlich nicht als Zustandsmaschinen, sondern als Planer bezeichnet werden.
Es ist eine Art hybrider Ansatz. Ich habe gesehen, wie ein Universitätsprofessor in jeder Zustandsfunktion einen statischen Variablenzähler führte, der eine ISR abschaltet. Oder er erstellt einige Setter/Getter-Funktionen, um einige global definierte Zähler zu modifizieren. Ich glaube, ich habe es in den Griff bekommen, ist die Aufteilung auf mehrere Zustandsmaschinen, wie Sie sagen. Wenn Sie jemals den Drang verspüren, ein globales Flag zu erstellen, sollten Sie diese Logik in einen anderen definierten Zustand versetzen und Ihre Zustandsmaschine zum nächsten Zustand zurückschleifen lassen usw. Dieser Artikel hat mir gefallen: isa.uniovi.es/docencia/ Reden/…

Wenn Sie Super-Low-Tech-Multitasking wollen, kann Ihr Timer-Interrupt so aussehen:

timer_isr()
{
    process1();
    process2();
    process3();
}

Wenn Ihr Timer also beispielsweise 100x pro Sekunde ausgelöst wird, dann jedes Mal, wenn jede process() -Funktion aufgerufen wird. Diese Funktionen sind FSMs, die Ihre verschiedenen "Multitasking"-Aufgaben implementieren. Wenn sie alle unabhängig sind, dann ist es einfach.

Wenn Ihre Aufgaben nun abhängig sind, können Sie Folgendes tun:

timer_isr()
{
    check_buttons();
    blink_red();
    blink_blue();
}

In diesem Fall kommunizieren die Tasks über hässliche globale Variablen (wir werden keine Messageboxen und FIFOs auf einem 8-Bit machen, oder). Zum Beispiel würde check_buttons() entprellen usw. und einige Flags setzen und/oder direkt den Zustand der anderen zwei FSMs beeinflussen, die die LEDs blinken lassen.

Wir können sogar diese Spitzentechnologie namens C++ verwenden:

timer_isr()
{
    check_buttons();
    red.blink();
    blue.blink();
}

In diesem Fall würde check_buttons() beispielsweise "red.setBlinkMode(some value)" aufrufen, wenn der entsprechende Button gedrückt wird. "rot" und "blau" sind globale Objekte. In diesem Fall können Sie mit diesem winzigen Stück OO denselben Algorithmus für beide implementieren, ohne sich mit Tonnen von Globals oder Zeigern auf Strukturen usw. herumschlagen zu müssen.

Es ist eine nette Sache, Ihre Schaltflächen nur an einer Stelle in Ihrem Code zu behandeln. Vor allem, wenn die Tasten mehrere Dinge steuern, zum Beispiel eine Taste zum Auswählen der LED und eine andere Taste zum Ändern des Blinkmusters der ausgewählten LED.

Die .blink()-Methode würde zum Beispiel einen LED-spezifischen Zähler erhöhen, bis er die Blinkperiode erreicht, oder eine PWM zeitabhängig anpassen, damit sie ausgefallen blinkt, solche Sachen.

Wenn Ihre Zeiten beispielsweise alle Millisekunden aufgerufen werden, könnte Ihre Methode blink() so aussehen:

LED::blink()
{
    if( led_on) {
        if( counter++ > period ) {
            counter=0;
            led_pin = !led_pin;
        }
    } else { led_pin = 0; counter=0; }
}

...so ähnlich. Sie werden alle zur gleichen Zeit aufgerufen, also kennen all diese kleinen Zustandsmaschinen die Zeit, indem sie zählen, wie oft sie aufgerufen werden. In diesem Fall ist der Zustand des FSM led_on und counter.

Ja, ich verstehe das, aber sehen Sie, wie Sie eine Reihe von Kontrollvariablen eingeführt haben? Ich habe das Wenn/Sonst-Durcheinander, aber ich habe auch die Berge von Kontroll-/Logik-Variablen-Durcheinander. Ich versuche, beide Stapel aufzuräumen. ;)
Versuchen Sie, es in kleine unabhängige Boxen zu zerlegen, die über "Kanäle" (in Ihrem Fall einfache Signalvariablen) kommunizieren. Je kleiner jede Box / FSM ist, desto besser.
@ Leroy105 Wenn Sie beispielsweise 20 Flag- / Signalvariablen für die Kommunikation zwischen Ihren Modulen verwenden müssen, MÜSSEN Sie 20 Variablen dafür verwenden, entweder als Bitmasken oder als separate flüchtige Variablen oder als separate Kommunikationsstruktur oder sogar ein separates Kommunikationsmodul mit mehreren Strukturen im Inneren - je nach Komplexität liegt es an Ihnen. Sie müssen nur Ihre Kommunikationsvariablen/Flags außerhalb Ihrer Hauptlogik isolieren.
Eine enge Kopplung zwischen einem LED-Treiber und einem Tastentreiber ist keineswegs besser als die ursprünglichen Spaghetti. So macht man kein richtiges OO! Ihre Klassen sind nicht autonom, wissen aber von Dingen, die sie nicht wissen sollten. Das ist kein OO-Design, weder in C noch in C++. Für ein korrektes Design sollte Code auf höherer Ebene sowohl Tasten als auch LEDs verfolgen.

Im Allgemeinen habe ich festgestellt, dass es am besten ist, ein kleines Zeitinkrement auszuwählen (vielleicht ein paar Millisekunden bis vielleicht 250 us für ein 8-Bit-Mikro) und dieses für den größten Teil oder das gesamte Timing zu verwenden. Dies ist vergleichbar mit der Granularität in einem RTOS.

Es ist einfacher, wenn Sie ein Mikro mit einer anständigen Architektur haben, die verschachtelte Interrupts zulässt.

Sphero, meinst du damit, dass du die Zustandsmaschine alle 250 us aufweckst? Was ist, wenn Ihre LED alle 1000 Sekunden blinken muss, würden Sie dann einen Zähler in Ihrem Zustandsmaschinencode verwenden? Wenn LedCounter = 1 ist, geht die LED an usw. Ich versuche, so viele gesprengte Zählervariablen und Logikflags zu eliminieren. Das System ist derzeit im Grunde ein 500us-Timer, und alles läuft davon ab.
Ja ein Zähler. Stellen Sie sich ein Array von Zählern vor, die im Interrupt-Code jeweils auf Null dekrementiert werden (deklarieren Sie sie offensichtlich als flüchtig).
Wenn ich sagte – okay, wir werden jetzt eine SysTick-Timer-ISR haben, und dann werden wir LedTimer-ISR hinzufügen, anstatt nur Zählervariablen für den LedTimer innerhalb des SysTick-Timers auszuführen – insgesamt hat sich die Systemkomplexität mit zwei ISRs erhöht vorgestellt werden? Das ist mein Bauchgefühl, es ist ein schlechter Ansatz.