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.

Diagnose of Softwarebus

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 dem TO_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_stringwurden 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 FALSEund Release mit TRUE. Dazu erstellen wir die Methode Initialize().
  • 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_Softwarebuseinem 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:

  1. Übergabe der Array-Größen an den FB und dynamische Array-Bildung mithilfe des _new-Operators
  2. 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 BYTEfunktioniert 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 über IN_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 und diagnosisArrayAction: in diesen beiden Arrays werden alle Signale gespeichert, unabhängig davon, ob das entsprechende Bit im Softwarebus gerade aktiv ist.
  • hmiArrayReleaseund hmiArrayRelease: 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 diagnosisArraymit 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.

Disadvantage of GetAction()

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:

GetAction() with online view

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.

No access via Intellisense

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.

Datagrid Columns

Datenbindung

Als erstes mappe ich in der TwinCAT HMI Configuration die zwei Arrays aus dem FB_Softwarebus.

Mapped symbols

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.


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