Implementation of functions, methods and actions

Topics:

  1. No "call by value" of large parameters in functions/methods [++]
  2. Do not declare large variables in functions/methods [++]
  3. Do not use actions [+]
  4. Use all parameters of a function/method internally [+]
  5. Assign return value of a function/method only in one place [+]
  6. Restrict access to methods as much as possible [+]
  7. 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: