Building Modbus RTU Frames in S7-1500 TCP Communication

David Krause19 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

When a Siemens S7-1500 CPU must talk to a serial Modbus RTU device, the usual path is the MB_CLIENT instruction shipped in TIA Portal, which speaks Modbus TCP (MBAP header + PDU) over a TCP socket. There are real situations where you cannot use MB_CLIENT and must instead send a raw Modbus RTU frame (address + function code + data + CRC-16) directly into a TCP socket. Typical reasons include:

  • The remote device sits behind a transparent serial-to-Ethernet gateway (for example, the Advantech EKI-1521) that forwards the serial bytes inside a TCP payload. The gateway speaks TCP, but the payload is RTU-framed.
  • You are using a third-party Modbus-to-Profibus or Modbus-to-fieldbus gateway that exposes a Modbus RTU-style service over TCP.
  • You must support a vendor-specific function code that MB_CLIENT blocks do not cover.
  • You need sub-millisecond deterministic cycle time on a custom request/response without going through the MB_CLIENT state machine.

This reference shows how to construct, send, receive, and validate Modbus RTU frames directly from S7-1500 user logic using the standard TSEND_C and TRCV_C TIA Portal blocks. It includes a working SCL CRC-16 routine, a complete request/response code sample, gateway wiring details, and a troubleshooting matrix.

Scope reminder. A Modbus RTU frame and a Modbus TCP PDU are not the same thing. Modbus TCP adds a 7-byte MBAP header (Transaction ID, Protocol ID, Length, Unit ID) and removes the CRC. This article deals with the RTU form: no MBAP header, CRC-16 appended at the end, transported over a TCP socket rather than over RS-485.

Prerequisites

Before you build the first frame, confirm that the following items are available and configured.

Hardware

  • Siemens S7-1500 CPU (any firmware from V2.0 onward; tested on CPU 1511-1 PN and CPU 1515-2 PN with firmware V2.9). All Open User Communication blocks are available from the instruction list under Communication → S7 Communication or Communication → Other in TIA Portal V17 and later.
  • CPU with an integrated PROFINET interface, or a CP 1543-1 communication module. A second PROFINET port is recommended so that the Modbus traffic can be isolated from the engineering network.
  • Serial device or remote Modbus RTU node. For development, a USB-to-RS-485 converter on a laptop running modpoll or a Modbus simulator is ideal.
  • Advantech EKI-1521 (or compatible) serial-to-Ethernet gateway, set to Server mode, port 502, with the Modbus Gateway operating mode enabled. EKI-1521 firmware V1.60 or later is recommended for stable RTU-over-TCP forwarding.

Software

  • TIA Portal V17 (or V16, V18, V19). Open User Communication blocks are documented in the S7-1500/ET 200MP System Manual.
  • S7-PLCSIM (V17 or later) if you want to dry-run the logic without hardware.
  • Wireshark with the modbus_rtu dissector disabled (or set to "don't decode") so the raw TCP stream is visible while commissioning.

Network Configuration

  • The S7-1500 CPU IP address, subnet mask, and router must be set in the device configuration (e.g. 192.168.10.10 / 255.255.255.0).
  • The EKI-1521 must have a fixed IP in the same subnet (e.g. 192.168.10.20 / 255.255.255.0) with the TCP port set to 502 (default Modbus port) or any free port if 502 is already in use.
  • Open a PG/PC firewall exception for the TCP port used by the gateway if Windows Defender or an IT firewall is active on the engineering station.

Modbus RTU Frame Structure and Function Codes

The Modbus RTU frame used on the wire is a fixed-structure PDU with a 2-byte trailing CRC. Every byte except the start and end delimiters is implicit (the 3.5-character silent interval marks the boundaries on a serial line, but over TCP the framing is taken from the TCP stream itself, so the boundary is irrelevant).

Field Length (bytes) Description Range / Example
Slave Address 1 Receiver address; 0 = broadcast, 1–247 = individual slave, 248–255 = reserved. 0x01 (slave 1)
Function Code 1 Service identifier. Bit 7 set in the response means exception. 0x03 (read holding registers)
Data 0–252 Function-specific payload (registers, coil states, exception sub-code, etc.). 0x00 0x00 0x00 0x06 (start addr + quantity for FC03)
CRC-16 (Modbus) 2 CRC-16 polynomial 0xA001, init 0xFFFF, reflected input/output. Transmitted low byte first. 0xC4 0x0B

Common Function Codes

