IEC 61131-3: Additional language extensions

The focus of my post has been the object-oriented extensions so far. But there are several general, usually not so far-reaching innovations within TwinCAT 3. Below, they are briefly described.

Array initialization

If a declared array has to be initialized, the list with the initialization values should be set into square brackets.

VAR
  aTest1	: ARRAY [1..5] OF INT := [1, 2, 3, 4, 5];
  aTest2	: ARRAY [1..5] OF INT := [1, 3(5), 2];  (* short for [1, 5, 5, 5, 2] *)
END_VAR

So far, the square brackets have not been necessary.

Single line comments

C++, Java and C# have certainly their share in it: a comment can be now started with //. The comment ends with the line break.

VAR
  bVar    : BOOL;  // Single line comment
  nVar1   : INT;   (* Comment over
                      several lines *)
  nVar2   : INT;
END_VAR

Comments over several lines can be created with (**).

CONTINUE in loops

So far, a loop could be terminated prematurely within FOR, WHILE and REPEAT-loops with the statement EXIT. It is also possible now to prematurely resume a loop with CONTINUE.

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
  // other commands
  // ...
END_FOR

In the example above, the value of variable nCounter can only be increased if nVar is an even number. All commands below CONTINUE will not be executed after the calling of CONTINUE.

UNION in user-defined data types

With a UNION, it is possible to define your own data type, all the elements of which share the same storage space. The storage areas of the single elements overlap either completely or at least partly. Thereby, a UNION occupies at least so much memory as its largest component.

TYPE U_Test :
UNION
  nVar1      : WORD;
  nVar2      : BYTE;
END_UNION
END_TYPE

Here an example, where a 16bit variable can be accessed individually on the low byte and the high byte. For this purpose, a structure is created consisting of two bytes.

TYPE ST_Bytes :
STRUCT
  nVar1      : BYTE;
  nVar2      : BYTE;
END_STRUCT
END_TYPE

This structure, which occupies 16 bits, is connected with a WORD in a UNION.

TYPE U_Test :
UNION
  nVar1      : WORD;
  stVar2     : ST_Bytes;
END_UNION
END_TYPE

Both variables of the UNION start from the same address in the memory. The access to the single elements of the UNION is obtained in the same way as in one structure. The only difference is that an access to the variable uVar.nVar1 has also an influence on the variable uVar.stVar2 in this case.

PROGRAM MAIN
VAR
  uVar      : U_Test;
  nA, nB    : BYTE;
END_VAR

uVar.nVar1 := 16#1234;
nA := uVar.stVar2.nVar1;    // Value: 16#34 (LSB)
nB := uVar.stVar2.nVar2;    // Value: 16#12 (MSB)

The value of the variable stVar2 changes in the UNION after the start of the program. This example shows, how the least significant bit (LSB) and the most significant bit (MSB) can be determined from the variable of type WORD without bit operations.

Data type LTIME

The data type TIME allows resolution in the range of milliseconds. Since the cycle times of controllers are meanwhile below the 1 ms threshold, it was necessary to define a more exact data type for date specification. LTIME has a size of 64 bits (instead of 32 bits) and allows a resolution in the range of nanoseconds.

PROGRAM MAIN
VAR
  tTest      : LTIME;
END_VAR

tTest := LTIME#134D12H13M34S354MS2US74NS;

Data type WSTRING

The data type STRING codes the characters according to the ASCII character set. Each character is represented by a byte. Thus, it is possible to represent the most letters and other characters, but not all of them. The data type WSTRING was implemented in order to meet the requirements of other languages. It codes the characters according to Unicode. Unicode uses up to 4 bytes per character. In TwinCAT 3, a Unicode variant is used which always occupies 2 bytes per character. In such a way, more than 65.000 different characters can be distinguished. Thus, the most characters used by people are representable, as far as they are included in the Unicode standard. The disadvantage is the higher storage requirement, as the following example shows:

PROGRAM MAIN
VAR
  wsTest          : WSTRING(10) := "abcdefäüöß";
  sTest           : STRING(10) := 'abcdefäüöß';
  nSizeWString    : UDINT;
  nSizeString     : UDINT;
