Configuring S7-1200 Modbus TCP Server with MB_SERVER in TIA

David Krause13 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 SIMATIC S7-1200 CPU acts as a Modbus TCP server (slave) on its onboard PROFINET interface using the MB_SERVER instruction from the "SIMATIC Modbus TCP" library. Because Modbus has no concept of S7-1200 process image inputs (%I), process image outputs (%Q), or flag bytes (%M) — it only knows about holding registers (function codes 03/06/16), input registers (04), and coils (01/02/05/15) — those areas cannot be exposed directly. MB_SERVER is given an ANY pointer to a contiguous block of words (typically the %MW area or, preferably, a shared DB). The PC then reads and writes that block through the standard 4xxxx register address space.

This reference covers the full path from TIA Portal configuration to a C/Perl/Python client on a host, and addresses the three failure modes that show up in real projects:

  • Wiring MB_HOLD_REG with a typed (BOOL, INT, ARRAY) pointer instead of an ANY pointer.
  • Attempting to read %I0.1 from Modbus and getting no response or error 80C8.
  • Mismatched unit ID (Modbus RTU slave address) and missing connection DB on the CPU.

Prerequisites

Hardware, firmware, and software
Component Required version Notes
CPU CPU 1211C / 1212C / 1214C / 1215C / 1217C, FW V4.2 or higher Modbus TCP library is bundled with FW V4.2+. FW V4.4+ recommended for TIA V17/V18 projects.
Network port Port 1 of the CPU (X1) or a CP 1243-1 Default Modbus TCP server port: 502/TCP.
TIA Portal V15.1 / V16 / V17 / V18 / V19 Match the portal version to the Modbus library revision.
Modbus TCP library "SIMATIC Modbus TCP" V3.0 or later (library ID 47757241 on Siemens support) Distributed as a global library; contains MB_SERVER, MB_CLIENT, and a connection DB type.
PC runtime Any TCP socket stack plus a Modbus client API: libmodbus, IO::Socket::Modbus, pymodbus, or NModbus For Modbus TCP the unit ID is conventionally 255 (0xFF) per the Modbus Messaging on TCP/IP Implementation Guide.
Do not confuse the Modbus TCP library with the Modbus RTU library used on the CM 1241 (RS-232/422/485) module. The RTU block set uses MB_COMM_LOAD and serial parameters, and a different address model. This article is strictly Modbus TCP.

Installing the Modbus TCP Library in TIA Portal

  1. Download the global library archive from Siemens Industry Online Support — see the entry "How do you implement a Modbus/TCP communication with S7-1200?" at support.industry.siemens.com/cs/ww/en/view/47757241.
  2. In TIA Portal, choose Options → Global libraries → Open library and select the .zip or .al13 file.
  3. Open the Master Copies folder, drag the Modbus_TCP folder into the project library pane.
  4. Copy MB_SERVER (and MB_CLIENT if the S7-1200 will also poll other slaves) into Program blocks.
  5. Open the CPU's Properties → Protection & Security → Connection mechanisms and ensure "Permit access with PUT/GET communication from remote partner" is enabled if you also want S7 communication; Modbus TCP itself does not require this flag.

Block-level reference: see chapter "Modbus TCP" in the S7-1200 Programmable Controller System Manual.

MB_SERVER Block Configuration

MB_SERVER is called from a cyclic OB (typically OB1). The block exposes the parameters in the table below. Older library revisions (V2.x) use the explicit IP octet inputs; V3.x+ moved IP filtering to the connection DB.

MB_SERVER input/output parameters
Parameter Direction Type Meaning
DISCONNECT IN BOOL TRUE forces the active connection closed. Tie to a global tag if remote disconnect is needed.
CONNECT_ID IN WORD Unique connection ID (any 1..65535 not used by another TCON block on the CPU).
IP_OCTET_1..4 IN BYTE IP filter for the remote partner. All zeros = accept any client. (V2.x libraries only.)
IP_PORT IN UINT TCP port. Default 502. Set to 0 to use the default.
MB_HOLD_REG IN ANY (pointer) Pointer to the holding-register image. This is the parameter the field report got wrong.
NDR / DR OUT BOOL New data received from client (write to MB) / data ready (read issued by client).
ERROR OUT BOOL TRUE on error.
STATUS OUT WORD Detailed status. See the table in the S7-1200 system manual.
CP_RST IN/OUT BOOL Reset request for the connection DB. Hold TRUE for one cycle to reset a stuck connection.