Code Name Direction Data Field (request) Data Field (response)
0x01 Read Coils Master → Slave Start addr (2 B) + Quantity (2 B) Byte count (1 B) + coil bytes (N)
0x02 Read Discrete Inputs Master → Slave Start addr (2 B) + Quantity (2 B) Byte count (1 B) + input bytes (N)
0x03 Read Holding Registers Master → Slave Start addr (2 B) + Quantity (2 B) Byte count (1 B) + register words (2·N)
0x04 Read Input Registers Master → Slave Start addr (2 B) + Quantity (2 B) Byte count (1 B) + register words (2·N)
0x05 Write Single Coil Master → Slave Output addr (2 B) + value (2 B, 0xFF00 = ON, 0x0000 = OFF) Echo of request
0x06 Write Single Register Master → Slave Register addr (2 B) + value (2 B) Echo of request
0x0F (15) Write Multiple Coils Master → Slave Start addr (2 B) + Quantity (2 B) + Byte count (1 B) + coil bytes (N) Start addr (2 B) + Quantity (2 B)
0x10 (16) Write Multiple Registers Master → Slave Start addr (2 B) + Quantity (2 B) + Byte count (1 B) + register words (2·N) Start addr (2 B) + Quantity (2 B)

Exception Frame

If the slave cannot fulfil the request, the response function code has the high bit set (0x80 | function_code) and the data field is a single exception code byte.

Exception code Name Typical cause
0x01 Illegal Function Function code not supported by the slave
0x02 Illegal Data Address Register or coil address outside the slave's map
0x03 Illegal Data Value Quantity, byte count, or value outside the allowed range
0x04 Slave Device Failure Hardware fault on the slave while processing
0x05 Acknowledge Long-running command accepted, processing continues
0x06 Slave Device Busy Slave processing another command; retry later
0x08 Memory Parity Error Parity error in slave memory
0x0A (10) Gateway Path Unavailable Gateway cannot reach the downstream device
0x0B (11) Gateway Target No Response Downstream device did not reply to the gateway

The complete Modbus Application Protocol Specification is published by the Modbus Organization; the original Modbus over Serial Line specification that defines the RTU framing and CRC-16 algorithm is available as PI_MBUS_300.pdf.

Why Raw RTU over TCP Instead of MB_CLIENT

The MB_CLIENT instruction in TIA Portal expects a Modbus TCP server, i.e. a peer that prefixes every PDU with an MBAP header. A serial Modbus RTU device cannot speak Modbus TCP natively. There are three ways to bridge the gap, and the choice dictates whether you need raw RTU framing in user logic.

Bridge Method Frame on the TCP socket Required on the CPU
Modbus TCP → serial converter (acts as Modbus TCP server) TCP socket + MBAP header removed, RTU frame produced on RS-485 Modbus TCP (MBAP + PDU) MB_CLIENT directly
Serial-to-Ethernet gateway in TCP Server (transparent) mode, e.g. EKI-1521 TCP socket carries the exact bytes that appear on RS-485 Raw Modbus RTU (no MBAP, CRC-16 present) TSEND_C/TRCV_C + custom CRC
Modbus-to-Profibus gateway (e.g. Hilscher NT 50) Serial bytes embedded in a vendor-specific wrapper Vendor proprietary or raw RTU Either, depending on the gateway profile

The second row is the one covered here. The EKI-1521 in TCP Server, Modbus Gateway Mode opens a TCP listener on port 502 (configurable) and forwards every byte received on that socket to the downstream RS-485 master. The CPU must therefore generate a valid Modbus RTU frame — slave address, function code, payload, and a valid CRC-16 — and put those bytes into the TCP send buffer.

CRC-16 (Modbus) Implementation in SCL

The Modbus RTU CRC-16 uses polynomial 0xA001 (bit-reversed 0x8005), an initial value of 0xFFFF, and is processed with reflected input and output. The result is transmitted low byte first. The function block below takes a pointer to the data and a length and returns a 16-bit CRC.

// FB_ModbusCRC16 — Modbus RTU CRC-16, SCL for S7-1500
FUNCTION_BLOCK "FB_ModbusCRC16"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      pData    : POINTER TO BYTE;   // pointer to first byte of frame
      uiLength : UINT;              // number of bytes to cover (without CRC)
   END_VAR

   VAR_OUTPUT
      wCRC     : WORD;              // Modbus CRC, LO/HI ordered for on-wire use
   END_VAR

   VAR
      iCRC : WORD;
      i    : INT;
      j    : INT;
      bTmp : BYTE;
      bIn  : BYTE;
   END_VAR

