Bisher lag der Schwerpunkt meiner Posts in den objektorientierten Erweiterungen. Es gibt aber noch einige allgemeine, meist nicht so tiefgreifende, Neuerungen innerhalb von TwinCAT 3. Im Folgenden sollen diese kurz vorgestellt werden.
Initialisieren von Arrays
Soll ein Array beim Anlegen initialisiert werden, so muss die Liste mit den Initialisierungswerten in eckige Klammern gesetzt werden.
VAR
aTest1 : ARRAY [1..5] OF INT := [1, 2, 3, 4, 5];
aTest2 : ARRAY [1..5] OF INT := [1, 3(5), 2]; (* kurz für [1, 5, 5, 5, 2] *)
END_VAR
Bisher waren die eckigen Klammern nicht notwendig.
Einzeilige Kommentare
C++, Java und C# lassen grüßen: Jetzt kann ein Kommentar mit // begonnen werden. Der Kommentar endet mit dem Zeilenumbruch.
VAR
bVar : BOOL; // einzeiliger Kommentar
nVar1 : INT; (* mehrzeiliger
Kommentar *)
nVar2 : INT;
END_VAR
Kommentare über mehrere Zeilen können weiterhin mit (* … *) angelegt werden.
CONTINUE in Schleifen
Bisher konnte innerhalb von FOR, WHILE und REPEAT-Schleifen mit der Anweisung EXIT die Schleife vorzeitig beendet werden. Jetzt ist es mit CONTINUE auch möglich, eine Schleife vorzeitig fortzusetzen.
PROGRAM MAIN
VAR
nVar : INT;
nCounter : INT;
END_VAR
nCounter := 0;
FOR nVar := 1 TO 10 DO
IF ((nVar MOD 2) = 0) THEN
nCounter := nCounter + 1;
CONTINUE;
END_IF
// weitere Befehle
// ...
END_FOR
Im obigen Beispiel wird die Variable nCounter nur dann erhöht, wenn nVar eine gerade Zahl ist. Alle Befehle, die unterhalb von CONTINUE stehen, werden nach dem Aufruf von CONTINUE nicht ausgeführt.
UNION in benutzerdefinierten Datentypen
Mit einer UNION (Verbund) ist es möglich, eigene Datentypen zu definieren, deren Elemente alle denselben Speicherplatz belegen. Die Speicherbereiche der einzelnen Elemente überlappen sich ganz oder zumindest teilweise. Eine UNION belegt dabei mindestens so viel Speicher, wie ihre größte Komponente.
TYPE U_Test :
UNION
nVar1 : WORD;
nVar2 : BYTE;
END_UNION
END_TYPE
Hier ein Beispiel, bei dem auf eine 16-Bit Variable auch einzeln auf das Low-Byte und auf das High-Byte zugegriffen werden kann. Dazu wird zuerst eine Struktur angelegt, die aus zwei Bytes besteht.
TYPE ST_Bytes :
STRUCT
nVar1 : BYTE;
nVar2 : BYTE;
END_STRUCT
END_TYPE
Diese Struktur, die eine Größe von 16-Bit belegt, wird in einer UNION mit einem WORD verbunden.
TYPE U_Test :
UNION
nVar1 : WORD;
stVar2 : ST_Bytes;
END_UNION
END_TYPE
Beide Variablen der UNION beginnen im Speicher ab der gleichen Adresse. Der Zugriff auf die einzelnen Elemente einer UNION erfolgt genauso, wie bei einer Struktur. Mit dem Unterschied, dass in diesem Fall ein Zugriff auf die Variable uVar.nVar1 auch die Variable uVar.stVar2 beeinflusst.
PROGRAM MAIN
VAR
uVar : U_Test;
nA, nB : BYTE;
END_VAR
uVar.nVar1 := 16#1234;
nA := uVar.stVar2.nVar1; // Wert: 16#34 (LSB)
nB := uVar.stVar2.nVar2; // Wert: 16#12 (MSB)
Nach dem Start des Programms ändert sich in der UNION auch der Wert der Variable stVar2. Dieses Beispiel zeigt, wie ohne Bit-Operationen aus einer Variablen vom Typ WORD das niedrigere Byte (LSB) und das höherwertige Byte (MSB) ermittelt werden.
Datentyp LTIME
Der Datentyp TIME erlaubt eine Auflösung nur im Millisekunden-Bereich. Da die Zykluszeiten von Steuerungen mittlerweile die 1 ms-Grenze unterschreiten, wurde es notwendig einen genaueren Datentyp für Zeitangaben zu definieren. LTIME hat eine Größe von 64 Bit (statt 32 Bit) und erlaubt eine Auflösung im Nanosekunden Bereich.
PROGRAM MAIN
VAR
tTest : LTIME;
END_VAR
tTest := LTIME#134D12H13M34S354MS2US74NS;
Datentyp WSTRING
Der Datentyp STRING kodiert die Zeichen nach ASCII. Jedes Zeichen wird durch ein Byte repräsentiert. Dadurch ist es zwar möglich, die meisten Buchstaben und Zeichen darzustellen, aber eben nicht alle. Um den Anforderungen anderer Sprachen gerecht zu werden, wurde der Datentyp WSTRING eingeführt. Dieser codiert die Zeichen nach Unicode. Unicode verwendet bis zu 4 Byte pro Zeichen. Bei TwinCAT 3 wird eine Unicode Variante benutzt, die immer 2 Byte pro Zeichen belegt. Über 65.000 verschiedene Zeichen können damit unterschieden werden. Damit sind die meisten, von Menschen verwendeten Schriftzeichen darstellbar, sofern sie in den Unicode-Standard aufgenommen wurden. Der Nachteil ist der höhere Speicherbedarf, wie folgendes Beispiel zeigt:
PROGRAM MAIN
VAR
wsTest : WSTRING(10) := "abcdefäüöß";
sTest : STRING(10) := 'abcdefäüöß';
nSizeWString : UDINT;
nSizeString : UDINT;
END_VAR
nSizeWString := SIZEOF(wsTest); // Wert: 22
nSizeString := SIZEOF(sTest); // Wert: 11
Unterschiede zwischen WSTRING und STRING gibt es auch bei der Initialisierung. Während eine STRING-Konstante mit dem Hochkomma definiert wird (Zeile 4), wird bei einer WSTRING-Konstante das Anführungszeichen verwendet (Zeile 3).
Datentyp REFERENCE
Dieser Datentyp ist vergleichbar mit den Datentyp POINTER. Auch eine Referenz enthält einen Verweis auf eine andere Variable. Im Gegensatz zu einem Zeiger wird der Wert, auf den gezeigt wird, direkt beeinflusst. Ein Dereferenzieren wie bei Pointern ist nicht notwendig. Zuweisungen erfolgen bei Referenz-Variablen mit dem speziellen Zuweisungsoperator REF=.
PROGRAM MAIN
VAR
refSample : REFERENCE TO INT;
nA : INT;
pSample : POINTER TO INT;
nB : INT;
END_VAR
refSample REF= nA;
refSample := 12;
pSample := ADR(nB);
pSample^ := 12;
Sowohl die Variable nA als auch nB haben nach dem Starten des Programms den Wert 12.
Referenzen können auch direkt bei der Deklaration initialisiert werden. Hierbei wird allerdings der übliche Zuweisungsoperator verwendet (Zeile 3).
PROGRAM MAIN
VAR
refSample : REFERENCE TO INT := nA;
nA : INT;
pSample : POINTER TO INT;
nB : INT;
END_VAR
refSample := 12;
pSample := ADR(nB);
pSample^ := 12;
Mit Hilfe des Operators __ISVALIDREF kann überprüft werden, ob eine Reference-Variable einen gültigen Wert enthält.
PROGRAM MAIN
VAR
refSampleA : REFERENCE TO INT := nA;
nA : INT;
refSampleB : REFERENCE TO INT;
a, b : BOOL;
END_VAR
a := __ISVALIDREF(refSampleA); // TRUE
b := __ISVALIDREF(refSampleB); // FALSE
Eine Reference-Variable wird ungültig, wenn ihr der Wert 0 zugewiesen wird.
refSample REF= 0;
a := __ISVALIDREF(refSample); // FALSE
Zugriff auf Strings per [ ]
Auf die einzelnen Zeichen einer Variablen vom Typ STRING oder WSTRING kann mit dem Index-Operator zugegriffen werden. Bei einer Variablen vom Typ STRING liefert dieser den ASCII-Code als Byte zurück. Wird auf eine Variable vom Typ WSTRING zugegriffen, so wird der Unicode als WORD zurückgeliefert.
Es kann auch schreibend auf die Zeichen zugegriffen werden (Zeile 11). Das Array beginnt bei 0, d.h. das 1. Zeichen (von links beginnend) hat den Index 0.
PROGRAM MAIN
VAR
wsTest : WSTRING(10) := "abcdefgh";
nLetterWString : WORD;
sTest : STRING(10) := 'abcdefgh';
nLetterString : BYTE;
END_VAR
nLetterString := sTest[3]; // ASCII-Code von 'd': 100
nLetterWString := wsTest[5]; // Unicode von 'f': 102
wsTest[5] := 120; // der 6. Buchstabe wird ein 'x'
bedingte Kompilierung
Bedingte Kompilierung ermöglicht dem Programmierer, das Kompilieren bestimmter Programmteile vom Zutreffen bestimmter Bedingungen abhängig zu machen. Bedingte Kompilierungsanweisungen sind so konzipiert, dass sie nicht zur Laufzeit, sondern während der Kompilierung durchgeführt werden.
Mit {IF} und {END_IF} kann ein Quelltextbereich eingrenzt werden, der nur dann kompiliert wird, wenn eine bestimmte Bedingung erfüllt ist. Ein {IF}/{END_IF} kann bei Bedarf auch mehrere {ELSIF}-Zweige und einen {ELSE}-Zweig aufweisen.
Die bedingte Kompilierung ist sehr hilfreich, wenn ein Programm für unterschiedliche Ausprägungen entwickelt werden soll. Bereiche, die nicht immer nötig sind, können somit vom Kompilieren ausgeschlossen werden. Dadurch wird nur der Code auf die Steuerung geladen, der tatsächlich benötigt wird.
Es können verschiedene Bedingungen abgefragt werden:
{IF defined (identifier)}
Die Abfrage ist TRUE, wenn identifier definiert wurde. Das Definieren kann mit der Anweisung {define identifier} im Quelltext erfolgen. Dieses bezieht sich aber nur auf den aktuellen Gültigkeitsbereich. Der Gültigkeitsbereich beginnt direkt nach der Anweisung von {define identifier} und endet spätestens mit dem Ende des jeweiligen Quelltextes oder mit der Anweisung {undefine identifier}.
Soll für den gesamten Gültigkeitsbereich des SPS-Projektes die Definition erfolgen, so kann dieses über den Eigenschaftsdialog des SPS-Projektes erfolgen. Mehrere Definitionen werden durch ein Komma voneinander getrennt.