END_VAR

nSizeWString := SIZEOF(wsTest);  // Value: 22
nSizeString := SIZEOF(sTest);    // Value: 11

There are also differences between WSTRING and STRING during initialization. While a STRING constant is defined with an apostrophe (line 4), quotation marks are used for a WSTRING constant (line 3).

Data type REFERENCE

This data type is similar to the data type POINTER. A reference refers to another variable as well. In contrast to a pointer, the pointed value is directly affected. Dereferencing is not necessary as with pointers. Assignments for reference variables take place with the help of the special assignment operator 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;

Both variables nA and nB have a value of 12 after the program start.

References can be initialized directly during the declaration. However, the common assignment operator is used here (line 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;

With the help of the operator __ISVALIDREF, one can check, whether a reference variable contains a valid value.

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

A reference variable becomes invalid, if a value of 0 is assigned to it.

refSample REF= 0;
a := __ISVALIDREF(refSample);    // FALSE

Access to strings via []

Single characters of a variable of type STRING or WSTRING can be accessed via index operator. It returns the ASCII code as byte for a variable of type STRING. If a variable of type WSTRING is accessed, the Unicode as WORD is returned.

It has also write access to the characters. The array starts with 0, i.e. the first character (starting from the left) has the index 0.

PROGRAM MAIN
VAR
  wsTest            : WSTRING(10) := "abcdefgh";
  nLetterWString    : WORD;
  sTest             : STRING(10) := 'abcdefgh';
  nLetterString     : BYTE;
END_VAR

nLetterString := sTest[3];     // ASCII code of 'd': 100
nLetterWString := wsTest[5];   // Unicode of 'f': 102
wsTest[5] := 120;              // the 6th letter becomes a 'x'

Conditional compilation

Conditional compilation allows the programmer to make the compilation of certain program parts dependant on certain conditions. Conditional compilation assignments are designed in such a way, that they are executed not at runtime, but during the compilation.

With the help of {IF} and {END_IF}, a source text area can be limited, so that it will be only compiled if a certain condition is fulfilled. If necessary, an {IF}/{END_IF}can have several {ELSIF} instructions and one {ELSE} instruction.

The conditional compilation is very helpful, if a program has to be developed for different specifications. Not always required areas can be excluded from compilation. Thus, only the code is loaded to the controller, which is actually needed.

You can query different conditions:

{IF defined (identifier)}

A query is TRUE, if identifier was defined. The definition can be assigned with {define identifier} in the source text. It refers only to the current validity range. The validity range starts directly after the statement {define identifier} and terminates at latest with the end of the respective source text or with the statement {undefine identifier}.

If the definition has to be made for the whole validity range of the PLC project, it can be done using the properties dialogue of the PLC project. Several definitions are separated by commas.

Picture01

A global definition cannot be unset by {undefine identifier}.

Statements for the conditional compilation are allowed only in the program area of structured text and in global variable lists. These are (unfortunately) not allowed in the declaration part of variables or in the definition of data types.

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)}

The condition is met, if the variable variableName was declared in the current validity range.

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}

After starting the program, the variable nX has the value of 100.

You can check whether the elements are available within structures, unions and enums. For instance, if a respective structure is located within PLC library, the conditional compilation can help to compile an application either with the new version or with the older version of the PLC library.

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)}

The query is TRUE, if there exists a structure, enumeration or object composition with the name typeName.

{IF hastype (variable:variableName, dataType)}

The condition is met, if the variable variableName is of type dataType.

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}

The variable varSample gets the value of 123,456 assigned after the start of the program.

{IF hasvalue (identifierName, identifierValue)}

An identifier can get the value of type STRING optionally assigned, when it is defined. This value can be requested in a condition.

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}

Unfortunately, the assignment of a value to an identifier is not possible in the properties dialogue of the PLC project.

After starting the program, the variable nSample has the value of 100.

{IF defined (pou:pouName)}

