Bei der Ausführung eines SPS-Programms kann es zu unerwarteten Laufzeitfehlern kommen. Diese treten auf, sobald das SPS-Programm versucht eine unzulässige Operation auszuführen. Auslöser solcher Szenarien kann z.B. eine Division durch 0 sein oder ein Pointer verweist auf einen ungültigen Speicherbereich. Mit den Schlüsselwörtern __TRY und __CATCH kann auf diese Ausnahmen deutlich besser reagiert werden als bisher.
Die Liste der möglichen Ursachen für Laufzeitfehler kann endlos erweitert werden. Allen Fehlern ist aber gemeinsam: Sie führen zum Absturz des Programms. Bestenfalls wird durch eine Meldung auf den Laufzeitfehler hingewiesen:
Da sich anschließend das SPS-Programm in einem nicht definierten Zustand befindet, wird das System gestoppt. Dies ist anhand des gelben TwinCAT Icon in der Windows Taskleiste zu erkennen:
Für in Betrieb befindliche Anlagen ist das unkontrollierte Stoppen nicht immer die optimalste Reaktion. Außerdem gibt die Meldung nur unzureichend Auskunft darüber, wo genau im SPS-Programm der Fehler aufgetreten ist. Eine Optimierung der Software ist dadurch nur schwer möglich.
Um Fehler schneller ausfindig zu machen, können in dem SPS-Programm Überprüfungsfunktionen eingefügt werden.
Überprüfungsfunktionen werden jedes Mal aufgerufen, wenn die entsprechende Operation ausgeführt wird. Am bekanntesten dürfte die Funktion CheckBounds() sein. Bei jedem Zugriff auf ein Arrayelement wird vorher diese Funktion implizit aufgerufen. Als Parameter erhält die Funktion die Arraygrenzen und den Index des Elements, auf das der Zugriff erfolgen soll. Die Funktion kann so angepasst werden, dass bei einem Zugriff außerhalb der Arraygrenzen eine Korrektur erfolgt. Dieser Ansatz hat allerdings einige Nachteile:
- In CheckBounds() kann nicht festgestellt werden auf welches Array zugegriffen wird. Somit kann nur für alle Arrays die gleiche Fehlerkorrektur implementiert werden.
- Da bei jedem Arrayzugriff die Überprüfungsfunktion aufgerufen wird, kann sich die Laufzeit des Programms erblich verschlechtern.
Ähnlich verhält es sich auch bei den anderen Überprüfungsfunktionen.
Nicht selten werden die Überprüfungsfunktionen nur während der Entwicklungsphase eingesetzt. In den Funktionen werden Breakpoints aktiviert, die, sobald eine fehlerhafte Operation ausgeführt wird, das SPS-Programm anhalten. Über den Callstack kann anschließend die entsprechende Stelle im SPS-Programm ermittelt werden.
Die ‚try/catch‘-Anweisung
Allgemein werden Laufzeitfehler als Ausnahmen (Exceptions) bezeichnet. Für das Erkennen und Bearbeiten von Exceptions gibt es in der IEC 61131-3 die Anweisungen __TRY, __CATCH und __ENDTRY:
__TRY // statements __CATCH (exception type) // statements __ENDTRY // statements
Der TRY-Block (die Anweisungen zwischen __TRY und __CATCH) beinhaltet die Anweisungen, die potenziell eine Exception verursachen können. Tritt keine Exception auf, werden alle Anweisungen im TRY-Block ausgeführt. Anschließend setzt das SPS-Programm hinter __ENDTRY seine Arbeit fort. Verursacht eine der Anweisungen innerhalb des TRY-Blocks jedoch eine Exception, so wird der Programmablauf unmittelbar im CATCH-Block (die Anweisungen zwischen __CATCH und __ENTRY) fortgeführt. Alle übrigen Anweisungen innerhalb des TRY-Blocks werden dabei übersprungen.
Der CATCH-Block wird nur im Falle einer Exception ausgeführt und enthält die gewünschte Fehlerbehandlung. Nach der Abarbeitung des CATCH-Blocks wird das SPS-Programm mit den Anweisungen nach __ENDTRY fortgesetzt.
Hinter der __CATCH-Anweisung wird in runden Klammern eine Variable vom Typ __SYSTEM.ExceptionCode angegeben. Der Datentyp __SYSTEM.ExceptionCode enthält eine Auflistung aller möglichen Exceptions. Wird der CATCH-Block durch eine Exception aufgerufen, so kann über diese Variable die Ursache der Exception abgefragt werden.
In dem folgenden Beispiel werden zwei Elemente aus einem Array dividiert. Das Array wird hierbei durch einen Pointer an die Funktion übergeben. Ist der Rückgabewert der Funktion negativ, so ist bei der Ausführung ein Fehler aufgetreten. Der negative Rückgabewert der Funktion gibt genauere Informationen über die Ursache der Exception:
FUNCTION F_Calc : LREAL VAR_INPUT pData : POINTER TO ARRAY [0..9] OF LREAL; nElementA : INT; nElementB : INT; END_VAR VAR exc : __SYSTEM.ExceptionCode; END_VAR __TRY F_Calc := pData^[nElementA] / pData^[nElementB]; __CATCH (exc) IF (exc = __SYSTEM.ExceptionCode.RTSEXCPT_ARRAYBOUNDS) THEN F_Calc := -1; ELSIF ((exc = __SYSTEM.ExceptionCode.RTSEXCPT_FPU_DIVIDEBYZERO) OR (exc = __SYSTEM.ExceptionCode.RTSEXCPT_DIVIDEBYZERO)) THEN F_Calc := -2; ELSIF (exc = __SYSTEM.ExceptionCode.RTSEXCPT_ACCESS_VIOLATION) THEN F_Calc := -3; ELSE F_Calc := -4; END_IF __ENDTRY
Die ‚finally‘-Anweisung
Mit __FINALLY kann optional ein Codeblock definiert werden, der immer aufgerufen wird, unabhängig davon ob eine Exception aufgetreten ist oder nicht. Es gibt nur eine einzige Randbedingung: Das SPS-Programm muss zumindest in den TRY-Anweisungsblock eintreten.
Das Beispiel soll so erweitert werden, dass das Ergebnis der Berechnung zusätzlich um Eins erhöht wird. Dieses soll unabhängig davon erfolgen, ob ein Fehler aufgetreten ist oder nicht.
FUNCTION F_Calc : LREAL VAR_INPUT pData : POINTER TO ARRAY [0..9] OF LREAL; nElementA : INT; nElementB : INT; END_VAR VAR exc : __SYSTEM.ExceptionCode; END_VAR __TRY F_Calc := pData^[nElementA] / pData^[nElementB]; __CATCH (exc) IF (exc = __SYSTEM.ExceptionCode.RTSEXCPT_ARRAYBOUNDS) THEN F_Calc := -1; ELSIF ((exc = __SYSTEM.ExceptionCode.RTSEXCPT_FPU_DIVIDEBYZERO) OR (exc = __SYSTEM.ExceptionCode.RTSEXCPT_DIVIDEBYZERO)) THEN F_Calc := -2; ELSIF (exc = __SYSTEM.ExceptionCode.RTSEXCPT_ACCESS_VIOLATION) THEN F_Calc := -3; ELSE F_Calc := -4; END_IF __FINALLY F_Calc := F_Calc + 1; __ENDTRY
Beispiel 1 (TwinCAT 3.1.4024 / 32 Bit) auf GitHub
Die Anweisung im FINALLY-Block (Zeile 24) wird immer aufgerufen, unabhängig davon ob eine Exception erzeugt wird oder nicht.
Wird im TRY-Block keine Exception ausgelöst, so wird der FINALLY-Block direkt nach dem TRY-Block ausgerufen.
Tritt eine Exception auf, so wird erst der CATCH-Block ausgeführt und anschließend auch der FINALLY-Block. Erst danach wird die Funktion verlassen.
__FINALLY gestattet es somit, diverse Operationen unabhängig davon auszuführen, ob eine Exception aufgetreten ist oder nicht. Dabei handelt es sich in der Regel um die Freigabe von Ressourcen, wie z.B. das Schließen einer Datei oder das Beenden einer Netzwerkverbindung.
Besonders sorgfältig sollte man die Implementierung der CATCH– und FINALLY-Blöcke vornehmen. Tritt in einem dieser Codeblöcke eine Exception auf, so löst dieses einen unerwarteten Laufzeitfehler aus. Mit dem Ergebnis, dass das SPS-Programm unmittelbar gestoppt wird.
An dieser Stelle möchte ich noch auf den Blog von Matthias Gehring hinweisen. In einem seiner Posts (https://www.codesys-blog.com/tipps/exceptionhandling-in-iec-applikationen-mit-codesys) wird das Thema Exception Handling ebenfalls behandelt.
Das Beispielprogramm ist unter 32-Bit Systemen ab TwinCAT 3.1.4024 lauffähig. 64-Bit Systeme werden derzeit noch nicht unterstützt.
Hallo,
Gibt es einen Unterschied, ob man Code in einem Finally-Block ausführt oder ob dieser einfach nach dem Try-Catch-Block ausgeführt wird?
Hallo Oswald,
derzeit gibt es noch keinen Unterschied. So wie TRY/CATCH aktuell implementiert ist, kann die FINALLY-Anweisung auch entfallen.
In C# wird z.B. finally immer angesprungen, auch dann, wenn im catch-Block oder try-Block ein return steht:
try
{
Debug.WriteLine(“A1”);
throw new ApplicationException();
Debug.WriteLine(“A2”);
return;
}
catch (Exception ex)
{
Debug.WriteLine(“B”);
return;
}
finally
{
Debug.WriteLine(“C”);
}
Debug.WriteLine(“D”);
Ausgabe mit Exception: A1, B, C
Ausgabe ohne Exception: A1, A2, C
Dadurch das im try-Block ein return steht, wird ‘D’ nicht ausgegeben. Was im finally-Block steht, wird immer vor dem Verlassen der Funktion aufgerufen. Auch dann, wenn die Funktion vorzeitig durch eine Exception oder return beendet wird. Alles was unterhalb von finally steht, wird in diesem Fall nicht ausgeführt.
Derzeit verhält sich die Implementierung in der SPS etwas anders. Befindet sich im CATCH-Block eine RETRUN-Anweisung, so wird der FINALLY-Block nicht aufgerufen. Stattdessen wird mit den Anweisungen nach __ENDTRY vorgefahren. Allgemein ist die Implementierung in der SPS noch etwas unvollständig. Ich vermisse noch die Möglichkeit eine Exception auszulösen. Andere Programmiersprachen bieten hierzu entsprechende Anweisungen an (meistens throw).
Ich hoffe, dass dieses in Zukunft noch umgesetzt wird. Exceptions halte ich grundsätzlich für eine gute Sache, da diese helfen Fehlerzustände innerhalb eines SPS-Programms einfacher abzufangen.
Stefan