User defined functions with params - codesys

In codesys some functions support what in other languages is usually called 'params', i.e. a function that can take a varied amount of similarly typed variables. For example the ADD Operator (function in ladder).
My question is, if there's any way to do the same in user defined functions?
The only idea that I have so far is to take an ARRAY [*] OF SOMETHING and use LOWER_BOUND and UPPER_BOUND to do the computations. This does work, but requires the user to create an additional array variable every time they want to call my function. For example, we have the CONCAT function that concatenates 2 strings. Suppose I want a CONCAT_ALL function that takes n strings and concatenates them all:
STRS: ARRAY [0..9] OF STRING := [STR1, STR2, STR3, STR4, STR5, STR6, STR7, STR8, STR9, STR10];
// This works, but I want to avoid creating an array variable!
CONALL1: STRING := CONCAT_ALL(STRINGS := STRS);
// This doesn't work!
CONALL2: STRING := CONCAT_ALL(STRINGS := [STR1, STR2, STR3, STR4, STR5, STR6, STR7, STR8, STR9, STR10]);
(EDIT: As I was asked, I am using Schneider Electric Machine Expert 1.2, or CODESYS compiler 3.5.12.80)

There is hope in the future!
In Codesys V3.5 SP16 it seems to be finally possible to use FUNCTIONs and METHODs with optional arguments. Of course this will be in non-codesys products, like TwinCAT and Schneider, in later versions.
This means you can finally create a CONCAT with 100 arguments and call it with just for example 3! Awesome.
https://www.codesys.com/fileadmin/data/Images/Download/features-and-improvements-V35SP16-en.pdf

Here is an object oriented example of a string concatenator Function Block:
First we define an Interface with 2 methods:
INTERFACE I_MultipleConcat
METHOD concatString : I_MultipleConcat
VAR_INPUT
sTarget : STRING;
END_VAR
METHOD getResult
VAR_IN_OUT
sRetrieveResult : STRING(1000);
END_VAR
Then the Function Block which implements the Interface:
FUNCTION_BLOCK FB_MultipleConcat IMPLEMENTS I_MultipleConcat
VAR_OUTPUT
uiLen : UINT;
END_VAR
VAR
sResult : STRING(1000);
END_VAR
//-------------------------------------------------------------------
METHOD concatString : I_MultipleConcat
VAR_INPUT
sTarget : STRING;
END_VAR
//make sure that the length of sResult is not exceeded
IF uiLen + INT_TO_UINT(LEN(sTarget)) <= (SIZEOF(sResult)-1)
THEN
//add uiLen as offset to sResult memory access
memcpy(ADR(sResult) + uiLen,ADR(sTarget),LEN(sTarget));
uiLen := uiLen + INT_TO_UINT(LEN(sTarget));
END_IF
//return the instance of this FuncBlock in order to concat new strings
//with concatString() or pass the result to another STRING with getResult()
concatString := THIS^;
//-------------------------------------------------------------------
METHOD getResult
VAR_IN_OUT
sRetrieveResult : STRING(1000);
END_VAR
sRetrieveResult := sResult;
sResult := '';
uiLen := 0;
You can call it like this:
IF NOT bInit
THEN
bInit := TRUE;
//s1 must be a STRING(1000) otherwise compile error
fbMultipleConcat
.concatString('Test1 ')
.concatString('Test2 ')
.concatString('Test3 ')
.getResult(s1);
END_IF

Short answer: There is no way to pass n arguments to a function.
Structured text is a strongly and statically typed language designed for hard real time requirements and it is not a scripting language like Python.
If you have a lot of string manipulations in your code that you don't want to do in python but in your real time loop (and you should assess if it's really necessary depending on your requirements) and still want to make it in a comfortable way, then you have to put some effort in it and build a string manipulation library yourself.
After that you could have a very comfortable function call like this:
sResult := F_Concat6(str1,str2,str3,str4,str5,str6);
I understand that it is tempting to adopt thought and programming patterns learned from other programming languages, but structured text and real time industrial control programming is really another kind of beast compared to common user land programming.
With that I mean, that there are specific reasons why the language is designed as it is and when those principles are correctly understood and applied, rock solid architectures derive from them.
To sum it up, my two cents of advice on this:
Think and write software as expected by your domain and do not port incompatible working methods from other domains.

