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 von MAIN 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:

Strukts and GVL Softwarebus

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 stReadund stWritebesteht. 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 MAINnoch 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 stWriteaus 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.

Softwarebus Pattern

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.


  1. 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. ↩︎

  2. In der Realität verfügen sicherheitsrelevante Aktoren über eine separate Abschaltung im Sicherheitsprogramm und würden natürlich trotzdem abschalten. ↩︎