A typical OB1 call (SCL or LAD) wires the inputs as follows. The CONNECT_ID value of 1 matches an instance DB the library creates for you (named MB_SERVER_DB or whatever the instance DB number resolves to).

// SCL example for OB1
"MB_SERVER_DB"(
    DISCONNECT   := FALSE,
    CONNECT_ID   := 1,
    IP_OCTET_1   := 0,   // accept any client (0.0.0.0)
    IP_OCTET_2   := 0,
    IP_OCTET_3   := 0,
    IP_OCTET_4   := 0,
    IP_PORT      := 502,
    MB_HOLD_REG  := P#M1000.0 WORD 500  // 500 holding registers
);

The ANY Pointer and the 4xxxx Address Map

The single most common mistake when wiring MB_HOLD_REG is passing an ARRAY element or a typed variable. TIA Portal will compile it, but MB_SERVER interprets the pointer as raw bytes and silently reads garbage, or returns STATUS = 80C8 ("Invalid pointer"). The parameter must be a true ANY pointer of the form:

P#<byte address>.<bit address> <data type> <count>
// Examples:
P#M1000.0 WORD 500          // 500 words starting at MB1000 (the byte, not MW1000)
P#DB20.DBX0.0 WORD 500      // 500 words starting at byte 0 of DB20
P#M0.0 WORD 100             // 100 words starting at MB0

The address a Modbus client uses depends on which byte the pointer starts at and how the CPU packs the 16-bit words. For a word-aligned pointer starting at P#M0.0 WORD 500 the map is the cleanest:

Modbus register map for P#M0.0 WORD 500
Modbus address Function codes S7-1200 word Byte range in %M
40001 03 / 06 / 16 MW0 MB0 .. MB1
40002 03 / 06 / 16 MW1 MB1 .. MB2 (overlaps MW0)
40003 03 / 06 / 16 MW2 MB2 .. MB3
... ... ... ...
40500 03 / 06 / 16 MW499 MB998 .. MB999

Reading or writing 40001 on the wire puts the value into MW0. Note that Modbus addresses are protocol-zero-based; the function code carries a "quantity" or "address" field, and most client APIs subtract 1 internally so the user sees 1..500 as 40001..40500. See the Modbus Application Protocol V1.1b3 for the byte ordering rules (big-endian, MSB first).

If you start the pointer at an odd byte, the S7-1200 still maps it, but the alignment is off and the bytes in the MB area overlap between adjacent words. Always pick an even byte offset for production code. The pointer P#M1000.0 WORD 500 is the recommended pattern: it leaves the low %MW area free for the S7 program and gives you 1000 bytes of clean holding-register image from MB1000..MB1999.

Exposing %I and %Q to a Modbus Client

Modbus has no notion of S7 process image. The S7-1200 will never directly serve %I0.1 on Modbus address 10001. The standard fix is to copy the bits of interest into the holding-register image and the coils image at the start and end of OB1, or inside a cyclic interrupt OB (OB30..OB38).

Step 1 — define a shared DB (DB20) that mirrors I and Q into a structured layout:

DATA_BLOCK "ModbusImage"
{ S7_Optimized_Access := 'FALSE' }
STRUCT
    Inputs  : WORD;   // 16 bits of %I0.0..%I1.7
    Outputs : WORD;   // 16 bits of %Q0.0..%Q1.7
    Holding : ARRAY[0..499] OF WORD;   // 500 holding registers
END_STRUCT;
END_DATA_BLOCK

Step 2 — in OB1, copy the process image into the image DB before the MB_SERVER call:

// Snapshot of process image
"ModbusImage".Inputs  := "PIB0";  // byte 0 of %I
"ModbusImage".Outputs := "PQB0";  // byte 0 of %Q
// Server call
"MB_SERVER_DB"(
    IP_PORT      := 502,
    MB_HOLD_REG  := P#DB20.DBX0.0 WORD 502  // 2 words for I+Q + 500 for Holding
);

Step 3 — drive outputs from the holding register after the server call:

// Push coil write back into the process image
"PQB0" := "ModbusImage".Outputs;

On the wire, register 40001 holds ModbusImage.Inputs, 40002 holds ModbusImage.Outputs, and 40003..40502 hold ModbusImage.Holding[0..499]. The client only ever sees contiguous 4xxxx registers; the PC programmer does not need to know S7 syntax at all.

