Useful features of ST + Codesys 3 that many do not notice

Introduction

While working as a PLC software engineer, very often in the course of development there were not the most obvious, but quite simple and beautiful solutions to both typical and specialized tasks. In this article I want to share my experience and tell you how to make PLC development more enjoyable and efficient.

About the author’s experience

Experience with PLC: 3 years.

PLC development: Beckhoff CX series, SE Modicon M221, WAGO 750 series.

Development environments: TwinCAT 3, EcoStruxure Machine Expert-Basic, CODESYS V2.3.

Most of the experience comes from ST+TwinCAT 3, which is based on CODESYS and IEC 61131.

I decided to write an article because I’m leaving OT and move into the world IT. I would like to share my experience so that these 3 years are not in vain.

Development environment

If you often have to comment parts of the code – then find out what keyboard shortcut will allow you to do this, it will save a lot of time. In TwinCAT XAE Shell, to comment on selected code: Ctrl+K+C and Ctrl+K+U to uncomment.

Disable the Stop button to avoid accidentally stopping the PLC, sometimes such an accidental press can lead to undesirable consequences. In TwinCAT XAE Shell, you can choose which buttons to display on the toolbar. After debugging the program locally, I recommend hiding the PLC stop button.

Structured Text

STRING vs. WSTRING

TwinCAT 3 has the ability to use Unicode strings. They can come in handy if you need to pass specific characters, but it’s best not to use WSTRING unless absolutely necessary.

STRING

WSTRING

Format

ASCII

Unicode

size of character

BYTE (1 byte)

WORD (2 bytes)

Terminator

null character

0

date and time

In almost any project, you need to know the exact time, calculate time intervals. Often, working with times and dates brings a lot of problems and pain. For myself, I found a solution, I’m sure it will make life easier for many.

F_GetSystemTime() (Функция из модуля Tc2_System)

This function can be used to read the timestamp of the operating system. The timestamp is a 64-bit integer value with 100 ns precision. Among other things, it can be used to synchronize tasks or measure time. One unit corresponds to 100 ns. The time is the number of 100 ns intervals since January 1, 1601.

Marks are stored in variables of type ULINT. Knowing all this, we can easily calculate time intervals with an accuracy of 100ns! You just need to find the difference between the marks.

Unfortunately, I did not find any standard functions for converting a mark into a DATETYPE type, so I had to implement such a function myself:

(*
:Description: Convert time since 1 January 1601 in 100 ns to DATE_AND_TIME  (Преобразует время с 1 Января 1601 года в 100 нс в DATE_AND_TIME)
:Usability: Convert timestamp to datetime

:Note: check then nSystemType more then 01.01.1970 00:00:00

Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)

FUNCTION F_SystemTimeToDT : DT
VAR CONSTANT
    SECONDS_BETWEEN_1601_AND_1970 : ULINT := 11_644_473_600;
END_VAR
VAR_INPUT
    nSystemTime : ULINT; // One unit is 100 ns since 1 January 1601
END_VAR
VAR
    nSeconds : ULINT;
END_VAR
nSeconds := (nSystemTime / 10_000_000) - SECONDS_BETWEEN_1601_AND_1970;
F_SystemTimeToDT := ULINT_TO_DT(nSeconds);

As you can see from the code, the difficulty was in calculating the interval between the initial countdown of the PLC system time and the DATETIME type.

Function to get current date/time in DATETIME format
(*
:Description: Return datetime now in format DATE_AND_TIME (DT)
:Usability: For getting datetime now in format DATE_AND_TIME (DT)

Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)

FUNCTION F_DateTimeNow : DT
F_DateTimeNow := F_SystemTimeToDT(F_GetSystemTime());
Function to get elapsed time in TIME format
(*
:Description: Time passed since tStart (Прошло времени c tStart)
:Usability: If need check how long time past

Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)

FUNCTION F_TimePassed : TIME
VAR_INPUT
    tStart: ULINT; (* Время начала в 100нс от 01.01.1601,
                    текущее время в данном формате предоставляет функция F_GetSystemTime()*)
