The CODESYS-derived IEC 61131-3 logic engine inside an ABB COM600 gateway gives you operators such as ADD, AND and EQ, but no data sheet publishes how many microseconds each one costs. That is not an omission. Per-instruction time is a property of the generated code, the CPU, the memory path to the operands, and the other tasks competing for the processor — it is measured on the target, not looked up in a table. This note gives a repeatable differential-loop procedure that yields a defensible number on your own unit, plus the guardrails that keep the benchmark from tripping a watchdog or disturbing an in-service gateway.
Why No Instruction-Time Table Exists
Two identical ADD statements in the same project can differ by an order of magnitude in cost depending on operand type and where the operands live. The variables below all move the result, so any single published figure would be wrong for most projects.
| Factor | Effect on measured time |
|---|---|
| Operand data type |
BOOL/INT/DINT integer operations map to few machine instructions. REAL/LREAL arithmetic is far more expensive if the CPU has no hardware floating-point unit and the runtime emulates it in software. |
| Operand location | Local stack variables are cheapest. Globals, retains, and variables that are mapped into a process image or resolved through the communication/data-model layer add address resolution and memory traffic. |
| Code generation | Constant folding, common-subexpression elimination and dead-code removal can delete the very statement you are trying to time if its result is never consumed. |
| Preemption | Communication stacks, protocol handling, I/O servicing and visualization run as tasks and interrupt handlers you cannot see in the task list. They inflate max and average readings. |
| Tool and firmware build | Any change to the logic tool build or device firmware can change code generation. Record both with every result set. |
Configure a Dedicated Benchmark Task
Never benchmark inside a production POU. Isolate the code under test so the reported cycle value contains only the benchmark body plus fixed task overhead, which the differential method then cancels.
- Open
Resources → Task configurationand add a new task used only for benchmarking. - Set the task type to freewheeling. A freewheeling task restarts as soon as the previous pass ends, so the monitored cycle value tracks real execution. A cyclic task with an interval longer than the execution time can mask the value you want.
- Assign one program call: the benchmark POU. No other POUs in this task.
- Give the task the lowest priority (highest priority number) in the configuration so the benchmark can never starve communication, protocol or visualization work.
- Disable the task watchdog, or raise it well above the expected worst-case loop duration. A 10 000-iteration loop will exceed a typical watchdog setting.
- Download, start, and switch the task configuration view to online monitoring to read the cycle values.
Differential Loop Method
A single measurement is useless because it contains task-switch overhead, monitoring overhead and the FOR-loop control cost. Take two measurements that differ in exactly one variable and subtract. Two independent forms:
Form A - scale the iteration count, fixed body:
t_op = ( T(N2) - T(N1) ) / (N2 - N1)
Form B - scale the body, fixed iteration count (preferred):
t_op = ( T(k+1 ops, N) - T(k ops, N) ) / N
Form B cancels loop-control overhead exactly, because both runs execute the same number of loop iterations. Form A cancels task and call overhead. Run both; they should agree within your measurement noise.
Choose N so the measured difference is at least 100 x the resolution of the cycle-time display. If the monitor shows milliseconds, N = 1000 is usually too small — go to N = 10000 or higher and raise the watchdog accordingly.
PROGRAM PRG_Bench
VAR
i : DINT;
N : DINT := 1000; (* iterations - change online between runs *)
ops : INT := 1; (* 0 = control loop, 1 or 2 = ops in body *)
a, b : DINT;
acc : DINT;
END_VAR
VAR_GLOBAL (* declared in a GVL, read by another POU to defeat dead-code removal *)
gAcc : DINT;
END_VAR
a := a + 1; (* operands vary, so no constant folding *)
b := 3;
acc := 0;
CASE ops OF
0: FOR i := 1 TO N DO
acc := acc; (* control loop: loop overhead only *)
END_FOR;
1: FOR i := 1 TO N DO
acc := acc + (a + b); (* 1 x ADD under test *)
END_FOR;
2: FOR i := 1 TO N DO
acc := acc + (a + b) + (a + b);(* 2 x ADD under test *)
END_FOR;
END_CASE;
gAcc := acc; (* sink: result must be consumed somewhere *)
Variants for the other operators — keep the same loop skeleton and only swap the body:
(* AND on BOOL *) xAcc := xAcc AND (xA AND xB);
(* AND on DINT bits *) acc := acc + (a AND b);
(* EQ comparison *) xEq := xEq XOR (a = b);
(* REAL add - expect a very different result *)
rAcc := rAcc + (rA + rB);
The XOR and accumulate patterns exist purely to force the compiler to keep the operation and to keep the result live all the way to gAcc.
Read, Verify and Interpret the Numbers
Log the monitored values for each run. Use the minimum cycle value as the cleanest estimate of uncontended execution, the maximum as the preempted worst case, and the average for cycle budgeting.
| Run | N | Ops/iteration | Min cycle (example only) | Derived |
|---|---|---|---|---|
| Control loop | 1000 | 0 | 950 us | loop overhead ~0.95 us/iter |
| 1 x ADD | 1000 | 1 | 1300 us | — |
| 2 x ADD | 1000 | 2 | 1650 us | t_add = (1650-1300)/1000 = 0.35 us |
| 1 x ADD | 10000 | 1 | 13000 us | Form A cross-check, slope linear |
The figures above are placeholders that demonstrate the arithmetic only. Substitute your own readings; do not carry these numbers into a design.
Verification checklist before you trust a result:
- Linearity: measure at N = 100, 1000, 10 000. Plotted time versus N must be a straight line. A knee indicates watchdog interference, memory effects or a task-scheduling artefact — investigate before using the slope.
- Body scaling: going from 1 op to 2 ops must add the same increment as going from 2 to 3. If it does not, the compiler is optimizing part of the body away.
- Field identity: double N and confirm the monitored value doubles, proving you are reading execution duration and not a configured interval.
- Independent clock cross-check: on a freewheeling task, note the cycle counter and wall-clock time at two points. Average period = elapsed time / cycle delta. It should match the monitored average.
- Loaded versus unloaded: repeat the whole set with representative communication traffic and visualization clients connected. The spread between min and max under load is the jitter your design must tolerate.
Convert the Result Into a Cycle-Time Budget
The per-operation number is only useful as a budget input. For a task that must complete within an interval T:
N_max = ( T * U - T_fixed ) / t_op
T = task interval or required response time
U = fraction of that interval you allow the logic task to consume
T_fixed = measured overhead of the task with no logic in it
t_op = per-operation time from the differential measurement
Keep U conservatively below 1 so communication, protocol and visualization tasks retain headroom; the exact figure is an engineering decision, and the loaded max/min spread from the verification step tells you how much margin the platform actually needs. Size against the maximum observed value, not the average, whenever the logic feeds a time-critical output.
Re-measure whenever any of the following changes: device firmware, logic tool build, data types used in the hot path, task priorities or intervals, the number of connected visualization clients, or the protocol/communication load. A per-instruction time captured on one build is not portable to another.
FAQ
Does ABB or CODESYS publish execution times for ADD, AND and EQ?
No per-instruction timing table is published for the logic engine in COM600, because the cost depends on data type, operand storage, generated code and competing tasks. Measure it on your own unit with a differential loop and record the firmware and tool build alongside the result.
Why use a freewheeling task instead of a cyclic task for benchmarking?
A freewheeling task restarts immediately after each pass, so the monitored cycle value follows the actual execution duration. With a cyclic task whose interval exceeds the execution time, you can end up reading the configured interval instead of the work you are trying to time.
How many loop iterations should I use?
Pick N so the difference between two runs is at least 100 times the resolution of the cycle-time display — typically 1000 for microsecond resolution and 10 000 or more for millisecond resolution. Always take at least two values of N and confirm the response is linear.
Will a 10 000-iteration benchmark loop trip the task watchdog?
Yes, that is a common failure. Disable or substantially raise the watchdog on the benchmark task only, keep the task at the lowest priority, and restore the original settings before the device returns to service.
Why does my measured time change between runs on the same device?
Communication stacks, protocol handling, I/O servicing and visualization run as higher-priority tasks and interrupt handlers that preempt your benchmark. Use the minimum value for uncontended instruction cost and the maximum under representative load for worst-case cycle budgeting.