No you cannot pass n arguments to function.
But you can pass an array, with none fixed number of elements. Syntaxyx for Codesys 2.3.
FUNCTION CONCAT_ALL : STRING(250)
VAR_INPUT
asParts: POINTER TO ARRAY[0..10000] OF STRING(20); (* Array of strings *)
iNum: INT; (* Number of elements *)
END_VAR
VAR
iCount: INT; (* For cycle *)
END_VAR
FOR iCount := 0 TO 10000 DO
IF iCount > iNum THEN
EXIT;
END_IF;
CONCAT_ALL := CONCAT(CONCAT_ALL, asParts^[iCount]);
END_FOR;
END_FUNCTION
PROGRAM PLC_PRG
VAR
(* Array 1 to test *)
asTest1: ARRAY[1..2] OF STRING(20) := 'String 1', 'String 2';
(* Array 2 to test *)
asTest2: ARRAY[1..3] OF STRING(20) := 'String 1', 'String 2', 'String 3';
s1: STRING(250);
s2: STRING(250);
END_VAR
s1 := CONCAT_ALL(ADR(asTest1), 2);
s1 := CONCAT_ALL(ADR(asTest2), 3);
END_PROGRAM

Related

IF statement in Structured text in PLCs

I have a function block like this:
FUNCTION_BLOCK chujwamwdupe
VAR_INPUT
en:BOOL;
val:INT;
END_VAR
VAR_OUTPUT
oval:INT;
END_VAR
VAR
END_VAR
And the code:
IF (en = TRUE) THEN
oval := val;
END_IF
As simple as it is. Why on earth this block always executes? No matter if en is true or false it always changes oval to val.
I assume with PLC you mean CODESYS I tried this out with 3.5.18.0 and it seems the compiler employs variable hoisting in this case. Restructuring your code to something like this:
oval := val;
IF (en = TRUE) THEN
oval := val;
END_IF
What you probably want is to use oval as an input output variable like this:
FUNCTION_BLOCK chujwamwdupe
VAR_INPUT
en:BOOL;
val:INT;
END_VAR
VAR_IN_OUT
oval:INT;
END_VAR
VAR
END_VAR
If defined as an input output variable oval will retain its original value if en is False
If you dont want to retain its original value
You could initialize oval:
oval := 11; // magic number
IF (en = TRUE) THEN
oval := val;
END_IF
Or you could add an else statement:
IF (en = TRUE) THEN
oval := val;
ELSE
oval := 11; // magic number
END_IF
I have tested it with TwinCAT 3.1.4022 and here it works as I think it is intended.
But the FB remembers it's oval value from call to call, is this maybe why it looks like it always updates the output value?

Declare values of array of structures in POU using PLC ST

