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_REGwith a typed (BOOL, INT, ARRAY) pointer instead of an ANY pointer. - Attempting to read
%I0.1from Modbus and getting no response or error 80C8. - Mismatched unit ID (Modbus RTU slave address) and missing connection DB on the CPU.
Prerequisites
| 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. |
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
- 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.
- In TIA Portal, choose Options → Global libraries → Open library and select the .zip or .al13 file.
- Open the Master Copies folder, drag the Modbus_TCP folder into the project library pane.
- Copy
MB_SERVER(andMB_CLIENTif the S7-1200 will also poll other slaves) into Program blocks. - 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.
| 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 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)
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
-
Online watch table. Open the S7-1200 online, drag
MB1000..MB1001andModbusImage.Inputsinto a watch table. Aread_holding_registers(0, 1)from the PC should toggle the value at MB1000/MB1001 every cycle. -
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 0Afor "read 10 registers from 40001"). The reply begins with the same transaction ID, function code 0x03, byte count, then the register words MSB-first. -
STATUS sanity check. After a successful client call,
MB_SERVER'sSTATUSoutput should read16#0000or16#D281("connection established"). Persistent non-zero STATUS with ERROR = TRUE points to the wrong pointer type, a duplicateCONNECT_ID, or a port conflict with another TCON block. -
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
| 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_SERVERadds 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.0is rejected. Either uncheck the optimised-access flag on the DB, or use symbolic pointers by selecting the DB tag at theMB_HOLD_REGinput 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_REGfrom 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.