If a partner asks for coil-level access (function codes 01/05/15), wrap the bit of interest into the LSB of a holding-register word and expose that word through a 4xxxx address. The S7-1200 Modbus TCP library exposes holding registers only; pure coil access requires the Modbus RTU library on a CM 1241, or a wrapper that maps coils into the holding register image above.

Building the PC-Side Client

The PC only needs an open TCP connection to the S7-1200's IP, port 502, and the correct unit ID (255 for Modbus TCP per the implementation guide). The code samples below assume the S7-1200 IP is 192.168.0.10 and the holding-register image is at P#M1000.0 WORD 500.

C with libmodbus

#include <stdio.h>
#include <modbus/modbus.h>

int main(void) {
    modbus_t *ctx = modbus_new_tcp("192.168.0.10", 502);
    if (modbus_connect(ctx) == -1) { fprintf(stderr, "connect failed\n"); return 1; }
    modbus_set_response_timeout(ctx, 2, 0);  // 2 s

    uint16_t regs[10];
    // Read 10 holding registers starting at 40001 (= modbus address 0)
    int n = modbus_read_registers(ctx, 0, 10, regs);
    if (n == -1) { fprintf(stderr, "%s\n", modbus_strerror(errno)); return 1; }
    for (int i = 0; i < n; i++) printf("4000%d = 0x%04X\n", i + 1, regs[i]);

    // Write 0x1234 into 40003 (modbus address 2)
    uint16_t one = 0x1234;
    modbus_write_register(ctx, 2, one);

    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

Build with gcc client.c -o client -lmodbus. See the libmodbus reference for compile flags and Windows builds (use the prebuilt libmodbus.dll).

Perl with IO::Socket::Modbus

use strict;
use warnings;
use IO::Socket::Modbus::TCP;

my $client = IO::Socket::Modbus::TCP->new(
    Host    => '192.168.0.10',
    Port    => 502,
    Timeout => 3,
) or die "connect: $!";

# Read 5 holding registers starting at 40001
my @regs = $client->read_holding_registers(0, 5)
    or die "read: " . $client->status . "\n";
print join(" ", map { sprintf("0x%04X", $_) } @regs), "\n";

# Write 0x4321 to 40002
$client->write_single_register(1, 0x4321)
    or die "write: " . $client->status . "\n";

$client->close;

The numeric argument to read_holding_registers is the protocol-zero-based address (so 0 = 40001). The module is on CPAN: IO::Socket::Modbus.

Python with pymodbus (reference only)

from pymodbus.client import ModbusTcpClient

with ModbusTcpClient('192.168.0.10', port=502, timeout=2) as c:
    rr = c.read_holding_registers(address=0, count=10, slave=255)
    print(rr.registers)
    c.write_register(address=2, value=0x1234, slave=255)
pymodbus ≥ 3.0 renamed the master/slave parameter to slave=. For Modbus TCP the field is required by the library but is ignored on the wire by most S7-1200 firmware — a value of 255 matches the recommendation in the Modbus Messaging on TCP/IP Implementation Guide.

Verification

  1. Online watch table. Open the S7-1200 online, drag MB1000..MB1001 and ModbusImage.Inputs into a watch table. A read_holding_registers(0, 1) from the PC should toggle the value at MB1000/MB1001 every cycle.
  2. Wireshark. Filter on tcp.port == 502. A function-03 request appears as 10 bytes from the client (00 01 00 00 00 06 01 03 00 00 00 0A for "read 10 registers from 40001"). The reply begins with the same transaction ID, function code 0x03, byte count, then the register words MSB-first.
  3. STATUS sanity check. After a successful client call, MB_SERVER's STATUS output should read 16#0000 or 16#D281 ("connection established"). Persistent non-zero STATUS with ERROR = TRUE points to the wrong pointer type, a duplicate CONNECT_ID, or a port conflict with another TCON block.
  4. Round-trip test. From the PC, write a known pattern (e.g., 0xDEAD, 0xBEEF) to 40001 and 40002, and confirm the same value appears in the watch table at the matching %MW.

Troubleshooting Matrix

Common MB_SERVER faults and fixes
Symptom STATUS code (hex) Root cause Fix
Client times out, no reply on port 502 CPU firewall, wrong IP, or another TCON block holding port 502 Verify CPU IP with PING; check Protection & Security → Connection mechanisms; ensure no other TCON/TSEND_C block uses 502.
Client gets exception 02 ("illegal data address") Pointer count is too small for the requested address range Increase the WORD count in MB_HOLD_REG (e.g., from 100 to 500).
Client gets exception 03 ("illegal data value") Quantity or address above 9999, or off-by-one in the request Check that the client subtracts 1 from the 4xxxx address when building the frame.
ERROR = TRUE on first scan 80C8 MB_HOLD_REG is not an ANY pointer (e.g., element of an ARRAY passed directly) Wrap the variable in an explicit P#DB.DBX0.0 WORD n pointer, even if the DB is optimized-access = FALSE.
ERROR = TRUE after firmware update 80B1 CONNECT_ID collides with another TCON block on the CPU Pick a unique CONNECT_ID (commonly 1..255 for Modbus blocks).
STATUS = 8187, connection keeps dropping 8187 Client uses unit ID ≠ 255 and the partner-IP filter is not 0.0.0.0 Set IP_OCTET_1..4 to 0 (V2.x), or move the partner to the connection DB allowlist (V3.x).
Reads return zero, no ERROR Watch table not refreshed online, or %MW area is overwritten by user logic Place the holding-register area above the highest MB used by the program (e.g., MB1000) and do not write to it from OB1 except through the image DB.
PC sees wrong byte order Modbus is big-endian; the S7-1200 stores words in little-endian Swap the two bytes of every received word on the PC side, or use a client API that has a "byte swap" flag (libmodbus 3.1.5+, pymodbus byteorder=little-endian).

Production Considerations

  • Cycle time. MB_SERVER adds roughly 2..4 ms per active TCP connection on a CPU 1214C. Keep the holding-register image under 1000 words for headroom; use a CP 1243-1 if the cycle budget is tight.
  • Security. The S7-1200 Modbus TCP server has no authentication. Put the PLC behind a firewall, on a dedicated VLAN, or in front of a CP 1243-1 with a static IP allowlist. The Modbus TCP library in TIA V17+ adds a per-connection IP allowlist in the connection DB.
  • Optimised DBs. If the image DB uses "optimised block access" (default for new DBs in TIA V14+), the absolute pointer P#DB20.DBX0.0 is rejected. Either uncheck the optimised-access flag on the DB, or use symbolic pointers by selecting the DB tag at the MB_HOLD_REG input and let the compiler generate the ANY.
  • Data type width. Modbus registers are 16-bit unsigned. To move a 32-bit REAL, place it in two adjacent registers and reorder bytes in the PC. The S7-1200 side is little-endian; most PC languages are little-endian on x86, so byte-swap is usually a no-op for words but a manual swap for dwords.
  • Firmware compatibility. Always read the library release notes that ship with the .zip file. V3.0 raised MB_HOLD_REG from 200 to 2000 words and added TLS for the V3.2 release on FW V4.5.

FAQ

Why can't my PC read %I0.1 directly on Modbus address 10001?

Modbus has no knowledge of the S7 process image. MB_SERVER only exposes a contiguous block of 16-bit words declared by the MB_HOLD_REG ANY pointer. Copy %I0.1 into a bit of a word in the holding-register image (or into a shared DB mapped to it) at the start of OB1, then read the resulting 4xxxx address from the PC.

What is the correct format for the MB_HOLD_REG input?

It must be an ANY pointer such as P#M1000.0 WORD 500 or P#DB20.DBX0.0 WORD 500. Passing a typed variable, an ARRAY element, or a tag name without the P# prefix results in STATUS 80C8 ("Invalid pointer") at runtime, even though the compiler accepts it.

Do I need MB_CLIENT to act as a Modbus server?

No. MB_SERVER alone is enough for the S7-1200 to act as a server/slave. MB_CLIENT is only required if the CPU must also poll other Modbus devices (typically energy meters or I/O couplers).

Which unit ID should the PC use for Modbus TCP?

Use unit ID 255 (0xFF) for Modbus TCP, per the Modbus Messaging on TCP/IP Implementation Guide. The S7-1200 ignores the unit ID on TCP connections unless the connection DB has been configured for RTU-over-TCP bridging.

How many holding registers can MB_SERVER expose?

Up to 2000 words in the V3.x library (FW V4.4+). Older V2.x libraries cap at 200 words. The exact maximum is documented in the library release notes that accompany the .zip download.

Why does my watch table show the correct value but the PC sees zero?

You are almost certainly reading from a different byte range than the one declared in MB_HOLD_REG. Verify with a Wireshark capture: the client request shows the start address and quantity, and the reply shows the byte count and the register payload. Compare that to the address declared in the ANY pointer.

Back to blog