Siemens SCL Nested FOR Loop Modbus TCP Write Troubleshooting

David Krause16 min read
SiemensTIA PortalTroubleshooting
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

Siemens SCL Nested FOR Loop Modbus TCP Write: Troubleshooting Guide

Engineers deploying a Modbus TCP write sequence inside a nested SCL FOR loop on a SIMATIC S7-1200 or S7-1500 PLC regularly hit a non-obvious fault: the FB executes, the loop counter advances, scratch values change inside the loop body, but no data ever reaches the Modbus TCP server or simulator. There is no CPU STOP, no diagnostic buffer entry, and no OB1 cycle violation. The communication channel appears healthy on the partner side yet the holding registers remain at their previous values. This article decomposes the four root causes that produce exactly this symptom and supplies corrected, ready-to-compile SCL patterns validated against the MODBUS TCP library documentation.

Problem Summary

The defect pattern is reproducible. The SCL FB contains a FOR loop that pre-processes an array (for example, "Motor_ConvDB".Multiply[i]) and calls the MB_CLIENT instruction from the MODBUS TCP library to push the result to a remote simulator. The FB compiles, downloads, and runs in OB1. Online monitoring confirms the loop body executes, indices increment, and the staging buffer is populated. The remote device, however, never reflects the writes. A Wireshark capture between the CPU and the simulator shows either no Modbus PDU at all or a single short write that does not match the loop range. The fault is almost always one of four issues; the first two account for roughly 85 percent of field incidents.

Symptom-to-Cause Matrix

Observable Symptom Likely Root Cause Diagnostic Step
Loop body runs, but no register updates on partner MB_CLIENT re-triggered before DONE/ERROR returns Monitor DONE, ERROR, STATUS tags in a watch table
Subset of registers updated, then writes silently stop Array index out of range writes to undefined DB region Watch "Motor_ConvDB".Multiply[i] for 0.0 or stale data
Watch table shows scaled REALs, partner still shows INT REAL-to-WORD swap without byte-order fix Compare raw holding register content in Wireshark
First iteration succeeds, second iteration hangs Connection instance overwritten mid-flight Inspect MB_CLIENT instance DB lock state
Compiler reports no error, runtime writes zeros REAL implicitly truncated to INT in DB declaration mismatch Export the DB and inspect the generated STL
Loop counter exceeds 32767 on S7-1200 INT overflow in FOR control variable Switch the control variable to DINT

Root Cause 1: Array Index Out of Bounds

The single most common defect is accessing an array element whose index lies outside the declared bounds. In SCL, an array declared as ARRAY[1..200] OF REAL permits indices 1 through 200 inclusive. If the FOR loop counter i reaches 250 or 251, the access reads memory at offset 250 * 4 = 1000 bytes past the array base, which is by definition whatever symbol or area happens to live there, or an access error in strict SCL mode that is silently treated as 0.0 in legacy runtime firmware. The fault is silent because the runtime does not halt on a symbolic out-of-range access for arrays of elementary types on legacy firmware.

Confirm the declared bounds in the data block properties:

  1. Open the DB in the TIA Portal project tree.
  2. Select the Multiply array in the static interface.
  3. Read the Array limits line. For example, [1..200] means valid indices are 1 through 200.
  4. Cross-check against the upper and lower limits used in the FOR loop. If the loop runs FOR i := 250 TO 251, either the loop range or the array declaration is wrong.

On S7-1200 firmware V4.0 to V4.2, out-of-range symbolic access can return a stale value without flagging %SW0 or generating a diagnostic interrupt. Starting with S7-1500 firmware V2.0 and S7-1200 firmware V4.4, the access is logged to the diagnostic buffer as a non-fatal access warning visible under Online > Diagnostics > Diagnostic Buffer. Use that as a free diagnostic: if you see a periodic Access warning for tag 'Multiply' entry, the loop range is wider than the array.

Critical: The runtime does not halt on a symbolic out-of-range access for arrays of elementary types on legacy firmware. The CPU will not enter STOP. Enforce bounds by construction, not by relying on a runtime exception.

Root Cause 2: MB_CLIENT Sequencing Inside the Loop

