Softwarebus - Teil 2: Diagnose
Ein Softwarebus ist also ein Kommunikationskanal, über den mehrere Softwaremodule miteinander kommunizieren können, ohne sich gegenseitig zu kennen. Er begünstigt die lose Kopplung der Module und führt somit zu einer verständlicheren, wiederverwendbaren, test- und wartbaren Software. Ein Vorteil des Softwarebus ist, dass er zu gut lesbarem Code führt. Eine Zeile wie
IF NOT bEmergencyStop THEN
GVL_Softwarebus.stWrite.stRelease.bMachineOn := FALSE;
END_IF
ist sofort verständlich. Die Reaktionen der Maschine werden dort programmiert, wo sie auch ausgelöst werden. Dadurch ist sofort ersichtlich, zu welcher Maschinenreaktion dieser Codeteil führt. Es ist allerdings nicht sofort ersichtlich, welche weiteren Stellen im Code zu dieser Reaktion führen. Zudem wird eine andere Struktur geschrieben (stWrite
), als gelesen wird (stRead
), wodurch die Querverweis-Suche von Hand angepasst werden. Die Frage, welche Stellen im Code zu einer bestimmten Reaktion der Maschine führen lässt sich also nicht auf Anhieb beantworten.
Im Zuge der Fehlersuche lautet eine häufig gestellte Frage beispielsweise:
Warum lässt sich die Maschine nicht einschalten?
Kennt man die Stellen in der Software, die ein Einschalten der Maschine verhindern, so lässt sich diese Frage leicht beantworten. In diesem Artikel sehen wir uns an, wie man sie sichtbar macht.
Die Testumgebung
Wir bauen auf das Beispiel des ersten Artikels auf, werden aber den Softwarebus diesmal auf eine andere Art darstellen. Weiterhin fügen wir in die Visualisierung eine Diagnosemöglichkeit des Softwarebus ein.
Das Ziel
Wir wollen im HMI eine Diagnosemöglichkeit für die SPS-Software schaffen. Es soll angezeigt werden:
- welche Freigabe- bzw. Aktionssignale im Moment aktiv sind,
- in welchem Baustein sie gebildet werden und
- eine Begründung, warum sie aktiv sind.
Dazu werden wir ein einfaches Datagrid verwenden, das wir an eine Struktur in der SPS binden, das die benötigten Informationen bereitstellt.
Neben dem Meldungssystem bietet diese Diagnoseseite eine detaillierte Übersicht über den Maschinenstatus. Im Fehlerfall bekommt der Programmierer bzw. die Instandhaltung einen genauen Hinweis, in welchem Baustein mit der Diagnose angesetzt werden kann.
Ausgehend von dem Beispiel aus Teil 1 werden wir die Struktur des Softwarebus’ leicht abwandelnd, um die Diagnose zu integrieren. Bisher haben wir eine Struktur mit boolschen Variablen genutzt, die doppelt vorhanden war: in der stRead
-Struktur und in der stWrite
-Struktur. Action-Signale wurden mit FALSE
initialisiert, Release-Signale mit TRUE
. Über diese Stukturen war ein einfacher Lese-bzw. Schreibzugriff möglich:
GVL_Softwarebus.stWrite.stRelease.bMachineOn := FALSE; // write access
bDummy := GVL_Softwarebus.stRead.stRelease.bMachineOn; // read access
Enumeration statt Struktur
Statt einer Struktur werden wir eine Enumeration einsetzen, da diese zwei Vorteile bietet:
- die Enumerationskonstante kann als Index eines Array-Zugriffs verwendet werden (in diesem Array werden unter anderem die Diagnosedaten gespeichert)
- über das Attribut
'to_string'
kann mit demTO_STRING
-Operator statt dem Enumerationswert der Name der Enumerationskomponente als Zeichenfolge ausgegeben werden. Mithilfe dieses Operators können wir das ausgelöste Signal generisch in die Dignose-Tabelle schreiben.
Wir legen jeweils für den Action-Softwarebus und den Release-Softwarebus eine Struktur an:
{attribute 'qualified_only'}
{attribute 'strict'}
{attribute 'to_string'}
TYPE E_SoftwarebusAction :
(
Reset,
Start,
Stop
)UINT;
END_TYPE
{attribute 'qualified_only'}
{attribute 'strict'}
{attribute 'to_string'}
TYPE E_SoftwarebusRelease :
(
MachineOn,
MachineAutoOn
)UINT;
END_TYPE
Werden für die Enumerationskomponenten keine expliziten Werte angegeben, so weist TwinCAT ihnen automatisch von Null beginnend aufsteigende Werte zu. Neben dem Attribut to_string
wurden auch die Attribute strict
und qualified_only
angegeben. Diese werden automatisch beim Anlegen einer Enumeration eingefügt und es empfiehlt sich, sie beizubehalten.
Die Signale des Softwarebus sind nun definiert. Nur durch eine Enumeration allein können aber noch keine boolschen Werte gesetzt/rückgesetzt werden. Um das zu erreichen, erstellen wir einen Baustein, der das für uns übernimmt: Den FB_Softwarebus
.
FB_Softwarebus
Bis der Softwarebus funktioniert, bleibt noch einiges an Arbeit übrig:
- Es muss eine Struktur geschaffen werden, in der der Softwarebus “lebt”, also ein
ARRAY [*] OF BOOL
. - Dieses ist vierfach vorhanden, für den Read- und den Write-Teil, die sich ihrerseits noch in Action und Release unterteilen.
- Zu Beginn des SPS-Zyklus wird der Write-Teil initialisiert: Action mit
FALSE
und Release mitTRUE
. Dazu erstellen wir die MethodeInitialize()
. - Am Ende des Zyklus wird Write auf Read kopiert. Diese Methode nennen wir
Finalize()
. - Zur Diagnose legen wir eine Struktur mit den Elementen Signalname, Pfad und Beschreibung an, die im HMI dargestellt wird.
- Den Instanzpfad des
FB_Softwarebus
lesen wir aus und legen ihn in der Diagnose-Struktur ab. - Schließlich benötigen wir noch eine Handvoll Hilfsfunktionen, um das Datagrid im HMI zu befüllen.
In der Regel gibt es sinnvollerweise nur einen Softwarebus in der Maschine. Daher werden in dem FB statische Variablen verwendet.
Auf den ersten Blick ähnelt der FB_Softwarebus
einem Singleton, einem Design-Pattern der Gang of Four. Es beschreibt ein Objekt, von dem es projektweit genau eine Instanz gibt. Das ist unter TwinCAT nicht möglich, da ein FB schon bei seiner Deklaration instantiiert wird und somit über die Deklaration automatisch eine weitere Instanz entsteht1.
Zudem wollen wir mit dem FB_Softwarebus
zwar nur auf einen Softwarebus zugreifen, aber durchaus mehrere Instanzen des FBs selbst erstellen. Der Instanzpfad der FBs wird ausgelesen, im HMI zur Anzeige gebracht und lässt so Rückschlüsse auf die Verwendungsstelle zu.
Lokale Softwarebus-Strukturen
Zuerst legen wir im statischen Variablenbereich vier Arrays an, in denen die Bits des Softwarebus geschrieben werden können.
VAR_STAT
// Softwarebus
SoftwarebusActionRead : ARRAY[0..SOFTWAREBUS_ACTION_SIZE] OF BOOL;
SoftwarebusActionWrite : ARRAY[0..SOFTWAREBUS_ACTION_SIZE] OF BOOL;
SoftwarebusReleaseRead : ARRAY[0..SOFTWAREBUS_RELEASE_SIZE] OF BOOL;
SoftwarebusReleaseWrite : ARRAY[0..SOFTWAREBUS_RELEASE_SIZE] OF BOOL;
// Diagnosis
...
END_VAR
VAR CONSTANT
SOFTWAREBUS_ACTION_SIZE : UINT := 3;
SOFTWAREBUS_RELEASE_SIZE : UINT := 2;
END_VAR
Fest hinterlegte Array-Größen? Bei jeder Erweiterung der Softwarebus muss diese Deklaration aktualisiert werden, ein klarer Verstoß gegen das Open-Closed-Prinzip! Um das zu umgehen, gibt es zwei Möglichkeiten:
- Übergabe der Array-Größen an den FB und dynamische Array-Bildung mithilfe des
_new
-Operators - Deklaration der Arrays in einer globalen Variablenliste. Diese Arrays werden dann über den
IN_OUT
-Bereich an den Baustein übergeben.
Die erste Lösung hat den Nachteil, dass neben den Softwarebus-Arrays auch die Arrays zur Diagnose dynamisch erstellt werden. Diese allerdings werden an das Datagrid im HMI gebunden und eine Datenbindung zu POINTER TO BYTE
funktioniert nicht.
Die zweite Lösung wäre die korrekte Vorgehensweise. Sie setzt jedoch zwei Dinge voraus:
- Die globale Variablenliste muss jeder Instanz übergeben werden.
- Dies muss geschehen, bevor in dieser Instanz über Methoden auf den Softwarebus zugegriffen wird. Bei jedem Zugriff muss mit
_ISVALIDREF
geprüft werden, ob überIN_OUT
eine gültige Referenz übergeben wurde.
Daher bleibe ich für dieses Beispiel bei den lokalen, statischen Arrays mit festen Größen und betrachte es als einen der Kompromisse, die ich als SPS-Programmierer gegenüber der Hochsprachenwelt eingehen muss.
Der Softwarebus ist nun eingerichtet und wird haben die altbekannte Struktur, in denen die Read/Write-Bits initialisiert, gesetzt und geschrieben werden können.
Diagnose-Strukturen
Um die drei Informationen Signalname, Pfad und Beschreibung zur Verfügung zu stellen, legen wir eine passende Struktur an.
TYPE ST_HmiSoftwarebus :
STRUCT
Signal : STRING;
Path : STRING;
AdditionalText : STRING;
END_STRUCT
END_TYPE
Von diesem Typ bilden wir nun vier Arrays:
diagnosisArrayRelease
unddiagnosisArrayAction
: in diesen beiden Arrays werden alle Signale gespeichert, unabhängig davon, ob das entsprechende Bit im Softwarebus gerade aktiv ist.hmiArrayRelease
undhmiArrayRelease
: im HMI sollen nur die aktiven Signale zur Anzeige gebracht werden. Daher enthalten diese beiden Arrays eine gefilterte Menge der Signale.
VAR_STAT
// Softwarebus
...
// Diagnosis
diagnosisArrayRelease : ARRAY[0..SOFTWAREBUS_RELEASE_SIZE] OF ST_HmiSoftwarebus;
hmiArrayRelease : ARRAY[0..SOFTWAREBUS_RELEASE_SIZE] OF ST_HmiSoftwarebus;
diagnosisArrayAction : ARRAY[0..SOFTWAREBUS_ACTION_SIZE] OF ST_HmiSoftwarebus;
hmiArrayAction : ARRAY[0..SOFTWAREBUS_ACTION_SIZE] OF ST_HmiSoftwarebus;
END_VAR
Schreibzugriff: SetAction() und SetRelease()
Nachdem die grundlegenden Strukturen eingerichtet sind, können wir auf diesen operieren.
Wenn nun ein Signal im Softwarebus geschrieben wird, so soll gleichzeitig ein Diagnoseeintrag entstehen, der Rückschlüsse erlaubt, an welcher Stelle in der Software und aus welchem Grund das Signal geschrieben wurde. Dazu bedienen wir uns zweier Methoden: SetAction()
und SetRelease()
.
METHOD SetAction : BOOL
VAR_INPUT
Signal : E_SoftwarebusAction;
Text : STRING := '';
END_VAR
SoftwarebusActionWrite[Signal] := TRUE;
AddDiagnosisEntry(TO_STRING(Signal), Signal, Text, E_SoftwarebusType.Act);
Zuerst einmal wird im Softwarebus-Array das entsprechende Bit gesetzt. Die Enumerationskomponente fungiert dabei als Index. Weiterhin wird ein Diagnoseeintrag angelegt.
Die Enumerationskomponente Signal
wird dabei doppelt übergeben: einmal verpackt in Funktion TO_STRING()
, die Namen der Komponente ausgibt anstelle des Werts und noch einmal als Wert selbst.
AddDiagnosisEntry()
Diese Funktion befüllt den Eintrag im diagnosisArray
mit den entsprechenden Daten.
METHOD PRIVATE AddDiagnosisEntry : BOOL
VAR_INPUT
EnumString : STRING;
EnumValue : UINT;
Text : STRING;
END_VAR
VAR_IN_OUT
SwBusType : ARRAY[*] OF ST_HmiSoftwarebus;
END_VAR
SwBusType[EnumValue].AdditionalText := Text;
SwBusType[EnumValue].Path := GetShortenedPath();
SwBusType[EnumValue].Signal := EnumString;
Rückgabe des Instanznamens
Im lokalen Bereich des FB_Softwarebus
definieren wir eine Variable path
, die den Instanzpfad dieses FBs enthält.
{attribute 'reflection'}
FUNCTION_BLOCK FB_Softwarebus
VAR
{attribute 'instance-path'}
{attribute 'noinit'}
path : STRING;
END_VAR
Dazu muss zum einen der FB mit dem Attribut-Pragma {attribute 'reflection'}
gekennzeichnet sein und zum anderen die Variable selbst mit den Attributen {attribute 'instance-path'}
und {attribute 'noinit'}
. Ersteres sorgt dafür, dass in dieser Variable der Instanzpfad gespeichert wird, zweiteres verhindert eine Initialisierung der Variable als leerer String.
Die Methode GetShortenedPath()
, die in der Methode AddDiagnosisEntry
aufgerufen wird, liest den Instanzpfad diese Bausteins aus und gibt ihn verkürzt zurück. Dabei werden der Projektname zu Beginn des Instanzpfades sowie die Instanz FB_Softwarebus
am Ende ausgeblendet, da sie irrelevant sind.
Lesezugriff über GetAction() und GetRelease()
Die Methoden GetAction()
und GetRelease()
liefern den Status des Softwarebus zurück.
METHOD GetAction : BOOL
VAR_INPUT
Signal : E_SoftwarebusAction;
END_VAR
GetAction := SoftwarebusActionRead[Signal];
Allerdings birgt dies auch einen entscheidenden Nachteil bei der Diagnose des Programms: Der Rückgabewert einer Funktion kann leider nicht in der Onlineansicht angezeigt werden.
Zwei zusätzliche Eigenschaften sorgen für Komfort beim Lesezugriff
Um diesen Nachteil zu umgehen, empfiehlt es sich, den Read-Teil des Softwarebus über Eigenschaften nach außen zu führen. Dazu fügen wir die Eigenschaften ActionRead
und ReleaseRead
hinzu, die jeweils nur über einen Getter verfügen und das entsprechende Softwarebus-Array zurückgeben.
PROPERTY ActionRead : ARRAY[0..SOFTWAREBUS_ACTION_SIZE] OF BOOL;
ActionRead := SoftwarebusActionRead;
Dieses Array kann nun beispielsweise auf ein Array gelegt werden, das sich in einer globalen Variablentabelle befindet.
GVL_Softwarebus.ActionRead := Softwarebus.ActionRead;
GVL_Softwarebus.ReleaseRead := Softwarebus.ReleaseRead;
Es stellt sich erneut die Frage, ob man nicht gleich die Arrays aus der GVL über den IN_OUT
-Bereich an den FB_Softwarebus
übergibt. Doch auch diesmal würde ich es nicht tun um den Code möglichst schlank zu halten.
Nun lässt sich der Status des Signals im Softwarebus auch in der Online-Ansicht beobachten:
Theoretisch gibt es noch eine dritte Möglichkeit: lesend auf das interne Array des Softwarebus im FB_Softwarebus
zuzugreifen. TwinCAT erlaubt einen lesenden Zugriff von außerhalb des Bausteins. Es ist allerdings nicht empfehlenswert, da es der Idee der Datenkapselung wiederspricht. Aus diesem Grund listet IntelliSense die internen Variablen eines Bausteins auch nicht auf, sobald er extern angesprochen wird. Nur Methoden und Eigenschaften werden gelistet.
Initialize() und Finalize()
Abschließend bleibt die Initialisierung der Softwarebus-Arrays sowie die Kopie von Write auf Read übrig, analog zum grundlegenden Softwarebus-Beispiel aus Teil 1.
// Initialize software bus
MEMSET(ADR(SoftwarebusActionWrite), 0, SIZEOF(SoftwarebusActionWrite));
MEMSET(ADR(SoftwarebusReleaseWrite), 1, SIZEOF(SoftwarebusReleaseWrite));
// Copy diagnostic data to display in HMI
FOR i:=0 TO SOFTWAREBUS_RELEASE_SIZE BY 1 DO
ResetHmiArrayRow(hmiArrayRelease[i]);
IF SoftwarebusReleaseRead[i] = FALSE THEN
MEMCPY(ADR(hmiArrayRelease[hmiArrayIndex]), ADR(diagnosisArrayRelease[i]), SIZEOF(diagnosisArrayRelease[i]));
hmiArrayIndex := hmiArrayIndex+1;
ELSE
ResetHmiArrayRow(diagnosisArrayRelease[i]);
END_IF
END_FOR
hmiArrayIndex:=0;
FOR i:=0 TO SOFTWAREBUS_ACTION_SIZE BY 1 DO
ResetHmiArrayRow(hmiArrayAction[i]);
IF SoftwarebusActionRead[i] = TRUE THEN
MEMCPY(ADR(hmiArrayAction[hmiArrayIndex]), ADR(diagnosisArrayAction[i]), SIZEOF(diagnosisArrayAction[i]));
hmiArrayIndex := hmiArrayIndex+1;
ELSE
ResetHmiArrayRow(diagnosisArrayAction[i]);
END_IF
END_FOR
Zudem werden die Diagnosedaten ausgewertet: nur die Signale des Softwarebus, die von ihrem Initialisierungswert abweichen, werden im HMI zur Anzeige gebracht.
Aufruf im Programm
Softwarebus.Initialize();
GVL_Softwarebus.ActionRead := Softwarebus.ActionRead;
GVL_Softwarebus.ReleaseRead := Softwarebus.ReleaseRead;
// ### Write examples ###
// ----------------------
// Action
IF bJogButton THEN
Softwarebus.SetAction(Text:='Jog Button pressed.',
Signal := E_SoftwarebusAction.Start);
END_IF
IF bResetButton THEN
Softwarebus.SetAction(Text:='Reset pressed.',
Signal := E_SoftwarebusAction.Reset);
END_IF
// Release
IF bEmergencyStop THEN
Softwarebus.SetRelease(Text:='Emergency stop active.',
Signal := E_SoftwarebusRelease.MachineOn);
END_IF
IF bCriticalFault THEN
Softwarebus.SetRelease(Text:='Critical fault active.',
Signal := E_SoftwarebusRelease.MachineOn);
END_IF
IF bCircuitBreakerTripped THEN
Softwarebus.SetRelease(Text:='Circuit breaker has tripped.',
Signal := E_SoftwarebusRelease.MachineOn);
END_IF
IF bManualMode THEN
Softwarebus.SetRelease(Text:='Manual mode active.',
Signal := E_SoftwarebusRelease.MachineAutoOn);
END_IF
// ### Read examples ###
// ---------------------
// read-access via getter
bResetLED := Softwarebus.GetAction(E_SoftwarebusAction.Reset);
// read-access via global variable list
bResetLED := GVL_Softwarebus.ActionRead[E_SoftwarebusAction.Reset];
Softwarebus.Finalize();
TwinCAT HMI
Bleiben wir für dieses Beispiel bei Beckhoff und nehmen zur SPS ein HMI-Projekt hinzu. Dazu bedienen wir uns des TwinCAT HMI. Es ist ebenfalls in das Visual Studio integriert und ermöglicht es mithilfe von HTML5 und JavaScript moderne Bedienoberflächen zu entwickeln.
Da auch Siemens in der TIA Version 16 mit WinCC Unified nachgezogen hat, werden sich diese Webtechnologien in der SPS-Welt zum neuen Standard für Visualisierungen entwickeln. Der Schritt weg von proprietären Lösungen hin zu diesen weit verbreiteten Technologien lässt auch die Nutzung unzähliger existierender Bibliotheken zu, was mit einem Schlag eine Vielzahl neuer Möglichkeiten eröffnet.
Aufbau
Am HMI sollen alle aktiven Signale des Softwarebus Action und Release angezeigt werden. Ein einfaches Datagrid ist dazu ausreichend, die Bindung an hmiArrayRelease
bzw. hmiArrayAction
soll sich über zwei Buttons umschalten lassen.
Im Desktop.view
lege ich ein Datagrid sowie zwei Buttons an. Um ein Datagrid an eine Struktur zu binden, müssen die Spalten die Namen der Variablen in der Struktur enthalten.
Datenbindung
Als erstes mappe ich in der TwinCAT HMI Configuration die zwei Arrays aus dem FB_Softwarebus
.
Nun fehlt nur noch die Datenbindung des Datagrids an das jeweilige Array. Ein kurzes Stück Code in dem OnPressed
-Ereignis der zwei Buttons erledigt das:
var myControl = TcHmi.Controls.get('TcHmiDatagrid');
if(myControl){
TcHmi.Binding.createEx('%s%PLC1.FB_Softwarebus.hmiArrayAction%/s%', 'setSrcData', myControl);
}
Dabei bindet der erste Button das Action- und der zweite das Release-Array.
Schlussbemerkung
Das einfache Setzen und Rücksetzen von boolschen Variablen wurde durch ein komplexes Gebilde ersetzt. Rechtfertigt das Ergebnis den Aufwand? In kleinen Projekten: nein, auf keinen Fall. Bei mittelgroßen Projekten sieht es schon bald anders aus: mit jeder Zeile Code steigt die Komplexität der Software. Übersichtlichkeit des Codes rückt in den Hauptfokus. Mehr und mehr Module tragen zu einem globalen Maschinenstatus bei. Von diesem Moment an spielt der Softwarebus seine Stärke aus. Maschinenreaktionen können lokal präzise programmiert werden und über die Diagnosefunktion können sie auch leicht nachverfolgt werden. Einmal eingerichtet, stehen diese Informationen auch der Instandhaltung zur Verfügung und lassen tiefe Einblicke in die Maschinenfunktionalität zu.
Tatsächlich ist dies nur ein Grund von mehreren. Armando Rene Naravez Contreras untersucht in seiner Thesis unter anderem den Einsatz des Singleton-Patterns unter CoDeSys. ↩︎