Overview
The RUNTIME instruction in Siemens SCL measures the wall-clock or scan-time interval between two execution points. Engineers often want to know how many milliseconds the CPU spends inside a specific code block, a sequence of operations, or an entire OB1 cycle, in order to:
- Validate worst-case cycle time against the configured maximum OB1 watch-dog.
- Diagnose performance regressions after a firmware update (for example, moving from V4.4 to V4.5.x).
- Benchmark blocks before and after refactoring.
- Generate a runtime metric that an HMI or SCADA tag can trend over a shift.
The classic mistake is to read the memory tag passed as the first argument of RUNTIME and treat it as the measurement. That tag is internal scratch storage for the instruction; the actual elapsed time is returned through the RET_VAL (return value) parameter. The same confusion explains almost every "my RUNTIME value never changes" support thread on every S7-1200 / S7-1500 project. This article walks through the correct SCL usage on a SIMATIC S7-1200 CPU 1212C AC/DC/Relay (6ES7 212-1BE40-0XB0) with TIA Portal V15.1 and firmware V4.5.1, exactly the configuration described in the original question.
Prerequisites
Before adding the code, verify the engineering environment and target hardware:
-
Engineering tool: TIA Portal V15.1 or later (V16 / V17 / V18 also expose the same
RUNTIMEFB, with the addition of the IEC timerRTMon S7-1500). Update 4 or higher is recommended when working with the S7-1200 V4.5 firmware family. -
Target CPU: SIMATIC S7-1200 CPU 1212C AC/DC/Relay (6ES7 212-1BE40-0XB0). The
0XB0suffix identifies the AC 85–264 V supply, 24 V DC digital inputs, relay output variant with two onboard 0–10 V analog inputs. The earlier1BE31-0XB0variant is functionally identical for this exercise but is firmware-locked to V4.x. -
CPU firmware: V4.5.1 (the version in the original problem report). Earlier V4.2 / V4.3 firmware behaves the same way, but V4.4+ tightens the SCL compiler diagnostics for missing
RET_VALhandling. -
Programming language: SCL (Structured Control Language). LAD/FBD examples of
RUNTIMEare widely published; the SCL syntax differs only in how the return value is captured. - Optional I/O: A SCALANCE X200 switch for PROFINET connectivity (mentioned in the source) is not strictly required for the logic to compile, but the HMI / watch table will typically be attached through it.
Confirm the wiring side of the 1212C before commissioning. The wiring diagrams in the official TIA Portal manual collection show the relay contact commoning, the 24 V sensor power output, and the M-to-chassis-ground recommendation that suppresses high-frequency noise on the analog inputs. See the CPU 1212C wiring diagrams page in the S7-1200 manual collection for the AC/DC/Relay pin-out.
Hardware Reference: CPU 1212C AC/DC/Relay (6ES7 212-1BE40-0XB0)
Understanding the hardware envelope is necessary to interpret any runtime measurement, because OB1 cycle time and execution slice for RUNTIME are bounded by the CPU's bit-instruction time and process-image size.
| Parameter | Value |
|---|---|
| MLFB | 6ES7 212-1BE40-0XB0 |
| Work memory (data) | 50 KB |
| Work memory (code) | 100 KB |
| Load memory | 4 MB internal, expandable via SIMATIC memory card |
| Bit instruction time | 0.08 µs typical |
| Onboard digital inputs | 8 DI 24 V DC, sourcing, IEC type 1 |
| Onboard digital outputs | 6 DO relay 2 A, volt-free changeover not available on-board |
| Onboard analog inputs | 2 AI 0–10 V DC, 10-bit resolution |
| Supply voltage | AC 85–264 V at 47–63 Hz |
| Process image size (default) | 1024 bytes DI / 1024 bytes DO |
| Firmware family | V4.x (V4.2 baseline, V4.4 adds optimized block access, V4.5.1 used here) |
| Maximum OB1 cycle (configurable) | 150 ms default, max 6000 ms |
Source: Siemens Industry Mall catalog page for 6ES7 212-1BE40-0XB0 and the S7-1200 system manual collection on docs.tia.siemens.cloud.
Understanding the RUNTIME Instruction
In SCL on S7-1200 and S7-1500, RUNTIME is a built-in function with the signature:
RUNTIME(LO <memory_tag>) : LREAL;
where:
-
LO <memory_tag> is an
LREALtag the function uses as internal scratch memory between calls. The instruction stores a hardware tick stamp there. It is the input, not the output, despite the syntax that lets you pass what looks like a writable variable. - The function returns an
LREALcontaining the elapsed time in nanoseconds since the previous call that used the same memory tag. On the very first call the return is 0 (the function is seeding the reference tick).
Mechanically this is identical to a stopwatch that you start on the first invocation and read on every subsequent invocation. The memory tag is the stopwatch, the return value is the lap time.
memory_tag directly, converted it to REAL, and expected to see an incrementing runtime. That tag is a tick value, not an elapsed time, so the displayed value either did not change between two consecutive scans, or jumped by values that do not match a millisecond-scale cycle. The runtime is sitting in the function's return value, which was discarded because the call was made as a statement (memory_tag := RUNTIME(memory_tag);) rather than an assignment into a separate return tag.Common Mistakes When Using RUNTIME in SCL
Before showing the working example, the failure modes that engineers hit on the S7-1200 firmware V4.5.1 are worth listing because they share a root cause.
- Reading the memory tag. The value stored in the first parameter is the high-resolution tick stamp from the CPU's internal counter. It only changes at the speed of the tick source, not at the speed of OB1.
-
Losing the return value. Calling
RUNTIME(myTag);as a statement compiles cleanly but the elapsed time evaporates. You must assign the result to a different tag. -
Using a temporary tag for the memory parameter. A
VAR_TEMPis initialised to zero on every call to the FB, which means the function never sees the tick from the previous scan and returns 0 every cycle. Use aVARorVAR_STAT(static) or an instance DB tag instead. -
Reading the result at the wrong point in the cycle. If the second
RUNTIMEcall is inside a branch that the first call does not share (for example, two different OBs), the return is meaningless because the function has no way to identify the two calls as a pair. -
Converting nanoseconds to milliseconds with the wrong divisor.
1 ms = 1 000 000 ns, not1 000. TheLREALprecision is the reason the instruction uses nanoseconds internally; do not downcast toREALuntil you have divided by 1 000 000 (or 1 000 000 000 for seconds).
Step-by-Step SCL Implementation
The cleanest pattern is a dedicated function block that owns the memory tag and the return tag as static members. That way the FB is the single source of truth and the result can be wired to an HMI tag or a watch table without polluting global memory.
Step 1: Create the runtime function block
In the project tree, right-click Program blocks > Add new block > Function block, name it FB_RuntimeMeasure, set the language to SCL, and tick Number automatically. Open the block.
Step 2: Declare the interface
FUNCTION_BLOCK "FB_RuntimeMeasure"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
VAR
// Input: edge-trigger or cycle enable
iEnable : BOOL; // TRUE = measurement active
END_VAR
VAR_INPUT
// No additional inputs required for the basic pattern.
END_VAR
VAR_OUTPUT
oRuntimeNs : LREAL; // elapsed time in nanoseconds, raw
oRuntimeUs : LREAL; // microseconds
oRuntimeMs : REAL; // milliseconds (REAL for HMI)
oDone : BOOL; // one-shot, true on the cycle the timer is rearmed
END_VAR
VAR
// Internal scratch tag for the RUNTIME function.
// MUST be VAR (static in the instance DB) - not VAR_TEMP.
rtMemory : LREAL;
rtFirst : BOOL := TRUE;
END_VAR
BEGIN
IF iEnable THEN
// First call: RUNTIME seeds the reference tick and returns 0.
// Assign the result - do not call as a bare statement.
oRuntimeNs := RUNTIME(rtMemory);
rtFirst := FALSE;
oDone := TRUE;
ELSE
// Subsequent call: RUNTIME returns the elapsed nanoseconds.
oRuntimeNs := RUNTIME(rtMemory);
oRuntimeUs := oRuntimeNs / 1.0E+03; // ns -> us
oRuntimeMs := DINT_TO_REAL(REAL_TO_DINT(oRuntimeNs / 1.0E+06)) / 1.0; // ns -> ms with 1 us resolution
oDone := FALSE;
END_IF;
END_FUNCTION_BLOCK
Key points in the declaration block:
-
S7_Optimized_Access := 'TRUE'matches the S7-1200 V4.5 default and is required for symbolic-only access on the new firmware. -
rtMemoryis in theVARsection, which maps to the instance DB's static area and is preserved between scans.VAR_TEMPwould be re-initialised every call and break the function. -
oRuntimeNscaptures the actual return value ofRUNTIME; readingrtMemoryhere would be the original mistake.
Step 3: Instantiate the FB in OB1
Open Main (OB1), ensure the language is SCL (right-click > Switch programming language), and add the call:
// Measure the runtime of the entire OB1 cycle.
// Tie the enable input to a cyclic flag so the timer
// re-arms on every scan.
"instRuntime"(iEnable := "tagCyclic1Hz");
// Hand the result to the HMI / SCADA tag database.
"tagHmi_RuntimeMs" := "instRuntime".oRuntimeMs;
"tagHmi_RuntimeUs" := "instRuntime".oRuntimeUs;
// --- process logic that you want to time goes here ---
END_OB1;
tagCyclic1Hz is a clock-bit from the CLOCK or CTRL_HSC instruction (or a simple counter that toggles every call). On the S7-1200 the more idiomatic approach is to keep iEnable permanently TRUE and accept that the timer self-rearms every OB1 cycle. The first call returns 0, every later call returns the OB1 wall-clock interval.
Step 4: Alternative - measure a slice inside OB1
To measure only the block between two RUNTIME calls, you keep the first call of the scan as the reference and the second as the read. Use the same instance and place the second RUNTIME call after the code you want to time:
// Begin measurement
"instRuntime".iEnable := TRUE;
// ... process logic to be timed ...
// Read measurement - this is where the slice elapsed time is produced
"instRuntime".iEnable := FALSE;
When iEnable drops to FALSE, the FB falls into the ELSE branch and updates oRuntimeNs / oRuntimeUs / oRuntimeMs for that scan only. The next scan starts a fresh measurement.
Variable Reference Table
| Tag | Scope | Data type | Purpose |
|---|---|---|---|
instRuntime |
OB1 local / global DB | FB_RuntimeMeasure |
Single instance of the runtime FB |
rtMemory |
Inside the FB (instance DB static) | LREAL |
Internal tick storage for the RUNTIME instruction |
oRuntimeNs |
FB output | LREAL |
Raw nanoseconds, do not display directly |
oRuntimeUs |
FB output | LREAL |
Microseconds, useful for trending |
oRuntimeMs |
FB output | REAL |
Milliseconds, wired to HMI / SCADA |
tagCyclic1Hz |
Global DB or M-bit | BOOL |
Optional re-arm signal |
tagHmi_RuntimeMs |
HMI tag DB | REAL |
Final value sent to the HMI |
Converting Nanoseconds to Readable Units
Direct division in SCL:
// Nanoseconds -> microseconds
rt_us := rt_ns / 1.0E+03;
// Nanoseconds -> milliseconds
rt_ms := rt_ns / 1.0E+06;
// Nanoseconds -> seconds (REAL for HMI)
rt_s := DWORD_TO_REAL(rt_raw) / 1.0E+09;
The S7-1200 RUNTIME return value is LREAL; the CPU is single-precision friendly for HMI display, so downcasting to REAL only after dividing preserves precision. Avoid the temptation to cast to DINT first, because that truncates the mantissa and can produce integer wrap-around on long cycle times.
Verification Procedure
- Compile and download. In TIA Portal V15.1, right-click the S7-1200 device > Download to device > Hardware and software (only changes). Confirm that the active firmware in the device matches V4.5.1 (Online > Diagnostics > Device information).
-
Open a watch table. Add the tags
"instRuntime".oRuntimeNs,"instRuntime".oRuntimeUs,"instRuntime".oRuntimeMs, andrtMemory. Monitor in Update with trigger mode at 1 second. -
Confirm the tag you are reading is the return value, not the memory tag. The
oRuntimeMsshould oscillate within the range of one OB1 cycle. On an idle 1212C with the supplied program the typical value is 1–5 ms.rtMemorywill look like a continuously increasing 64-bit tick count; do not be tempted to interpret it as runtime. -
Cross-check with the system clock. Force a 500 ms delay with
WAIT_FOR_CONDITION(S7-1500 only) or an equivalent timer in the OB1, and confirm theoRuntimeMstag reflects the additional 500 ms ± 1 scan jitter. -
Trend the value in the HMI. Add a trend view in WinCC Professional or a Comfort Panel, point it at
tagHmi_RuntimeMs, and confirm the curve moves with the CPU load.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
oRuntimeMs stays at 0 |
Memory tag declared as VAR_TEMP or the call is a bare statement |
Move the memory tag to VAR (static) and assign the return value to a different tag |
oRuntimeMs shows a huge constant |
Reading the memory tick directly | Read oRuntimeNs / oRuntimeUs / oRuntimeMs outputs instead |
| Compiler warning "Statement does not return a value" | Calling RUNTIME as a statement in SCL |
Assign the result: rtNs := RUNTIME(rtMemory);
|
| Value oscillates wildly between 0 and the cycle time | The FB is being re-instantiated (called from a different instance or a multi-instance DB) | Use a single, non-shared instance DB for the runtime FB |
| Runtime appears stuck at a fixed large value | Firmware V4.2 does not support LREAL in this context, or the divider is wrong |
Upgrade to V4.4+ and verify the divisor is 1.0E+06 for ms, 1.0E+09 for s |
Watch table shows "Invalid value" on LREAL tag |
Watch table column not set to Display in hexadecimal/floating point for LREAL
|
Right-click the column > Display format > Floating-point |
| Cycle time exceeds OB1 watch-dog | The measured slice includes the watch-dog reset itself or interrupts | Take the measurement between two well-defined points inside OB1, not across OB1 boundaries |
| Different values on every scan even when the program is idle | Communication load (HMI polling, PUT/GET) is varying | Use the RUNTIME of a single FB (not the whole OB1) to isolate the user code |
Firmware and TIA Portal Compatibility
The RUNTIME instruction has been part of the S7-1200 SCL instruction set since firmware V4.0. The behaviour described in this article was confirmed against the following combination reported in the original question:
- TIA Portal: V15.1 (Update 4 or higher recommended for V4.5 firmware).
- CPU firmware: V4.5.1 on a 6ES7 212-1BE40-0XB0.
On S7-1500 the equivalent functionality is the IEC 61131-3 RTM (runtime measurement) function block, but it is intentionally not available on S7-1200; for the 1212C the RUNTIME SCL function shown above is the only sanctioned approach. Do not confuse it with the cyclic OB OB_PREEXEC hook, which exists only on S7-1500.
RUNTIME measurement inside an OB that has a tight watch-dog can itself push the cycle over the limit. Place the instrumentation code in OB1 (default 150 ms watch-dog) or in OB200 (cyclic interrupt) where you control the watchdog through OB properties > Time error OB. Never instrument inside an OB that drives a safety function.Performance Tips and Field Notes
- One FB per measurement point. If you need to time three independent slices, instantiate the FB three times. Sharing the instance causes the instruction to overwrite its own tick and produce nonsense.
- Do not call RUNTIME from OB100 / OB101. Startup OBs do not run cyclically; the first call after OB1 starts will appear as a very large value because the tick reference is the last OB100 execution, not the previous OB1 execution.
- Average, do not display the raw value. On the HMI, smooth the tag with a 5–10 sample moving average; the OB1 jitter on the 1212C is typically ±0.2 ms and raw numbers will look unstable.
-
Calibrate the result. Compare your
oRuntimeMsto the cycle time reported in Online > Diagnostics > Cycle time. They should match within 0.5 ms. If they do not, theRUNTIMEinstruction is being called from a context that does not share the tick domain. -
Watch the process image. On firmware V4.5 the default process image is updated at the end of OB1, so the values seen by the HMI are one scan old. Use
SYNC_PIin the HMI tag properties only if you need fresh values, otherwise the trending curve will be smooth and predictable.
Why does my SCL RUNTIME call return 0 on the first scan?
The first call seeds the internal tick reference and intentionally returns 0 ns. This is by design; the function has no prior reference to subtract. Begin your measurement on scan two. Many first-scan issues are also caused by the memory tag being a VAR_TEMP; the tag must be a static (instance-DB) LREAL so the tick survives between calls.
Can I read the memory tag directly to get the runtime?
No. The memory tag holds the internal tick stamp, not the elapsed time. The elapsed time is the LREAL value returned by RUNTIME. Reading the memory tag and converting it to REAL is the most common reason engineers report that the value "never changes" or "looks like a counter".
What is the resolution of RUNTIME on a CPU 1212C (6ES7 212-1BE40-0XB0)?
On the S7-1200 the instruction returns nanoseconds as an LREAL. In practice the useful resolution is around 1 µs because the 1212C's internal timer tick is 1 µs; the nanosecond figure is the float representation, not a true nanosecond hardware counter.
Does RUNTIME work in OB100 (startup) or only in OB1?
It works in any OB, but it is meaningful only when called cyclically. In OB100 the function seeds the tick and returns 0; the next call usually happens inside OB1 and reports the OB100 + OB1 interval, which is not what you want. For startup diagnostics use the RD_SINFO / RD_SYSST instructions instead.
Which TIA Portal and firmware versions support the SCL RUNTIME pattern shown here?
TIA Portal V15.1 with S7-1200 firmware V4.5.1 (the configuration in the original problem) is the baseline. The same code compiles unchanged on V15.1+, V16, V17, and V18 with firmware V4.2 through V4.6. On S7-1500 use the RTM IEC block instead; it is the standard equivalent and produces microsecond values directly.