Is it possible to add static parameters to function blocks?

700 views Asked by At

Is it possible to write function blocks with some static parameterization? Specifically, can I make a buffer with a static capacity, but such that different instances can have different capacities?

Ideally, I'd imagine some constant parameters, like so:

FUNCTION_BLOCK FB_Buffer
VAR_INPUT CONSTANT
    StorageSize : DINT;
END_VAR
VAR
    Storage : ARRAY [1..StorageSize] OF REAL;
END_VAR

Instancing would then be something like this:

FUNCTION_BLOCK FB_Usage
VAR
    SmallBuffer : FB_Buffer := (StorageSize := 10);
    LargeBuffer : FB_Buffer := (StorageSize := 1000);
END_VAR

Assuming this isn't possible, what is the best approach to manage different storage sizes for different function block instances?

I'll post my least-bad workaround as an aswer.

3

There are 3 answers

5
Steve On BEST ANSWER

I was thrown a little by your referencing of 'Static' variables, as VAR STAT is a separate thing to what you want, and used to make all instances of an FB share a common element.

What you really are looking for are the wonders of FB_INIT and __NEW


Example

You have to manage your own data access, making sure you don't overflow and all the other dangerous things, but otherwise this should work as per your posted answer. Then initializing this code with a couple of different lengths is as simple as:

FUNCTION_BLOCK FB_Usage
VAR
  SmallBuffer : fb_DataArray( 100 );
  LargeBuffer : fb_DataArray( 10000 );
END_VAR
// Function block for handling a data array that is generated at boot time
FUNCTION_BLOCK fb_DataArray
VAR
  pZeroElem  : POINTER TO REAL;  // Pointer to the head of the array
  ArrayLength : UDINT;  // Length of the array in elements 
END_VAR

// Do something by indexing through ring
METHOD FB_init : BOOL
// Initialisation method for fb_DataArray, allocates memory space to array
VAR
    bInitRetains    :   BOOL;   // required
    bInCopyCode     :   BOOL;   // required 
    Length          :   UDINT;  //  Number of element in the array
END_VAR

pZeroElem := __NEW( REAL, Length ); 
// Generate a pointer to the first element of a dataspace that is precisely big enough for <Length> Real elements. 
Method FB_exit
// Needs to be called to de-allocate the memory space allocated in fb_init. 
VAR
 bInCopyCode : BOOL; // Required
END_VAR

IF pZeroElem <> 0 THEN
  // Checks if the pointer is valid, then deletes the allocation
  __DELETE( pZeroElem ); 
END_IF
0
relatively_random On

The only thing that comes to mind is making an abstract base FB and letting different subclasses define concrete storage. There's still noticeable duplication and boiler plating, but at least it's better than copy-pasting the entire FB just to change one number.

Base declaration:

FUNCTION_BLOCK ABSTRACT FB_BufferBase
VAR
    Storage : POINTER TO REAL;
    StorageSize : DINT;
END_VAR

Abstract method declaration:

METHOD ABSTRACT GetStorage
VAR_OUTPUT
    ZeroElement : POINTER TO REAL;
    ElementCount : DINT;
END_VAR

Base body code:

GetStorage (ZeroElement => Storage, ElementCount => StorageSize);

// Do stuff.

Small concrete declaration:

FUNCTION_BLOCK FB_BufferSmall EXTENDS FB_BufferBase
VAR
    ConcreteStorage : ARRAY [0..ConcreteStorageSize-1] OF REAL;
END_VAR
VAR CONSTANT
    ConcreteStorageSize : DINT := 10;
END_VAR

Small concrete method implementation:

ZeroElement := ADR(ConcreteStorage[0]);
ElementCount := ConcreteStorageSize;

(FB_BufferLarge is the same as FB_BufferSmall, just that ConcreteStorageSize is 1000 instead of 10.)

Instantiation:

FUNCTION_BLOCK FB_Usage
VAR
    SmallBuffer : FB_BufferSmall;
    LargeBuffer : FB_BufferLarge;
END_VAR
15
Filippo Boido On

If you do not want to create arrays with dynamic memory and at the same time you want to reduce the amount of types in your projects you can use conditional pragmas.

example:

//Declaration part of MAIN
PROGRAM MAIN
VAR
    {define variant_b}
        
    {IF defined(variant_a)}
        conveyor_buffer : ARRAY[1..10] OF INT;
        sensor_buffer : ARRAY[1..5] OF BOOL;
    {ELSIF defined(variant_b}
        conveyor_buffer : ARRAY[1..100] OF INT;
        sensor_buffer : ARRAY[1..20] OF BOOL;
    {END_IF}
    
    fbConveyor : FB_Conveyor;
END_VAR

//Implementation part of MAIN
fbConveyor(buffer:=conveyor_buffer);

//Declaration part of FB_Conveyor
FUNCTION_BLOCK FB_Conveyor
VAR_IN_OUT
    buffer      : ARRAY [*] OF INT;
END_VAR
VAR_OUTPUT

END_VAR
VAR
    length      : DINT;
END_VAR

//Implementation part of FB_Conveyor
length := UPPER_BOUND(buffer,1);

You then pass the buffers to the objects that actually make use of them as a reference. In those Function Blocks you need to check the UPPER and LOWER Bound in order not to have problems.

If you don't like conditional pragmas but still want your project to be simple and clear you can have variants represented as GIT branches in a GIT repository. This way you always know which machine has which features and can keep a clean structure and architecture. Another strategy is to make use of the Beckhoff automation interface to automatically create code and build your projects following a structure you decide. Here is the link: https://infosys.beckhoff.com/index.php?content=../content/1031/tc3_automationinterface/242682763.html&id=

By autogenerating code with the automation interface you again reduce the possibility to inject human error in the machine operation and export complexity to the "higher levels" making your system even more reliable.

So there are many approaches you can use to achieve a reusable solution. Even though I understand that there are many complex machines out there, if your plc architecture is getting to complex it may be time to think about which modules and functions can be "outsourced" to higher levels in order stick to the KISS principle and secure production at lower levels.