The condition is satisfied, if POU (FUNCTION, FUNCTION BLOCK, or PROGRAM) with the name pouName is available. This can be a POU from a PLC library.

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‘)}

The condition is fulfilled, if the POU pouName has the attribute attributeName. An attribute is assigned to the POUs as follows:

{attribute 'debug'}
FUNCTION TraceInfos : STRING
VAR_INPUT
  i : INT;
END_VAR

{IF hasattribute (variable:variableName,‘attributeName‘)}

Variables can be also declared with attributes, as well as the POUs.

VAR
  {attribute 'debug'}
  nSample : INT;
END_VAR

Operators AND, OR, NOT und ()

Several queries can be also combined with one another.

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}

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

8 thoughts on “IEC 61131-3: Additional language extensions”

  1. Dear Stefan,

    Great posts by the way! And for argument’s sake, can you do some more OOP patterns in IEC61131-3? Like some general usage patterns?

    PS
    Could you please also save your project as an PLCOpen.xml file? This way, everybody in the PLC community can enjoy your examples! => Just open VS2013 with TwinCAT3 and right-click on FB’s to save them as an PLC open xml file.

    Thanks in advance

  2. Hi Stefan,

    Thx for your Ideas.

    Maybe I have something for you, that you didn’t know. Have you ever tried to combine a UNION with an EXTENDS? If you extend a BIT-Structure like

    TYPE Bits:
    STRUCT
    Bit0 : BIT;
    Bit1 : BIT;
    END_STRUCT
    END_TYPE

    TYPE BitsAndByte EXTENDS Bits:
    UNION
    Value : BYTE; (*BITs as BYTE*)
    END_UNION
    END_TYPE

    This way you have the BITs and the Value in the same SubStructure. We use this method for BIT-access and masking by the Value.

    uVar1.Bit0 := TRUE;
    uVar2.Bit1 := TRUE;
    uVar3.Value := uVar1.Value OR uVar2.Value;

    Mabe you could add this to your UNION-Section, because it is not documented in the official Help.

    Steffen

  3. @Steffen

    Your point about using EXTENDS is useful information but, in this specific example, can’t you just use TwinCAT’s built in bit referencing like so:

    VAR
    uVar : BYTE;
    END_VAR

    uVar.0 := TRUE;
    uVar.1 := TRUE;
    uVar.2 := TRUE;

    uVar.7 : = TRUE;

    1. Hi Ryan

      Your example will work.
      Accessing the bits of integer variables by the index of the addressed bit is always possible. You can also use global constants to access the bits.
      So you have to decide what you need in your application (naming conventions, comments, crossreference, refacturing).

      As you can see in my example, you still have the opportunity to access bits by the index, when you use the EXTENDS:

      — GVL —
      VAR_GLOBAL CONSTANT
      cBit0 : INT := 0;
      END_VAR

      — DUT —
      TYPE Bits:
      STRUCT
      Bit0 : BIT;
      END_STRUCT
      END_TYPE

      TYPE BitsAndByte EXTENDS Bits:
      UNION
      Value : BYTE; (*BITs as BYTE*)
      END_UNION
      END_TYPE

      — POU —
      VAR
      uVar : BitsAndByte;
      END_VAR

      uVar.Value.0 := TRUE;
      uVar.Value.cBit0 := TRUE;
      uVar.Bit0 := TRUE;

      1. Hi Steffen,

        I’m confused. Our examples are functionally equivalent, but your example is much more complex. Why would you go to all the trouble of a UNION and EXTENDS just to access bits when those bits are already accessible? Is there some added benefit that I am not recognizing?

    2. Don’t be confused. With my example I just wanted to show, how bits can be accessed.

      I prefer using UNION and EXTENDS, because in most cases I use bit access, these bits are part of an interface to another part of the plc program. There you can get a little lost, when you try to track the meaning of a bit-number. When you use the bit-stuct, you can use cross-reference and refactoring. These are great tools for larger plc programs.

      With the “trick” of the UNION and EXTENDS you combine the advantages of named bits and the masking-features of integer values.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: