Yes: CODESYS V3 creates and removes function block instances at runtime with the __NEW and __DELETE operators, and Version 2.3 can allocate through SysMemAlloc where the target supports SysLibMem. Neither route has a garbage collector, so allocations minus frees must stay bounded. The guard code, the leak arithmetic and the 500-cycle test that shows the balance are below.
CODESYS can create function block instances while the controller runs, and the route depends on the generation of the programming system. Version 3 gives you the operators __NEW and __DELETE. Version 2.3 has no operator: you allocate raw bytes with SysMemAlloc from the SysLibMem library, where the target runtime supports it, and call the instance through a pointer. The IEC 61131-3 standard defines instances as declared, static data, so both routes are additions from the compiler vendor, and neither has a garbage collector. Every allocation needs a matching release that you write. That is the whole design problem, and the deciding number is allocations minus frees.
A declared instance and a heap instance
A declared instance, fbTest : FBTest; in a VAR block, occupies memory the compiler reserved at build time. The download manages its lifetime, an online change carries its data along with the code, and you never hold a pointer to it. A heap instance is the opposite: your code decides when it appears, how many exist and when it goes away, and it outlives any single scan. Take the heap route only when the count genuinely varies at runtime: a recipe loader building a variable-length list, a tester tracking a variable number of running jobs. When you can name a ceiling for the count, the static array further down is the better tool.
Version 3: allocate with __NEW, release with __DELETE
Declare a pointer to the function block type, allocate once under a state guard, check the pointer against 0 before every call, and release with __DELETE. The vendor's operator reference for __NEW, in the runtime documentation that ships with your programming system, covers the target-specific behavior.
VAR
pInst : POINTER TO FBTest;
bCreate : BOOL;
bRelease : BOOL;
bAllocFault : BOOL;
uAllocCnt : UDINT;
uFreeCnt : UDINT;
END_VAR
IF bCreate AND pInst = 0 THEN
pInst := __NEW(FBTest);
IF pInst <> 0 THEN
uAllocCnt := uAllocCnt + 1;
ELSE
bAllocFault := TRUE; (* alarm and leave the pointer at 0 *)
END_IF
END_IF
IF pInst <> 0 THEN
pInst^(a := l, b := m, c := n, x => l, y => m, z => n);
END_IF
IF bRelease AND pInst <> 0 THEN
__DELETE(pInst);
pInst := 0;
uFreeCnt := uFreeCnt + 1;
END_IF
Four rules keep this safe:
- Allocate under a condition that is true once. An unguarded __NEW in a cyclic task allocates on every scan.
- A failed allocation leaves the pointer at 0, and the guard turns that into a skipped call instead of a dereference. Alarm on it.
- One owner per pointer. Every copy you hand to another block is a future duplicate delete or a call into freed memory.
- Release the allocated memory before you use __NEW on the same pointer again, never delete memory you did not allocate, and zero the pointer in the same scan as the release.
Version 2.3: SysMemAlloc, SIZEOF and INI
Version 2.3 needs a runtime that supports SysLibMem. You allocate SIZEOF(pinst^) bytes, initialize the instance once with the INI operator, and call it through the pointer. The function description for SysMemAlloc in the SysLibMem documentation shows the shape of the call: size in, pointer out, and the release is your job. SysMemFree returns the block.
VAR
pinst : POINTER TO FBTest;
bInit : BOOL := TRUE;
bDummy : BOOL;
END_VAR
IF bInit THEN
bInit := FALSE;
pinst := SysMemAlloc(SIZEOF(pinst^)); (* bytes for one instance *)
bDummy := INI(pinst^, TRUE); (* run the instance initialization *)
END_IF
IF pinst <> 0 THEN
pinst^(a := l, b := m, c := n);
END_IF
Keep this route on the test target. Nothing calls SysMemFree for you, and the two hazards below make it a poor fit for production logic.
Downloads and online changes are the hazards
On a download the runtime replaces your program without calling your free routine, so with the V2.3 method every live allocation from the old program is orphaned. Twenty live instances of a 240-byte block strand 4,800 bytes of heap on every download, and the recovery is a restart.
An online change that edits the interface of FBTest leaves existing heap objects with the old memory layout while the new code expects the new one. On the V2.3 pointer method that mismatch stops the runtime, and the __NEW route inherits the same class of problem after interface edits. The safe sequence is identical for both: release every instance, apply the change, then recreate.
The number that decides it: allocations minus frees
Increment one counter on every successful allocation and one on every release. At steady state their difference equals the number of live instances, and it must be a number you can name: 0 for a create-and-release design, a constant for a create-and-hold design. A difference that climbs over the hours has one cause, allocations outrunning releases. Log both counters like any process value, the same pattern as logging PLC variables to a database, and alarm the moment the difference leaves its expected value.
Then run the bench test: cycle create and release 500 times and read free heap before and after. It returns to its starting value. A design that fails this test on the bench will fail on the machine, on a longer schedule.
Example: one unguarded __NEW in a 10 ms task
Assume FBTest occupies 240 bytes and the task cycle time is 10 ms, which is 100 scans per second. One __NEW per scan with no state guard allocates 100 instances per second, or 24 kB per second. On a controller with a 1,000,000-byte heap:
1,000,000 / 24,000 = 41.7
The heap is exhausted about 42 seconds after the task starts, and every allocation after that returns 0. With the guard in place, the same task settles at uAllocCnt minus uFreeCnt equal to 1, the live instance, with free heap changing 0 bytes per scan. After the 500-cycle test both counters read 500, the difference is 0 and free heap sits at its starting value. Four numbers, and the design is either clean or it is not.
What a leak looks like when it fails
| What you see | Mechanism | Where to check |
|---|---|---|
| Allocation stops succeeding after hours or days | Allocations outrun frees until the heap is exhausted | uAllocCnt minus uFreeCnt, then free heap |
| Task faults on a release | The same pointer was freed twice | The pointer must read 0 after the release scan |
| Erratic values from one block | A pointer to freed memory is still called | Every copy of the pointer; keep one owner |
| Runtime fault after an online change | Heap objects keep the old layout, code expects the new | Reproduce with an interface edit on a stopped test target |
| Free heap shrinks after every download | The old program's allocations were orphaned | Free heap after repeated downloads of the same project |
The static array that avoids all of it
When the count is bounded, declare a pool: fbPool : ARRAY[1..50] OF FBTest; with an in-use flag per element. The compiler fixes the memory at build time, downloads replace it cleanly, online changes treat it like any other static data, and there is no pointer to copy, free twice or call after release. Fifty elements occupy 12,000 bytes, paid once at build time. For most machines this is the right answer, and the dynamic route stays a bench technique.
What the floor screen sees
A leaking design reaches the operator screen as stale values or a frozen update long before anyone opens the memory diagnostics; diagnosing a factory screen update delay starts with the controller, not the display. Keep allocation out of fast motion tasks as well: the time an allocator takes varies with heap state, so allocate in the init phase and let the cyclic task do nothing but call instances. Per-scan work adds up in other ways too, which is why an image cycle stalls while logging data in a loop.
When these values also belong on a plant-level status board, the screen is a separate decision; start from planning a read-only process display. The shop-floor displays we build run either offline content or your own Android app, so the controller keeps the logic and the screen keeps the presentation.
The decision
Recommendation: Use __NEW and __DELETE, with the zero-pointer guard, one owner per pointer and both counters alarmed, when the instance count truly varies at runtime. Use a statically declared array with in-use flags whenever you can name a ceiling for the count, which covers most machines. On Version 2.3, keep SysMemAlloc on the test target and ship the static array, because downloads and online changes strand or crash the heap route.
- Count varies at runtime, Version 3: __NEW with the guard, the counters and the 500-cycle bench test on the actual target.
- Count has a known ceiling, any version: static array pool, in-use flags, no pointers.
- Version 2.3 in production: static array only; reserve SysMemAlloc for the bench.
Questions
Can __NEW allocate arrays and user-defined types as well as function blocks?
Yes. The operator reserves dynamic memory for function blocks, user-defined data types and arrays of standard types. For an array you give the element count and receive a pointer to the first element. The accounting rule is the same for every form: one __DELETE for each successful allocation, made through the same pointer you received.
What is the retry strategy after a failed allocation?
Back off and alarm. A failed allocation means the heap is exhausted or the size was refused, and retrying in the same scan adds load while still failing. Raise a fault, let the operator acknowledge it, and retry from the init path after a release has freed memory. Repeated failures on a healthy design mean allocations are outrunning releases, so treat them as a leak.
Where do I read free heap on the controller?
In the memory diagnostics of the runtime, viewed online from the programming system; it reports free and used memory. Read it before and after the 500-cycle create and release test, and again after each of ten repeated downloads of the same project. The value after the test must match the value before it; growth after the downloads points at allocations orphaned by the download.
Is dynamic instantiation acceptable in safety-related program parts?
No. Keep safety-related logic on statically declared data whose layout is fixed at build time, and keep dynamic instances in the standard logic only. An allocation can fail mid-cycle and leave a zero pointer with no defined safe state, so route every allocation failure to a fault that stops motion and raises an alarm the operator sees.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.