Eine globale Definition kann nicht durch ein {undefine identifier} aufgehoben werden.
Anweisungen für die bedingte Kompilierung sind nur im Programmbereich vom strukturierten Text und in globale Variablenlisten erlaubt. Im Deklarationsteil der Variablen oder in der Definition von Datentypen wie z.B. Strukturen sind diese (leider) nicht zulässig.
PROGRAM MAIN
VAR
nScaling : INT;
END_VAR
{IF defined (MachineTypeA)}
nScaling := 5;
{ELSIF defined (MachineTypeB)}
nScaling := 10;
{ELSE}
nScaling := 20;
{END_IF}
{IF defined (variable:variableName)}
Die Abfrage ist erfüllt, wenn die Variable variableName im aktuellen Gültigkeitsbereich definiert wurde.
PROGRAM MAIN
VAR
nScaling : INT := 1;
// nOffset : INT := 2;
nX : INT;
END_VAR
{IF defined (variable:nOffset)}
nX := 100 * nScaling + nOffset;
{ELSE}
nX := 100 * nScaling;
{END_IF}
Nach dem Starten des Programms, ist die Variable nX 100.
Es kann auch auf das Vorhandensein von Elementen innerhalb von Strukturen, Unions und Enums geprüft werden. Befindet sich z.B. eine entsprechende Struktur innerhalb einer SPS-Bibliothek, so kann die bedingte Kompilierung helfen, das eine Applikation sowohl mit der neueren Version, als auch mit der älteren Version der SPS-Bibliothek kompiliert werden kann.
PROGRAM MAIN
VAR
stTest : ST_Test;
END_VAR
stTest.nA := 1;
stTest.nB := 2;
{IF defined (variable:stTest.nC)}
stTest.nC := 3;
{END_IF}
{IF defined (type:typeName)}
Die Abfrage ist TRUE, wenn eine Struktur, Aufzählung oder Verbund mit dem Namen typeName existiert.
{IF hastype (variable:variableName, dataType)}
Die Bedingung ist erfüllt, wenn die Variable variableName vom Type dataType ist.
PROGRAM MAIN
VAR
varSample : LREAL;
END_VAR
{IF hastype (variable:varSample, LREAL)}
varSample := 123.456;
{END_IF}
{IF hastype (variable:varSample, INT)}
varSample := 123;
{END_IF}
Die Variable varSample bekommt nach dem Starten des Programms den Wert 123.456 zugewiesen.
{IF hasvalue (identifierName, identifierValue)}
Bei der Definition eines Identifiers kann diesem optional ein Wert vom Typ STRING zugewiesen werden. Dieser Wert kann abgefragt werden.
PROGRAM PLC_PRG
VAR
nSample : INT;
END_VAR
{define identifier '1'}
{IF hasvalue (identifier, '1')}
nSample := 100;
{END_IF}
{IF hasvalue (identifier, '2')}
nSample := 200;
{END_IF}
Leider ist die Zuweisung eines Wertes zu einem Identifier nicht in den Eigenschaftendialog vom SPS-Projekt möglich.
Nach dem Starten des Programms hat die Variable nSample den Wert 100.
{IF defined (pou:pouName)}
Die Bedingung ist erfüllt, wenn ein POU (FUNCTION, FUNCTION BLOCK oder PROGRAM) mit den Namen pouName vorhanden ist. Es kann sich hierbei auch um einen POU aus einer SPS-Bibliothek handeln.
PROGRAM MAIN
VAR
nSample : INT;
END_VAR
{IF defined (pou:F_CheckRange)}
nSample := 100 * F_CheckRange(GVL.nValue);
{ELSE}
nSample := 100 * GVL.nValue;
{END_IF}
{IF hasattribute (pou:pouName,‘attributeName‘)}
Die Bedingung ist erfüllt, wenn an dem POU pouName das Attribut attributeName vorhanden ist. POUs werden wie folgt mit einem Attribut versehen:
{attribute 'debug'}
FUNCTION TraceInfos : STRING
VAR_INPUT
i : INT;
END_VAR
{IF hasattribute (variable:variableName,‘attributeName‘)}
Auch Variablen können mit Attributen deklariert werden, so wie es bei POUs ebenfalls möglich ist.
VAR
{attribute 'debug'}
nSample : INT;
END_VAR
Operatoren AND, OR, NOT und ()
Mehrere Abfragen können auch miteinander kombiniert werden.
PROGRAM MAIN
VAR
nScaling : INT;
END_VAR
{IF hasvalue (identifier, '1') AND hastype (variable:nSample, WORD)}
nScaling := 5;
{ELSIF defined (MachineTypeB)}
nScaling := 10;
{ELSE}
nScaling := 20;
{END_IF}
Danke für den tollen Beitrag. Hat mir wirklich sehr weitergeholfen!
Ben
Hi, sehr übersichtliche Zusammenstellung. Hat mir auch nach über 10 Jahren sehr geholfen.
Kleine Anmerkung/Nachfrage:
Sollte im Abschnitt
“{IF defined (pou:pouName)}” nicht der ‘True’ und ‘False’ Zweig getauscht werden?
Hallo Jonas,
vielen Dank für den Hinweis. Ich habe das Beispiel entsprechend angepasst.
Stefan