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:
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:
ManualModeundAutomaticMode - Vier Unterzustände in jedem:
Open,Closing,Close,Opening - Timerbasiertes Schließen im Automatikbetrieb via
do / waitmit 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:

| Datei | Zweck |
|---|---|
| Door.hh | Klassendeklaration der Zustandsmaschine |
| Door.cpp | Implementierung der Zustandsmaschine |
| Door_DataType.hh | Datenstruktur der Zustandsmaschine |
| Door_Types.hh | Abgeleitete Typdeklarationen |
| Door_Auxilary.cpp.template | Vorlagen für nutzerseitige Guard- und Aktionsfunktionen |
| Door_DataType.cpp.template | Vorlage für Initialisierung und Abbau der Datenstruktur |
| Door_UserTypes.hh.template | Vorlage 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:

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?
| Anforderung | Ansatz |
|---|---|
| Automatisches Schließen | do / wait + Completion-Transition |
| Handbetrieb | Bewegung durch Tastenereignis ausgelöst |
| Moduswechsel | Ereignis Manual / Automatic, symmetrische Übergänge |
| Moduswechsel während Bewegung | Kein Entry-Action auf dem Übergang, Motor läuft weiter |
| Wiederherstellung nach Stromausfall | persistent 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.
