c systems notebook

Mehrdimensionale Sprungtabellen für Zustandsautomaten

Eine zweidimensionale Sprungtabelle erweitert das Prinzip der einfachen Sprungtabelle um eine zweite Dimension. Statt nur eine Liste von Funktionen zu verwalten, entsteht eine Matrix, bei der zwei Indizes den Funktionsaufruf bestimmen, also nicht mehr Index → Funktion, sondern (Zustand, Ereignis) → Funktion. Dieses Schema eignet sich besonders für Zustandsautomaten, bei denen die Reaktion davon abhängt, in welchem Zustand sich das System befindet und welches Ereignis eintritt.

Ein Zustandsautomat befindet sich zu jedem Zeitpunkt genau in einem Zustand und wechselt durch Ereignisse in einen anderen. Der aktuelle Zustand bestimmt dabei, wie das System reagiert. Eine Ampel etwa kennt die Zustände Rot, Gelb und Grün. Wenn die Ampel im Zustand Rot ist und das Ereignis Timer eintritt, folgt der Zustand Grün. Auf Grün folgt bei demselben Ereignis Gelb, und auf Gelb wieder Rot.

Das gleiche Prinzip lässt sich auf ›Alltagssituationen‹ übertragen:

  • Wenn ich Party mache und müde werde, gehe ich schlafen.
  • Wenn mir bei der Party langweilig wird, beginne ich zu spielen.
  • Wenn ich beim Spielen müde werde, gehe ich wieder schlafen.

Bleiben wir bei diesem Partybeispiel und realisieren den folgenden Zustandsautomaten in C:

Abbildung

Zustandsautomat für das Partybeispiel

Diese Art von Logik lässt sich auch mit verschachtelten switch-Anweisungen umsetzen, wird aber bei mehreren Zuständen und Ereignissen schnell unübersichtlich. Eine zweidimensionale Sprungtabelle bleibt dagegen übersichtlich und effizient, weil sie alle Kombinationen aus Zuständen und Ereignissen direkt abbildet.

Eine saubere Umsetzung verwendet zwei separate Tabellen:

  • Die Aktionstabelle enthält Funktionszeiger zu den ausgeführten Aktionen, wenn ein bestimmtes Ereignis in einem bestimmten Zustand auftritt. Sie beschreibt also das Verhalten: »Was passiert, wenn ich im Zustand X das Ereignis Y bekomme?« Die Aktion selbst ändert den Zustand nicht; sie ist rein reaktiv und erzeugt nur einen Effekt.

  • Die Übergangstabelle enthält keine Funktionen, sondern nur den Folgezustand, der nach dem Ereignis gelten soll. Sie beschreibt die Steuerlogik: »In welchen Zustand gehe ich über, wenn ich im Zustand X Ereignis Y erlebe?«

Das nachfolgende Programm realisiert genau diesen Ansatz. Die Aktionstabelle heißt actions und die Übergangstabelle heißt next_state:

#include <stdio.h>

typedef enum { PARTY, SLEEP, PLAY, DRINK, STATE_COUNT } State; // Zustände
typedef enum { MUSIC, TIRED, BORED, EVENT_COUNT } Event;       // Ereignisse

typedef void (*Action)();

// Aktionen zu (State, Event)
void party_music() { puts("🎶 Party: Musik läuft, alle bleiben dran"); }
void party_tired() { puts("💤 Party: alle müde, Licht aus"); }
void party_bored() { puts("🎲 Party: es wird langweilig, Spiele holen"); }

void sleep_music() { puts("😵 Schlafen: laute Musik, aufgewacht!"); }
void sleep_tired() { puts("😴 Schlafen: weiter schlafen"); }
void sleep_bored() { puts("😴 Schlafen: nichts los, schnarch"); }

void play_music()  { puts("🎉 Spielen: Musik zieht auf die Tanzfläche"); }
void play_tired()  { puts("😪 Spielen: zu spät, ab ins Bett"); }
void play_bored()  { puts("🍻 Spielen: langweilig, was trinken"); }

void drink_music() { puts("💃 Trinken: Stimmung steigt, zurück zur Party"); }
void drink_tired() { puts("😵 Trinken: zu viel, schlafen"); }
void drink_bored() { puts("🎲 Trinken: na gut, dann spielen"); }

// Sprungtabelle für Aktionen: [state][event] -> Action
Action actions[STATE_COUNT][EVENT_COUNT] = {
    { party_music,  party_tired,  party_bored  },  // PARTY
    { sleep_music,  sleep_tired,  sleep_bored  },  // SLEEP
    { play_music,   play_tired,   play_bored   },  // PLAY
    { drink_music,  drink_tired,  drink_bored  }   // DRINK
};

// Zustandsübergänge
// [state][event] -> Folgezustand
State next_state[STATE_COUNT][EVENT_COUNT] = {
    { PARTY,  SLEEP,  PLAY  },  // PARTY + MUSIC/TIRED/BORED
    { PARTY,  SLEEP,  SLEEP },  // SLEEP
    { PARTY,  SLEEP,  DRINK },  // PLAY
    { PARTY,  SLEEP,  PLAY  }   // DRINK
};

int main() {
    State s = PARTY;  // Startzustand
    puts("Start: PARTY");

    // Sequenz: TIRED -> MUSIC -> BORED -> TIRED
    actions[s][TIRED]();  // Aktion zum Event ausführen
    s = next_state[s][TIRED];  // Folgezustand setzen

    actions[s][MUSIC]();
    s = next_state[s][MUSIC];

    actions[s][BORED]();
    s = next_state[s][BORED];

    actions[s][TIRED]();
    s = next_state[s][TIRED];
}

Die Aktionstabelle enthält Funktionszeiger zu den Aktionen. Beispiel:

  • actions[PARTY][TIRED]() ruft party_tired() auf → Ausgabe: »alle müde, Licht aus«

Die Übergangstabelle enthält den Folgezustand, der nach dem Ereignis gelten soll. Beispiel:

  • next_state[PARTY][TIRED] ergibt SLEEP → neuer Zustand ist Schlafen.

Die Hauptfunktion beginnt im Zustand PARTY und durchläuft eine feste Ereignisfolge. Bei jedem Ereignis wird zunächst die passende Aktion aus actions[state][event] ausgeführt, anschließend der Zustand über next_state[state][event] aktualisiert. So führt TIRED im Zustand PARTY zur Aktion party_tired() und wechselt in den Zustand SLEEP. Danach löst MUSIC im Zustand SLEEP die Aktion sleep_music() aus und führt zurück zu PARTY.

Diese Struktur bildet eine vollständige Zustandsübergangstabelle ab: Jede Kombination aus Zustand und Ereignis hat eine definierte Reaktion und einen eindeutigen Folgezustand. Die Trennung zwischen Verhalten und Steuerung macht den Code wartbar. Reaktionen lassen sich ändern, ohne die Zustandslogik anzupassen, und umgekehrt. Ein neuer Zustand erfordert nur eine neue Zeile in beiden Tabellen und die entsprechenden Aktionsfunktionen -- die Hauptlogik bleibt unberührt.

In realen Anwendungen würden die Ereignisse aus Benutzereingaben, Sensordaten oder Netzwerknachrichten stammen. Die Tabellen selbst bleiben dabei unverändert, nur die Quelle der Ereignisse variiert.