I have structure declared like this:
TYPE board:
STRUCT
number: INT;
color: DWORD;
END_STRUCT
END_TYPE
And i want to declare array of these structures with starting values in POU. I do it like this:
Program PLC_PRG
VAR
arr1: ARRAY[1..61] OF board;
board: board;
arr1[1].color := 16#FF0000;
END_VAR
But i get an error which say: "Error 4024: PLC_PRG(10): Expecting ':' before '['".
Do anybody know how to solve this problem?
You didn't specify a platform but I'll assume TwinCAT. To initialize an array of structures, you have to do it all in the variable declaration line.
arr1: ARRAY[1..61] OF board := [(number:=7, color:=16#FF0000), (number:=5, color:=16#FF0001)];
This example will initialize elements 1 and 2 of the array. To my knowledge you can't selectively initialize individual array elements as you are maybe wanting to do. You could use statements like arr1[17].color := 16#FF0000 of course but it would have to be outside of your variable declaration block.

Multi-variable assignment to an array in TwinCAT

How can assign a new variable to an array in TwinCAT?
in TwinCAT you can initialize all your array's argument directly for example for array a we can use:
a : ARRAY [1..3] OF INT := [3(0)];
or
a : ARRAY [1..3] OF INT := [0,0,0];
but if you want to assign the array in the main program(not initializing part) for example
a:=[2,8,5];
you will face this error tip: Unexpected array initialisation.
any help would be appreciated.
You cannot directly initialize arrays inside the program part.
That beeing said, the best option is porbably to define a constant containing the desired initialization values and then assigning the value of that constant to your array:
VAR
aiMyArray : ARRAY [1..2] OF INT;
END_VAR
VAR CONSTANT
aiInitializerMyArrayOptionA : ARRAY [1..2] OF INT := [1,2];
aiInitializerMyArrayOptionB : ARRAY [1..2] OF INT := [3,4];
END_VAR
IF bCondition THEN
aiMyArray := aiInitializerMyArrayOptionA;
ELSE
aiMyArray := aiInitializerMyArrayOptionB;
END_IF
The other option would be to manually initialize each index one by one which easily gets impracticale with decent array sizes:
IF bCondition THEN
aiMyArray[1] := 1;
aiMyArray[2] := 2;
ELSE
aiMyArray[1] := 3;
aiMyArray[2] := 4;
END_IF
Looping thorugh all elements and assigning values might be an option too. But that would only be usefull when the values are no arbitrary constatns but you are able to calculate the values from some formula.

Initialize Array of Custom Types in Structured Text Syntax

In my project I have a type like:
TYPE myDataStruct :
STRUCT
A : UINT;
B : WORD;
C : REAL;
D : Custom_Obj;
END_STRUCT
END_TYPE
And I need to keep an array of this type for persistent memory. I can't just use VAR RETAIN because this particular piece of memory needs to persist through a download. The controller I am using has a way to do this but in order for it to work I need to be able to set the array equal to an initial value. So if I have declared
myarray := ARRAY[0..20] OF myDataStruct;
How do I then initialize this array to a blank array? What is the equivalent of new in other languages?
I have guessed
myarray := [21(A := 0,
B := '',
C := 0.0,
D := ??? )];
But that doesn't appear to be right. It could be simplified if there were only one level deep of custom structs and for this application I could do that. However, I still don't think I have the syntax right.
What is the equivalent of new in other languages?
The analog of this is
VAR
EmptyArray : ARRAY[0..20] OF myDataStruct;
END_VAR
If you want to pre-populate it with default values
VAR
EmptyArray : ARRAY[0..20] OF myDataStruct := [
(A := 100, B := 200, С := 0.0, D := ???),
(A := 34, B := 45, С := 0.1, D := ???),
..... etc
];
END_VAR
For CoDeSys 2.3 delete [ and ].
What you have to understand that EmptyArray is not a prototype of data you need but already initialized variable.
There is no way to initialize it in "x = new struct()" way. You also can't assign the whole array in the code with something like myarray = [1, 2, 3] etc, as far as I know.
If you just want to set it empty with values like 0, '', etc, then there are two ways that I would use:
1. Use MEMSET function to set all bytes to 0
Link to the online help
//Something like
MemSet(
pbyBuffer := ADR(myarray), //Address of the variable
byValue := 0, //Byte that address is filled with
dwSize := SIZEOF(myarray) //How many bytes? (variable size)
)
2. Create a dummy variable and assign it to the myarray
The variable is always initialized to zeros, so EmptyArray values are all 0/empty etc.
VAR
EmptyArray : ARRAY[0..20] OF myDataStruct;
END_VAR
//In the code
myarray := EmptyArray;
I hope I understood your question correctly.

Split IEC 61131-3 DINT into two INT variables (PLC structured text)

I want to publish a DINT variable (dintTest) over MODBUS on a PLC to read it with Matlab Instrument Control Toolbox. Turns out, Matlab can read Modbus variables but only INT16. So i want to split the DINT variable into two INT variables in IEC. I found this solution, but this only allows values from +- 0 ... 32767^2:
dintTest := -2;
b := dintTest MOD 32767;
a := dintTest / 32767;
result := 32767 * a + b;
c := DINT_TO_INT(b); // publish over modbus
d := DINT_TO_INT(a); // publish over modbus
What would be the solution for the whole range of DINT?
Thanks!
edit:
I read with a matlab function block in simulink (requires Instrument Control Toolbox):
function Check = MBWriteHoldingRegs(Values,RegAddr)
coder.extrinsic('modbus');
m = modbus('tcpip', '192.169.237.17');
coder.extrinsic('write');
write(m,'holdingregs',RegAddr,double(Values),'int16');
Check = Values;
I would better split DINT to 2 WORD
VAR
diInt: DINT := -2;
dwTemp: DWORD;
w1: WORD;
w2: WORD;
END_VAR
dwTemp := DINT_TO_DWORD(diInt);
w1 := DWORD_TO_WORD(dwTemp);
w2 := DWORD_TO_WORD(SHR(dwTemp, 16));
And then I could build it back in matlab.
The point here is not using mathematic but bit masks.