How to correctly override a persistent input variable on instanciation and call in a derived FUNCTION_BLOCK? - codesys

I have a input variable c_timTransmissionRate which is usually initalized using the persistent variable list. The default value for the persistent variable list in the function block TxRaw is then overriden by the instanciating function block ´TxDbc´. This again is application specific overriden in the persistent variable list itself.
In a few cases the value for c_timTransmissionRate is provided dynamically by the instanciating function block TxTsc1 during runtime.
My problem is that I currently always see the original TIME#0ms value in the debugger instead of the values given in the persistent variable list.
Extract of my current code:
FUNCTION_BLOCK TxRaw
VAR_INPUT PERSISTENT CONSTANT
c_timTransmissionRate : TIME := T#0ms;
END_VAR
(* instructions *)
END_FUNCTION_BLOCK
FUNCTION_BLOCK Tx
VAR
tx : TxRaw;
END_VAR
(* instructions *)
tx();
(* instructions *)
END_FUNCTION_BLOCK
FUNCTION_BLOCK TxTsc1 EXTENDS Tx
(* instructions *)
tx.c_timTransmissionRate := PI()*THUMB();
SUPER^();
END_FUNCTION_BLOCK
FUNCTION BLOCK TxDbc EXTENDS Tx
VAR
tx : TX := (c_timTransmissionRate := T#20ms);
END_VAR
(* instructions *)
SUPER^();
END_FUNCTION_BLOCK

Related

How to assign variable length to an array in twincat3

I need to change the length of the array dynamically.Right now the code looks like this:
VAR
arrData : ARRAY[1..200] OF INT;
length : INT := 200;
END_VAR
The size of the array depends on the length variable.Here the value of length variable can be changed during runtime using the VISU(gui). So if I change the value of the length = 180 then 20 bytes of arrData are unused.Is there a way to declare the array as variable length similar to vectors in c++ such that the memory is not assigned during declaration but during runtime.
Edit:
How to deallocate the memory safely?
PROGRAM MAIN
VAR
arrData : POINTER TO INT;
length : INT := 200; // can be changed at runtime
bNew : BOOL := TRUE;
oldLength : INT; // to hold the old length variable
isInit : BOOL := FALSE;
END_VAR
IF NOT isInit THEN
oldLength := length; // initialise only once
isInit := TRUE;
END_IF
// if length is changed during runtime then delete the array
IF oldLength <> length THEN
IF arrData <> 0 THEN
__DELETE(arrData);
bNew := TRUE;
END_IF
oldLength := length;
END_IF
// during the first run or when the length is changed
IF bNew THEN
arrData := __NEW(INT,length);
bNew := FALSE;
END_IF
// deallocate the memory when the MAIN program goes out of scope
// as we are not deleting the array when the length variable is not
// changed during runtime
END_CASE
The way to do it is to use __NEW __NEW in Infosys
pMyPointer := __NEW(INT, length);
__NEW will return a pointer to first element of an array. You can access latter elements by offsetting this pointer.
You can check if length was changed by comparing value from this and previous cycle. If so, __DELETE the old array and initialize a new one.
Edit:
I think, that you get your error the moment TwinCAT runtime is stopped, as the momory allocated by __NEW is not freed at that point.
Your code should be placed not in a Program (PRG) but in a Function Block (FB). The reason for that is that you need to implement FB_exit method (This method is called implicitly when FB instance is destroyed, i.e. when stopping TwinCAT runtime like you do by activating configuration). There is no equivalent method for a PRG as far as I know.
To do this:
Create a new FB, instantiate it and call it in your MAIN and move your code from MAIN to the FB
Add a FB_exit method to this FB. Exact naming is crucial
In your FB_exit method write the following code:
IF __ISVALIDREF(arrData) THEN
__DELETE(arrData);
END_IF
This method will be called every time you stop your runtime and free the memory.
Links to Infosys:
__ISVALIDREF - equal to pMyPointer <> 0 but more readable
FB_exit

User defined functions with params

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

Store variable value in each PLC cycle

Is it possible to store a variable value in each PLC cycle? I need the first 10 values each time to perform some calculations. I am using the OpenPCS platform and ST for programming.
You can create an array of values and then store as array values
VAR
aBuffer : ARRAY[1..32] OF WORD;
init:BOOL; (* Init array *)
rest:BOOL; (* Reset *)
val:WORD; (* Value *)
iCount:INT; (* Array index *)
END_VAR
VAR_TEMP
iTmp : INT;
END_VAR
iTmp := UINT_TO_INT(N) - 1;
IF NOT init OR rest THEN
init := TRUE;
FOR iCount := 1 TO iTmp DO
aBuffer[iCount] := val;
END_FOR;
END_IF
iCount := INC1(iCount, 32);
aBuffer[iCount] := val;
This is a code example that created 32 elements array and every new PLC cycle assign new element and rotates.
After that, you can calculate the average or min and max.
INC1 increments given value by one until it reaches 32 and then reset to 1.

TwinCAT CoE: Write to SDO

I'm pretty new to the EtherCAT/CANopen universe and trying to implement a custom slave.
The slave is passing conformance test so far and want to write to one of my Slave Data Objects, the slave is attached to a CX5120, which is found by the XAE and also shows the Slave device.
For that, I copied my ESI-file to the TwinCAT folder (C:\TwinCAT\3.1\Config\Io\EtherCAT).
I've created a small Structured Text PLC program that uses FB_EcCoESdoWrite to write data to address 0x607A. But when I set it active and try to connect, Visual Studio tells me that the device needs at least one Sync Master. Also, when setting bExecute to TRUE, I'm getting an error from the function. As far as I understand, I have to link variables between my ST program and the slave, but I don't see the need of linking variables because afaik the function call should manage the transmission? What are the steps to write to a SDO of an ESC? Can someone tell me what I'm missing or has a small example at hand?
PROGRAM MAIN
VAR
heartbeat : UINT;
fbSdoWrite : FB_EcCoESdoWrite;
sNetId : T_AmsNetId := '5.76.204.148.1.1'; (* NetId of EtherCAT Master *)
nSlaveAddr : UINT := 1001; (* Port Number of EtherCAT Slave *)
nIndex : WORD := 16#607A; (* CoE Object Index *)
nSubIndex : BYTE := 0; (* Subindex of CoE Object *)
nValue : UINT := 16#AAAA; (* variable to be written to the CoE Object *)
bExecute : BOOL; (* rising edge starts writing to the CoE Object *)
bError : BOOL;
nErrId : UDINT;
END_VAR
fbSdoWrite(
sNetId := sNetId,
nSlaveAddr := nSlaveAddr,
nIndex := nIndex,
nSubIndex := nSubIndex,
pSrcBuf := ADR(nValue),
cbBufLen := SIZEOF(nValue),
bExecute := bExecute
);
IF NOT fbSdoWrite.bBusy THEN
bExecute := FALSE;
IF NOT bError THEN
(* write successful *)
bError := FALSE;
nErrId := 0;
ELSE
(* write failed *)
bError := fbSdoWrite.bError;
nErrId := fbSdoWrite.nErrId;
END_IF
fbSdoWrite(bExecute := FALSE);
END_IF
Fixed problem by linking variable from PLC code to DevState-input of the device.
Linking to plain InfoData doesn't seem to work though.
You should assign a task to your devices who is responsible to read/write data. double click your master device, go to EtherCAT tab and click on Sync Unit Assignment
there select your terminals then available tasks and apply!

TwinCAT 3 Task Start/Stop from PLC

I need to run some code every time the PLC starts. This code should only be run once and then never again until the PLC is restarted. I initialize some global variables and validate the persistent data before allowing the main PLC to run. This is because the actions of the machine can be damaging if some of these variables are not setup correctly.
Is there a way to start/stop the other PLC tasks? I noticed TwinCAT doesn't support initialization and shutdown interrupts for PLC tasks.
TwinCAT has a 'PlcTaskSystemInfo' struct containing a boolean for FirstCycle. You can use that to run the initializing code only once.
VAR fbGetCurTaskIdx: GETCURTASKINDEX; (* Further example+explanation in Infosys *)
fbGetCurTaskIdx();
IF _TaskInfo[fbGetCurTaskIdx.index].FirstCycle THEN
(* Initialization code here *)
ELSE
(* Normal code here *)
END_IF;
I don't know of a way to start/stop individual PLC tasks. You can start/stop a runtime though.
But perhaps it can be as simple as this code below, which will only run when your PLC starts.
VAR initialized: BOOL := FALSE;
IF NOT initialized THEN
(* Run your initialization code here *)
initialized := TRUE;
END_IF
(* Rest of your program here *)
Edit:
I used a state machine inside the initialization code to help with the task allowed time issue.
Example:
VAR
Initialized : BOOL := FALSE;
Init_State : UINT := 0;
END_VAR
IF NOT Initialized THEN
(* Initialization State Machine *)
CASE Init_State OF
0: (* First step in initialization *)
Init_State := Init_State + 1;
1: (* Second step in initialization *)
Init_State := Init_State + 1;
.
.
.
n: (* Last step in initialization *)
Initialized := TRUE;
END_CASE
END_IF