2

I would like to create a type-generic STRUCT and a paired Function Block that accept and return variables of a generic type (assumed ANY_NUM).

This is desired to condense many existing STRUCT and FB pairs in the same format using generic number types probably belonging to the ANY_NUM type into a single generic pair.

In C++, the Generic Structure would be accomplished with a Template Class, but I cannot find a similar structure in Structured Text.

I tried the generic Function Block on Beckhoff's ANY/ANY_(TYPE) page, however it quickly failed to convert type 'LREAL' to type '__SYSTEM.AnyType'.

Question:

To what extent can I accomplish this goal in Structured Text?

EDIT:

I mistakenly assumed that ANY is the only ST generic of relevance. I have been directed to type T_Arg as a potentially viable candidate.

Example Format of Attempt:

Structure:

TYPE Bounded_Value:
STRUCT
    Value   : ANY_NUM;
    Min_    : ANY_NUM;
    Max_    : ANY_NUM;
END_STRUCT
END_TYPE

Function Block:

FUNCTION_BLOCK Bind_Value
VAR_IN_OUT
    value_struct: Bounded_Value;
END_VAR

(Implementation would bind value_struct.Value to between value_struct.min_ and value_struct.max_)

Graeme Rock
  • 601
  • 5
  • 21
  • `REAL` and `LREAL` are not `ANY_NUM`. Those have to be converted. From the perspective on math as we know it we consider it a number, but from perspective of data types, `INT` and `REAL` are different types. – Sergey Romanov Apr 29 '18 at 15:22
  • @SergeyRomanov I do not understand what you mean. According to [the Beckhoff page on ANY](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/9007201784167563.html&id=2058661990612177947), ANY_NUM encompasses ANY_REAL and ANY_INT. – Graeme Rock Apr 30 '18 at 14:30
  • @SergeyRomanov Oh, you are meaning to express that ANY_NUM and LREAL are not equivalent types, as ANY_NUM is a struct containing the type, pointer, and size of a number, while REAL/LREAL/INT are the numbers themselves. Correct? – Graeme Rock Apr 30 '18 at 15:13
  • you are right. I missed it with ANY_INT. Sorry. – Sergey Romanov May 01 '18 at 12:20

3 Answers3

3

I've recently investigated this (ANY-type) in TwinCAT. What you basically need to do is to convert every byte that the ANY-pointer points to into a LREAL (which you know according to IEC61131-3 will always be 8bytes). The ANY type holds information on what type it points to, so you will know when it is a LREAL by parsing the structure of data the ANY-pointer points to. Please read my complete investigation on my blog: The wonders of ANY

