1

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.

relatively_random
  • 4,505
  • 1
  • 26
  • 48
  • I agree with Steves answer on how to theoretically achieve what you asked. The point I would like to rise though is that there is a reason the compiler won't allow you to do dynamic declaration of arrays in a function block. The reason is linked to the fact that machines need to work around the clock every day of the year and we need to minimize downtime at all costs. Machine programs should follow the KISS principle and that's why even though you can do a construct like Steve suggested, you should not. Conversly you should statically declare your arrays and have a clean and easy architecture. – Filippo Boido Oct 21 '21 at 11:32
  • 1
    The program parts that are not machine related should be transferred outside the plc runtime and porgrammed in a higher level language (example: Python or Java or C#). So you drastically reduce the chance of you machine code to produce exceptions. A backup or emergency production plan should be then created in order to make sure that production can still function when the overstructure doesn't ( connection to database for example ) This way your machine always continue to produce even though part of the system does not work. You see where I want to point to? – Filippo Boido Oct 21 '21 at 12:35
  • 1
    @FilippoBoido I don't want dynamic declaration of arrays, that's the whole point. I understand why PLCs and other real-time systems never use dynamic memory management. What I want is an ability to define multiple variables of a certain type, but with different *static* capacity. You can already do that with strings: declare one variable of type `STRING(10)` and another of `STRING(50)`. The alternative would be to either make all buffer variables ridiculously huge, even if maybe just one of them requires more than 10 capacity, or have copy-pasted types which are a maintenance nightmare. – relatively_random Oct 22 '21 at 13:09
  • @FilippoBoido I agree with your points about managing risk: keep everything that must run no matter what as reliable as possible and separate from all the "bonus" features. It's just that sometimes you also need to make real-time things more complex to make them feasible or reusable. And reusability doesn't just save time, it even makes things more reliable in the long term. – relatively_random Oct 22 '21 at 13:13
  • By sticking to the KISS principle you make your system fututre proof and reusable. I posted an answer with additional suggestions. – Filippo Boido Oct 22 '21 at 15:05

3 Answers3

3

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
Steve
  • 963
  • 5
  • 16
  • 1
    Great answer. If you add `{attribute 'enable_dynamic_creation'}` to the top of your functionblock it will be possible to create your functionblock dynamically. Meaning that you can use `__NEW` in runtime and decide the size of the buffer based on, for example, user inputs. Here you again have to remember to `__DELETE` everything that you have dynamically allocated to prevent a possible disastrous memory leak. [Beckhoff reference.](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/5383475339.html&id=). – Mikkel Oct 21 '21 at 09:28
  • Yeah, I meant static as "known at compile-time, not requiring run-time information", not as in static variables. Sorry for the confusion. – relatively_random Oct 22 '21 at 12:52
  • @Mikkel I wonder how __NEW and __DELETE work during runtime? I'd assume it requires dynamic memory management, but that's not real-time and thus a big no-no in the PLC world. – relatively_random Oct 22 '21 at 13:00
  • Yes, unfortunately Beckhoff Infosys is really poor on this subject (as it is on most subjects). Yet they give examples of how it can be used in runtime and I expect from them that the system still delivers real-time performance. The documentation says that the call can fail. I suspect that this could be because there is not enough unfragmented memory on the router memory pool or other measures to make it real-time and deterministic. – Mikkel Oct 25 '21 at 07:27
  • Runtime application of __NEW and __DELETE works just fine, I've used it pretty cleanly to handle the factory method for producing and managing scalers. That being said it is generally recommended to not handle memory operations in the PLC during run given the desired reliability from most PLCs. – Steve Nov 07 '21 at 19:10
2

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.

Filippo Boido
  • 1,136
  • 7
  • 11
  • Taking buffers from outside your function block by using in/out variables is a solution, but i don't understand what pragmas have to do with it. You can answer my question by just calling two instances of FB_Conveyor with two differently sized buffers. The code generation link i can't read, unfortunately, because it's in German and Google translate refuses to translate Infosys pages (at least on mobile). – relatively_random Oct 23 '21 at 07:29
  • The downsides of this solution are lack of encapsulation and more complex use, both making the code more prone to human error, not less. The conveyor function block itself may be less complex, but every use of it throughout your projects requires developers to provide the buffer for each instance, make sure they don't accidentally write into it or use one buffer with two conveyor instances (e.g. by copy pasting and forgetting to rename the parameter). Steve's answer my be more complex *once*, but it's as simple *to use* as possible. That's what reusable library code should be like IMO. – relatively_random Oct 23 '21 at 07:35
  • Btw having separate long running git branches seems like a good idea and we've tried that a long time before. Unfortunately, that's not what git is designed for. Maintaining multiple branches is a nightmare because you can't just apply a bugfix to one of them, you then have to remember to check out every single branch and cherry pick the common fix into it. Unlike merging, cherry picking often doesn't work, especially when you are applying a fix to something that has differences in those branches. Which means you have to copy-paste, which is, again, laborious and error prone. – relatively_random Oct 23 '21 at 07:45
  • This answer is just one on top of Steves, I also upvoted his answer, nevertheless my answers needs to be seen in a broader architectural context and relates to the question how do you design software for machine systems as simple as possible so that a) errors are rare and can be mostly excluded at low levels b) Engineers with little knowledge of the system can debug it fast if necessary and restore production. – Filippo Boido Oct 23 '21 at 11:17
  • The pragmas are useful if you have machine variants that require different buffers as your question pointed to.The example above is just a basic example and shows you how thanks to a variant defined centrally (in the example variant_b) you can define buffers of different sizes for different machine types – Filippo Boido Oct 23 '21 at 11:20
  • This code and variant could also have been autogenerated by the Beckhoff interface through a scheme you decide in upper levers of the software pyramide. The link may be in German but there is a change language in the upper left corner and you can switch to english. – Filippo Boido Oct 23 '21 at 11:22
  • I used the __NEW operator many times in my projects and I also know how dangerous it can be. I'm not against using it but it should be used with caution and since the memory is allocated from the router memory pool it may run out unexpectedly in the worst moment.. – Filippo Boido Oct 23 '21 at 11:27
  • My whole point is that you can do complex programs in Structured Text but you should not. Complex logic and dynamic memory allocation should happen in Python or Java and abstracted from the machine operation. – Filippo Boido Oct 23 '21 at 11:28
  • Regarding the example with the fbConveyor, the standard programming conventions say that you call your Function Block once in a central place in the program, so your assumption that developers have to pass the data every time is wrong – Filippo Boido Oct 23 '21 at 11:30
  • What developers call are methods of the FunctionBlock many times in the code ore they "start" and "stop" the FunctionBlock many times with the syntax like this fbConveyor.start := True; or fbConveyor.stop := True; for example – Filippo Boido Oct 23 '21 at 11:32
  • Regarding your GIT response, again, it's always a question how you organize things. You can make your life miserable or have easy and mantainable solutions. Parts of the core logic of a machine that are common to all machines are normally programmed in a library and are a different GIT repository..so no mantaining hell at all – Filippo Boido Oct 23 '21 at 11:35
  • Thanks for the input, just to clear some things up. By passing the buffer every time, I meant that you have to declare a separate buffer variable every time you declare a conveyor variable and then you have to make sure you pass that buffer to the conveyor at the call site. I didn't mean calling the conveyor multiple times. – relatively_random Oct 24 '21 at 06:54
  • As for git, having branches with infinite lifetime is abusing the system. At best, it makes using it awkward. At worst, it requires constant manual interventions, which are risky. The branches are there only to facilitate concurrent work on the same piece of software, not for maintaining different variants of it. For that, you are supposed to use preprocessor definitions or whichever options your build system has. – relatively_random Oct 24 '21 at 06:57
  • Regarding the conveyor example, it's just that..an example. A theretical approach to a theoretical question you asked.In fact, in practice, you almost never need to implement what you suggested and static declared arrays in Function Blocks are the way to go for 99.9% of function blocks. – Filippo Boido Oct 24 '21 at 16:59
  • It makes perfect sense to have different branches in a repository for the same machine with different variants and I say that out of experience because we used exactly this system in my former company.It is neither abusing the system nor awkward in fact it's very practical, but to each his own ;) Besides the variants with the preprocessor definitions is exactly what I showed in the example. – Filippo Boido Oct 24 '21 at 17:08
0

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
relatively_random
  • 4,505
  • 1
  • 26
  • 48