Blog

Vom UMT-Modell zum C++-Code: Eine Türsteuerung in 30 Minuten

Veröffentlicht am 2026-05-25

Vom UMT-Modell zum C++-Code: Eine Türsteuerung in 30 Minuten

Teams, die eingebettete Software auf Basis von Zustandsmaschinen entwickeln, begegnen häufig derselben Frage: Wer schreibt das Modell, wer schreibt den Code, und wie bleiben beide synchron?

In diesem Beitrag zeige ich anhand eines konkreten Beispiels, wie UMTSM diese Frage beantwortet. Wir überführen eine Türsteuerung von einer .umt-Datei in produktionsreifen C++-Code — Schritt für Schritt.


Das Problem: Eine Tür im Automatik- und Handbetrieb

Das System muss folgende Anforderungen erfüllen:

  • Im Automatikbetrieb wartet die Tür nach dem Öffnen eine festgelegte Zeit und schließt sich dann selbstständig.
  • Im Handbetrieb bewegt sich die Tür nur bei Tastendruck.
  • Das Umschalten zwischen den beiden Modi ist zur Laufzeit möglich.
  • Ein Moduswechsel während des Öffnens oder Schließens unterbricht die Bewegung nicht.
  • Nach einem Stromausfall merkt sich das System seinen letzten Zustand — beim Neustart macht es genau dort weiter, wo es aufgehört hat.

Dies sind echte Produktanforderungen. Diese fünf Punkte in einer handgeschriebenen FSM sauber zu halten kann Wochen dauern. Als grobes UML-Diagramm ausgedrückt sähe das System so aus:

Zustandsmaschinendiagramm der Tür im Dualbetrieb
Zustandsmaschinendiagramm der Tür im Dualbetrieb

In UMTSM ausgedrückt sieht die .umt-Datei folgendermaßen aus:


Schritt 1: Die Zustandsmaschine modellieren

type sm Engine;

sm Door
{
    persistent deep history -> ManualMode_Open;

    state ManualMode
    {
        Open:
            ButtonPressed -> Closing / engineRunACCW;

        Closing:
            entry / engineRunACCW;
            DoorClosed / engineStop -> Close;
            ButtonPressed / engineStop -> Opening;

        Close:
            ButtonPressed -> Opening;

        Opening:
            entry / engineRunCCW;
            DoorOpen / engineStop -> Open;
            ButtonPressed / engineStop -> Closing;

        Automatic -> AutomaticMode;
    }

    state AutomaticMode
    {
        Open:
            entry / resetWaitingTime;
            do    / wait;
            ButtonPressed / resetWaitingTime;
            -> Closing;

        Closing:
            entry / engineRunACCW;
            DoorClosed / engineStop -> Close;
            ButtonPressed / engineStop -> Opening;

        Close:
            ButtonPressed -> Opening;

        Opening:
            entry / engineRunCCW;
            DoorOpen / engineStop -> Open;

        Manual -> ManualMode;
    }
}

Wenige Zeilen definieren:

  • Zwei übergeordnete zusammengesetzte Zustände: ManualMode und AutomaticMode
  • Vier Unterzustände in jedem: Open, Closing, Close, Opening
  • Timerbasiertes Schließen im Automatikbetrieb via do / wait mit Completion-Transition
  • Wiederherstellung nach Stromausfall via persistent deep history
  • Abhängigkeitsdeklaration auf die Engine-Zustandsmaschine via type sm Engine

Schritt 2: Codegenerierung

UMTSM parst das Modell und überführt es in eine hierarchische interne Repräsentation (IR). CppGen generiert aus dieser IR folgende Dateien:

Generierte Dateien
Generierte Dateien
DateiZweck
Door.hhKlassendeklaration der Zustandsmaschine
Door.cppImplementierung der Zustandsmaschine
Door_DataType.hhDatenstruktur der Zustandsmaschine
Door_Types.hhAbgeleitete Typdeklarationen
Door_Auxilary.cpp.templateVorlagen für nutzerseitige Guard- und Aktionsfunktionen
Door_DataType.cpp.templateVorlage für Initialisierung und Abbau der Datenstruktur
Door_UserTypes.hh.templateVorlage für nutzerdefinierte externe Typdeklarationen

