While doing software development in TwinCAT, I have always been missing some sort of generic data type/container, to have some level of conformance to generic programming. “Generic programming… what’s that?”, you may ask. I like Ralf Hinze’s description of generic programming:
A generic program is one that the programmer writes once, but which works over many different data types.
I’ve been using generics in Ada and templates in C++, and many other languages have similar concepts. Why was there no such thing available in the world of TwinCAT/IEC 61131-3? For a long time there was a link to a type “ANY” in their data types section of TwinCAT3, but the only information available on the website was that the “ANY” type was not yet available. By coincidence I revisited their web page to check it out, and now a description is available! I think the documentation has done a good job describing the possibilities with the ANY-type, but I wanted to elaborate with this a little further.
First of all, the documentation only mentions that the only current possibility to use the ANY-type as VAR_INPUT inside (free) functions, but I’ve found them to be working fine as VAR_INPUT in function block methods as well (though not as VAR_INPUT inside function block bodies). The example in the documentation is good, though I would like to make some adjustments and show a good use case for such an example.
The following example compares two ANY-types to check whether they are the same or not. This is useful in an unit testing framework, especially so in the case of IEC61131-3, as operator overloading is not available in the IEC standard. Assume we would like to write an assert function block, which does comparisons of two values and checks whether they are the same or not. As operator overloading is not available, this would require us to have different names depending on what data type we would like to compare, such as:
- AssertEquals_BOOL(bExpected : BOOL; bActual : BOOL)
- AssertEquals_INT(nExpected : INT; nExpected : INT)
- AssertEquals_WORD(nExpected : WORD; nActual : WORD)
- AssertEquals_STRING(sExpected : STRING; sActual : STRING)
Comparing this with most other programming languages where you can have operator overloading usually makes the code much cleaner. In a unit testing framework you might also want to add more, such as a message, but we’ll keep it simple for this example. If we have an assert function block with the above mentioned methods, we might complement this with:
- AssertEquals(Expected : ANY; Actual : ANY)
With this method we can provide any data type on the call of it, which makes it much more flexible. While it is simpler to do a simple comparison of the “basic” data types, normally by checking with the equality (=) or inequality (<>) operator for the data types, for the ANY types it requires us to do a little more work in the method body. But before trying to do any implementation, let’s look a little bit of how ANY works.
What happens in the background in TwinCAT is that when the code is compiled, TwinCAT internally replaces the any instances of ANY in functions/method with a structure which has the following contents:
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
That is really neat. Through this information, we can derive what data type and information lies within what the ANY-type is pointing to. The pointer basically points to the actual data that is defined as input for the method using the ANY-type as VAR_INPUT. But what is this __SYSTEM.TYPE_CLASS? If I do the classic “right-click” on the type, TwinCAT/Visual studio doesn’t give me any more hints or options to find out what it is. Let’s say I declare some variables with different types and send them into a method and store them into a variable holding the __SYSTEM.TYPE_CLASS. Doing this gives me different results for the variables, which is expected. I applied the following function on different types:
METHOD PRIVATE GetTypeClass : UDINT VAR_INPUT AnyData : ANY; END_VAR GetTypeClass := AnyData.TypeClass;
And the result was as follows:
Now the question arises – “Is every data type always represented with this number, independent of what compiler, target (x86, x64, ARM) or TwinCAT version that this runs on?”. What is more relevant to ask is – “Is there an enumeration that represents the different type classes?”. Thanks to the help of the local Beckhoff support I was pointed to the library “Base interfaces”, which indeed holds an enumeration for the different type classes:
This is really great! With this information we can create a function that converts this enumeration into a string in case we for example want to utilize it for logging purposes.
FUNCTION F_AnyTypeClassToString : STRING VAR_INPUT AnyTypeClass : __System.TYPE_CLASS; END_VAR CASE UDINT_TO_INT(AnyTypeClass) OF IBaseLibrary.TypeClass.TYPE_BOOL : F_AnyTypeClassToString := 'BOOL'; IBaseLibrary.TypeClass.TYPE_BIT : F_AnyTypeClassToString := 'BIT'; IBaseLibrary.TypeClass.TYPE_BYTE : F_AnyTypeClassToString := 'BYTE'; IBaseLibrary.TypeClass.TYPE_WORD : F_AnyTypeClassToString := 'WORD'; IBaseLibrary.TypeClass.TYPE_DWORD : F_AnyTypeClassToString := 'DWORD'; IBaseLibrary.TypeClass.TYPE_LWORD : F_AnyTypeClassToString := 'LWORD'; IBaseLibrary.TypeClass.TYPE_SINT : F_AnyTypeClassToString := 'SINT'; IBaseLibrary.TypeClass.TYPE_INT : F_AnyTypeClassToString := 'INT'; ... ... ... ELSE F_AnyTypeClassToString := 'UNKNOWN'; END_CASE
Now to get a feeling of how we could implement the AssertEquals method mentioned above, I’ll demonstrate how this piece of code could look like. First, the method header:
METHOD PUBLIC AssertEquals VAR_INPUT Expected : ANY; Actual : ANY; END_VAR VAR nCount : DINT; bDataTypesNotEquals : BOOL := FALSE; bDataSizeNotEquals : BOOL := FALSE; bDataContentNotEquals : BOOL := FALSE; sExpectedDataString : STRING(80); sActualDataString : STRING(80); END_VAR
We declare three booleans to detect the three different use cases of when the two ANY data differs:
- When the types differ (e.g. real vs. int)
- When the size differ (e.g. 2 vs 4 bytes)
- When the content differ (e.g. 0x00A0 vs 0x00BD)
This can be realized with the following piece of code:
IF Expected.TypeClass <> Actual.TypeClass THEN bDataTypesNotEquals := TRUE; END_IF IF NOT bDataTypesNotEquals THEN IF (Expected.diSize <> Actual.diSize) THEN bDataSizeNotEquals := TRUE; END_IF END_IF IF NOT bDataTypesNotEquals AND NOT bDataSizeNotEquals THEN // Compare each byte in the ANY-types FOR nCount := 0 TO Expected.diSize-1 BY 1 DO IF Expected.pValue[nCount] <> Actual.pValue[nCount] THEN bDataContentNotEquals := TRUE; EXIT; END_IF END_FOR END_IF
And to create an useful string that we can show a user, we need to write some additional code. What the below code basically does is that:
- First it checks if the types are not equal. If not, we print the two types. Otherwise we:
- Check if the size equals. If not, we print the two sizes. Otherwise we:
- Check if the content of the data is the same. If not, we print the (byte) content of the two ANY data
IF bDataTypesNotEquals THEN sExpectedDataString := Tc2_Standard.CONCAT('(Type class = ', F_AnyTypeClassToString((Expected.TypeClass))); sExpectedDataString := Tc2_Standard.CONCAT(sExpectedDataString, ')'); sActualDataString := Tc2_Standard.CONCAT('(Type class = ', F_AnyTypeClassToString(Actual.TypeClass)); sActualDataString := Tc2_Standard.CONCAT(sActualDataString, ')'); ELSIF bDataSizeNotEquals THEN sExpectedDataString := Tc2_Standard.CONCAT('Data size = ', DINT_TO_STRING(Expected.diSize)); sExpectedDataString := Tc2_Standard.CONCAT(sExpectedDataString, ')'); sActualDataString := Tc2_Standard.CONCAT('Data size = ', DINT_TO_STRING(Actual.diSize)); sActualDataString := Tc2_Standard.CONCAT(sActualDataString, ')'); ELSIF bDataContentNotEquals THEN FOR nCount := 0 TO MIN(Expected.diSize-1, 38) BY 1 DO // One byte will equal two characters (example: 255 = 0xff, 1 = 0x01) sExpectedDataString := Tc2_Standard.CONCAT(STR1 := Tc2_Utilities.BYTE_TO_HEXSTR(in := Expected.pValue[nCount], iPrecision := 2, bLoCase := FALSE), STR2 := sExpectedDataString); END_FOR sExpectedDataString := Tc2_Standard.CONCAT(STR1 := '0x', STR2 := sExpectedDataString); FOR nCount := 0 TO MIN(Actual.diSize-1, 38) BY 1 DO // One byte will equal two characters (example: 255 = 0xff, 1 = 0x01) sActualDataString := Tc2_Standard.CONCAT(STR1 := Tc2_Utilities.BYTE_TO_HEXSTR(in := Actual.pValue[nCount], iPrecision := 2, bLoCase := FALSE), STR2 := sActualDataString); END_FOR sActualDataString := Tc2_Standard.CONCAT(STR1 := '0x', STR2 := sActualDataString); END_IF IF bDataTypesNotEquals OR bDataSizeNotEquals OR bDataContentNotEquals THEN // Send the 'sActualDataString' to a logger... END_IF
Now I’ll demonstrate all three use cases with examples.
Use case #1 – Different types
With example one I declare two different data types, INT and WORD, both with the same length (2 bytes).
VAR ValueOne : INT := 15000; ValueTwo : WORD := 120; END_VAR
Running the AssertEquals-code with the two variables above results in:
Use case #2 – Different lengths
How can we accomplish having two variables using the same data type but different sizes? Arrays! Remember, the ANY-type can literally take anything! We’ll declare:
VAR ValueOne : ARRAY[1..2] OF INT; ValueTwo : ARRAY[1..3] OF INT; END_VAR
And then we get:
The first array consists of two integers (each 2 bytes, total of 4 bytes) while the other one consists of three integers (totalling 6 bytes), which is exactly what the message tells us.
Use case #3 – Different data content
To demonstrate this one it’s enough to create two variables with the same type and length, but where the content differs. One example of this could be:
VAR ValueOne : DWORD := 16#01234567; ValueTwo : DWORD := 16#89ABCDEF; END_VAR
Which gives the runresult:
Which proves our code works really well, and also gives us a feeling of the capabilities of the ANY-type. I think it was really fun experimenting and playing around with the ANY-type. Do you know any use-case where you would find the ANY-type particularly useful? Please comment below!
[…] dem Artikel The wonders of ANY zeigt Jakob Sagatowski wie der Datentyp ANY sinnvoll eingesetzt werden kann. Im beschriebenen […]
Hi Jacob,
Very good article. In addition to the data type ‘ANY’, there is also the data type ‘T_ARG’. My first practical experience has been published on my blog: https://stefanhenneken.wordpress.com/2018/04/18/iec-61131-3-der-generische-datentyp-t_arg.
The English translation will follow asap.
Best regards
Stefan
Hi Stefan! Thanks for your feedback. I was not aware of the T_ARG. I’m happy to see that you’ve also evaluated the usage of a [*] ARRAY, but I see that the advantage of T_ARG to ANY is that it’s possible to use in the context of VAR_IN_OUT, and thus together with [*]-arrays, which is great! Thanks for this invaluable information.
[…] the article The wonders of ANY, Jakob Sagatowski shows how the data type ANY can be effectively used. In the example described, a […]
Hi, I am new to Beckhoff plc!
I would like to convert string to array of bytes…. My string length is 1000..
Maxstring to byte array converts untill 255 characters only….
Can any one please suggest….?
Hi Jakob,
I found that this function fails in some structs.
The example I use contains:
BOOL;
string(255);
BOOL;
REAL;
REAL;
REAL;
REAL;
REAL;
REAL;
It goes through the loop and fails at byte 82. The comparison happens on an unit test “Assert” where I am checking if my program updates the structure correctly. In the program, the actual value is changed from blank, to containing values. I have a suspicion this discrepancy relates to the string.
I’m not sure whether this relates to this actual post, or about the TcUnit framework? If it’s the TcUnit framework, could you file an issue with the example/description to reproduce the error?
Sure, although I’m not exactly using TcUnit, I can try and make an example of this later (a bit of a time crunch here).
I actually used some inspiration here to make a unit testing framework which uses OPC UA to find the ‘test methods’ and uses OPC UA method calls to execute the tests.
It came from a desire to try and make the PLC unit testing framework a bit more universal.
I did make use of this blog post (I have a line giving credit to your cleverness in the code 🙂 ) to create my Assert.Equal method in the framework.
I found some odd cases where objects I expected to be equal were not. When putting in a break point, it showed that the byte comparison failed… I didn’t have enough time to debug further so I changed from the generic Assert.Equal to Assert.String (which does simple string comparison) to get around the issue. This first issue was with comparing two structures.
Now I’m back here with a bit more information 🙂 The Assert.Equal method was working well for function blocks (even though it had a string in there) but just found an issue where it failed. The objects I am comparing share a common abstract base class. (in fact, it used to be the structure above, but after some refactoring, they are now function blocks)
ex://
FUNCTION_BLOCK ABSTRACT ObjectDetails
FUNCTION_BLOCK PartDetails EXTENDS ObjectDetails
I’ll get some more details when I finish my current deliverable and have some ‘breathing room’.
Would you still like to continue this discussion on the bug report?
Hi! Yes please send an example that reproduces the problem to the issue tracker on GitHub.
I think that you made a copy-paste error in
IF NOT bDataTypesNotEquals AND NOT bDataTypesNotEquals THEN
since the two conditions are equal.
Should not it be
IF NOT bDataTypesNotEquals AND NOT bDataSizeNotEquals THEN
?
Hi Stefano! Good catch! Fixed the typo. Thanks!
The sad thing with Any is that it requires you to have a SET for Properties… TwinCat is sometimes pretty stupid :(… Why is TwinCat forcing me to implement a Set Property when I just have a value to log….
And no, adding a Set is not always needed, I have for example an Wrapper FB accessing some information in the backend where I dont even need a private set Method for the Property…
Hello Jakob
First of all, thank you for your posts, they are always very interesting to read, and you provide very useful information.
I was recently working with Any type with a generic function an i just found a possible bug, maybe you are aware of this, or maybe i have just used incorrectly:
Let’s say you have a simple method defined as follows
METHOD TestingAnyType : BOOL
VAR_INPUT
anyValue : ANY;
intValue : INT;
END_VAR
TestingAnyType := anyValue.pValue^= intValue;
In this case i want to verify that the value provided by anyValue is the same value as initValue,
this method will work perfectly with values between 0-255 (1 Byte) and after that it will just give the modulo as a result, is it correct? i think is wrong, i would appreciate any infos regarding this topic.
I tested with the Build 4024.32 and with 4024.22
Best Regards
Miguel
All the functions/function blocks/methods with the name of the type in there name could do with an extension with any.
Examples:
AddBool, AddDint, AddLint, … in the Tc3_JsonXml library
BYTE_TO_HEXSTR, DWORD_TO_HEXSTR, LWORD_TO_DECSTR … in the Tc2_Utilities
Wrapper functions.
Example:
Using a wrapper around F_GetMappingStatus to check if a variable is linked/mapped just like the __ISVALIDREF operator
How you go about converting an ANY back to a REAL or an INT Etc?