END_VAR
F_TimePassed := ULINT_TO_TIME((F_GetSystemTime() - tStart) / 10000);

Numeric constants

Most industrial protocol communication documentation contains hexadecimal register addresses, function numbers, command symbols, and so on. For bitwise operations, it is necessary to represent numbers in binary form. To effectively solve problems where you have to deviate from the decimal number system, you need to know about the possibility of setting constant numbers of a given type in a given number system.

In general, setting a numeric constant looks like this:

{datetype}#{numeral system}#value 

Example: DINT#16#A1

Numeric values ​​can be binary, octal, decimal, or hexadecimal. If the integer value is not a decimal number, its base must be written before the integer constant followed by the hash character (#). For hexadecimal numbers, the digits for numbers 10 to 15 are, as usual, represented by the letters AF.

The type of these numeric values ​​can be BYTE, WORD, DWORD, SINT, USINT, INT, UINT, DINT, UDINT, REAL, or LREAL.

ANY type

In programming languages ​​with static typing, it is quite difficult to make generic functions/function blocks. When I was given the task of collecting and analyzing various data, I decided that copying function blocks and changing only the input value type in them was not the best option. Then the idea came up to cast all types to one, and for objective reasons this is the LREAL type.

When implementing a function or method, you can declare input data (VAR_INPUT) as variables with the ANY data type. Next, you can get a pointer to the value, data type, and size of the variable passed to this input.

ANY Data Type Structure
TYPE AnyType :
STRUCT
    // the type of the actual parameter
    typeclass : __SYSTEM.TYPE_CLASS ;
    // the pointer to the actual parameter
    pvalue : POINTER TO BYTE;
    // the size of the data, to which the pointer points
    diSize : DINT;
END_STRUCT
END_TYPE

In addition to the ANY type, there are also child types:

Type Inheritance Tree
Type Inheritance Tree

I would like to draw your attention to the fact that a constant cannot be passed to an input of type ANY, so in some cases you will have to create an additional variable.

Knowing about this type, I managed to implement a function that brought data of different types to LREAL.

Function to convert numeric types to LREAL
(*
:Description: Convert ANY_NUM and ANY_BIT to LREAL
:Usability: For development universal functions

:Note:
Valid types is:
ANY_NUM:
    - ANY_REAL: REAL, LREAL
    - ANY_INT: USINT, UINT, UDINT, ULINT, SINT, INT, DINT, LINT
ANY_BIT:
    - BYTE, WORD, DWORD, LWORD

Version history:
Kozhemaykin E. A. | Creating | 01.06.2021;
Kozhemaykin E. A. | {CLASS_TO_LREAL -> TO_LREAL | 03.11.2021;
 
*)

FUNCTION F_AnyNumToLREAL : LREAL
VAR_INPUT
    AnyNum: ANY; // Variable for converting, need have address
END_VAR
VAR
    pReal : POINTER TO REAL;   // pointer to a variable of the type REAL
    pLReal : POINTER TO LREAL;  // pointer to a variable of the type LREAL
    
    pUSInt : POINTER TO USINT;   // pointer to a variable of the type USInt
   	pUInt : POINTER TO UINT;  // pointer to a variable of the type UInt
   	pUDInt : POINTER TO UDINT;  // pointer to a variable of the type UDInt
    pULInt : POINTER TO ULINT;   // pointer to a variable of the type ULInt
    
   	pSInt : POINTER TO SINT;  // pointer to a variable of the type SInt
    pInt : POINTER TO INT;   // pointer to a variable of the type Int
   	pDInt : POINTER TO DINT;  // pointer to a variable of the type DInt
    pLInt : POINTER TO LINT;   // pointer to a variable of the type LInt
    
    pByte : POINTER TO BYTE;  // pointer to a variable of the type Byte
    pWord : POINTER TO WORD;   // pointer to a variable of the type Word
   	pDWord : POINTER TO DWORD;  // pointer to a variable of the type DWord
    pLWord : POINTER TO LWORD;   // pointer to a variable of the type LWord

END_VAR
VAR_OUTPUT
    OrginalType: __SYSTEM.TYPE_CLASS;
    bInvalidType: BOOL := FALSE;
END_VAR
// Real numbers
IF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_REAL) THEN
    pReal := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_REAL;
    F_AnyNumToLREAL := TO_LREAL(pReal^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LREAL) THEN
    pLReal := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LREAL;
    F_AnyNumToLREAL := pLReal^;

// Bit's numbers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_BYTE) THEN
    pByte := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_BYTE;
    F_AnyNumToLREAL := TO_LREAL(pByte^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_WORD) THEN
    pWord := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_WORD;
    F_AnyNumToLREAL := TO_LREAL(pWord^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_DWORD) THEN
    pDWord := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_DWORD;
    F_AnyNumToLREAL := TO_LREAL(pDWord^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LWORD) THEN
    pLWord := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LWORD;
    F_AnyNumToLREAL := TO_LREAL(pLWord^);

// Unsigned integers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_USINT) THEN
    pUSInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_USINT;
    F_AnyNumToLREAL := TO_LREAL(pUSInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_UINT) THEN
    pUInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_UINT;
    F_AnyNumToLREAL := TO_LREAL(pUInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_UDINT) THEN
    pUDInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_UDINT;
    F_AnyNumToLREAL := TO_LREAL(pUDInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_ULINT) THEN
    pULInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_ULINT;
    F_AnyNumToLREAL := TO_LREAL(pULInt^);

// Signed integers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_SINT) THEN
    pSInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_SINT;
    F_AnyNumToLREAL := TO_LREAL(pSInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_INT) THEN
    pInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_INT;
    F_AnyNumToLREAL := TO_LREAL(pInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_DINT) THEN
    pDInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_DINT;
    F_AnyNumToLREAL := TO_LREAL(pDInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LINT) THEN
    pLInt := AnyNum.pValue;
    OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LINT;
    F_AnyNumToLREAL := TO_LREAL(pLInt^);
    
//Invalid type
ELSE
    F_AnyNumToLREAL := 0;
    bInvalidType := TRUE;
END_IF

REFERENCE

Everyone knows about pointers (POINTER) and the problems associated with them, and so many of them can be avoided by using references (REFERENCE):

  • References are easier to use: the reference does not need to be dereferenced (with ^) to access the contents of the object referred to by the reference.

  • Cleaner syntax for passing values: If the input is a reference, then there is no need to write ADDR(value).

  • Unlike pointers, for references, the compiler checks data types when passing values.

It is worth noting that it is not always possible to replace a pointer with a reference, but when possible, do it.

pragmas

pragma statements affect the properties of variables related to the compilation or precompilation process. Feel free to look through the possibilities of each type of pragmas – be sure to find something useful for your project.

pragmas types:

Union

Union is a structure type that allows a value to be represented in different data types. This structure is useful when debugging code and also when processing input values.

If you need to access bits, then this can be done through a dot. But I see a huge drawback with this method: there is no way to iterate over bits. If you need to parse a variable into bytes or 16-bits or some other complicated way, then instead of writing complicated functions, try doing it with Union first.

SEL, MIN, MAX, LIMIT

Many PLC programmers often lack the syntactic sugar found in other programming languages. Using the SEL function as an example, I would like to show that perhaps this “sugar” in the form of a ternary operator is not particularly needed.

If you need to select a value depending on a condition, you can do it in one line:

value := SEL(condition, if false, if true);

If you need to limit the value to the top and/or bottom, this can also be done in one line:

value := MIN(value, max_limit);
value := MAX(value, min_limit);
or
value := LIMIT(min_limit, value, max_limit); 

Many functions and operators that we lack are already written – you just need to look.

Conclusion

The article describes what I personally wanted to pay attention to (the OOP decided not to touch it). I will be glad if my experience will be useful to someone. When using the provided functions, I will ask you to continue version history.

Share your experience in the comments. To keep abreast of events and communicate with colleagues, I suggest that you follow the links: tg-channel proPLC, tg-chat proPLC.

I hope they don’t get banned

No war

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *