In CoDeSys 2.3 the end of the cycle belongs to the runtime, so no project-side callback exists. Attach the data transfer as a driver in the runtime build, at the same layer as the analogue In/Out refresh and the CANopen stack. If your SC2x runtime ships compiled without that interface, wrap the platform call in one designated task, cap its wait, and accept the measured one-cycle lag.
In CoDeSys 2.3 there is no callback you can wire from a project to the end of the cycle. The boundary belongs to the runtime, which calls its own integrated services there. The CANopen stack and the analogue In/Out refresh behave automatically because they live inside the runtime image, not because the IDE offers a hook. Making your data transfer automatic takes the same address: attach it as a driver in the runtime build for your Beck IPC@CHIP SC2x, at the layer that already services the CANopen stack and the analogue In/Out refresh. If your runtime ships compiled with no driver interface exposed, no project-side code reaches that boundary. The honest stopgap is a wrapper POU in one designated task, with the platform call capped so a missing device cannot stretch the cycle, and the one-cycle lag measured. Both paths follow, with the arithmetic for a 10 ms task.
What the end of the cycle actually is
A CoDeSys 2.3 cycle runs in three runtime steps: refresh the inputs, execute the tasks, refresh the outputs. Your program exists only in the middle step. The analogue In/Out refresh is a runtime service that wraps your tasks, and the CANopen stack is a module inside the runtime that the runtime itself calls at those points. Neither is triggered from your program. The IDE exposes tasks, priorities and cycle times; it exposes no project object that fires at the boundary.
A POU placed last in your program is still inside the middle step. It runs before the output write, so anything it sends goes out ahead of the outputs it belongs with. With one task you can at least define a point after the last POU and hold the program to it. With two or more tasks there is no last POU: the order between tasks belongs to the runtime, and PLC execution order: tracing logic across task layers shows how to trace it.
Symptoms and what each points to
- The transfer stops after a project edit. The transfer belongs to the application, so every author who rebuilds the program has to remember the call. That dependency is the thing you are trying to remove.
- The cycle time grows when the device is slow or unplugged. A platform function is blocking inside the cycle path, and the cycle waits out a device timeout. This is the same failure mode as an image cycle that stalls while logging data in a loop: work that cannot be interrupted sits where the cycle needs to run.
- The device is written at an irregular rate with several tasks enabled. No task was designated as the cycle owner, so end of cycle is undefined and each task's timing leaks into the transfer rate.
- The runtime stops only while the device is disconnected. The watchdog time is shorter than the blocked call's timeout, so the first dead device ends the runtime instead of skipping one transfer.
The fix: attach the transfer as a runtime I/O driver
- Designate one task as the cycle owner and write the choice down. Every timing claim below rests on it.
- Get the runtime documentation and the OEM runtime sources or SDK for your CoDeSys 2.3 build from the runtime supplier. The interface you want is the I/O driver interface, the same layer that services the analogue In/Out refresh and the CANopen stack. The runtime vendor's pages for device makers describe this layer directly: I/O systems and fieldbuses are handled by runtime drivers that the runtime calls.
- Read the exact hook names and call order from that documentation. They differ between runtime builds; nothing in the IDE substitutes for them.
- Write the driver so the runtime calls it at the boundary: copy device data into the process image before the tasks run, and push outputs to the device after the output refresh.
- Keep every platform call inside the driver bounded. Cap the per-cycle wait; if the device needs a slower exchange, hand the data to a Beck-side task through a double buffer and swap the buffers at the cycle boundary.
- Map the device data to fixed %I and %Q addresses so the application reads and writes ordinary I/O. Application authors then need no platform call at all.
- Rebuild the runtime, load it on the SC2x, and download the project unchanged.
One caution belongs here: if the transferred data drives anything with a physical effect, confirm what your outputs do when the watchdog stops the runtime before you add a new wait inside the cycle.
Example: bounding the call for a 10 ms task
The inputs, and where each comes from:
- CoDeSys 2.3 on a Beck IPC@CHIP SC2x, one task. Cycle time 10 ms, read from the task configuration.
- Watchdog set to 3 times the cycle time, so 30 ms.
- Worst task execution with the transfer removed: 6 ms, read from the online task view over several minutes with the device at its slowest.
- Wait budget granted to the platform call: 5 ms per cycle, a design choice that keeps the exchange inside the cycle path.
The rule: the worst cycle, task execution plus call, must sit below the watchdog time, and the call must never wait out a device timeout on the cycle path.
The arithmetic:
- Worst cycle with the capped call: 6 ms + 5 ms = 11 ms.
- Margin left to the watchdog: 30 ms - 11 ms = 19 ms.
- The same call uncapped: one dead device costs a 500 ms timeout, so the cycle reaches 506 ms, about 17 times the 30 ms watchdog. The runtime stops on the first disconnect.
- Added call time the margin tolerates: 30 ms - 6 ms = 24 ms. Above 24 ms of call time at these inputs, the watchdog has nothing left.
The result: the capped transfer fits, 11 ms against a 30 ms watchdog with 19 ms of margin. The decision: cap any call whose wait can exceed 19 ms at these inputs and finish its exchange across cycles through a double buffer. The device then receives data one execution ahead of the physical outputs, up to 10 ms at this cycle time, and the counter test below measures that offset in your build.
The stopgap: a wrapper POU in one designated task
If the runtime ships compiled and the supplier offers no SDK or custom build, the driver path is closed and the wrapper is the only project-side option. Wrap the platform call in one function block in a library, call it once from the designated task, and place that call last in the task. Three rules keep it working:
- Cap the wait so a missing device cannot stretch the cycle past the watchdog.
- Write the device data through a single copy, so nothing reads a half-updated set while the call is in progress.
- Measure the lag before you trust it: the call runs before the output write, so device data sits one execution ahead of the physical outputs, up to one full cycle. At the 10 ms example cycle that is up to 10 ms.
Treat the wrapper as a bridge. Ask the runtime supplier, Beck IPC for the SC2x builds, whether an SDK, a driver interface or a custom runtime build exists for your controller, and move the transfer into the driver when that build is ready.
How to verify the timing
Read the cycle time in the online task view with the device connected, then with it unplugged or slowed. The value must stay bounded; growth means a call is waiting inside the cycle. If the runtime stops instead, the watchdog tripped, and check if a CODESYS runtime is running or stopped walks through confirming which state you are in before you touch anything.
Write a counter from the application into the transferred data. The device must see each value exactly once per cycle, with no skips and no repeats. Skips mean the call was capped mid-exchange; repeats mean a retry ran inside the same cycle.
Compare device data against the analogue inputs in the same cycle. They belong to the same refresh, so a mismatch tells you the transfer is running at a boundary other than the one you think.
Check the watchdog margin at the worst cycle you measured, with the device at its slowest rate, and only then at the typical rate.
The decision
Recommendation: attach the transfer as a runtime I/O driver whenever you can get the OEM runtime sources for your SC2x build; that is the only arrangement in which the transfer is as automatic as the CANopen stack. While that build is pending, run the wrapper POU once from one designated task with the call capped at a wait you have measured, place it last, and retire the wrapper the day the driver build loads.
Questions
Why does the CANopen stack behave automatically when my transfer does not?
The CANopen stack is compiled into the runtime image, and the runtime calls it at the cycle boundary. Your program never sees it and cannot disturb it. A project-side call sits inside the task execution step instead, so it inherits everything you are trying to remove: it depends on each author remembering the call, it runs before the output write, and a slow device stretches the cycle. The runtime's I/O driver interface is the layer that puts your transfer on the same footing.
Do newer runtime generations add an end-of-cycle hook I could use?
The runtime vendor's device-maker documentation for current CODESYS Control generations describes triggering external events and registering to application events. A CoDeSys 2.3 build offers only what its own runtime image exposes, so those features reach your project only if your build documents them. Check the documentation that came with your SC2x runtime, and ask the supplier what the current build for your controller provides.
What stops a blocked platform call?
The task watchdog. In CoDeSys 2.3 the watchdog time is set in the task configuration as a multiple of the cycle time, and the runtime stops when a task overruns it. Set the watchdog from the worst cycle you have measured with the device at its slowest, and cap every call inside the cycle so a device timeout can never become the cycle time.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.