Overview
The S7-1200 supports native TCP/IP communication on its PROFINET port through the Open User Communication instructions TCON, TSEND, TRCV, and TDISCON available in the TIA Portal instruction palette. These instructions allow a PC application written in C# (or any other language that exposes the Winsock API) to talk directly to the PLC over Ethernet without requiring Siemens SIMATIC NET OPC, S7-PCT, PC Access, or any third-party OPC server on the control PC.
This configuration is commonly used when:
- The control PC cannot install vendor middleware (locked image, anti-virus exclusions, or air-gapped machine).
- Data is read/written from a single DB (Data Block) with a fixed amount of bytes.
- The PC is running Windows XP SP3, Windows 7, or Windows 10 with .NET Framework 2.0+ and the application is a service or HMI replacement written in C#.
Prerequisites
| Item | Required Value / Version |
|---|---|
| CPU | S7-1200 (any DC/DC/DC or AC/DC/RLY variant with PROFINET port) |
| Firmware | V4.0 or later recommended (V2.2 minimum for TSEND_C / TRCV_C) |
| TIA Portal | V13 SP1 or later for full instruction support |
| PC | Windows XP SP3 / Windows 7 / Windows 10 with .NET Framework 2.0+ |
| IDE | Visual Studio 2008 / 2010 / 2015+ with C# language |
| Network | Direct Ethernet cable or managed switch, both on the same subnet (e.g. 192.168.0.x / 255.255.255.0) |
| DB | One optimized-access DB or standard DB that holds the payload (see Data Layout below) |
Network and IP Configuration
Set a static IP on the S7-1200 from Devices & Networks > PROFINET interface > Ethernet addresses. Pick an address that is not served by a corporate DHCP server to avoid address conflicts. A typical engineering setup uses:
- PLC IP:
192.168.0.1(Subnet:255.255.255.0) - PC IP:
192.168.0.100(Subnet:255.255.255.0)
Disable the Windows firewall on the control PC for the engineering phase, or open the TCP port you intend to use (the example below uses 2500). Siemens documentation consistently recommends unallocated ports in the IANA dynamic/private range 49152–65535 for production systems to avoid clashes with services like HTTP (80) or Siemens S7 protocol (102).
Step 1 - Create the Payload Data Block
Create a standard (non-optimized) DB. Optimized access must be disabled for direct byte-level TCP transfer, because byte offsets must be deterministic. Example for a 100-byte payload:
DATA_BLOCK "DB_CommPayload"
{ S7_Optimized_Access := 'FALSE' }
AUTHOR : EngTeam
FAMILY : Comm
VERSION : 0.1
STRUCT
Header : BYTE; // 0xAA start marker
Cmd : BYTE; // 0=read, 1=write, 2=ack
Length : WORD; // payload length excluding header/trailer
Data : ARRAY[0..91] OF BYTE; // 92-byte payload
Crc16 : WORD; // CRC-16/CCITT over Cmd..Data
Trailer : BYTE; // 0x55 end marker
END_STRUCT;
END_DATA_BLOCK
Total size: 1 + 1 + 2 + 92 + 2 + 1 = 99 bytes (rounded to 100 for transfer). The header / trailer markers and CRC make the protocol tolerant of half-received packets on noisy links.
Step 2 - Configure the TCON Connection
Drag TCON from Instructions > Communication > Open User Communication into OB1 (or a cyclic OB). TIA Portal will prompt you to create a Connection Description DB automatically. Use these parameters:
| Parameter | Value | Notes |
|---|---|---|
| ID | 1 | Connection ID (W#16#0001). Must be unique per CPU. |
| CONNECT | "TCON_Config_DB".Config | Auto-generated connection DB. |
| REQ | TRUE (first scan) | Use a rising edge of a startup flag to trigger once. |
| DONE / BUSY / ERROR | monitor only | ERROR = 0x80xx style extended error codes. |
Open the generated connection DB and edit the Interface / Configuration UDT:
InterfaceId := 64; // TCP/IP (0x40)
Id := 1; // matches TCON ID
ConnectionType := 16#0B; // TCP (0x0B = 11 decimal)
ActiveEstablished := TRUE; // PLC acts as the *active* connection establisher
RemoteAddress := '192.168.0.100'; // PC
RemotePort := 2500;
LocalPort := 0; // 0 = any free local port
BUSY = 1 forever. Set ActiveEstablished = TRUE on the PLC side and use TcpListener.AcceptTcpClient() on the PC side.Step 3 - Send and Receive with TSEND / TRCV
With the connection established, drop TSEND and TRCV into OB1.
| Block | REQ / EN_R | ID | LEN / DATA | Trigger |
|---|---|---|---|---|
| TSEND | Rising edge | 1 | LEN = 100, DATA = P#DB_CommPayload.DBX0.0 BYTE 100 | Operator button or scheduled task |
| TRCV | TRUE (enable) | 1 | LEN = 100, DATA = P#DB_CommPayload.DBX0.0 BYTE 100 | Always enabled; ADHOC mode if LEN = 0 |
TSEND/C send the entire DB region over TCP. TRCV supports ad-hoc mode by setting LEN = 0, which copies whatever bytes are currently buffered into the destination area. Use a length-prefixed protocol (as in the DB layout above) to handle partial frames.
Step 4 - TDISCON for Clean Shutdown
Add TDISCON in the startup OB (OB100) to close the connection when the CPU goes to STOP, or call it from a shutdown bit so the PC TcpListener does not throw IOException: An existing connection was forcibly closed on every restart.
"TDISCON_DB".REQ := TRUE;
"TDISCON_DB".ID := 1;
Step 5 - C# PC Application
The PC side is a standard System.Net.Sockets.TcpListener. The example below opens a listener on port 2500, accepts the S7-1200, and pumps the 100-byte DB back and forth at 100 ms intervals. This is the minimal viable C# program for a Windows XP SP3 workstation with .NET 2.0+.
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
class PlcLink
{
static TcpListener listener = new TcpListener(IPAddress.Any, 2500);
static NetworkStream ns;
static readonly object sync = new object();
static byte[] rx = new byte[100];
static byte[] tx = new byte[100];
static void Main()
{
listener.Start();
Console.WriteLine("Waiting for S7-1200 on 0.0.0.0:2500 ...");
using (TcpClient client = listener.AcceptTcpClient())
{
Console.WriteLine("PLC connected from " + client.Client.RemoteEndPoint);
ns = client.GetStream();
// Worker thread: 100 ms poll
new Thread(Pump) { IsBackground = true }.Start();
// Main thread: read raw TCP frames
while (true)
{
int n = ns.Read(rx, 0, rx.Length);
if (n == 0) break;
ParseFrame(rx, n);
}
}
}
static void Pump()
{
while (true)
{
lock (sync) BuildFrame(tx); // populate tx from PC state
ns.Write(tx, 0, tx.Length);
Thread.Sleep(100);
}
}
static void ParseFrame(byte[] b, int n)
{
if (b[0] != 0xAA || b[n - 1] != 0x55) { Console.WriteLine("Framing error"); return; }
byte cmd = b[1];
ushort len = BitConverter.ToUInt16(b, 2);
ushort crc = BitConverter.ToUInt16(b, 96);
if (crc != CrcCcitt(b, 1, 95)) { Console.WriteLine("CRC error"); return; }
switch (cmd)
{
case 0: /* read request */ break;
case 1: HandleWrite(b, len); break;
case 2: /* ack */ break;
}
}
static void BuildFrame(byte[] b)
{
b[0] = 0xAA; b[1] = 0x02; b[2] = 0; b[3] = 92;
// Fill b[4..95] from local model
ushort crc = CrcCcitt(b, 1, 95);
b[96] = (byte)(crc & 0xFF); b[97] = (byte)(crc >> 8);
b[98] = 0x55;
}
static void HandleWrite(byte[] b, int len) { /* update PC state */ }
static ushort CrcCcitt(byte[] data, int offset, int count)
{
ushort crc = 0xFFFF;
for (int i = 0; i < count; i++)
{
crc ^= (ushort)(data[offset + i] << 8);
for (int j = 0; j < 8; j++)
crc = (ushort)(((crc & 0x8000) != 0) ? (crc << 1) ^ 0x1021 : crc << 1);
}
return crc;
}
}
NetworkStream.Read and Write are not thread-safe. Use a single producer/consumer pattern or a SemaphoreSlim around the stream. The example uses a coarse lock on sync for clarity; in production, prefer a dedicated send and receive thread plus a ConcurrentQueue<byte[]>.Alternative - libnodave (No TIA Block Configuration)
For projects that need to read/write multiple DBs and I/O areas without writing a custom TCP protocol, the open-source libnodave library implements the S7 protocol (ISO-on-TCP port 102) directly from C, with .NET bindings used by many C# applications. libnodave works against the S7-1200 out of the box because the PLC keeps the S7 comm port open even when you do not configure TCON blocks.
| Attribute | Value |
|---|---|
| Project | libnodave (SourceForge mirror, maintained fork on GitHub) |
| Protocol | S7 communication (port 102, ISO-on-TCP / RFC 1006) |
| CPU side | No TIA blocks required. Default S7 server is enabled when "Permit access with PUT/GET" is set in CPU Properties > Protection > Connection mechanisms. |
| Throughput | ~30–50 DB reads/s from C# wrapper, depending on PC |
| Tested on | S7-1200 firmware 4.2, Windows XP SP3, .NET 2.0/4.0 |
Sample wrapper call (C# P/Invoke into libnodave.net.dll):
// Connect
daveConnection dc = dave.NewConnection(2, "192.168.0.1", 0, 2); // 2=S7ONLINE_TCP
dave.Connect(dc);
// Read 100 bytes from DB1 starting at byte 0
dave.ReadBytes(dc, dave.DaveDB, 1, 0, 100, out buf);
// Write 4 bytes (REAL) to DB1.DBD10
dave.WriteFloat(dc, dave.DaveDB, 1, 10, 12.34f);
dave.Disconnect(dc);
Step 6 - Enable PUT/GET on the S7-1200 (Required for libnodave)
- Open the CPU device configuration in TIA Portal.
- Select Properties > Protection > Connection mechanisms.
- Check Permit access with PUT/GET communication from remote partner.
- Download the hardware configuration to the CPU. The PLC will now answer on TCP/102 to any PC that knows the connection password (or has no password set, which is the default in protected projects).
Without this tick, libnodave (and any other third-party S7 client) will silently fail with daveResNoPeriphery or simply timeout.
Verification
Use this checklist after the program is loaded and the cable is connected:
- Link lights: Both PROFINET port LEDs on the CPU and the PC NIC are solid green. A blinking orange light indicates a speed/duplex mismatch.
-
Ping test: From a CMD window,
ping 192.168.0.1 -t. Replies must be <1 ms on a direct cable. Any value >5 ms suggests a switch loop or a duplex issue. -
TCON status: In TIA Portal's Online & Diagnostics > Status & Error, the connection ID 1 must show
ESTABLISHED. ABUSYstate that never clears means the PC is not listening; check Windows firewall and thatlistener.Start()was reached. -
TSEND done: After a rising edge on
TSEND.REQ, monitorDONEfor a single cycle.ERROR = 0x80C4means "connection terminated by remote" - the PC closed the socket. -
TRCV data: Watch
"DB_CommPayload".Headerin the watch table. It should change to16#AAwithin one cycle of the PC sending. -
Wireshark trace (optional): Capture on the PC interface and filter
tcp.port == 2500. You should see a SYN/SYN-ACK/ACK handshake, then bidirectional PSH/ACK segments of 100 bytes each.
Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| TCON stays BUSY forever | PC not listening or wrong port | Verify netstat -an | findstr 2500 on PC shows LISTENING |
| TSEND ERROR = 0x80C4 | Partner closed socket | PC threw exception; wrap Read in try/catch and call listener.Stop(); listener.Start();
|
| TRCV.NDR but data is stale | Ad-hoc mode LEN mismatch | Use fixed LEN or implement length-prefix in payload |
| libnodave timeout on connect | PUT/GET not enabled | Enable as in Step 6 |
| CRC errors on every frame | Byte-order assumption wrong | Confirm little-endian on PC matches S7-1200 byte layout |
| PLC goes to SF on download | Optimized DB and TCON conflict | Disable optimized access on payload DB |
| Only first cycle works | TSEND REQ held high; S7 only sends on rising edge | Use a 1-Hz clock bit or pulse on DONE |
Open Communication Wizard (S7-300 Reference Still Valid for S7-1200)
Siemens' Open Communication Wizard (entry ID 25209116) was originally written for S7-300/400, but the generated function blocks (FB100 through FB105 for TCP/UDP, ISO-on-TCP) are source-compatible with the S7-1200 once you re-import them into TIA Portal. The companion entry 40556214 documents the variant where an S7-300 acts as the partner to an S7-1200, which is useful as a stepping-stone to migrate older machines without rewriting the PC side.
Field-Engineering Checklist Before Going Live
- Lock the PC's IP with DHCP reservations or static config to avoid silent address drift after a router reboot.
- Set the PC's Windows power profile to High Performance and disable NIC power saving, which can drop TCP sockets after long idle periods.
- Implement a heartbeat in the payload (e.g. incrementing
WORDat byte 4) and alarm the SCADA if it freezes for > 3 s. - Run a 72-hour soak test with Wireshark capturing in rolling mode; review for retransmissions, which indicate buffer overruns on the PLC side - the fix is usually to lower the TRCV frequency or increase the receive buffer (where supported on the firmware version).
FAQ
Which S7-1200 firmware versions support TCON, TSEND, TRCV, and TDISCON natively?
Full TCON/TSEND/TRCV/TDISCON support begins at firmware V4.0. Firmware 2.x and 3.x only expose the compact variants TSEND_C and TRCV_C, which combine connection setup, send, and receive in a single block and are usually sufficient for a C# TcpListener peer.
Do I need to open TCP port 102 on the Windows firewall for libnodave?
Yes. libnodave uses the S7 protocol on ISO-on-TCP port 102, so the firewall rule must allow inbound TCP/102 from the PLC's IP. The custom TCP/2500 example above requires inbound TCP/2500 instead. Restrict the rule to the PLC's source IP for production hardening.
Can the S7-1200 act as a TCP server instead of the PC?
Yes, by setting ActiveEstablished = FALSE on the connection DB. The PC's TcpClient.Connect() then triggers the handshake. This is the more common arrangement for SCADA software and is also the topology assumed by libnodave's dave.NewConnection() with connection type TCP.
Why does TSEND report ERROR = 0x80C4 after a few hours of stable operation?
0x80C4 means the partner closed the TCP socket cleanly. The most common cause is the PC's NetworkStream being disposed by an unhandled exception in the C# code. Wrap the read loop in try/catch and re-create the TcpListener on every disconnect; also enable SO_KEEPALIVE on the socket to detect broken cables within minutes rather than hours.
Is libnodave safe to use on a production machine in 2025?
libnodave is stable for non-safety, diagnostic-class polling against S7-1200 firmware 4.x. For new projects requiring vendor support, evaluate the commercial libnodave.NET fork or Snap7. For SIL/PLT applications, treat libnodave as untrusted middleware and validate under your own functional-safety change-management process.