Jakob
  • 1,288
  • 7
  • 13
  • Your blog does clear up some confusion I had regarding ANY_TYPE's use in functions, However, neither your blog nor [Stefan Henneken's blog on T_Arg](https://stefanhenneken.wordpress.com/2018/04/18/iec-61131-3-der-generische-datentyp-t_arg/) clear up use of these variables in a structure. – Graeme Rock Apr 30 '18 at 15:17
  • I suppose I could attempt to use a Function Block or Program to load values into each STRUCT, but that just creates more work and consumes far more space than just having a STRUCT for each variable. – Graeme Rock Apr 30 '18 at 15:25
3

(I gleaned the solution to my problem from Stefan Henneken's blog post on T_Arg.)

The goal can be accomplished, but not particularly cleanly. There are two generic types (that I have found so far) that are applicable: ANY_NUM and T_Arg.

(I use ANY_NUM because it is most relevant to this example. ANY, ANY_REAL, or ANY_INT would also be reasonable options)

Both options have similar structures and function similarly. Each is a structure containing information on the stored variable: its type, a pointer to it, and its size.

Yet there are pros and cons to each. To fulfill this problem most accurately, we would use T_Arg.

Here is the difference:

ANY / ANY_NUM / ETC

Advantage: Variable conversion to ANY_NUM is done implicitly when a variable is assigned. The input variable needs no pre-conversion before being input into the function, cutting down on the size of code.

Furthermore, it only accepts variables belonging to its domain, so strings will not be used accidentally.

Disadvantage: ANY_NUM cannot be declared outside of a VAR_INPUT block and, in fact, provides this error message upon attempt:

Variables of type 'ANY_NUM' only allowed as input of functions.

Therefore ANY_NUM cannot be used as a STRUCT variable, even if that STRUCT is declared as an input to a function. This is why it cannot be used to solve this particular problem.

T_Arg

Advantage: T_Arg can be declared and used anywhere.

Disadvantage: T_Arg requires conversion functions for any expected variable type, e.g.: F_INT(),F_REAL(),F_DINT(), etc.

Therefore type-checking needs to be performed before and after input.

Example Solution

Unfortunately, a variable stored in T_Arg cannot be directly manipulated. It is necessary to move the stored variable to a temporary variable to use it. So Value, Min_, and Max_ would each need conversion from type T_Arg into type REAL/INT/etc.

As we are trying to only use one STRUCT, Value would need to be converted again into T_Arg once Bind_Value finishes manipulating it.

In total, Value would be converted three times when instantiated and twice per call after.

Structure:

TYPE Bounded_Value:
STRUCT
    Value   : T_Arg;
    Min_    : T_Arg;
    Max_    : T_Arg;
END_STRUCT
END_TYPE

Function Block:

FUNCTION_BLOCK Bind_Value
VAR_IN_OUT
    value_struct: Bounded_Value;
    // Other variable type declarations
END_VAR
VAR
    val_int     :   INT;
    max_int     :   INT;
    min_int     :   INT;
END_VAR  

CASE (value_struct.Value.eType) OF
    E_ArgType.ARGTYPE_INT: // If the struct's Value's type is INT
        // Copy generic pointer information into typed pointer 
        MEMCPY(ADR(val_int), value_struct.Value.pData, value_struct.Value.cbLen);
        MEMCPY(ADR(max_int), value_struct.Max_.pData,  value_struct.Max_.cbLen);
        MEMCPY(ADR(min_int), value_struct.Min_.pData,  value_struct.Min_.cbLen);

        IF val_int > max_int THEN
            value_struct.Value.pData := value_struct.Max_.pData;
        ELSIF val_int < min_int THEN
            value_struct.Value.pData := value_struct.Min_.pData;
        END_IF
    // Other variable type handlings
END_CASE

MAIN:

PROGRAM MAIN
VAR
    val     : INT := -1; //Change this to test
    minim   : INT :=  0;
    maxim   : INT :=  5;
    newVal  : INT;
    bv      : Bounded_Value;
    bind    : Bind_Value;
END_VAR

// Convert INT variables to T_Arg in structure
bv.Value:= F_INT(val);
bv.Max_ := F_INT(maxim);
bv.Min_ := F_INT(minim);
// Bind_Value.value_struct := bv;
bind(value_struct := bv);
// Copy result to newVal
MEMCPY(ADR(newVal), bv.Value.pData, bv.Value.cbLen);
Community
  • 1
  • 1
Graeme Rock
  • 601
  • 5
  • 21
0

You could also create a generic type with the help of a Function Block and a Union. Let's say you define a Union with all your DUTs and POUs:

TYPE GenericType :
UNION
    generic : PVOID;
    bBool   : REFERENCE TO BOOL;
    nInt    : REFERENCE TO INT;
    nUint   : REFERENCE TO UINT;
    nUdint  : REFERENCE TO UDINT;
    fReal   : REFERENCE TO REAL;
    fLreal  : REFERENCE TO LREAL;
    fbTest  : REFERENCE TO FB_Test;
END_UNION
END_TYPE

Then you create a special Function Block:

FUNCTION_BLOCK Generic
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    uGenericType : GenericType;
END_VAR

And a Property that gets and sets a PVOID:

PROPERTY PUBLIC generic : PVOID

The getter:

generic := uGenericType.generic;

The setter:

uGenericType.generic := generic;

You also need Properties in order to retrieve your type later:

Image of the FB

An example of the getBool setter could be:

IF uGenericType.generic = 0 
THEN
    RETURN;
ELSE
    getBool := uGenericType.bBool;
END_IF

Now you create the FB that will make use of the generic type:

FUNCTION_BLOCK FB_Container
VAR_INPUT

    myGenericType       : Generic;
    nContainerOption    : INT;

END_VAR
VAR_OUTPUT
END_VAR
VAR

    testInt             : INT;
    testBool            : BOOL;
    testFB              : FB_Test;

END_VAR

CASE nContainerOption OF

    1:
        testInt := myGenericType.getInt;

    2:
        testFB := myGenericType.getFbTest;

    3:
        testBool := myGenericType.getBool;

END_CASE

The call could be like this:

fbContainer.myGenericType.generic := ADR(testInteger);
...
fbContainer(nContainerOption := 1);

Another approach is to extend our FBs with the Generic FB. We need to do some modifications to the Generic FB and to the GenericType Union though:

FUNCTION_BLOCK Generic
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    uGenericType    : GenericType;
    bInit           : BOOL;
END_VAR

IF NOT bInit THEN
 uGenericType.generic := ADR(THIS^);
 bInit := TRUE;
END_IF

TYPE GenericType :
UNION
    generic : PVOID;

    //Add the pointer of the FB you want to extend
    pAxis   : POINTER TO FB_Axis;

    bBool   : REFERENCE TO BOOL;
    nInt    : REFERENCE TO INT;
    nUint   : REFERENCE TO UINT;
    nUdint  : REFERENCE TO UDINT;
    fReal   : REFERENCE TO REAL;
    fLreal  : REFERENCE TO LREAL;
    fbTest  : REFERENCE TO FB_Test;

END_UNION
END_TYPE

The extended FB:

FUNCTION_BLOCK FB_Axis EXTENDS Generic
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    fPosition : LREAL;
END_VAR

And now the call of our container as before:

fbContainer.myGenericType := fbAxis;

in FB_Container you could call the axis as follows:

IF myGenericType.getPointerFbAxis <> 0 
THEN
    position := myGenericType.getPointerFbAxis^.fPosition;
END_IF
Filippo Boido
  • 1,136
  • 7
  • 11