Implementation of functions, methods and actions
Topics:
- No "call by value" of large parameters in functions/methods [++]
- Do not declare large variables in functions/methods [++]
- Do not use actions [+]
- Use all parameters of a function/method internally [+]
- Assign return value of a function/method only in one place [+]
- Restrict access to methods as much as possible [+]
- Grouping of parameters as structure [+]
No "call by value" of large parameters in functions/methods
Input and output parameters as well as the return value of a method are copied in most cases when the function/method is called, this is the "call by value". For parameters that occupy a lot of memory, this copy step takes more time. To write efficient program code, large parameters should be passed using "Call by reference". Thus, only one pointer is set. There are a few different possibilities for this, which are taken up in the sample. Please note that some options also allow write access to the input parameters.
See also "Transferring large strings".
Negative sample:
METHOD SampleMethod_neg : ST_DataCollection // return value with call by value
VAR_INPUT
aSensor1Data : ARRAY[1..100] OF LREAL; // input with call by value
aSensor2Data : ARRAY[1..200] OF LREAL; // input with call by value
aSensor3Data : ARRAY[1..300] OF LREAL; // input with call by value
aSensor4Data : ARRAY[1..400] OF LREAL; // input with call by value
END_VAR
VAR_OUTPUT
aOutputData : ARRAY[1..500] OF LREAL; // output with call by value
END_VAR
Positive sample:
METHOD SampleMethod_pos : HRESULT
VAR_INPUT
aSensor1Data : REFERENCE TO ARRAY[1..100] OF LREAL; // input with call by reference (write access possible)
pSensor2Data : POINTER TO ARRAY[1..200] OF LREAL; // input buffer, similar to call by reference (write access possible)
nSensor2DataSize : UDINT; // size in bytes of buffer to ensure the correct buffer size
aOutputData : REFERENCE TO ARRAY[1..500] OF LREAL; // output with call by reference
stDataCollection : REFERENCE TO ST_DataCollection; // output with call by reference
END_VAR
VAR_IN_OUT CONSTANT
aSensor3Data : ARRAY[1..300] OF LREAL; // input with call by reference
END_VAR
VAR_IN_OUT
aSensor4Data : ARRAY[1..400] OF LREAL; // input with call by reference (write access possible)
END_VAR
Do not declare large variables in functions/methods
For variable declarations within a function/method, you should avoid a large number as well as a large data size. Such variable declarations are taken from stack memory. If functions/methods are called nested, the required amount of memory is added to the stack. There is not an unlimited amount of stack memory available and "stack overflow" situations should be avoided in advance.
The maximum size of the stack memory is specified in the TwinCAT project below the node system in the node Real-Time. Often this is 64 KB. Therefore, it is recommended to keep the total declared amount of data in a function/method as small as possible, for example below 1 KB. Therefore, especially with STRING types and arrays of arbitrary types, make sure that the lengths are as short as possible.
Alternatively, variables can be declared within the function block instance. Note here that these variables are permanently available and are not initialized each time the method is called.
The same applies to parameters of a function/method. See "No "Call by value" of large parameters in functions/methods".
Negative sample:
FUNCTION_BLOCK FB_Sample_neg
VAR
END_VAR
METHOD SampleMethod_neg : LREAL
VAR
fSum : LREAL;
nCnt : UINT;
aLogList : ARRAY[1..50] OF STRING(1023); // locally declared in method leads to stack allocation
END_VAR
Positive sample:
FUNCTION_BLOCK FB_Sample_pos
VAR
aLogList : ARRAY[1..50] OF STRING(1023); // declared as member of the FB instance
END_VAR
METHOD SampleMethod_pos : LREAL
VAR
fSum : LREAL;
nCnt : UINT;
END_VAR
Positive sample (alternative):
FUNCTION_BLOCK FB_Sample2_pos
VAR
END_VAR
METHOD SampleMethod2_pos : LREAL
VAR
fSum : LREAL;
nCnt : UINT;
END_VAR
VAR_INST
aLogList : ARRAY[1..50] OF STRING(1023); // declared as member of the FB instance
END_VAR
Do not use actions
A program or a function block should not contain actions. Implement methods instead. These can be defined as PUBLIC, PRIVATE or PROTECTED depending on their use.
Use all parameters of a function/method internally
All parameters of a function or a method should also be used internally.
Negative sample:
FUNCTION F_Sample_neg : INT
VAR_INPUT
nA : INT; // Variable a in term y = a*a
nB : INT; // NON COMPLIANT: nB will not be used
END_VAR
F_Sample_neg := nA * nA;
Positive sample:
FUNCTION F_Sample_pos : INT // COMPLIANT: no unused parameter
VAR_INPUT
nA : INT; // Variable a in term y = a*a
END_VAR
F_Sample_pos := nA * nA;
Assign return value of a function/method only in one place
You should assign the return value of a function/method only in one place. An exception to this rule applies when the assignments are made in different branches of an IF or CASE statement.
Example 1:
Negative sample:
FUNCTION F_Sample_neg_1 : LREAL
VAR
fOnePlusHalfEpsilon : LREAL; // Auxiliary variable to calculate and validate the machine epsilon
END_VAR
// NON COMPLIANT: F_Sample_neg_1 is assigned or used more than once
F_Sample_neg_1 := 1.0;
REPEAT
F_Sample_neg_1 := 0.5 * F_Sample_neg_1; // NON COMPLIANT: see above
fOnePlusHalfEpsilon := 1.0 + 0.5 * F_Sample_neg_1; // NON COMPLIANT: see above
UNTIL fOnePlusHalfEpsilon <= 1.0
END_REPEAT;
Positive sample:
FUNCTION F_Sample_pos_1 : LREAL
VAR
fOnePlusHalfEpsilon : LREAL; // Auxiliary variable to calculate and validate the machine epsilon
fResult : LREAL := 1.0; // Temporary variable to
END_VAR
REPEAT
fResult := 0.5 * fResult;
fOnePlusHalfEpsilon := 1.0 + 0.5 * fResult;
UNTIL fOnePlusHalfEpsilon <= 1.0
END_REPEAT;
F_Sample_pos_1 := fResult; // COMPLIANT: F_Sample_pos_1 is only assigned here
Example 2:
General program elements for this example:
// Enumeration for sample 2
{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_SampleStatus :
(
Ok, // Used when the status is OK
Warning, // Used when a warning occurs
Error // Used when an error occurs
);
END_TYPE
// GVL_Sample - global variable list for sample 2
VAR_GLOBAL CONSTANT
cLimitMin : INT := 10; // Minimal limit to validate a sample value
cLimitMax : INT := 20; // Maximal limit to validate a sample value
cTolerance : INT := 2; // Upper and lower tolerance to validate a sample value
END_VAR
// Returns a status variable (ok, warning, error) for a measure value, using a constant range of values defined on a GVL
FUNCTION F_Sample : E_SampleStatus
VAR_INPUT
nValue : INT; // Sample value that is validated in respect of a range of values
END_VAR
Negative sample:
// NON COMPLIANT: F_Sample is assigned within different IF-statements. Multiple write access on return value cannot be excluded.
IF (nValue >= cLimitMin) AND (nValue <= cLimitMax) THEN
F_Sample := E_SampleStatus.Ok;
END_IF
IF (nValue >= (cLimitMin - cTolerance))
AND (nValue <= (cLimitMax + cTolerance)) THEN
F_Sample := E_SampleStatus.Warning;
END_IF
IF (nValue < (cLimitMin - cTolerance))
OR (nValue > (cLimitMax + cTolerance))
F_Sample := E_SampleStatus.Error;
END_IF
Positive sample:
// COMPLIANT: F_Sample is only assigned within one IF-statement. No multiple write access on return value occurs.
IF (nValue >= cLimitMin) AND (nValue <= cLimitMax) THEN
F_Sample := E_SampleStatus.Ok;
ELSIF (nValue >= (cLimitMin - cTolerance))
AND (nValue <= (cLimitMax + cTolerance)) THEN
F_Sample := E_SampleStatus.Warning;
ELSE
F_Sample := E_SampleStatus.Error;
END_IF
Restrict access to methods as much as possible
Access to methods should be restricted as much as possible using the PRIVATE or PROTECTED access modifiers. Thus, methods that are only intended to be called internally cannot be called externally. This helps to ensure that a function block is more likely to be used in the manner appropriate to its intended use. Encapsulation of methods therefore leads to safer code. In addition, subsequent changes to internal methods are easier if, as intended, they were not called by the user from the outside.
Grouping of parameters as a structure
If you have to specify many parameters when calling, it is a good idea to group them in one or more structures.
This has the advantage that default values are stored within the structure definition. Several constants of the structure data type can in turn provide different default values for different use cases.
The call can be implemented efficiently by passing the structure using "Call by reference". This avoids many individual "Call by value" transfers. See also the topic No "Call by value" of large parameters in functions/methods.
See also: