Softwarebus - Teil 1: Grundlagen
Mit steigender Komplexität von Maschinensteuerungen nehmen auch die Verbindungen zwischen den Submodulen der Software zu. Das widerspricht der Idee der losen Kopplung von Modulen, welche ein Qualitätskriterium für verständliche und wartbare Software darstellt. Das Softwarebus-Pattern hilft, die Kopplung zu reduzieren und skaliert strukturell sehr gut.
Die Codebeispiele basieren auf Beckhoffs TwinCAT 3 und verwenden ST als Sprache. Die Konzepte sind jedoch auf alle Plattformen und Sprachen übertragbar.
Die Ausgangssituation
Seit ich vor zehn Jahren mit der SPS-Programmierung begonnen habe, bin ich immer wieder über ein Muster gestolpert, mit dem ich mich nie richtig anfreunden konnte: Netzwerke mit unzähligen boolschen Verknüpfungen, an deren Ende ein Zuweisung zu einem Freigabe- oder Startsignal stand. Oft überschritten diese Netzwerke bei weitem die Breite meines Bildschirms, hatten mehrere Parallelzweige mit Sonderbedingungen und waren damit an Unübersichtlichkeit nicht mehr zu überbieten. Diese Netzwerke waren in KOP oder FUP noch entzifferbar, in AWL oder ST stieß ich jedoch schnell an die Grenze, an der ich den Code noch verstand.
Mit jeder Baugruppe, um die die Maschine wuchs, gediehen auch diese Netzwerke. Um wieder Struktur in das Chaos zu bringen, fing man an, Zwischenergebnisse zu bilden und diese wiederum in die Freigabenetzwerke aufzunehmen. Dies sorgte auf den ersten Blick für Übersichtlichkeit, schuf aber eine neue Hürde. In dem globalen Freigabenetzwerk sah man nun, welche Zwischenmerker für das Abfallen der Freigabe verantwortlich waren und musste dann an die Stelle springen, an der diese gebildet wurden. Dort waren sie möglicherweise noch ein weiteres Mal zwischenverknüpft, was einen weiteren Sprung bedeutete. Sobald man nun diesen Pfad verstanden hatte, konnte man sich wieder zurück in die oberste Ebene begeben und den nächsten Pfad verfolgen. Bis man am letzten Abzweig ankam, hatte man meist schon den Anfang wieder vergessen.
Diese Art der Programmstrukturierung ist also weder übersichtlich noch lose gekoppelt.
Initialisieren - Modifizieren - Verarbeiten
Eine erste Verbesserung brachte auf der lokalen Ebene ein Muster , bei dem man zu Beginn des Bausteins die Freigabesignale mit TRUE und die Steuersignale mit FALSE initialisierte.
// Initialize
bReleaseMotorOn := TRUE;
bSwitchMotorOn := TRUE;
// Modify
IF NOT bSafetyOk THEN bReleaseMotorOn := FALSE; END_IF
IF NOT bCircuitBreakerOk THEN bReleaseMotorOn := FALSE; END_IF
IF NOT bLimitSwitchFree THEN bReleaseMotorOn := FALSE; END_IF
IF stOpMode.bAutomatic AND NOT bSensorConveyorFree THEN
bReleaseMotorOn := FALSE;
END_IF
IF stOpMode.bAutomatic AND bSensorConveyorFree THEN
bSwitchMotorOn := TRUE;
END_IF
IF stOpMode.bManual AND bJogManualPositive THEN
bSwitchMotorOn := TRUE;
END_IF
// Process
outCoilMotor := bSwitchMotorOn AND bReleaseMotorOn;
Dieses Schema erleichtert die Verständlichkeit des Codes, weil es der menschlichen Sprache näher liegt. Es drückt Schritt für Schritt die Überlegungen des Programmierers aus:
Wenn der Sicherheitskreis nicht geschlossen ist, dann darf der Motor nicht eingeschaltet werden können.
Übersetzt in:
IF NOT bSafetyOk THEN bReleaseMotorOn := FALSE; END_IF
Bedingung für Bedingung können so durchgegangen werden und der Code liest sich nahezu wie Prosatext.1
Neben der Verständlichkeit liegt ein weiterer Vorteil darin, dass diese Zeilen länger überleben. Damit meine ich den Alltag in einer Stresssituation während der Inbetriebnahme oder Fehlersuche, bei der schnell ein Codeteil auskommentiert oder überbrückt wird. Programmiert man hingegen feingranular, so ist die Chance geringer, dass diese Zeilen im Zug der Fehlersuche aus Versehen verschwinden.
Ein dritter Vorteil besteht darin, dass man die Klippe der Mehrfachzuweisung von Ausgängen elegant umschifft. Modifiziert wird nur die Zwischenvariable, die am Ende des Bausteins dem Ausgang zugewiesen wird.
Neben den genannten Vorzügen ergeben sich jedoch auch Nachteile durch dieses Schema:
- Es kann nur innerhalb eines Bausteins sinnvoll verwendet werden. Global über das ganze Programm betrachtet hieße das, dass alle Zwischenvariablen am Anfang von
MAIN
initialisiert und alle Ausgänge am Ende vonMAIN
gesetzt werden müssten. Das ist nicht praktikabel. - Will man während einer Fehlersuche den Grund finden, warum die Freigabe des Motors nicht vorhanden ist, so sind dafür mitunter viele Sprünge zu den Verwendungsstellen von
bReleaseMotorOn
notwendig, die alle abgesucht werden müssen.
Wie man diese Nachteile überwinden kann, sehen wir uns im folgenden Abschnitt an.
Der Softwarebus
Wikipedia definiert den Softwarebus als
software architecture model where a shared communication channel facilitates connections and communication between software modules.
Um diesen Kommunikationskanal zu schaffen, knüpfen wir an den Gedanken Initialisieren - Modifizieren - Verarbeiten an und untersuchen ihn anhand eines ersten einfachen Beispiels.
Zur Initialisierung legen wir drei Structs und eine globale Variablenliste an:
Strukturen:
TYPE ST_SoftwarebusRelease
STRUCT
bMotorOn : BOOL;
bValveOn : BOOL;
bMachineOn : BOOL;
END_STRUCT
END_TYPE
TYPE ST_SoftwarebusAction
STRUCT
bMotorOn : BOOL;
bValveOn : BOOL;
bMachineOn : BOOL;
END_STRUCT
END_TYPE
TYPE ST_Softwarebus
STRUCT
stRelease : ST_SoftwarebusRelease;
stAction : ST_SoftwarebusAction;
END_STRUCT
END_TYPE
Globale Variablenliste GVL_Softwarebus
:
VAR_GLOBAL
stRead : ST_Softwarebus;
stWrite : ST_Softwarebus;
END_VAR
Nun erfolgt die Initialisierung zu Beginn von MAIN
:
MEMSET(ADR(GVL_Softwarebus.stWrite.stRelease), 1, SIZEOF(GVL_Softwarebus.stWrite.stRelease));
MEMSET(ADR(GVL_Softwarebus.stWrite.stAction, 0, SIZEOF(GVL_Softwarebus.stWrite.stAction));
Wie im vorangehenden Abschnitt werden die Freigabesignale mit TRUE und die Steuersignale mit FALSE initialisiert.
In jedem beliebigen Modul können diese Signale nun modifiziert werden:
IF NOT bSafetyOk THEN
GVL_Softwarebus.stWrite.stRelease.bMotorOn := FALSE;
GVL_Softwarebus.stWrite.stRelease.bValveOn := FALSE;
GVL_Softwarebus.stWrite.stRelease.bMachineOn := FALSE;
END_IF
Stillschweigend habe ich eine Zwischenstufe eingefügt: ST_Softwarebus
, der aus den Komponenten stRead
und stWrite
besteht. Was hat es damit auf sich?
Mithilfe von diesem Softwarebus werden die Vorteile des ersten Ansatzes behalten und zusätzlich möchten wir sie an jeder Stelle im Programm verwenden können, ohne darauf achten zu müssen, dass die (Ausgangs-)Zuweisungen erst am Ende des Programms erfolgen. Dies kann durch einen kleinen Trick erreicht werden: Man entkoppelt zwei Zyklen der SPS voneinander. Die Signale, die im ersten Zyklus geschrieben werden, werden erst im darauffolgenden Zyklus verarbeitet. Erreicht wird dies, indem wir am Ende von MAIN
noch folgenden Code ergänzen:
MEMCPY(ADR(GVL_Softwarebus.stRead), ADR(GVL_Softwarebus.stWrite), SIZEOF(GVL_Softwarebus.stWrite));
Wir haben also zwei Strukturen geschaffen: stWrite
, die immer geschrieben wird und stRead
, die im immer gelesen wird und den Status von stWrite
aus dem vorherigen Zyklus enthält. Dieses Schema muss dabei beibehalten werden! Folgender Code würde beispielsweise nie zum Abschalten des Motors führen:
outCoilMotor := GVL_Softwarebus.stRead.stAction.bMotorOn AND GVL_Softwarebus.stRead.stRelease.bMotorOn;
IF NOT bSafetyOk THEN
GVL_Softwarebus.stRead.stRelease.stMotorOn := FALSE;
END_IF
Am Ende des Zyklus wird stRead
mit stWrite
überschrieben und der Motor würde nach einer Verletzung des Sicherheitskreises nicht abschalten.2
Ein Softwarebus besteht also aus zwei identischen Strukturen, bei der die eine für den Schreibzugriff und die andere für den Lesezugriff verantwortlich ist. Zu Beginn von MAIN
erfolgt die Initialisierung der write-Struktur, am Ende von MAIN
wird diese auf die read-Struktur kopiert. Im SPS-Zyklus n wird der Softwarebus geschrieben, im darauffolgenden Zyklus n+1 stehen diese Informationen zum Lesen bereit.
Der Datenaustausch zwischen Modulen ist somit möglich, ohne dass sich die Module gegenseitig kennen. Dies führt zu einer schwächeren Kopplung der Programmteile untereinander. Zudem kann der Anlagenzustand an einer zentralen Stelle ausgelesen werden.
Es bleibt ein Nachteil bestehen: Verwendungsstellen dieses Softwarebusses sind nun über das ganze Programm verstreut und im Fehlerfall herauszufinden, an welcher Stelle in den Bus geschrieben wird, kann zeitaufwändig sein. Daher werde ich in einem zweiten Artikel zu diesem Thema beschreiben, wie der Softwarebus um eine Diagnosefunktion ergänzt werden kann.
Code verständlich wie Prosa zu schreiben steht im Mittelpunkt der “Clean Code”-Philosophie. Dabei wird besonders auf selbsterklärenden Code durch sinnvoll gewählte Variablennamen gelegt. Robert C. Martins Clean Code ist eine sehr empfehlenswerte Einführung. ↩︎
In der Realität verfügen sicherheitsrelevante Aktoren über eine separate Abschaltung im Sicherheitsprogramm und würden natürlich trotzdem abschalten. ↩︎