Diese Programmiertechnik ermöglicht es, komplexere Steuerungen zu programmieren, die übersichtlich bleiben und damit servicefreundlich sind.
Einführung
Eine Statemachine (Zustandsautomat) ist im Prinzip nichts anderes als ein Stück Code, mit einer bestimmten Struktur.
Die Aufgabe einer Statemachine ist es, komplexere Abläufe und Steuerungen zu realisieren.
Hierbei arbeitet die Statemachine in verschiedenen Zuständen (States), die der Programmierer definiert.
Diese Zustände werden in einer Variablen gespeichert und sind zentraler Bestandteil der Statemachine.
Die Struktur einer Statemachine besteht also darin, eine Aufgabe in Zustände zu zerlegen, die in einer Variablen gespeichert werden.
Abhängig vom Inhalt der Variablen wird Code ausgeführt.
Schauen wir uns mal das Grundgerüst einer Statemachine an:
Display All
Die Struktur, wie eben beschrieben, ist hier deutlich zu sehen.
In Zeile 11 ist eine Variable definiert, die den Zustand der Statemachine aufnimmt.
Darunter, in Zeile 14 bis 17, sind zur Veranschaulichung die Zustände definiert.
In den Zeilen 21 bis 31 wird geprüft, in welchem Zustand sich die Statemachine gerade befindet. Abhängig davon wird unterschiedlicher Code ausgeführt.
Die Unterscheidung der Zustände wird hier mit Select Case erreicht. Das Prüfen der States muss regelmäßig erfolgen, damit auf Ereignisse reagiert werden kann.
Im einfachsten Fall kann diese Select Case-Struktur in der Hauptschleife angelegt werden.
Natürlich kann man die Unterscheidung des aktuellen Zustands auch mit If-Abfragen umsetzen, ich finde jedoch, dass die Select Case in Punkto Übersicht überzeugt.
Das obige Grundgerüst zeigt den grundlegenden Aufbau, ist allerdings noch nicht ganz komplett.
Hier fehlen die sogenannten Ereignisse, die zu einem Zustandswechsel führen.
Um die Zustände zu ändern, abzufragen und die Handhabung zu vereinfachen, legen wir eine kleine Include-Datei an, welche die folgenden Funktionen bereitstellt.
Display All
Aufmerksamen Lesern ist bestimmt aufgefallen, dass die Zustands-Variable (_FSM_State) jetzt in der Include-Datei integriert ist.
Das ist nicht zwingend erforderlich, macht aber das Hauptprogramm zunächst mal übersichtlicher.
Aufgrund der Include-Datei ändert sich natürlich das Hauptprogramm und sieht nun wie folgt aus:. Die Variable State entfällt logischerweise, dafür muss die Include-Datei eingebunden werden.
Display All
Hinzu kam ebenfalls die Zeile 20, in der ein Anfangszustand festgelegt wird, in dem die Statemachine beginnen soll.
In diesem Grundgerüst werden bereits alle Routinen aus der Include-Datei verwendet.
Zur Erklärung des obigen Codes.
Die Variable, die den aktuellen Zustand der Statemachine enthält, ist in die Include-Datei verlegt worden.
Dort wird auf die Variable mit SetState() und GetState() zugegriffen.
In der Include-Datei wurde zusätzlich noch eine Flag-Variable angelegt, in der weitere Informationen zur Statemachine gespeichert werden können.
Im obigen Fall wurde das FLAG_CHANGED angelegt, das immer gesetzt wird, wenn sich der Zustand durch SetState() ändert.
Das Flag FLAG_CHANGED wird mit der Funktion IsStateChanged() abgefragt und im gleichen Aufwasch gelöscht. Das hat einen Vorteil.
Im Hauptprogramm, in den Case-Fällen (Zuständen) kann nun abgefragt werden, ob der Zustand neu ist (FLAG_CHANGED gesetzt), damit hat man die Möglichkeit einmalige Aktionen auszuführen.
Das ist sehr nützlich z.B. für Display-Ausgaben (vermeidet Flackern) und auch die Aktion selbst. In einem späteren Beispiel wird das noch deutlicher.
In den Case-Fällen der verschiedenen Zustände ist noch per Kommentar angedeutet, dass Code, der immer im jeweiligen Zustand ausgeführt werden soll, eingetragen werden kann.
Dort gehören auch die Exit-Bedingungen (Ereignisse) rein, die zu einen Zustandswechsel führen sollen, sofern gewünscht.
So, nun haben wir eigentlich alles, was wir brauchen, um etwas mit einer Statemachine zu steuern.
Das was bisher gezeigt wurde ist in Grunde die Statemachine.
Das FSM_Grundgerüst der Statemachine gibt's zum Download am Ende des Beitrags.
Praktischer Teil
Im praktischen Teil soll an einem kleinen Beispiel gezeigt werden, wie man vorgeht.
Also, in dem folgenden Fall-Beispiel werden wird notwendige Zustände und Ereignisse definieren, den Programmablauf bestimmen und dann den Code entsprechend schreiben.
Beispiel 1 (LED-Steuerung)
Ein wirklich einfaches Beispiel für eine Statemachine wäre eine LED, die man mit einem Taster einschaltet und mit einem anderen Taster wieder aus macht.
Klar kann man jetzt sagen, das geht doch einfach mit 2 If-Abfragen, das stimmt. Aber es geht hier darum zu erklären, wie man die Aufgabe mittels Statemachine lösen kann.
Denn wenn die Aufgaben kompliziert werden, wird es nach altem Schema immer schwieriger den Überblick zu behalten.
Zurück zum Beispiel mit der LED-Steuerung.
Stellt sich nun die Frage, welches sind die Ereignisse, auf die die Statemachine reagieren soll, und was sind die erforderlichen Zustände und die Aktionen, um die Aufgabe zu erfüllen.
Ereignisse definieren
Beginnen wir mit den Ereignissen. Ereignisse sind immer Vorgänge, die einen Zustandswechsel nach sich ziehen. Also was bewirkt einen Zustandswechsel?
Die Frage ist recht einfach zu beantworten. Im Beispiel kommen nur die die zwei Taster in Frage, die Einfluss auf die LED haben. Also sind die Ereignisse entsprechend die Betätigung der Taster.
Genauer ist eine Ereignis die Betätigung des Taster 1, den wir mal Taster_AN nennen, und das andere
Ereignis die Betätigung des Taster 2, den wir mal Taster_AUS nennen wollen.
Zustände definieren
Welche Zustände kennt die Steuerung?
Auch diese Frage ist einfach zu beantworten. Die Steuerung soll die LED entweder AN oder AUS schalten. Dazwischen gibt es nichts. Dimmen ist nicht vorgesehen.
Es gibt somit in unserem Beispiel nur 2 Zustände für den Automat.
Programm-Ablauf
Wie bereits beschrieben, soll die Steuerung bei Taste_AN die LED einschalten und bei Taste_AUS die LED ausschalten. Das ist aber gelinde gesagt recht lapidar ausgedrückt,
und reicht allenfalls als Umschreibung für die Steuerung aus.
Wir müssen das Verhalten der Steuerung präzisieren, denn ein Programm soll zuverlässig und ohne Seiteneffekte funktionieren. Daher muss man sich immer die folgenden Fragen stellen.
Befindet sich die Steuerung im Zustand LED_AUS, soll sie nur auf die Taste_AN reagieren und den ZUSTAND_LED_AN aktivieren. Die andere Taste hat keine Funktion.
Bedindet sich die Steuerung im ZUSTAND_AN, soll sie nur auf die Taste_AUS reagieren und den ZUSTAND_LED_AUS aktivieren. Die andere Taste hat keine Funktion.
Die jeweilige Aktion wird in dem Zustand erledigt, der als neuer Zustand definiert wird.
Es muss also für jeden möglichen Zustand definiert sein, wie die Steuerung zu reagieren hat.
Die Aktion, die durchgeführt werden soll, also Led ein- oder ausschalten, wird in dem Codeabschnitt untergebracht, der nur 1x ausgeführt wird.
Es bietet sich in diesem Zusammenhang oft an, die Zustände, Ereignisse und Aktionen in einer Tabelle festzuhalten. Bei Änderungen der Steuerung kann dadurch die Funktion der Steuerung recht schnell nachvollzogen werden. Eine Tabelle ist auch hilfreich bei Dokumentationen. Ich lege hierfür in der Tabelle die Zustände in Zeilen und die Ereignisse in Spalten an, eine zusätzliche Spalte für Aktion bzw. Kommentare. Das ganze kann dann etwa so aussehen:
Tabelle der Zustände
Damit können wir den Code für die Steuerung angehen, der dann wie folgt aussieht:
Display All
Das Programm FSM_Led-Steuerung kann am Ende des Beitrags herunter geladen werden.
Man erkennt deutlich die wiederkehrende Struktur, die eingangs erwähnt wurde. Nach diesem Schema lassen sich viele Programme als Statemachine ausführen.
Beispiel 2 (Notbeleuchtung)
Das obige Beispiel ist zugegebener weise sehr einfach. Deshalb möchte ich die Statemachine mit einem 2. Beispiel gerne vertiefen.
Wir wollen eine Notbeleuchtung bauen. Sie soll im Falle eines Stromausfalls einen Flur beleuchten.
Wie soll die Notbeleuchtung funktionieren?
Die Notbeleuchtung ist am Netz (Via Steckernetzteil) angeschlossen. Die Netz-Spannung wird verwendet, um festzustellen, ob der Strom ausgefallen ist. Solange der Strom da ist, wird die Netzspannung genutzt, um den Akku zu laden bzw. die Akkuladung zu erhalten. Setzt die Netzspannung aus (Stromausfall), so soll die Notbeleuchtung angehen. Allerdings nur, wenn es ausreichend dunkel ist. Bedeutet, bei ausreichender Helligkeit bleibt bei Stromausfall die Notbeleuchtung aus (die LED_Netz geht jedoch aus).
Weiterhin soll die Notbeleuchtung den Status des Akkus und der Netzspannung per LED anzeigen.
Die Notbeleuchtung soll noch Tasten erhalten mit folgenden Funktionen.
Ich denke an dieser Stelle ist recht klar, wie die Notbeleuchtung funktioniert. Zu erwähnen ist noch, dass ein LDR mit PullDown-Widerstand zur Helligkeitsmessung, und ein Poti zur Helligkeits-Einstellung verwendet werden soll.
Welche Zustände kennt die Notbeleuchtung?
Um zu ermitteln, welche Zustände die Steuerung benötigt, kann man auch fragen: In welchen Zuständen verharrt die Steuerung, bis ein Ereignis eintritt?
Nun, beim Einschalten muss man erst mal prüfen, wie die aktuelle Lage ist. Haben wir Netzspannung, wie ist die Akkuspannung, und daraufhin wird entschieden, mit welchem Zustand die Steuerung fortfährt. Die Feststellung der Lage kann also bereits als ein Zustand angesehen werden und kann auch diverse Initialisierungen darin vornehmen. Man kann ihn z.B. benennen als ZUSTAND_DEFAULT oder ZUSTAND_START. Da wir aber etwas schreibfaul sind verwenden wir anstelle ZUSTAND nur ST für State. So können sich folgende Zustände für die Steuerung ergeben.
Die obige Zustandstabelle zeigt an, was der Automat (Notbeleuchtung) in welchem Zustand macht und auf welche Ereignisse er reagiert. Auf diese weise kann man keine Ereignisse vergessen, auf die reagiert werden muss, da in jedem Feld etwas stehen muss.
Vielleicht ist die Tabelle für den einen oder anderen noch etwas unklar zu verstehen, daher ein greife ich zur Erklärung einen Zustand heraus.
Angenommen die Notbeleuchtung befindet sich gerade im Zustand ST_BATT_CHARGING. Das bedeutet, der Akku soll geladen werden. Dies ist auch in der Spalte Bemerkung/Aktion zu sehen. Nun können in diesem Zustand verschiedene Ereignisse eintreten. In den Spalten sind alle Ereignisse (siehe 1. Zeile) aufgeführt, die eintreten können. Nun steht in dem Feld in dem sich der Zustand mit dem Ereignis kreuzt der resultierenden Zustand. in den gewechselt werden soll. Tritt also im Zustand ST_BATT_CHARGING (Zeile 3) ein Ausfall der Netzspannung ein (Spalte Netzspannung), wird in den Zustand -> ST_POWER_FAILURE gewechselt. In Spalte Akkupflege steht z.B. ein Strich. Bedeutet, bei diesem Ereignis wird kein Zustandswechsel stattfinden. In Spalte Taster Set steht "-> ST_SET", das bedeutet, dass wenn Taste Set gedrückt wird immer in den Zustand ST_SET gewechselt wird.
Das Zeichen "->" deutet nur an, dass ein Zustandswechsel erfolgt. Davor kann eine Bedingung stehen. Nach dem Zeichen "->" steht der Ziel-Zustand, in den gewechselt wird.
Man könnte das auch kurz etwa so notieren: [Bedingung] -> Ziel-Zustand
Bedingung ist optional (deshalb in eckigen Klammern) und macht oft Sinn, eine anzugeben.
Was noch fehlt ist der Code, wie man die Notbeleuchtung in Code umsetzt.
Display All
Der Code des Automaten ist jetzt schon deutlich komplexer geworden.
Ladet den einfach unten herunter und öffnet den Source in die Bascom-IDE. Es sieht schlimmer aus als es ist.
Fakt ist, dass man im Code schon unterteilt die Zustände erkennt (Hauptschleife), in denen auch die Aktionen und die Ereignisse behandelt werden.
Man sollte sofort erkennen, dass ein so programmierter Code sehr Übersichtlich ist. Man erkennt auch deutlich, wie die Statetabelle und die Code-Struktur korreliert.
Der Code wurde übrigens nicht mit Hardware getestet und ist eigentlich nur für die Einführung in die Statemachine als etwas komplexeres Beispiel gedacht.
Wer es dennoch real ausprobieren möchte, muss auch mit Programmierfehlern rechnen. Die zu beheben sollte jedoch aufgrund der State-Tabelle und ausreichend kommentiertem Code nicht schwer fallen. Auch Anpassungen sollten keine größeren Schwierigkeiten bereiten.
Die Eingangs gestellt Frage, was eine Statemachine ist und wie man die in Bascom einbindet, dürfte damit geklärt sein. Bleibt noch die Frage, wozu man das braucht.
Nun, es erleichtert die Programmierung und auch die Dokumentation. Es lassen sich komplexe Steuerung und Abläufe realisieren, die immer noch übersichtlich bleiben. Wer mal versucht die obige Steuerung herkömmlich zu programmieren, also ohne Statemachine, wird schnell merken wo die Vorteile der Statemachine liegen. Der Code bleibt wartbar.
Viel Spaß damit.
Übungsaufgaben
Wer jetzt Blut geleckt hat und es mal selbst versuchen möchte, kann sich mal Gedanken zu folgenden Aufgaben machen und versuchen eine State-Tabelle anzulegen oder den Code umzusetzen.
Übung 1: Akkuwächter für 12V Akkus.
Die Spannung einer Autobatterie soll überwacht werden. LED's sollen den Zustand anzeigen. Ist die Spannung <11,5V soll eine rote LED den niedrigen Batteriezustand signalisieren. Eine gelbe LED signalisiert Spannungen über 13,8V. Eine grüne LED zeigt korrekte Akkuspannung an. Die Akkuspannung soll mittels ADC gemessen werden.
Übung 2: Torsteuerung
Ein elektrisch betriebenes Firmentor soll manuell gesteuert werden. Hierfür sind 3 Bedienknöpfe (Taster Auf, Zu, Stop) vorgesehen. Wenn das Tor geschlossen ist (Endschalter Tor zu aktiv) ist, kann nur die Taste Auf betätigt werden. Wenn das Tor geschlossen ist (Endschalter Tor zu aktiv), kann nur die Taste Auf bedient werden. Während das Tot auf- bzw. zu fährt kann das Tor (Taste Stop) vorzeitig gestoppt werden. Ist keiner der Endschalter aktiv, soll mit Taster Auf bzw. Taster Zu das Tor wieder in Bewegung versetzt werden können. Der Antrieb für das Tor wird durch 2 Ausgänge gesteuert. Ein Pin für Richtung (Tor Öffnen = HIGH, Tor Schließen = Low) und ein Pin für Motor an (An = High).
Torsteuerung: Ampel-Erweiterung
Wenn ihr die Steuerung bis hierhin geschafft habt, könnt ihr mal die Ampel-Erweiterung einbauen.
Nach einer gewissen Zeit möchte der Auftraggeber, dass eine Ampel (Rot/Grün) nachgerüstet wird. Sie soll anzeigen, ob man hineinfahren darf oder nicht. Ist das Tor vollständig geöffnet (Endschalter aktiv), soll die Ampel grün leuchten. Während sich das Tor bewegt oder wenn es vorzeitig gestoppt wurde, soll die Ampel rot zeigen. Ist das Tor geschlossen, geht die Ampel aus, um Energie zu sparen.
Torsteuerung: Aufprallschutz-Erweiterung
Wenn ihr es bis hier her geschafft habt, könnt ihr noch eine Erweiterung nachrüsten.
Der Auftraggeber hat festgestellt, dass es eine Gefahr darstellt, wenn das Tor zu fährt und sich noch etwas im Weg befindet wie eine Person oder ein Fahrzeug. Um Verletzungen und Schäden zu vermeiden, soll ein Aufprallschutz am Tor angebracht werden. Dieser besteht aus einem Taster, der betätigt wird, wenn das Tor beim Schließen auf ein Hindernis trifft. In dem Fall soll das Tor sofort stoppen. Die Manuelle Bedienung lässt dann nur die Taste Auf als Bedientaste zu.
Torsteuerung: Überwachung Torbewegung
Der Auftraggeber möchte, dass die Torbewegung überwacht werden soll. Hierfür soll ein Timeout erkennen, ob das Tor ordnungsgemäß schließt oder öffnet. Die Funktion sieht folgendermaßen aus.
Wenn das Taste Tor Zu betätigt wird, dauert es eine gewisse Zeit, bis das Tor den Endschalter ZU erreicht. Ist die Zeit deutlich länger, kann man davon ausgehen, dass ein Fehler am Antrieb vor liegt. Der Auftraggeber sagt, dass das Tor nie länger als 60 Sekunden zum Öffnen bzw. Schließen benötigt. Sollte nach 90 Sekunden der Endschalter nicht erreicht sein, kann man von einem Fehler ausgehen. In diesem Fall soll der Antrieb auf jeden Fall abgeschaltet werden. Und um den Fehler anzuzeigen, soll die rote Ampel im Sekundentakt blinken.
Schlussbemerkung
Eine Statemachine kann sehr unterschiedliche Anforderungen erfüllen und ist damit situationsbedingt sehr flexibel.
So kann es unter Umständen gewünscht sein, dass bestimmte Zustände auf keinerlei Ereignisse reagieren sollen. Das könnte z.B. Im Fehlerfall bei einer Notabschaltung der Fall sein, um größeren Schaden zu vermeiden. In der Regel sind aber in jedem Zustand Ereignisse notwendig, um den Zustandswechsel zu initiieren.
In meinen Beispielen habe ich die Aktion, die bei einem Zustandswechsel ausgeführt werden soll, stets in den Ziel-Zustand im Bereich der Bedingung "If IsStateChanged() = TRUE" eingefügt. Das hat einen einfachen Grund. Man muss die Aktion nur 1x ausführen und zwar in dem Zustand, in den gewechselt wird.
Zustandsautomaten sind nicht nur für Steuerungen (Hardware) einsetzbar, sondern können auch bei Regelungen eingesetzt werden. Ein weiterer Einsatzbereich ist neben der Hardwaresteuerung auch die Software-Steuerung. Hierbei ändert der Automat seinen Zustand abhängig von den Daten, die er verarbeitet. Dies kann z.B. beim dekodieren von seriellen Signalen der Fall sein (Serielle Schnittstelle, Netzwerk-Datenverkehr). Auch ein Interpreter ist im Grunde eine Statemachine. Denn sie interpretiert die geschriebenen Befehle und führt dann die entsprechende Aktion aus. Compiler sind im Grunde auch nichts anderes. Sie interpretieren Text und erzeugen abhängig davon einen Binärcode, den der Prozessor versteht.
Die hier vorgestellte Möglichkeit einer Statemachine kann ein einzelnen Zustandsautomat (Objekt) verwalten.
Es gibt Situationen, da reicht ein einzelner Automat nicht aus. Möchte man mehrere Automaten quasi parallel laufen lassen, ist das möglich, wenn die Include-Datei angepasst wird, so dass mehrere Zustände unabhängig voneinander verwaltet werden können. Ein typische Beispiel für parallelle Automaten sind beispielsweise Computer-Spiele.
Jeder kennt Spiele in denen man gegen den Computer spielt. Der Computer-Spieler ist in dem Fall ein Automat. Manche Spiele lassen es zu, den Computer gegen sich selbst spielen zu lassen. In dem Fall spielt allerdings ein weiterer Automat den 2. Spieler. Es spielt also Automat 1 gegen Automat 2. Jeder Automat kann hier als eigenständiges Objekt betrachtet werden das unabhängig vom anderen Objekt ist. Sie verwenden möglicherweise die gleichen Konstanten als Zustände, jeder Automat kann sich trotzdem in einem anderen Zustand befinden.
Man mag es kaum glauben, aber fast alles kann man, sofern man das möchte, als Automat betrachten.
Man verzeihe mir den Vergleich mit Babette, aber auch sie ist ein Automat. Babette ist entweder am schlafen, sie ist sauer oder gar eingeschnappt. Oder sie ist gelangweilt und erzählt einen Witz. Das alles sind Zustände.
In diesem Sinne frohes Gelingen!
Mitch
Eine Statemachine (Zustandsautomat) ist im Prinzip nichts anderes als ein Stück Code, mit einer bestimmten Struktur.
Die Aufgabe einer Statemachine ist es, komplexere Abläufe und Steuerungen zu realisieren.
Hierbei arbeitet die Statemachine in verschiedenen Zuständen (States), die der Programmierer definiert.
Diese Zustände werden in einer Variablen gespeichert und sind zentraler Bestandteil der Statemachine.
Die Struktur einer Statemachine besteht also darin, eine Aufgabe in Zustände zu zerlegen, die in einer Variablen gespeichert werden.
Abhängig vom Inhalt der Variablen wird Code ausgeführt.
Schauen wir uns mal das Grundgerüst einer Statemachine an:
BASCOM Source Code: FSM_Grundgerüst_1
- ' Grundgerüst Statemachine
- $Regfile = "m8def.dat"
- $Crystal = 1000000
- $HWStack = 24
- $SWStack = 24
- $Framesize = 24
- ' Variable für Zustand
- Dim State as Byte
- ' Zustände des Automaten mit sinnvollen Bezeichnungen
- Const ZUSTAND_A = 0
- Const ZUSTAND_B = 1
- Const ZUSTAND_C = 2
- Const ZUSTAND_D = 3
- Do
- Select Case State
- Case ZUSTAND_A
- Case ZUSTAND_B
- Case ZUSTAND_C
- Case ZUSTAND_D
- End Select
- Loop
Die Struktur, wie eben beschrieben, ist hier deutlich zu sehen.
In Zeile 11 ist eine Variable definiert, die den Zustand der Statemachine aufnimmt.
Darunter, in Zeile 14 bis 17, sind zur Veranschaulichung die Zustände definiert.
In den Zeilen 21 bis 31 wird geprüft, in welchem Zustand sich die Statemachine gerade befindet. Abhängig davon wird unterschiedlicher Code ausgeführt.
Die Unterscheidung der Zustände wird hier mit Select Case erreicht. Das Prüfen der States muss regelmäßig erfolgen, damit auf Ereignisse reagiert werden kann.
Im einfachsten Fall kann diese Select Case-Struktur in der Hauptschleife angelegt werden.
Natürlich kann man die Unterscheidung des aktuellen Zustands auch mit If-Abfragen umsetzen, ich finde jedoch, dass die Select Case in Punkto Übersicht überzeugt.
Das obige Grundgerüst zeigt den grundlegenden Aufbau, ist allerdings noch nicht ganz komplett.
Hier fehlen die sogenannten Ereignisse, die zu einem Zustandswechsel führen.
Um die Zustände zu ändern, abzufragen und die Handhabung zu vereinfachen, legen wir eine kleine Include-Datei an, welche die folgenden Funktionen bereitstellt.
- SetState(), zum Setzen eines neuen Zustands (Zustandswechsel)
- GetState(), zum Abfragen des aktuellen Zustands in der Select Case-Anweisung
- IsStateChanged(), zum Abfragen innerhalb einer Case-Anweisung, ob sich der Zustand geändert hat.
BASCOM Source Code: Statemachine.inc
- ' Include-Datei für Statemachine
- Dim _FSM_State as Byte ' Zustands-Variable des Automaten
- Dim _FSM_Flags as Byte ' Interne Flags des Automaten
- Const FLAG_CHANGED = 0 ' Gesetzt, wenn sich der Zustand geändert hat
- Declare Sub SetState(Byval state as Byte)
- Declare Function GetState() as Byte
- Declare Function IsStateChanged() as Byte
- ' --------------------------------------------------------
- Goto Modul_Statemachine_Exit
- ' --------------------------------------------------------
- ' Setzen eines neuen Zustandes und markieren als geänderten Zustand
- Sub SetState(Byval state as Byte)
- _FSM_State = state ' Neuer Zustand setzen
- Set _FSM_Flags.FLAG_CHANGED ' Flag Zustand hat sich geändert
- End Sub
- ' Aktueller Zustand ermitteln
- Function GetState() as Byte
- GetState = _FSM_State ' aktuellen Zustand an Aufrufer
- End Function
- ' Prüft, ob Zustand sich geändert hat und löscht FLAG_CHANGED
- Function IsStateChanged() as Byte
- If _FSM_Flags.FLAG_CHANGED = 1 then
- Reset _FSM_Flags.FLAG_CHANGED ' Flag geändert nach 1. Abfrage löschen
- IsStateChanged = TRUE ' Zustand geändert an Aufrufer
- Else
- IsStateChanged = FALSE ' Zustand nicht geändert an Aufrufer
- End If
- End Function
- ' --------------------------------------------------------
- Modul_Statemachine_Exit:
- ' --------------------------------------------------------
Das ist nicht zwingend erforderlich, macht aber das Hauptprogramm zunächst mal übersichtlicher.
Aufgrund der Include-Datei ändert sich natürlich das Hauptprogramm und sieht nun wie folgt aus:. Die Variable State entfällt logischerweise, dafür muss die Include-Datei eingebunden werden.
BASCOM Source Code: FSM_Grundgeruest_2
- ' Grundgerüst Statemachine 2
- $Regfile = "m8def.dat"
- $Crystal = 1000000
- $HWStack = 24
- $SWStack = 24
- $Framesize = 24
- Const TRUE = 1 ' Konstanten für True, False
- Const FALSE = 0
- $Include "Statemachine.inc" ' Routinen Statemachine einbilden
- ' Zustände des Automaten mit sinnvollen Bezeichnungen
- Const ZUSTAND_A = 0
- Const ZUSTAND_B = 1
- Const ZUSTAND_C = 2
- Const ZUSTAND_D = 3
- Call SetState(ZUSTAND_A) ' Festlegen des Anfangs-Zustands
- Do
- Select Case GetState() ' aktuellen Zustand abfragen
- ' -----------------------------------------------------
- Case ZUSTAND_A
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- End If
- ' Hier Code, der immer ausgeführt wird
- ' ..
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- ' ..
- ' -----------------------------------------------------
- Case ZUSTAND_B
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- End If
- ' Hier Code, der immer ausgeführt wird
- ' ..
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- ' ..
- ' -----------------------------------------------------
- Case ZUSTAND_C
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- End If
- ' Hier Code, der immer ausgeführt wird
- ' ..
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- ' ..
- ' -----------------------------------------------------
- Case ZUSTAND_D
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- End If
- ' Hier Code, der immer ausgeführt wird
- ' ..
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- ' ..
- End Select
- Loop
In diesem Grundgerüst werden bereits alle Routinen aus der Include-Datei verwendet.
Zur Erklärung des obigen Codes.
Die Variable, die den aktuellen Zustand der Statemachine enthält, ist in die Include-Datei verlegt worden.
Dort wird auf die Variable mit SetState() und GetState() zugegriffen.
In der Include-Datei wurde zusätzlich noch eine Flag-Variable angelegt, in der weitere Informationen zur Statemachine gespeichert werden können.
Im obigen Fall wurde das FLAG_CHANGED angelegt, das immer gesetzt wird, wenn sich der Zustand durch SetState() ändert.
Das Flag FLAG_CHANGED wird mit der Funktion IsStateChanged() abgefragt und im gleichen Aufwasch gelöscht. Das hat einen Vorteil.
Im Hauptprogramm, in den Case-Fällen (Zuständen) kann nun abgefragt werden, ob der Zustand neu ist (FLAG_CHANGED gesetzt), damit hat man die Möglichkeit einmalige Aktionen auszuführen.
Das ist sehr nützlich z.B. für Display-Ausgaben (vermeidet Flackern) und auch die Aktion selbst. In einem späteren Beispiel wird das noch deutlicher.
In den Case-Fällen der verschiedenen Zustände ist noch per Kommentar angedeutet, dass Code, der immer im jeweiligen Zustand ausgeführt werden soll, eingetragen werden kann.
Dort gehören auch die Exit-Bedingungen (Ereignisse) rein, die zu einen Zustandswechsel führen sollen, sofern gewünscht.
So, nun haben wir eigentlich alles, was wir brauchen, um etwas mit einer Statemachine zu steuern.
Das was bisher gezeigt wurde ist in Grunde die Statemachine.
Das FSM_Grundgerüst der Statemachine gibt's zum Download am Ende des Beitrags.
Praktischer Teil
Im praktischen Teil soll an einem kleinen Beispiel gezeigt werden, wie man vorgeht.
Also, in dem folgenden Fall-Beispiel werden wird notwendige Zustände und Ereignisse definieren, den Programmablauf bestimmen und dann den Code entsprechend schreiben.
Beispiel 1 (LED-Steuerung)
Ein wirklich einfaches Beispiel für eine Statemachine wäre eine LED, die man mit einem Taster einschaltet und mit einem anderen Taster wieder aus macht.
Klar kann man jetzt sagen, das geht doch einfach mit 2 If-Abfragen, das stimmt. Aber es geht hier darum zu erklären, wie man die Aufgabe mittels Statemachine lösen kann.
Denn wenn die Aufgaben kompliziert werden, wird es nach altem Schema immer schwieriger den Überblick zu behalten.
Zurück zum Beispiel mit der LED-Steuerung.
Stellt sich nun die Frage, welches sind die Ereignisse, auf die die Statemachine reagieren soll, und was sind die erforderlichen Zustände und die Aktionen, um die Aufgabe zu erfüllen.
Ereignisse definieren
Beginnen wir mit den Ereignissen. Ereignisse sind immer Vorgänge, die einen Zustandswechsel nach sich ziehen. Also was bewirkt einen Zustandswechsel?
Die Frage ist recht einfach zu beantworten. Im Beispiel kommen nur die die zwei Taster in Frage, die Einfluss auf die LED haben. Also sind die Ereignisse entsprechend die Betätigung der Taster.
Genauer ist eine Ereignis die Betätigung des Taster 1, den wir mal Taster_AN nennen, und das andere
Ereignis die Betätigung des Taster 2, den wir mal Taster_AUS nennen wollen.
Zustände definieren
Welche Zustände kennt die Steuerung?
Auch diese Frage ist einfach zu beantworten. Die Steuerung soll die LED entweder AN oder AUS schalten. Dazwischen gibt es nichts. Dimmen ist nicht vorgesehen.
Es gibt somit in unserem Beispiel nur 2 Zustände für den Automat.
- ZUSTAND_LED_AN und
- ZUSTAND_LED_AUS
Programm-Ablauf
Wie bereits beschrieben, soll die Steuerung bei Taste_AN die LED einschalten und bei Taste_AUS die LED ausschalten. Das ist aber gelinde gesagt recht lapidar ausgedrückt,
und reicht allenfalls als Umschreibung für die Steuerung aus.
Wir müssen das Verhalten der Steuerung präzisieren, denn ein Programm soll zuverlässig und ohne Seiteneffekte funktionieren. Daher muss man sich immer die folgenden Fragen stellen.
- In welchen Zustand soll die Steuerung beginnen?
- Auf welche Ereignisse ist in den einzelnen Zuständen wie zu reagieren?
- Welche Aktionen sind in den einzelnen Zuständen auszulösen?
Befindet sich die Steuerung im Zustand LED_AUS, soll sie nur auf die Taste_AN reagieren und den ZUSTAND_LED_AN aktivieren. Die andere Taste hat keine Funktion.
Bedindet sich die Steuerung im ZUSTAND_AN, soll sie nur auf die Taste_AUS reagieren und den ZUSTAND_LED_AUS aktivieren. Die andere Taste hat keine Funktion.
Die jeweilige Aktion wird in dem Zustand erledigt, der als neuer Zustand definiert wird.
Es muss also für jeden möglichen Zustand definiert sein, wie die Steuerung zu reagieren hat.
Die Aktion, die durchgeführt werden soll, also Led ein- oder ausschalten, wird in dem Codeabschnitt untergebracht, der nur 1x ausgeführt wird.
Es bietet sich in diesem Zusammenhang oft an, die Zustände, Ereignisse und Aktionen in einer Tabelle festzuhalten. Bei Änderungen der Steuerung kann dadurch die Funktion der Steuerung recht schnell nachvollzogen werden. Eine Tabelle ist auch hilfreich bei Dokumentationen. Ich lege hierfür in der Tabelle die Zustände in Zeilen und die Ereignisse in Spalten an, eine zusätzliche Spalte für Aktion bzw. Kommentare. Das ganze kann dann etwa so aussehen:
Tabelle der Zustände
Zustände | Taster_AN | Taster_Aus | Bemerkung, Aktion |
ZUSTAND_LED_AUS | Wechsel in ZUSTAND_LED_AN | keine Funktion | Led ausschalten |
ZUSTAND_LED_AN | keine Funktion | Wechsel in ZUSTAND_LED_AUS | Led einschalten |
Damit können wir den Code für die Steuerung angehen, der dann wie folgt aussieht:
BASCOM Source Code: FSM_Led-Steuerung
- ' LED-Steuerung als Automat
- $Regfile = "m8def.dat"
- $Crystal = 1000000
- $HWStack = 24
- $SWStack = 24
- $Framesize = 24
- Const TRUE = 1 ' Konstanten für True, False
- Const FALSE = 0
- ' Ausgang für LED festlegen
- LED Alias PortB.1 : Config LED = Output
- ' Eingänge für Taster (Low-Aktiv)
- Taster_AN Alias PinC.4 : Set PinC.4 ' Taster mit internen PullUps
- Taster_AUS Alias PinC.5 : Set PinC.5
- $Include "Statemachine.inc" ' Routinen Statemachine einbilden
- ' Zustände des Automaten mit sinnvollen Bezeichnungen
- Const ZUSTAND_LED_AUS = 0
- Const ZUSTAND_LED_AN = 1
- Call SetState(ZUSTAND_LED_AUS) ' Festlegen des Start-Zustands
- Do
- Select Case GetState() ' aktuellen Zustand abfragen
- ' -----------------------------------------------------
- Case ZUSTAND_LED_AUS
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- Reset Led ' Led ausschalten
- End If
- ' Hier Code, der immer ausgeführt wird
- ' .. ist nicht erforderlich
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- If Taster_AN = 0 then ' Taste an betätigt
- SetState(ZUSTAND_LED_AN)
- End If
- ' -----------------------------------------------------
- Case ZUSTAND_LED_AN
- ' -----------------------------------------------------
- If IsStateChanged() = TRUE then ' Code wird 1x ausgeführt nach Zustandswechsel
- Set Led ' LDE einschalten
- End If
- ' Hier Code, der immer ausgeführt wird
- ' .. ist nicht erforderlich
- ' Exitbedingungen (Ereignisse), die einen Zustandswechsel verursachen
- If Taster_AUS = 0 then ' Taste-Aus betätigt
- SetState(ZUSTAND_LED_AUS)
- End If
- End Select
- Loop
Man erkennt deutlich die wiederkehrende Struktur, die eingangs erwähnt wurde. Nach diesem Schema lassen sich viele Programme als Statemachine ausführen.
Beispiel 2 (Notbeleuchtung)
Das obige Beispiel ist zugegebener weise sehr einfach. Deshalb möchte ich die Statemachine mit einem 2. Beispiel gerne vertiefen.
Wir wollen eine Notbeleuchtung bauen. Sie soll im Falle eines Stromausfalls einen Flur beleuchten.
Wie soll die Notbeleuchtung funktionieren?
Die Notbeleuchtung ist am Netz (Via Steckernetzteil) angeschlossen. Die Netz-Spannung wird verwendet, um festzustellen, ob der Strom ausgefallen ist. Solange der Strom da ist, wird die Netzspannung genutzt, um den Akku zu laden bzw. die Akkuladung zu erhalten. Setzt die Netzspannung aus (Stromausfall), so soll die Notbeleuchtung angehen. Allerdings nur, wenn es ausreichend dunkel ist. Bedeutet, bei ausreichender Helligkeit bleibt bei Stromausfall die Notbeleuchtung aus (die LED_Netz geht jedoch aus).
Weiterhin soll die Notbeleuchtung den Status des Akkus und der Netzspannung per LED anzeigen.
- Netzspannung da, grüne LED (LED_Netz) leuchtet.
- Akku wird geladen, rote LED (LED_Batt_Charge) leuchtet.
- Akku voll, Ladung wird erhalten, grüne LED (LED_Batt_Full)
Die Notbeleuchtung soll noch Tasten erhalten mit folgenden Funktionen.
- Taste Test simuliert einen Stromausfall. Der Controller erkennt Stromausfall und verhält sich so, solange die Taste gedrückt bleibt. Taster ist ein Öffner und unterbricht die Spannung zum Eingang (Pin_Power), der feststellt ob Netz (Spannung vom Steckernetzteil) da ist oder nicht.
- Taste Set (Schließer) ermöglicht die Einstellung der Helligkeit, bei der die Notbeleuchtung einschaltet.
- Taste Akkupflege (Schließer), der Akku wird einmal komplett entladen und dann neu geladen. Damit soll ein Memory-Effekt des Akkus verhindert werden. Tritt während der Akkupflege ein Stromausfall ein, so soll die Notbeleuchtung wie oben beschrieben aktiv werden.
Ich denke an dieser Stelle ist recht klar, wie die Notbeleuchtung funktioniert. Zu erwähnen ist noch, dass ein LDR mit PullDown-Widerstand zur Helligkeitsmessung, und ein Poti zur Helligkeits-Einstellung verwendet werden soll.
Welche Zustände kennt die Notbeleuchtung?
Um zu ermitteln, welche Zustände die Steuerung benötigt, kann man auch fragen: In welchen Zuständen verharrt die Steuerung, bis ein Ereignis eintritt?
Nun, beim Einschalten muss man erst mal prüfen, wie die aktuelle Lage ist. Haben wir Netzspannung, wie ist die Akkuspannung, und daraufhin wird entschieden, mit welchem Zustand die Steuerung fortfährt. Die Feststellung der Lage kann also bereits als ein Zustand angesehen werden und kann auch diverse Initialisierungen darin vornehmen. Man kann ihn z.B. benennen als ZUSTAND_DEFAULT oder ZUSTAND_START. Da wir aber etwas schreibfaul sind verwenden wir anstelle ZUSTAND nur ST für State. So können sich folgende Zustände für die Steuerung ergeben.
- ST_START: In diesem Zustand startet die Statemachine, hier wird ermittelt, welche Spannung der Akku hat, und ob Netzspannung vorhanden ist. Entsprechend wird der Zustand der Statemachine eingestellt.
- ST_BATT_CHARGING: In diesem Zustand wird der Akku geladen, bis die Ladeschlussspannung erreicht ist. In Diesem Zustand wird die Netzversorgung überwacht, aber auch die Taster Test und Set. Anwahl der Akkupflege ist in diesem Zustand nicht erlaubt.
- ST_BATT_FULL: In diesem Zustand ist der Akku vollgeladen, die Ladung wird nur noch erhalten. In diesem Zustand wird die Netzspannung überwacht, aber auch die Tasten Test, Set und Akkupflege.
- ST_BATT_UNCHARGING: In diesem Zustand wird der Akku aufgrund der Benutzereingabe „Akkupflege“ entladen. Bei Stromausfall wird der Zustand natürlich beendet. Taste Test beendet den Zustand logischerweise ebenfalls, denn sie simuliert den Stromausfall.
- ST_POWER_FAILURE: In diesem Zustand dauert der Stomausfall an. Die Notbeleuchtung ist an, wenn der eingestellte Helligkeitswert unterschritten ist.
- ST_SET: In diesem Zustand kann mit dem Poti der Schwellwert eingestellt werden, bei dem die Notbeleuchtung eingeschaltet wird. Es wird also direkt der Poti-Wert mit dem LDR-Wert verglichen und entsprechend die Notbeleuchtung ein- oder ausgeschaltet. Bei erneutem Drücken der Set-Taste, wird der Poti-Wert im EEProm gespeichert und als Helligkeitswert im Falle des Stromausfalls als Grenzwert verwendet. Um anzuzeigen, dass der Einstellmode aktiv ist, soll die LED_Netz im Sekundentakt blinken.
- ST_BATT_CRITICAL: In diesem Zustand ist die Akkuspannung unter einem kritischen Wert. Die Notbeleuchtung wird abgeschaltet, LED’s alle aus, bis auf LED_Charge, diese soll blinken, um den kritischen Zustand anzuzeigen.
Zustand | Netzspannung (Pin_Power) | Akkuzustand (ADC0) | Taster Test | Taster Akkupflege | Taster Set | Bemerkung/Aktion |
ST_START | Ausfall -> ST_POWER_FAILURE | Wenn kein Netzausfall: Akku Nicht voll -> ST_BATT_CHARCHING Akku Voll -> ST_BATT_FULL | - | - | - | Überprüfen der Netzspannung Kein Netzausfall, dann wechsel in à ST_BATT_FULL oder à ST_BATT_CHARGING |
ST_BATT_CHARGING | Ausfall -> ST_POWER_FAILURE | Akku Voll -> ST_BATT_FULL | -> ST_POWER_FAILURE | - | -> ST_SET | Akku laden. Ladeelektronik aktivieren, LED-Status anzeigen |
ST_BATT_FULL | Ausfall -> ST_POWER_FAILURE | Akku Nicht voll -> ST_BATT_CHARGING | -> ST_POWER_FAILURE | -> ST_BATT_UNCHARGE | -> ST_SET | Akku ist voll. Umschaltung in Erhaltungsladen. |
ST_BATT_UNCHARGING | Ausfall -> ST_POWER_FAILURE | Akku kritisch -> ST_BATT_CHARGING | -> ST_POWER_FAILURE | - | - | Akku entladen. Umschalten in Akku entladen und LED-Status aktualisieren |
ST_SET | - | - | - | - | -> ST_START | Einstellung der Helligkeit Led_Netz blinkt |
ST_BATT_CRITICAL | Netz da -> ST_BATT_CHARGING | - | - | - | - | Akkuzustand kritisch. Alles ausschalten, Led_Charge blinkt |
ST_POWER_FAILURE | Netz da -> ST_BATT_CHARGING | Kritisch à ST_BATT_CRITICAL | - | - | - | Stromausfall. Notbeleuchtung aktivieren, LED-Status aktialisieren |
Die obige Zustandstabelle zeigt an, was der Automat (Notbeleuchtung) in welchem Zustand macht und auf welche Ereignisse er reagiert. Auf diese weise kann man keine Ereignisse vergessen, auf die reagiert werden muss, da in jedem Feld etwas stehen muss.
Vielleicht ist die Tabelle für den einen oder anderen noch etwas unklar zu verstehen, daher ein greife ich zur Erklärung einen Zustand heraus.
Angenommen die Notbeleuchtung befindet sich gerade im Zustand ST_BATT_CHARGING. Das bedeutet, der Akku soll geladen werden. Dies ist auch in der Spalte Bemerkung/Aktion zu sehen. Nun können in diesem Zustand verschiedene Ereignisse eintreten. In den Spalten sind alle Ereignisse (siehe 1. Zeile) aufgeführt, die eintreten können. Nun steht in dem Feld in dem sich der Zustand mit dem Ereignis kreuzt der resultierenden Zustand. in den gewechselt werden soll. Tritt also im Zustand ST_BATT_CHARGING (Zeile 3) ein Ausfall der Netzspannung ein (Spalte Netzspannung), wird in den Zustand -> ST_POWER_FAILURE gewechselt. In Spalte Akkupflege steht z.B. ein Strich. Bedeutet, bei diesem Ereignis wird kein Zustandswechsel stattfinden. In Spalte Taster Set steht "-> ST_SET", das bedeutet, dass wenn Taste Set gedrückt wird immer in den Zustand ST_SET gewechselt wird.
Das Zeichen "->" deutet nur an, dass ein Zustandswechsel erfolgt. Davor kann eine Bedingung stehen. Nach dem Zeichen "->" steht der Ziel-Zustand, in den gewechselt wird.
Man könnte das auch kurz etwa so notieren: [Bedingung] -> Ziel-Zustand
Bedingung ist optional (deshalb in eckigen Klammern) und macht oft Sinn, eine anzugeben.
Was noch fehlt ist der Code, wie man die Notbeleuchtung in Code umsetzt.
BASCOM Source Code: FSM_Notbeleuchtung
- ' Notbeleuchtung als Statemachine (Beispiel)
- ' Der Code ist nicht getestet, er kann somit noch Fehler enthalten
- ' und ist bei Verwendung anzupassen (z.B. GetBattState())
- $Regfile = "m8Adef.dat"
- $HWStack = 32
- $SWStack = 64
- $Framesize = 32
- $Crystal = 8000000
- Const TRUE = 1
- Const FALSE = 0
- ' --------------------------------------------------------
- ' Zustände des Automaten definieren
- Const ST_START = 0 ' Start-Zustand des Automaten
- Const ST_BATT_CHARGING = 1 ' Zustand Akku wird geladen
- Const ST_BATT_FULL = 2 ' Zustand Akku ist voll
- Const ST_BATT_UNCHARGING = 3 ' Zustand Akku wird entladen (Akkupflege)
- Const ST_BATT_CRITICAL = 4 ' Zustand Akkuspannung ist kritisch
- Const ST_SET = 5 ' Zustand Einstellung der Helligkeits-Schwelle
- Const ST_POWER_FAILURE = 6 ' Zustand Netzfall
- ' --------------------------------------------------------
- ' Mögliche Lade-Zustände des Akkus
- Const BATT_UNKNOWN = 0 ' Akkuzustand noch nicht ermittelt
- Const BATT_CRITICAL = 1 ' Akkuzustand kritisch (Leer)
- Const BATT_NOT_FULL = 2 ' Akkuzustand zwischen Kritisch und Voll
- Const BATT_FULL = 3 ' Akkuzustand voll
- ' --------------------------------------------------------
- ' Definition der ADC-Eingänge
- Const ADC_BATT = 0 ' ADC0, Akku wird am ADC0 gemessen
- Const ADC_LUMINANCE = 1 ' ADC1, Helligkeit von LDR
- Const ADC_POTI = 2 ' ADC2, Poti Helligkeitseinstellung
- ' --------------------------------------------------------
- ' Eingänge
- Pin_Power Alias PinB.0 ' High-Pegel -> Netzspannung da
- Taste_Set Alias PinD.3 : Set Taste_Set ' Einstellung der Helligkeitsschwelle
- Taste_Akkupflege Alias PinD.5 : Set Taste_Akkupflege ' Akkupflege starten
- ' --------------------------------------------------------
- ' Ausgänge
- Pin_Light Alias PortB.1 : Config Pin_Light = Output ' Anschluss Beleuchtung
- Pin_Charge Alias PortB.2 : Config Pin_Charge = Output ' Anschluss Laden aktivieren
- Pin_ChargeHold Alias PortB.3 : Config Pin_ChargeHold = Output ' Anschluss Ladeerhaltung
- Led_Charge Alias PortD.0 : Config Led_Charge = Output ' Lade-LED
- Led_Netz Alias PortD.1 : Config Led_Netz = Output ' Netz vorhanden LED
- Led_Batt_Full Alias PortD.2 : Config Led_Batt_Full = Output ' Akku Voll Led
- ' --------------------------------------------------------
- ' Include-Dateien
- $Include "Statemachine.inc" ' Routinen Statemachine einbinden
- ' --------------------------------------------------------
- ' Deklarationen Routinen Hauptprogramm
- Declare Function IsPowerOK() as Byte
- Declare Function GetBattState() as Byte
- Declare Function GetLuminanceState() as Byte
- Declare Sub ISR_Timer1() ' Timer1-Interrupt-Routine
- ' --------------------------------------------------------
- ' verwendete Variablen
- Dim Luminance as Word ' Helligheit
- Dim Luminance_Limit as Word ' Helligkeits-Schwellwert
- Dim eeLuminance_Limit as ERAM Word ' EEProm Wert des Luminamce_Limit
- Dim FlagTick as Byte ' Wird alle 0,5s von ISR-Routine auf True gesetzt
- ' --------------------------------------------------------
- ' Hardware konfigurieren
- ' --------------------------------------------------------
- ' --------------------------------------------------------
- ' Timer für blinkende LED
- Config Timer1 = Timer , Prescale = 256 , Clear Timer = 1
- Compare1A = 15525 - 1 ' Frequenz einstellen auf 2 Hz
- On Timer1 ISR_Timer1 ' ISR-Routine definieren
- Enable Timer1 ' Overflow-Interrupt zulassen
- ' --------------------------------------------------------
- ' ADC-Converter konfigurieren
- Config ADC = Single , Prescaler = Auto , Reference = Internal_2.56 ' Interne Referenz-Spannung 2.56V
- ' ----------------------------------------------------------------------------
- ' Hauptschleife
- ' ----------------------------------------------------------------------------
- Call SetState(ST_START) ' Startzustand festlegen
- Do
- Select Case GetState()
- ' --------------------------------------------------
- Case ST_START
- ' --------------------------------------------------
- If IsPowerOK() = FALSE then ' Netzausfall?
- Call SetState(ST_POWER_FAILURE)
- Else
- Select Case GetBattState() ' Akkuzustand ermitteln
- Case BATT_NOT_FULL
- Call SetState(ST_BATT_CHARGING)
- Case BATT_FULL
- Call SetState(ST_BATT_FULL)
- End Select
- End If
- ' Initialisieren des Helligkeits-Schwellwrtes
- If eeLuminance_Limit = &hFFFF then ' Limit noch nicht festgelegt?
- eeLuminance_Limit = 512 ' Halber ADC-Wert festlegen
- End If
- Luminance_Limit = eeLuminance_Limit ' Schwellwert aus EEProm laden
- ' --------------------------------------------------
- Case ST_BATT_CHARGING ' Akku wird geladen
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Set Pin_Charge ' Ladeelektronik aktivieren
- Reset Pin_ChargeHold ' Ladeerhaltung deaktivieren
- Set Led_Charge ' Akkustatus Laden anzeigen
- Reset Led_Batt_Full ' Led Akku Voll aus
- Set Led_Netz ' Netz anzeigen
- End If
- ' Exitbedingungen (Ereignisse)
- If IsPowerOK() = FALSE then ' Netzausfall/Simulation Netzausfall
- Call SetState(ST_POWER_FAILURE)
- ElseIf GetBattState() = BATT_FULL then ' Akku ist voll
- Call SetState(ST_BATT_FULL)
- ElseIf Taste_Set = 0 then ' Helligkeitsschwelle einstellen
- Call SetState(ST_SET)
- End If
- ' --------------------------------------------------
- Case ST_BATT_FULL ' Akku ist voll
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Reset Pin_Charge ' Ladeelektronik deaktivieren
- Set Pin_ChargeHold ' Ladeerhaltung aktivieren
- Set LED_Batt_Full ' LED Batt Full ein
- Reset LED_Charge ' LED Charge aus
- Set Led_Netz ' Netz anzeigen
- End If
- ' Exitbedingungen (Ereignisse)
- If IsPowerOK() = FALSE then ' Netzausfall/Simulation Netzausfall
- Call SetState(ST_POWER_FAILURE)
- ElseIf GetBattState() = BATT_NOT_FULL then ' Muss Akku nachgeladen werden?
- Call SetState(ST_BATT_CHARGING)
- ElseIf Taste_Set = 0 then ' Helligkeitsschwelle einstellen
- Call SetState(ST_SET)
- End If
- ' --------------------------------------------------
- Case ST_BATT_UNCHARGING ' Akku wird entladen
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Reset Pin_Charge ' Ladeelektronik deaktivieren
- Reset Pin_ChargeHold ' Ladeerhaltung deaktivieren
- Reset Pin_Charge ' Ladeelektronik deaktivieren
- Reset Pin_ChargeHold ' Ladeerhaltung deaktivieren
- End If
- ' Wiederkehrende Aktion / Exitbedingungen (Ereignisse)
- Select Case GetBattState()
- Case BATT_FULL
- Set Led_Batt_Full ' Akkustatus Voll anzeigen
- Case BATT_NOT_FULL
- Reset Led_Batt_Full ' Akkustatus Voll ausschalten
- Case BATT_CRITICAL ' Akku leer, Exitbedingung
- Call SetState(ST_BATT_CHARGING)
- End Select
- If IsPowerOK() = FALSE then ' Stromausfall
- Call SetState(ST_POWER_FAILURE)
- End If
- Led_Netz = Pin_Power ' Netz-Status anzeigen
- ' --------------------------------------------------
- Case ST_BATT_CRITICAL ' Akkuspannung kritisch
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Reset Led_Batt_Full
- Reset Led_Charge
- End If
- ' Wiederkehrende Aktionen
- Led_Netz = Pin_Power ' Netzstatus anzeigen
- ' Exitbedingungen (Ereignisse)
- If IsPowerOK() = TRUE then
- Call SetState(ST_BATT_CHARGING)
- ElseIf FlagTick = TRUE then ' Led-Charge soll blinken
- Toggle Led_Charge
- FlagTick = FALSE
- End If
- ' --------------------------------------------------
- Case ST_SET ' Schwellwert der Helligkeit einstellen
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Bitwait Taste_Set , Set ' Warten, bis Taste losgelassen wird
- End If
- Luminance = GetAdc(ADC_LUMINANCE) ' Helligkeit messen
- Luminance_Limit = GetAdc(ADC_POTI) ' Poti-Stellung lesen
- If Luminance >= Luminance_Limit then
- Reset Pin_Light ' Notbeleuchtung aus
- Else
- Set Pin_Light ' Notbeleuchtung an
- End If
- If Taste_Set = 0 then ' Wert übernehmen
- eeLuminance_Limit = Luminance ' Neuer Wert ins EEProm
- Bitwait Taste_Set , Set ' Warten, bis Taste Losgelassen wird
- Call SetState(ST_START)
- ElseIf FlagTick = TRUE then
- Toggle Led_Netz
- FlagTick = FALSE
- End If
- ' --------------------------------------------------
- Case ST_POWER_FAILURE ' Stromausfall aktiv
- ' --------------------------------------------------
- If IsStateChanged() = TRUE then ' Einmalige Aktion
- Reset Led_Netz ' Netzausfall anzeigen
- If GetLuminanceState() = True then ' Notbeleuchtung einschalten?
- Set Pin_Light
- End If
- End If
- ' Wiederkehrende Aktionen
- Select Case GetBattState() ' Akkuanzeige aktualisieren
- Case BATT_FULL
- Set LED_Batt_Full ' Akkuzustand anzeigen
- Case BATT_NOT_FULL
- Reset LED_Batt_Full ' Akkuzustand anzeigen
- End Select
- ' Exitbedingungen (Ereignisse)
- If IsPowerOK() = TRUE then ' Netz wieder da?
- Call SetState(ST_BATT_CHARGING)
- ElseIf GetBattState() = BATT_CRITICAL then ' Akku leer?
- Call SetState(ST_BATT_CRITICAL)
- End If
- End Select
- Loop
- ' ----------------------------------------------------------------------------
- ' Unter-Routinen / Hilfsroutinen
- ' ----------------------------------------------------------------------------
- ' --------------------------------------------------------
- ' Pin abfragen, ob Netzspannung verfügbar ist
- ' --------------------------------------------------------
- Function IsPowerOK() as Byte
- If Pin_Power = 1 then
- IsPowerOK = TRUE
- Else
- IsPowerOK = FALSE
- End If
- End Function
- ' --------------------------------------------------------
- ' Gibt Akku-Zustand zurück
- ' Die Schwellwerte müssen in der Schaltung ermittelt und angepasst werden
- ' --------------------------------------------------------
- Function GetBattState() as Byte
- local tmp as Word
- tmp = Getadc(ADC_BATT)
- Select Case tmp
- Case Is >= 1000 ' Akku voll
- Case Is >= 500 ' Akku nicht voll
- Case Is < 430 ' Akku kritisch
- End Select
- End Function
- ' --------------------------------------------------------
- ' Gibt zurück, ob aktuelle Helligkeit kleiner dem Helligkeits-Limit ist.
- ' Also liefert TRUE, wenn Notbeleuchtung eingeschaltet werden soll, sonst FALSE
- ' --------------------------------------------------------
- Function GetLuminanceState() as Byte
- local tmp as Word
- tmp = GetAdc(ADC_LUMINANCE)
- If tmp < Luminance_Limit then
- GetLuminanceState = TRUE
- Else
- GetLuminanceState = FALSE
- End If
- End Function
- ' --------------------------------------------------------
- ' ISR_Routine Timer1 Overflow
- ' Wird alle 0,5s aufgerufen (2Hz)
- ' --------------------------------------------------------
- Sub ISR_Timer1()
- FlagTick = True
- End Sub
Der Code des Automaten ist jetzt schon deutlich komplexer geworden.
Ladet den einfach unten herunter und öffnet den Source in die Bascom-IDE. Es sieht schlimmer aus als es ist.
Fakt ist, dass man im Code schon unterteilt die Zustände erkennt (Hauptschleife), in denen auch die Aktionen und die Ereignisse behandelt werden.
Man sollte sofort erkennen, dass ein so programmierter Code sehr Übersichtlich ist. Man erkennt auch deutlich, wie die Statetabelle und die Code-Struktur korreliert.
Der Code wurde übrigens nicht mit Hardware getestet und ist eigentlich nur für die Einführung in die Statemachine als etwas komplexeres Beispiel gedacht.
Wer es dennoch real ausprobieren möchte, muss auch mit Programmierfehlern rechnen. Die zu beheben sollte jedoch aufgrund der State-Tabelle und ausreichend kommentiertem Code nicht schwer fallen. Auch Anpassungen sollten keine größeren Schwierigkeiten bereiten.
Die Eingangs gestellt Frage, was eine Statemachine ist und wie man die in Bascom einbindet, dürfte damit geklärt sein. Bleibt noch die Frage, wozu man das braucht.
Nun, es erleichtert die Programmierung und auch die Dokumentation. Es lassen sich komplexe Steuerung und Abläufe realisieren, die immer noch übersichtlich bleiben. Wer mal versucht die obige Steuerung herkömmlich zu programmieren, also ohne Statemachine, wird schnell merken wo die Vorteile der Statemachine liegen. Der Code bleibt wartbar.
Viel Spaß damit.
Übungsaufgaben
Wer jetzt Blut geleckt hat und es mal selbst versuchen möchte, kann sich mal Gedanken zu folgenden Aufgaben machen und versuchen eine State-Tabelle anzulegen oder den Code umzusetzen.
Übung 1: Akkuwächter für 12V Akkus.
Die Spannung einer Autobatterie soll überwacht werden. LED's sollen den Zustand anzeigen. Ist die Spannung <11,5V soll eine rote LED den niedrigen Batteriezustand signalisieren. Eine gelbe LED signalisiert Spannungen über 13,8V. Eine grüne LED zeigt korrekte Akkuspannung an. Die Akkuspannung soll mittels ADC gemessen werden.
Übung 2: Torsteuerung
Ein elektrisch betriebenes Firmentor soll manuell gesteuert werden. Hierfür sind 3 Bedienknöpfe (Taster Auf, Zu, Stop) vorgesehen. Wenn das Tor geschlossen ist (Endschalter Tor zu aktiv) ist, kann nur die Taste Auf betätigt werden. Wenn das Tor geschlossen ist (Endschalter Tor zu aktiv), kann nur die Taste Auf bedient werden. Während das Tot auf- bzw. zu fährt kann das Tor (Taste Stop) vorzeitig gestoppt werden. Ist keiner der Endschalter aktiv, soll mit Taster Auf bzw. Taster Zu das Tor wieder in Bewegung versetzt werden können. Der Antrieb für das Tor wird durch 2 Ausgänge gesteuert. Ein Pin für Richtung (Tor Öffnen = HIGH, Tor Schließen = Low) und ein Pin für Motor an (An = High).
Torsteuerung: Ampel-Erweiterung
Wenn ihr die Steuerung bis hierhin geschafft habt, könnt ihr mal die Ampel-Erweiterung einbauen.
Nach einer gewissen Zeit möchte der Auftraggeber, dass eine Ampel (Rot/Grün) nachgerüstet wird. Sie soll anzeigen, ob man hineinfahren darf oder nicht. Ist das Tor vollständig geöffnet (Endschalter aktiv), soll die Ampel grün leuchten. Während sich das Tor bewegt oder wenn es vorzeitig gestoppt wurde, soll die Ampel rot zeigen. Ist das Tor geschlossen, geht die Ampel aus, um Energie zu sparen.
Torsteuerung: Aufprallschutz-Erweiterung
Wenn ihr es bis hier her geschafft habt, könnt ihr noch eine Erweiterung nachrüsten.
Der Auftraggeber hat festgestellt, dass es eine Gefahr darstellt, wenn das Tor zu fährt und sich noch etwas im Weg befindet wie eine Person oder ein Fahrzeug. Um Verletzungen und Schäden zu vermeiden, soll ein Aufprallschutz am Tor angebracht werden. Dieser besteht aus einem Taster, der betätigt wird, wenn das Tor beim Schließen auf ein Hindernis trifft. In dem Fall soll das Tor sofort stoppen. Die Manuelle Bedienung lässt dann nur die Taste Auf als Bedientaste zu.
Torsteuerung: Überwachung Torbewegung
Der Auftraggeber möchte, dass die Torbewegung überwacht werden soll. Hierfür soll ein Timeout erkennen, ob das Tor ordnungsgemäß schließt oder öffnet. Die Funktion sieht folgendermaßen aus.
Wenn das Taste Tor Zu betätigt wird, dauert es eine gewisse Zeit, bis das Tor den Endschalter ZU erreicht. Ist die Zeit deutlich länger, kann man davon ausgehen, dass ein Fehler am Antrieb vor liegt. Der Auftraggeber sagt, dass das Tor nie länger als 60 Sekunden zum Öffnen bzw. Schließen benötigt. Sollte nach 90 Sekunden der Endschalter nicht erreicht sein, kann man von einem Fehler ausgehen. In diesem Fall soll der Antrieb auf jeden Fall abgeschaltet werden. Und um den Fehler anzuzeigen, soll die rote Ampel im Sekundentakt blinken.
Schlussbemerkung
Eine Statemachine kann sehr unterschiedliche Anforderungen erfüllen und ist damit situationsbedingt sehr flexibel.
So kann es unter Umständen gewünscht sein, dass bestimmte Zustände auf keinerlei Ereignisse reagieren sollen. Das könnte z.B. Im Fehlerfall bei einer Notabschaltung der Fall sein, um größeren Schaden zu vermeiden. In der Regel sind aber in jedem Zustand Ereignisse notwendig, um den Zustandswechsel zu initiieren.
In meinen Beispielen habe ich die Aktion, die bei einem Zustandswechsel ausgeführt werden soll, stets in den Ziel-Zustand im Bereich der Bedingung "If IsStateChanged() = TRUE" eingefügt. Das hat einen einfachen Grund. Man muss die Aktion nur 1x ausführen und zwar in dem Zustand, in den gewechselt wird.
Zustandsautomaten sind nicht nur für Steuerungen (Hardware) einsetzbar, sondern können auch bei Regelungen eingesetzt werden. Ein weiterer Einsatzbereich ist neben der Hardwaresteuerung auch die Software-Steuerung. Hierbei ändert der Automat seinen Zustand abhängig von den Daten, die er verarbeitet. Dies kann z.B. beim dekodieren von seriellen Signalen der Fall sein (Serielle Schnittstelle, Netzwerk-Datenverkehr). Auch ein Interpreter ist im Grunde eine Statemachine. Denn sie interpretiert die geschriebenen Befehle und führt dann die entsprechende Aktion aus. Compiler sind im Grunde auch nichts anderes. Sie interpretieren Text und erzeugen abhängig davon einen Binärcode, den der Prozessor versteht.
Die hier vorgestellte Möglichkeit einer Statemachine kann ein einzelnen Zustandsautomat (Objekt) verwalten.
Es gibt Situationen, da reicht ein einzelner Automat nicht aus. Möchte man mehrere Automaten quasi parallel laufen lassen, ist das möglich, wenn die Include-Datei angepasst wird, so dass mehrere Zustände unabhängig voneinander verwaltet werden können. Ein typische Beispiel für parallelle Automaten sind beispielsweise Computer-Spiele.
Jeder kennt Spiele in denen man gegen den Computer spielt. Der Computer-Spieler ist in dem Fall ein Automat. Manche Spiele lassen es zu, den Computer gegen sich selbst spielen zu lassen. In dem Fall spielt allerdings ein weiterer Automat den 2. Spieler. Es spielt also Automat 1 gegen Automat 2. Jeder Automat kann hier als eigenständiges Objekt betrachtet werden das unabhängig vom anderen Objekt ist. Sie verwenden möglicherweise die gleichen Konstanten als Zustände, jeder Automat kann sich trotzdem in einem anderen Zustand befinden.
Man mag es kaum glauben, aber fast alles kann man, sofern man das möchte, als Automat betrachten.
Man verzeihe mir den Vergleich mit Babette, aber auch sie ist ein Automat. Babette ist entweder am schlafen, sie ist sauer oder gar eingeschnappt. Oder sie ist gelangweilt und erzählt einen Witz. Das alles sind Zustände.
In diesem Sinne frohes Gelingen!
Mitch
9,728 times viewed