BEGIN
   iCRC := 16#FFFF;
   FOR i := 0 TO UINT_TO_INT(uiLength) - 1 DO
      bIn  := pData[i];
      iCRC := iCRC XOR WORD#16#0000 OR bIn;
      FOR j := 0 TO 7 DO
         IF (iCRC AND 16#0001) <> 0 THEN
            iCRC := SHR(IN := iCRC, N := 1) XOR 16#A001;
         ELSE
            iCRC := SHR(IN := iCRC, N := 1);
         END_IF;
      END_FOR;
   END_FOR;
   // Swap LO/HI for Modbus on-wire order
   bTmp := WORD_TO_BYTE(SHR(IN := iCRC, N := 8) AND 16#00FF);
   wCRC := SHL(IN := WORD#16#0000 OR WORD_TO_BYTE(iCRC AND 16#00FF), N := 8)
         OR WORD#16#0000 OR bTmp;
END_FUNCTION_BLOCK

Stand-alone Test Vector

The standard Modbus test vector 01 03 00 00 00 0A must yield CRC C5CD, on the wire as CD C5. The block above produces wCRC = 16#CDC5, which when written to a byte array as Lo = wCRC.B0 and Hi = wCRC.B1 gives CD C5 on the wire. Verify this once with an HMI tag or a watch table before relying on the routine on a live system.

Configuring TSEND_C and TRCV_C in TIA Portal

TSEND_C and TRCV_C are the Open User Communication blocks used for the TCP stream. TSEND_C establishes the connection, sends, and disconnects in a single block; TRCV_C receives. They are documented in the TIA Portal help under Communication > Open User Communication and in the S7-1500 System Manual.

Connection Parameters

Parameter Value (example) Note
REQ (TSEND_C) Rising edge starts a send job Use a one-shot from the request trigger
ID 1 (must match for TSEND_C and TRCV_C) Local connection identifier
CONNECT TCON_IP_V4 data block Auto-generated by the TIA Portal wizard
DATA Pointer to the frame DB (e.g. P#DB100.DBX0.0 BYTE 8) Length taken from the LEN input, not the data type
LEN Total frame length including CRC, e.g. 8 0 = use the full DATA area
CONT TRUE Keep connection open between requests
DONE / BUSY / ERROR Status outputs Done = 1 means a single job finished; do not use as connection status

TCON_IP_V4 (Connection Description)

Use the wizard ("Add new connection" in the program editor) to generate the connection DB. The fields the CPU program needs to verify are:

  • InterfaceId: HW identifier of the PROFINET interface (e.g. 64 for the first PN port of a CPU 1515-2 PN).
  • ID: Connection ID — choose a value that is not used by any other Open User Communication block on the same CPU.
  • ConnectionType: 16#0B for TCP (decimal 11). Do not use 16#0C (ISO-on-TCP) or 16#0D (UDP); the EKI-1521 is a plain TCP listener.
  • ActiveEstablished: TRUE — the S7-1500 is the TCP client and opens the socket to the gateway.
  • RemoteAddress: 192.168.10.20 (EKI-1521).
  • RemotePort: 502.
  • LocalPort: 0 (any free local port, assigned by the CPU).

Building the Request Frame

The request is constructed in a data block of type Array[0..255] of Byte. The function block FB_BuildRequest below fills the array for a generic read-holding-registers (FC 0x03) request. Adapt the same pattern for any other function code.

// FB_BuildReadHoldingRegs — populates a Modbus RTU request frame
FUNCTION_BLOCK "FB_BuildReadHoldingRegs"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      bSlave     : BYTE;   // slave address 1..247
      wStartAddr : WORD;   // first register address
      wQuantity  : WORD;   // number of registers, 1..125
   END_VAR

   VAR_IN_OUT
      aFrame : ARRAY[0..255] OF BYTE;
   END_VAR

   VAR_OUTPUT
      uiFrameLen : UINT;   // total length including CRC, ready for TSEND_C
   END_VAR

   VAR
      fbCRC : FB_ModbusCRC16;
   END_VAR

BEGIN
   aFrame[0] := bSlave;                       // Slave address
   aFrame[1] := 16#03;                        // Function code 03 = read holding regs
   aFrame[2] := WORD_TO_BYTE(SHR(IN := wStartAddr, N := 8) AND 16#00FF);  // Start HI
   aFrame[3] := WORD_TO_BYTE(wStartAddr       AND 16#00FF);              // Start LO
   aFrame[4] := WORD_TO_BYTE(SHR(IN := wQuantity,  N := 8) AND 16#00FF);  // Qty   HI
   aFrame[5] := WORD_TO_BYTE(wQuantity        AND 16#00FF);              // Qty   LO

   fbCRC(pData  := ADR(aFrame[0]),
         uiLength := 6,
         wCRC   => aFrame[7]);
   // CRC bytes occupy indices 6 and 7 in LO/HI order
   aFrame[6] := aFrame[7] AND 16#00FF;        // CRC LO
   aFrame[7] := (SHR(IN := aFrame[7], N := 8) AND 16#00FF);  // CRC HI

   uiFrameLen := 8;
END_FUNCTION_BLOCK
Endianness trap. The W#16#0100 register address (256) is sent as bytes 01 00, with the high byte first. The same convention applies to the quantity field. The CRC is the opposite: it is sent low byte first. The code above explicitly splits both halves to make the byte order obvious. Do not rely on WORD_TO_BLOCK_DB for this.

Receiving and Parsing the Response

The TCP receive side uses the same connection. The complete response from a successful FC 0x03 read of N registers is 1 + 1 + 1 + 2·N + 2 = 2·N + 5 bytes. An exception response is 5 bytes. Set LEN = 5 on TRCV_C to get the function code and exception sub-code in one shot; if the function code is below 0x80 and the response is not an exception, issue a second TRCV_C call with LEN = (2·N − 1) to fetch the data payload. A simpler approach is to set LEN = 255 and rely on RCV_LEN on TRCV_C to tell you how many bytes arrived.

// FB_ParseResponse — validates CRC and extracts the register payload
FUNCTION_BLOCK "FB_ParseResponse"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      uiBytesReceived : UINT;
   END_VAR

   VAR_IN_OUT
      aRx : ARRAY[0..255] OF BYTE;
   END_VAR

   VAR_OUTPUT
      bOK         : BOOL;
      bException  : BOOL;
      bExCode     : BYTE;
      wValue      : ARRAY[0..124] OF WORD;
      uiValueCnt  : UINT;
   END_VAR

   VAR_TEMP
      i : INT;
      wComputed : WORD;
      wOnWire   : WORD;
   END_VAR

BEGIN
   bOK := FALSE;
   bException := FALSE;
   bExCode := 16#00;
   uiValueCnt := 0;

   IF uiBytesReceived < 5 THEN
      RETURN;
   END_IF;

   // Check for exception: high bit set in function code
   IF (aRx[1] AND 16#80) <> 0 THEN
      bException := TRUE;
      bExCode    := aRx[2];
      RETURN;
   END_IF;

   // Recompute CRC over the frame minus the trailing 2 bytes
   // and compare to the on-wire CRC.
   "FB_ModbusCRC16"(pData    := ADR(aRx[0]),
                    uiLength := uiBytesReceived - 2,
                    wCRC     => wComputed);
   wOnWire := SHL(IN := WORD#16#0000 OR aRx[uiBytesReceived - 1], N := 8)
            OR WORD#16#0000 OR aRx[uiBytesReceived - 2];

   IF wComputed <> wOnWire THEN
      bOK := FALSE;
      RETURN;
   END_IF;

   // For FC 0x03 the byte count is aRx[2]; data starts at aRx[3]
   uiValueCnt := UINT#0;
   FOR i := 0 TO (aRx[2] / 2) - 1 DO
      wValue[i] := SHL(IN := WORD#16#0000 OR aRx[3 + 2 * i],     N := 8)
                OR WORD#16#0000 OR aRx[3 + 2 * i + 1];
      uiValueCnt := uiValueCnt + 1;
   END_FOR;

   bOK := TRUE;
END_FUNCTION_BLOCK

Complete Code Example — Cyclic Read with Timeout

The snippet below ties TSEND_C, TRCV_C, and the two helper function blocks into a single request/response cycle that runs every tCycle ms. Drop it into a cyclic OB (e.g. OB1 or a watchdog OB) and adjust the timer preset to suit your scan rate.

// MainModbusRTU_OB1 — cyclic read of 10 holding regs from slave 1, start 0
DATA_BLOCK "dbModbus" { S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   STRUCT
      aTx        : ARRAY[0..255] OF BYTE;
      aRx        : ARRAY[0..255] OF BYTE;
      wRegisters : ARRAY[0..124]  OF WORD;
      uiTxLen    : UINT;
      uiRxLen    : UINT;
      bTrigger   : BOOL;
      bBusy      : BOOL;
      bDone      : BOOL;
      bError     : BOOL;
      wStatus    : WORD;
   END_STRUCT;
END_DATA_BLOCK

// In OB1:
IF "dbModbus".bTrigger AND NOT "dbModbus".bBusy THEN
   "FB_BuildReadHoldingRegs"(bSlave     := 16#01,
                             wStartAddr := 16#0000,
                             wQuantity  := 16#000A,
                             aFrame     := "dbModbus".aTx,
                             uiFrameLen => "dbModbus".uiTxLen);
   "TSEND_C"(REQ   := TRUE,
             ID    := 1,
             LEN   := "dbModbus".uiTxLen,
             DATA  := "dbModbus".aTx,
             CONT  := TRUE,
             DONE  => "dbModbus".bDone,
             BUSY  => "dbModbus".bBusy,
             ERROR => "dbModbus".bError,
             STATUS => "dbModbus".wStatus);
END_IF;

// Fire TRCV_C alongside the SEND so the receive is armed
"TRCV_C"(EN_R   := "dbModbus".bTrigger,
         ID     := 1,
         LEN    := 255,
         DATA   := "dbModbus".aRx,
         CONT   := TRUE,
         NDR    => ,                // raise when bytes received
         BUSY   => ,
         ERROR  => ,
         STATUS => ,
         RCV_LEN => "dbModbus".uiRxLen);

IF "dbModbus".uiRxLen > 0 THEN
   "FB_ParseResponse"(uiBytesReceived := "dbModbus".uiRxLen,
                      aRx             := "dbModbus".aRx,
                      bOK             => ,
                      bException      => ,
                      bExCode         => ,
                      wValue          := "dbModbus".wRegisters,
                      uiValueCnt      => );
   "dbModbus".uiRxLen := 0;
   "dbModbus".bTrigger := FALSE;
END_IF;
Timeout strategy. The Open User Communication blocks do not implement an application-layer timeout. If TRCV_C never sees bytes, it will wait indefinitely. Drive TRCV_C with an IEC_TIMER (TON with PT = 1000 ms typical for serial-over-TCP) and call TRCV_C again with EN_R = FALSE to cancel, or set CONT = FALSE and use TDISCON to close the socket on timeout. The CPU will then re-establish the connection on the next cycle.

Connecting Through the EKI-1521 Gateway

The Advantech EKI-1521 presents itself as a TCP server on the LAN side and an RS-232/422/485 master on the field side. The relevant configuration is reachable through the EKI-1521 web interface (default 192.168.10.20, user admin, password admin).

EKI-1521 setting Recommended value Why
Operation Mode Modbus Gateway Translates between Modbus TCP and Modbus RTU on the serial port
TCP Port 502 Default Modbus port; matches the example in this article
Serial Interface RS-485, 2-wire Matches the majority of industrial Modbus RTU slaves
Baud Rate / Parity / Stop Bits Match the slave device, e.g. 19200 / Even / 1 The frame is forwarded byte-for-byte; mismatched settings corrupt the CRC
Inter-character Timeout 3.5 character times (default) Allows the gateway to detect end-of-frame and forward the response
Slave ID Mapping Transparent (do not remap) The CPU program already puts the slave address in the RTU frame
Wiring. On a 2-wire RS-485 bus, connect A+ (or D+) on the gateway to A+ on every slave, B− to B−, and place a 120 Ω termination at both physical ends. Do not ground the shield at more than one point. If the slave count exceeds 32, fit a repeater such as the Advantech EKI-1521 or a Phoenix Contact PSI-REP.

Verification and Testing

Run the verification in this order to catch the most common issues early.

  1. Watch-table check. Force bTrigger in the watch table of the request DB. The TSEND_C should show BUSY = 1 for a few scan cycles, then DONE = 1 and STATUS = 16#0000.
  2. Wireshark capture. Filter on tcp.port == 502 && ip.addr == 192.168.10.20 and inspect the first eight bytes of the payload. They must match the slave address, function code, start address, quantity, and CRC you expect. If the payload contains an MBAP header (00 01 00 00 00 06 FF), the gateway is in Modbus TCP mode and you should switch to MB_CLIENT instead.
  3. Slave simulator cross-check. Connect the gateway to a PC running Modbus Poll or QModMaster in slave mode, set the same parameters, and confirm that the simulator sees the same request bytes the CPU is sending.
  4. CRC validation in the CPU. In the watch table, observe the computed CRC against the on-wire CRC inside FB_ParseResponse. A mismatch with the Wireshark capture almost always points to a byte-order mistake in the request assembly.
  5. Exception decoding. If the response is consistently 0x83 0x02, the slave is rejecting the start address. Use the slave's register map to correct the offset (some vendors are 0-based, others 1-based; Modbus is conventionally 0-based at the PDU layer, but the device's manual is authoritative).
  6. Long-run stability. Let the cycle run for at least 10,000 transactions. Watch for STATUS = 16#80C4 (connection terminated by remote), which indicates either a firewall reset on the EKI-1521 or a watchdog timeout on the CPU. Increase the gateway's TCP keep-alive from 30 s to 600 s if it occurs without other symptoms.

Troubleshooting Matrix

Symptom on the CPU Likely root cause Diagnostic step Fix
TSEND_C reports STATUS = 16#80C4 immediately TCP port blocked or gateway not in TCP server mode From the engineering station, Test-NetConnection 192.168.10.20 -Port 502 Open the port, enable Modbus Gateway mode on the EKI-1521
TSEND_C succeeds, TRCV_C never reports NDR EKI-1521 inter-character timeout too short for the response size Increase the gateway inter-character timeout to 50 ms Match the gateway timeout to the slave response time plus 5 ms
Response received, CRC mismatch in FB_ParseResponse Byte order on the CRC field, or wrong polynomial Compare the on-wire bytes in Wireshark with the test vector 01 03 00 00 00 0A → CD C5 Verify the LO-byte-first append in FB_BuildReadHoldingRegs
Exception 0x02 (Illegal Data Address) Register address offset wrong for the vendor Read the slave's register map; check whether it is 0-based or 1-based Adjust wStartAddr by ±1 or the vendor-specified offset
Exception 0x04 (Slave Device Failure) Serial line noise, A/B swapped, or termination missing Replace terminators, check the A+/B− polarity with a scope Re-terminate at both ends, fix polarity
TCP socket closes after every transaction CONT left at default (FALSE) on TSEND_C Watch CONT in the watch table Hard-code CONT = TRUE
Intermittent STATUS = 16#80A1 CPU interface overload or scan time too long Check OB1 scan time in the diagnostic buffer Move the Modbus logic to a slower cyclic OB (e.g. OB30 at 100 ms)
Slave receives the wrong slave address Byte 0 of the request overwritten by a stray copy Inspect the request DB just before TSEND_C fires Make sure no other code writes to aTx[0] after build

FAQ

What is the difference between Modbus RTU over TCP and Modbus TCP?

Modbus TCP adds a 7-byte MBAP header (transaction ID, protocol ID, length, unit ID) and drops the CRC-16. Modbus RTU over TCP keeps the original RTU frame structure (address + function code + data + CRC-16) and transports those raw bytes inside a TCP payload. Use the former with MB_CLIENT; use the latter with TSEND_C/TRCV_C as described in this article.

Can I use the standard MB_CLIENT instruction to talk to a device behind an EKI-1521?

Yes, but only if the EKI-1521 is set to the Modbus TCP server (transparent Modbus) operating mode, in which case it converts MBAP + PDU on the LAN side into RTU on the serial side. If the EKI-1521 is in the raw Modbus Gateway mode and forwards RTU bytes verbatim, you must build the RTU frame yourself and use TSEND_C/TRCV_C.

Why does my slave report exception 0x02 Illegal Data Address even though the address exists?

Many vendors use a 1-based register number in their documentation while Modbus PDU addressing is 0-based. Subtract 1 from the address in the documentation. If that still fails, request a contiguous block one or two registers before and after to find the actual valid range exposed by the slave firmware.

Where can I find a reference Modbus RTU test vector to verify my CRC routine?

The Modbus Organization publishes the Modbus over Serial Line specification (PI_MBUS_300.pdf), which contains a worked example: the request 01 03 00 00 00 0A must yield a CRC of C5CD, transmitted as CD C5 (low byte first). Validate this against your FB_ModbusCRC16 implementation before going live.

How do I get reliable timing on the TCP socket when the slave response is slow?

Drive TRCV_C with an IEC_TIMER (TON) preset to the expected worst-case round trip (e.g. 1000 ms for a 19200 bit/s bus with 10 registers). On timeout, call TRCV_C with EN_R = FALSE to discard the partial buffer and either re-issue the request immediately or escalate the error to the HMI alarm log.

Back to blog