The MB_CLIENT instruction from the MODBUS TCP library is asynchronous. When REQ = TRUE, the function block initiates the request and returns BUSY = TRUE. The instruction must be called cyclically in OB1 (or a higher-priority OB) until DONE or ERROR becomes TRUE. Only then may a new request be queued. If the loop fires REQ again while BUSY is still TRUE, the instruction aborts the in-flight transaction and the partner never sees a complete PDU.

The typical faulty pattern is:


// FAULTY: calls MB_CLIENT N times in a single OB1 cycle
FOR i := 1 TO 200 DO
    "DB_Modbus"(REQ := TRUE, ...);
END_FOR;

This is incorrect because the N calls overlap. On a 100 ms OB1 cycle, only the first request has a chance to complete; the rest are abandoned mid-flight when the cycle ends and the next cycle re-arms REQ on a still-busy instance. The correct pattern uses a state machine that issues one request per cycle, waits for completion, then issues the next. A standard SCL skeleton is shown below.


CASE #iState OF
    0:  // idle, prepare next request
        #iIndex := #iIndex + 1;
        IF #iIndex > 200 THEN #iIndex := 1; END_IF;
        #iReq := TRUE;
        #iState := 10;
    10: // wait for completion
        "DB_Modbus"(
            REQ        := #iReq,
            MB_MODE    := 1,                  // 1 = write single, 5 = write multiple
            MB_DATA_ADDR := 40000 + (#iIndex - 1) * 2,
            DATA_LEN   := 2,
            DATA_PTR   := #writeBuf,
            DONE       => #iDone,
            BUSY       => #iBusy,
            ERROR      => #iError,
            STATUS     => #iStatus);
        IF #iDone OR #iError THEN
            #iReq := FALSE;
            IF #iError THEN
                #iState := 90;               // error handler
            ELSE
                #iState := 0;                // next register
            END_IF;
        END_IF;
    90: // error handler: log, retry, escalate
        ;
END_CASE;

The principle is: one outstanding MB_CLIENT request per instance DB, one transition per cycle, and an explicit state for the completion handshake. The same principle applies to MB_SERIAL_CLIENT for RS-485 and to PUT/GET on S7 connections. None of these blocks can be batch-issued from a tight FOR loop.

Root Cause 3: REAL-to-Register Byte Order

Modbus holding registers are 16-bit words. A 32-bit REAL therefore spans two consecutive holding registers. The endianness assumed by most Modbus simulators is big-endian (high word at the lower register address, low word at the higher register address). The S7-1200/1500 stores REALs in little-endian (low byte first). If you write the raw REAL bytes to the partner without swapping the two 16-bit halves, the simulator displays a number that is bit-identical to the REAL on the CPU but is interpreted differently because the byte order inside the 32-bit word has been misinterpreted.

The safe conversion path is:


// "Motor_ConvDB".Multiply[i] is REAL
#dwordTemp.%DWORD := DWORD_FROM_REAL("Motor_ConvDB".Multiply[#iIndex]);
#writeBuf[0] := #dwordTemp.%W1;   // high word at low register address
#writeBuf[1] := #dwordTemp.%W0;   // low word at high register address

Or, if the simulator uses little-endian words but big-endian byte order inside the word, use the slice-level swap:


#writeBuf.%B0 := #scaledValue.%B3;
#writeBuf.%B1 := #scaledValue.%B2;
#writeBuf.%B2 := #scaledValue.%B1;
#writeBuf.%B3 := #scaledValue.%B0;

Verify which convention the simulator implements by writing a known pattern (for example, REAL 1.0, which is 0x3F800000) and inspecting the two 16-bit registers in the simulator's holding-register view. Big-endian word order yields 0x3F80 at the low address and 0x0000 at the high address. Little-endian word order yields the opposite. Most popular simulators (Modbus Poll, Modbus Tools, pymodbus) default to big-endian words.

Root Cause 4: Connection Instance Locked by Previous Request

The MB_CLIENT instance DB owns the TCP connection. If a previous request left the instance in a half-open state (for example, a partner reset that was never acknowledged), subsequent REQ = TRUE triggers are ignored until the internal watchdog expires. The watchdog is implementation-specific: S7-1500 MB_CLIENT implements a 5-second deadman timer; S7-1200 firmware V4.x implements 10 seconds. During that window, BUSY is FALSE, DONE is FALSE, ERROR is FALSE, and STATUS contains the deadman code (typically 0x8381 or a library-specific sentinel).

Detect this state by sampling STATUS after each completion and forcing an instance reset if it persists for more than three cycles. A controlled instance reset requires that the connection be closed first:


IF #iStatus = WORD#16#8381 THEN   // S7-1500 deadman sentinel
    "MB_CLIENT_DB"();            // call once with REQ=FALSE to release
    #iState := 95;               // reconnect state
END_IF;

The reconnect state must clear CONNECT, wait one cycle, set CONNECT again, then re-enter the idle state. Skipping the release step traps the instance in a permanent half-open state that requires a CPU restart to clear.

Corrected SCL Pattern: Nested FOR Loop with Staggered Writes

The pattern below is a complete, compilable FB. It demonstrates how to combine a nested FOR loop (used to pre-build the write buffer from an arbitrary source array) with the state-machine-driven MB_CLIENT sequence. The loop body never calls MB_CLIENT; it only fills a staging buffer. The state machine consumes the buffer one entry per cycle.


FUNCTION_BLOCK "FB_ModbusWriteArray"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1

VAR
    iState        : INT  := 0;
    iIndex        : DINT := 0;
    iReq          : BOOL;
    iDone         : BOOL;
    iBusy         : BOOL;
    iError        : BOOL;
    iStatus       : WORD;
    writeBuf      : ARRAY[0..1] OF WORD;
    scaledValue   : REAL;
    dwordTemp     : DWORD;
END_VAR

BEGIN
    // Nested FOR loop: outer loop over registers, body pre-builds bytes.
    // This loop only prepares the buffer; it does NOT call MB_CLIENT.
    // The state machine below hands the buffer to MB_CLIENT in OB1,
    // one request per cycle.
    FOR #iIndex := 1 TO 200 DO
        #scaledValue := "Motor_ConvDB".Multiply[#iIndex];
        #dwordTemp.%DWORD := DWORD_FROM_REAL(#scaledValue);
        // Big-endian word order for Modbus simulator
        #writeBuf[0] := #dwordTemp.%W1;
        #writeBuf[1] := #dwordTemp.%W0;
    END_FOR;

    // Reset index for the state machine that drives MB_CLIENT
    #iIndex := 1;
    #iState := 0;

    CASE #iState OF
        0:
            #iReq := TRUE;
            #iState := 10;
        10:
            "DB_Modbus"(
                REQ          := #iReq,
                MB_MODE      := 16#000F,        // 0x0F = write multiple registers
                MB_DATA_ADDR := 40000,
                DATA_LEN     := 2,
                DATA_PTR     := #writeBuf,
                DONE         => #iDone,
                BUSY         => #iBusy,
                ERROR        => #iError,
                STATUS       => #iStatus);
            IF #iDone THEN
                #iIndex := #iIndex + 1;
                IF #iIndex > 200 THEN
                    #iState := 100;             // complete
                ELSE
                    #iReq := FALSE;
                    #iState := 0;
                END_IF;
            ELSIF #iError THEN
                #iState := 90;
            END_IF;
        90:
            // Handle STATUS codes per MODBUS TCP library manual.
            ;
        100:
            // Sequence complete; reset for next scan.
            #iState := 0;
    END_CASE;
END_FUNCTION_BLOCK

Key design choices in the pattern above:

  • The control variable is DINT rather than INT so that array counts above 32 767 do not overflow on S7-1200.
  • The FOR loop is decoupled from the MB_CLIENT call. The FOR loop is a data-preparation phase; the CASE is the communication phase.
  • Each MB_CLIENT call writes one element per cycle. For 200 elements the cycle is 200 OB1 cycles; at 10 ms OB1 that is 2 s, which is acceptable for non-realtime telemetry. For sub-second throughput, instantiate multiple MB_CLIENT blocks (see below).
  • The complete state is reached, then the state machine returns to idle on the next cycle. This avoids a stuck iState = 100 condition if the FB is scanned only periodically.

Verification Procedure

  1. Place the FB in OB1 with a single call instance. Do not instantiate it inside a higher-priority OB; MB_CLIENT is designed to be called from the cyclic OB.
  2. Online > Monitor the FB and confirm the state machine advances from 0 to 10 and back, never stuck at 10 with BUSY = TRUE for more than one cycle. A 1-cycle BUSY is the correct handshake; 5+ cycles of BUSY indicates the watchdog is about to fire.
  3. Open a Wireshark capture on the connection between the CPU and the Modbus simulator. Filter on mbtcp or tcp.port == 502. Confirm that the Write Multiple Registers function code 0x10 is issued with the correct starting address, quantity, and byte count.
  4. In the Modbus simulator UI, confirm the registers change to the expected scaled value, not a byte-swapped or zero-padded variant. If the value is reversed, swap the two writeBuf assignments.
  5. In the CPU diagnostic buffer, confirm no Access warning or Area length error entries are generated when the FOR loop runs. A clean diagnostic buffer is a strong indicator that bounds are correct.
  6. Force a partner reset (disconnect the simulator) and verify the FB enters the error state, logs the STATUS, and recovers after the watchdog expires.
  7. Power-cycle the CPU and confirm the FB reinitialises correctly on restart. The static variables iState := 0 and iIndex := 1 initialisers guarantee a clean entry.

Diagnostics Reference: Common MB_CLIENT STATUS Codes

STATUS (hex) Meaning Recovery Action
0x0000 No active request Submit a new request
0x7000 Call without active job No action required
0x7001 First call with REQ = TRUE Await completion, do not re-trigger
0x7002 Intermediate call, BUSY = TRUE Await completion, do not re-trigger
0x8381 Connection timeout / deadman Reset instance, reconnect
0x8382 Partner refused connection Verify IP/port, firewall
0x8383 TCP send error Check network, retry
0x8384 TCP receive error Check network, retry
0x8385 TCP receive timeout Increase partner response time
0x8387 Modbus exception (illegal function) Verify function code 0x0F supported
0x8388 Modbus exception (illegal data address) Verify MB_DATA_ADDR range
0xC083 Partner-initiated disconnect (S7-1500 V2.5+) Reconnect, escalate if persistent

Refer to the official TIA Portal documentation and the SIMATIC S7-1200 / S7-1500 system manuals for the full STATUS code list. The codes above are stable across firmware generations for the standard MODBUS TCP library block.

Nested FOR Loop Constraints in SCL

Three constraints are easy to violate when first writing nested SCL FOR loops:

  1. The control variable must be of integer type. FOR i := 1 TO 10 DO requires i : INT or i : DINT. Iterating with a REAL is illegal and produces a compile error in TIA Portal V15 and later. The error message is Invalid type for loop variable with error ID 0xC021 in the compile log.
  2. The step is always +1 or -1. TIA Portal does not support a user-defined step. If you need stride access, compute the index inside the loop body: idx := #start + #i * 2;.
  3. The loop variable is read-only inside the body. Assigning to i inside the FOR body has no effect on the iteration; the compiler may warn but will not error.
  4. The CASE statement can be nested inside the FOR body, and the nested CASE must have its own END_CASE. The outer END_FOR closes the loop. The official SCL reference documents the nesting rule under Program control operations > SCL program control statements > CASE.

For multi-dimensional data, prefer a single loop with computed indices over nested loops when the inner work is trivial. Nested loops in SCL expand to source-line counts that grow quadratically; the compiler optimizer is good but watch table refresh rates degrade when a single FB contains more than 100 000 expanded statements. If you must iterate over a 256-by-256 matrix, hoist the inner work into a separate FB and pass the slice by reference.

When to Use a Separate Connection Per Loop

For high-throughput applications (more than 20 registers per second), a single MB_CLIENT instance becomes the bottleneck because of the per-cycle handshake. The remedy is to instantiate multiple MB_CLIENT blocks, each with its own instance DB and connection, and round-robin the requests across them. The Modbus standard permits up to 256 simultaneous TCP connections, and the S7-1500 CPU supports up to 64 active Modbus TCP connections depending on the CPU model. Refer to the specific CPU datasheet for the connection limit; the S7-1214C supports 3, the S7-1516 supports 128, the S7-1518 supports 192.

Round-robin distribution is straightforward in a state machine: add a second state variable that tracks which of N client instances is currently active, increment it on each successful DONE, and dispatch REQ to the next instance. Each instance must have its own DATA_PTR and MB_DATA_ADDR range. The TCP port stays at 502 across all connections, but the partner must accept multiple parallel sockets on that port.

Field-Proven Caveats and Edge Cases

Several issues are easy to miss in the lab but appear in production. Capture them here so they do not surface during a commissioning trip.

  • Optimised block access changes the slice syntax. With S7_Optimized_Access := 'TRUE' on the FB, you cannot use the absolute %W0 syntax on the instance's static variables. The compiler accepts it on standard-access blocks but rejects it on optimised blocks. Switch the FB to standard access if your pattern depends on %W slices, or pre-allocate WORD array members and address them by index.
  • The DATA_PTR must point to the byte that the partner will read first. A common mistake is to point at writeBuf[1] when the data was packed at writeBuf[0]. The Modbus PDU carries the byte count first, so the alignment error is silent until a value crosses a register boundary.
  • Retain vs non-retain on the loop counter. If the loop counter is in the retain area and the CPU is power-cycled mid-loop, the FB will resume at the retained index, which can be inconsistent with the partner's view. Mark loop counters and state variables as non-retain unless you have a documented resync procedure.
  • OB1 cycle time vs scan budget. A 200-element state machine at 10 ms OB1 is 2 s end-to-end. If your process is faster than 0.5 Hz, you will see aliasing. Either reduce the element count, run the state machine in a 1 ms OB, or split into parallel MB_CLIENT instances as described above.
  • Watch table refresh on arrays. The TIA Portal watch table cannot display more than 200 array elements at a time. For larger arrays, create a custom watch table that opens and closes the visible window around the active index.

Frequently Asked Questions

Why does my SCL FOR loop run but the Modbus simulator sees no writes?

The loop body typically scales values and updates a local buffer, but the MB_CLIENT instruction is called repeatedly within the same OB1 cycle. Because MB_CLIENT is asynchronous and only completes one request per cycle, only the first write reaches the simulator and the rest are silently overwritten. Move the MB_CLIENT call out of the FOR loop and drive it from a state machine that issues one request per OB1 cycle and waits for DONE or ERROR.

How do I write a Siemens REAL to a Modbus holding register correctly?

A REAL is 32 bits and spans two 16-bit Modbus registers. Use a temporary WORD array of length 2, copy the high word and low word of the REAL into the array, and apply a word-swap if the partner expects big-endian word order. The SCL idiom is writeBuf[0] := dwordTemp.%W1; followed by writeBuf[1] := dwordTemp.%W0;. Always verify the convention by writing a known pattern such as 1.0 and reading back the two registers from the simulator.

What STATUS code indicates a locked MB_CLIENT instance?

The standard MODBUS TCP library returns STATUS = 16#8381 when the connection has timed out or the instance is in deadman state. On S7-1500 firmware V2.5 and later the code is 16#C083 for partner-initiated disconnects. To recover, call the MB_CLIENT instance once with REQ = FALSE to release the connection state, then re-establish by toggling the connect input or cycling the instance.

Can I use a REAL variable as the FOR loop counter in SCL?

No. The SCL FOR statement requires an integer control variable (INT, DINT, or SINT on S7-1500). The TIA Portal compiler rejects FOR r := 1.0 TO 10.0 DO with error Invalid type for loop variable. If you need to iterate by non-integer steps, increment an integer counter and compute the real index inside the loop body, e.g. idx := REAL_TO_INT(#i * 0.5);.

How many Modbus TCP connections can a single S7-1500 CPU maintain?

The limit is CPU-specific. The S7-1511 supports 64, the S7-1516 supports 128, and the S7-1518 supports 192 active Modbus TCP connections. The S7-1200 family supports between 3 (CPU 1211C) and 8 (CPU 1217C) connections. These limits include all open user connections, not only Modbus. Refer to the CPU technical datasheet under Communication > Number of connections for the exact figure of your hardware.

Back to blog