Generierte Dateien werden nicht angefasst — sie werden beim nächsten Generierungslauf überschrieben. Nutzerdateien liegen unter src/door/ und sind von der Generierung nicht betroffen.


Schritt 3: Die Aktionen implementieren

Mithilfe der generierten Vorlage erstellen wir src/door/Door_Auxilary.cpp:

void Door::engineRunCCW([[maybe_unused]] Door_DataType const& input)
{
    instanceData.doorActionTimeStart = std::chrono::system_clock::now();
    instanceData.pEngine->trigger_runCCW();
}

void Door::engineRunACCW([[maybe_unused]] Door_DataType const& input)
{
    instanceData.doorActionTimeStart = std::chrono::system_clock::now();
    instanceData.pEngine->trigger_runACCW();
}

void Door::engineStop([[maybe_unused]] Door_DataType const& input)
{
    instanceData.pEngine->trigger_stop();
}

void Door::resetWaitingTime([[maybe_unused]] Door_DataType const& input)
{
    instanceData.waitUntil =
        std::chrono::system_clock::now() + DOOR_AUTO_CLOSE_DELAY;
}

void Door::wait([[maybe_unused]] Door_DataType const& input)
{
    std::this_thread::sleep_until(instanceData.waitUntil);
}

Aktionen sind virtual-Methoden — für unterschiedliche Hardware können verschiedene Implementierungen bereitgestellt werden, ohne das SM-Gerüst anzutasten. In der Simulation wird der Motor durch einen schlafenden Thread nachgebildet; auf echter Hardware wird stattdessen ein GPIO-Ausgang angesteuert.


Schritt 4: Das System verdrahten

In src/main/main.cpp werden alle Zustandsmaschinen instanziiert und miteinander verbunden:

Door     door;
Engine   engine;
Button   button;
// ...

door.instanceData.pEngine   = &engine;
button.instanceData.pDoor   = &door;

engine.start();
door.start();      // persistent history wird hier geladen
button.start();
// ...

Beim Aufruf von door.start() liest load_Deep_Main() den letzten Zustand aus dem persistenten Speicher. Beim allerersten Start wird standardmäßig mit ManualMode::Open begonnen. Bei allen weiteren Starts wird der zuletzt aktive Zustand wiederhergestellt.


Schritt 5: Ausführen

cmake -S . -B build/Release -DCMAKE_BUILD_TYPE=Release
cmake --build build/Release
./build/Release/src/main/door

Die Anwendung öffnet sich mit einem vollbildschirmigen ncurses-Interface:

Simulations-Steuerpanel
Simulations-Steuerpanel

Ein Druck auf A schaltet das System in den Automatikbetrieb. Die Tür öffnet sich, der Timer startet, und wenn die Zeit abläuft beginnt das Schließen. Ein Druck auf M in der Zwischenzeit lässt die Tür ihre aktuelle Bewegung fortsetzen — der Motor stoppt nicht — doch ab diesem Moment gelten die Regeln des Handbetriebs.


Was haben wir erreicht?

AnforderungAnsatz
Automatisches Schließendo / wait + Completion-Transition
HandbetriebBewegung durch Tastenereignis ausgelöst
ModuswechselEreignis Manual / Automatic, symmetrische Übergänge
Moduswechsel während BewegungKein Entry-Action auf dem Übergang, Motor läuft weiter
Wiederherstellung nach Stromausfallpersistent deep history + store_Deep_Main

Handgeschriebener Code insgesamt: ~150 Zeilen Aktionsimplementierungen und ~50 Zeilen main.cpp. Alles andere wurde generiert.


Fazit

Der Kern dessen, was UMTSM in diesem Beispiel leistet, ist schlicht: Modell und Code aus derselben Quelldatei ableiten. Wenn die .umt-Datei sich ändert, wird das generierte Gerüst aktualisiert und die Nutzerimplementierungen bleiben erhalten. Modell und Code verlieren nie ihre Synchronisation.

Den vollständigen Beispielcode gibt es auf GitHub: https://github.com/demiralp/umtsm-examples-cpp

Bei Fragen oder Projekten rund um UMTSM können Sie gerne Kontakt aufnehmen.