Measuring S7-1200 CPU Runtime with SCL RUNTIME: 1212C Example

David Krause14 min read
S7-1200SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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 RUNTIME FB, with the addition of the IEC timer RTM on 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 0XB0 suffix identifies the AC 85–264 V supply, 24 V DC digital inputs, relay output variant with two onboard 0–10 V analog inputs. The earlier 1BE31-0XB0 variant 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_VAL handling.
  • Programming language: SCL (Structured Control Language). LAD/FBD examples of RUNTIME are 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 LREAL tag 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 LREAL containing 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.

Why the source code failed: The original program read the 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.

  1. 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.
  2. 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.
  3. Using a temporary tag for the memory parameter. A VAR_TEMP is 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 a VAR or VAR_STAT (static) or an instance DB tag instead.
  4. Reading the result at the wrong point in the cycle. If the second RUNTIME call 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.
  5. Converting nanoseconds to milliseconds with the wrong divisor. 1 ms = 1 000 000 ns, not 1 000. The LREAL precision is the reason the instruction uses nanoseconds internally; do not downcast to REAL until 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.
  • rtMemory is in the VAR section, which maps to the instance DB's static area and is preserved between scans. VAR_TEMP would be re-initialised every call and break the function.
  • oRuntimeNs captures the actual return value of RUNTIME; reading rtMemory here 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

  1. 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).
  2. Open a watch table. Add the tags "instRuntime".oRuntimeNs, "instRuntime".oRuntimeUs, "instRuntime".oRuntimeMs, and rtMemory. Monitor in Update with trigger mode at 1 second.
  3. Confirm the tag you are reading is the return value, not the memory tag. The oRuntimeMs should oscillate within the range of one OB1 cycle. On an idle 1212C with the supplied program the typical value is 1–5 ms. rtMemory will look like a continuously increasing 64-bit tick count; do not be tempted to interpret it as runtime.
  4. 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 the oRuntimeMs tag reflects the additional 500 ms ± 1 scan jitter.
  5. 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.

Warning: Adding a 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 oRuntimeMs to the cycle time reported in Online > Diagnostics > Cycle time. They should match within 0.5 ms. If they do not, the RUNTIME instruction 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_PI in 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.

Back to blog