Skip to main content

ControlForge Siemens S7 Protocol Guide

James M. Belcher Founder, JMB Technical Services LLC June 2026 | ControlForge v1.0.1064


1. Architecture Overview

ControlForge implements a complete Siemens S7 communication stack — both client and server — callable directly from IEC 61131-3 Structured Text. No external libraries, no TIA Portal add-ons, no code generation. You create connections, read/write data blocks and process image areas, and run an S7 server for testing — all with plain function calls in your ST programs.

RoleFunctionsUse Case
ClientS7_CLIENT_CREATE / S7_READ_DB_* / S7_WRITE_DB_* / S7_READ_I / S7_WRITE_MRead/write DB blocks, inputs, outputs, markers on S7-300/400/1200/1500 PLCs
ServerS7_SERVER_CREATE / S7_SERVER_SET_DB / S7_SERVER_GET_MSimulate an S7 PLC for TIA Portal testing, HMI development, or protocol bridging

Both roles can run simultaneously. A single ControlForge instance can poll two S7-1500 PLCs as a client while serving data blocks to a WinCC HMI — all from the same ST program.

System Diagram

S7 Data Model

Siemens PLCs organize memory into distinct areas:

AreaS7 CodeAccessDescription
Data Blocks (DB)0x84Read/WriteUser-defined data storage — the primary way to exchange structured data
Inputs (I / PE)0x81Read-Only*Physical input image (sensors, switches)
Outputs (Q / PA)0x82Read/Write*Physical output image (actuators, valves)
Markers (M / Flags)0x83Read/WriteInternal memory bits/bytes — often used for HMI exchange

DB blocks are king: In modern S7 programming, data blocks are the standard interface between PLC and external systems. Inputs and outputs are typically mapped into DBs by the PLC program. Direct I/Q access is useful for diagnostics and simple configurations.

S7 Connection Parameters

Every S7 connection requires rack and slot numbers that identify which CPU to talk to:

PLC FamilyRackSlotNotes
S7-30002CPU always in slot 2
S7-40002-17Check hardware config in STEP 7
S7-120000Single slot, rack 0
S7-150000Single slot, rack 0

S7-1200/1500 access: These PLCs require PUT/GET communication to be enabled in TIA Portal: Device configuration > Protection & Security > Connection mechanisms > Permit access with PUT/GET. Without this setting, connection attempts will be rejected.


2. Client Functions

The S7 client connects to Siemens PLCs over ISO-on-TCP (port 102) and performs typed read/write operations on data blocks and process image areas.

2.1 Connection Management

S7_CLIENT_CREATE — Create Named Connection

ParamTypeRequiredDescription
nameSTRINGYesUnique connection name
hostSTRINGYesIP address or hostname of the S7 PLC
rackINTYesPLC rack number
slotINTYesPLC slot number
portINTNoTCP port (default 102)
timeout_msINTNoConnection timeout in milliseconds (default 5000)
poll_rate_msINTNoBackground poll interval in milliseconds (default 100)

Returns: BOOL — TRUE if the connection was created successfully.

(* Connect to S7-1500 — rack 0, slot 0 *)
ok := S7_CLIENT_CREATE('plc1', '10.0.0.34', 0, 0);

(* Connect to S7-300 — rack 0, slot 2, custom timeout *)
ok := S7_CLIENT_CREATE('plc2', '10.0.0.35', 0, 2, 102, 3000);

(* Connect with fast polling for time-critical data *)
ok := S7_CLIENT_CREATE('plc3', '10.0.0.36', 0, 0, 102, 5000, 50);

Named connections: Every S7 client connection has a unique string name. This name is used in all subsequent calls. Create one connection per PLC — multiple connections to the same PLC waste resources and may hit the PLC's connection limit.

S7_CLIENT_CONNECT — Establish S7 Connection

ParamTypeDescription
nameSTRINGConnection name from S7_CLIENT_CREATE

Returns: BOOL — TRUE if connected successfully.

ok := S7_CLIENT_CONNECT('plc1');

Connection sequence: S7 connection involves three steps: TCP connect, ISO-on-TCP COTP negotiation, and S7 session setup. S7_CLIENT_CONNECT handles all three. If any step fails, the function returns FALSE.

S7_CLIENT_DISCONNECT — Close S7 Connection

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if disconnected successfully.

ok := S7_CLIENT_DISCONNECT('plc1');

S7_CLIENT_IS_CONNECTED — Check Connection State

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if the S7 session is active.

IF NOT S7_CLIENT_IS_CONNECTED('plc1') THEN
S7_CLIENT_CONNECT('plc1');
END_IF;

S7_CLIENT_DELETE — Remove Connection

ParamTypeDescription
nameSTRINGConnection name

Returns: BOOL — TRUE if the connection was deleted.

S7_CLIENT_DISCONNECT('plc1');
ok := S7_CLIENT_DELETE('plc1');

S7_CLIENT_LIST — List All Connections

Returns: []STRING — Array of all client connection names.

clients := S7_CLIENT_LIST();
(* Returns: ['plc1', 'plc2'] *)

S7_CLIENT_GET_STATS — Connection Statistics

ParamTypeDescription
nameSTRINGConnection name

Returns: MAP — Statistics including request count, response count, error count, and poll metrics.

stats := S7_CLIENT_GET_STATS('plc1');
(* Returns: {"requests": 12450, "responses": 12448, "errors": 2,
"poll_count": 8200, "avg_poll_ms": 12} *)

Error tracking: Compare requests vs. responses to detect communication problems. A growing error count may indicate network issues, PLC CPU stop, or resource limits on the PLC.

Example: Connection Lifecycle

PROGRAM POU_S7Init
VAR
state : INT := 0;
ok : BOOL;
retry_count : INT := 0;
END_VAR

CASE state OF
0: (* Create connection to S7-1500 *)
ok := S7_CLIENT_CREATE('plc1', '10.0.0.34', 0, 0);
IF ok THEN
state := 1;
END_IF;

1: (* Connect *)
ok := S7_CLIENT_CONNECT('plc1');
IF ok THEN
retry_count := 0;
state := 10;
ELSE
retry_count := retry_count + 1;
IF retry_count > 5 THEN
state := 99; (* Fault *)
END_IF;
END_IF;

10: (* Running — read/write in other programs *)
IF NOT S7_CLIENT_IS_CONNECTED('plc1') THEN
state := 1; (* Reconnect *)
END_IF;

99: (* Fault — connection failed *)
(* Log error, wait for operator intervention *)
END_CASE;
END_PROGRAM

2.2 DB Read Functions

Data block reads are the primary way to get data from an S7 PLC. Each function reads a specific data type at a byte address within a numbered DB.

S7_READ_DB_BYTE — Read Unsigned Byte (USINT)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset within the DB

Returns: INT — Unsigned 8-bit value (0-255).

(* Read byte at DB10.DBB0 *)
value := S7_READ_DB_BYTE('plc1', 10, 0);

S7_READ_DB_WORD — Read Unsigned Word (UINT)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset (must be even for word alignment)

Returns: INT — Unsigned 16-bit value (0-65535).

(* Read word at DB10.DBW2 *)
value := S7_READ_DB_WORD('plc1', 10, 2);

S7_READ_DB_DWORD — Read Unsigned Double Word (UDINT)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset (should be 4-byte aligned)

Returns: DINT — Unsigned 32-bit value.

(* Read double word at DB10.DBD4 *)
value := S7_READ_DB_DWORD('plc1', 10, 4);

S7_READ_DB_INT — Read Signed Integer (INT, 16-bit)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset

Returns: INT — Signed 16-bit value (-32768 to 32767).

(* Read signed integer at DB10.DBW8 *)
temperature := S7_READ_DB_INT('plc1', 10, 8);
(* Returns: -15 for a below-zero temperature *)

S7_READ_DB_DINT — Read Signed Double Integer (DINT, 32-bit)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset

Returns: DINT — Signed 32-bit value.

(* Read signed 32-bit integer at DB10.DBD10 *)
encoder_count := S7_READ_DB_DINT('plc1', 10, 10);
(* Returns: -142857 *)

S7_READ_DB_REAL — Read Floating Point (REAL, 32-bit)

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset

Returns: REAL — IEEE 754 single-precision float.

(* Read real at DB10.DBD14 — motor speed in RPM *)
speed := S7_READ_DB_REAL('plc1', 10, 14);
(* Returns: 1487.5 *)

Byte order: Siemens PLCs use big-endian byte order. ControlForge handles the byte-swapping automatically — you always get native values.

S7_READ_DB_BOOL — Read Single Bit

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset
bitINTBit number within the byte (0-7)

Returns: BOOL — TRUE or FALSE.

(* Read DB10.DBX0.0 — first bit of first byte *)
motor_running := S7_READ_DB_BOOL('plc1', 10, 0, 0);

(* Read DB10.DBX0.3 — fourth bit of first byte *)
alarm_active := S7_READ_DB_BOOL('plc1', 10, 0, 3);

(* Read DB10.DBX1.7 — eighth bit of second byte *)
limit_reached := S7_READ_DB_BOOL('plc1', 10, 1, 7);

Bit addressing in S7: Bit 0 is the least significant bit. DBX0.0 is byte 0, bit 0. DBX0.7 is byte 0, bit 7. DBX1.0 is byte 1, bit 0. This matches the TIA Portal variable table layout exactly.

Example: Reading a Complete DB Structure

PROGRAM POU_ReadDB
VAR
(* Mapped to S7 PLC DB10 layout:
Offset 0: BOOL - motor_running (DBX0.0)
Offset 0: BOOL - fault_active (DBX0.1)
Offset 2: INT - speed_setpoint (DBW2)
Offset 4: INT - speed_actual (DBW4)
Offset 6: REAL - temperature (DBD6)
Offset 10: REAL - pressure (DBD10)
Offset 14: DINT - total_cycles (DBD14)
*)
motor_running : BOOL;
fault_active : BOOL;
speed_setpoint : INT;
speed_actual : INT;
temperature : REAL;
pressure : REAL;
total_cycles : DINT;
END_VAR

(* Read all values from DB10 *)
motor_running := S7_READ_DB_BOOL('plc1', 10, 0, 0);
fault_active := S7_READ_DB_BOOL('plc1', 10, 0, 1);
speed_setpoint := S7_READ_DB_INT('plc1', 10, 2);
speed_actual := S7_READ_DB_INT('plc1', 10, 4);
temperature := S7_READ_DB_REAL('plc1', 10, 6);
pressure := S7_READ_DB_REAL('plc1', 10, 10);
total_cycles := S7_READ_DB_DINT('plc1', 10, 14);
END_PROGRAM

2.3 DB Write Functions

Each write function mirrors its read counterpart. All return BOOL — TRUE if the write was acknowledged by the PLC.

S7_WRITE_DB_BYTE — Write Unsigned Byte

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset
valueINTValue to write (0-255)

Returns: BOOL — TRUE if write succeeded.

ok := S7_WRITE_DB_BYTE('plc1', 10, 0, 128);

S7_WRITE_DB_WORD — Write Unsigned Word

ok := S7_WRITE_DB_WORD('plc1', 10, 2, 5000);

S7_WRITE_DB_DWORD — Write Unsigned Double Word

ok := S7_WRITE_DB_DWORD('plc1', 10, 4, 1000000);

S7_WRITE_DB_INT — Write Signed Integer (16-bit)

(* Write speed setpoint — signed allows negative values *)
ok := S7_WRITE_DB_INT('plc1', 10, 2, -500);

S7_WRITE_DB_DINT — Write Signed Double Integer (32-bit)

ok := S7_WRITE_DB_DINT('plc1', 10, 10, 142857);

S7_WRITE_DB_REAL — Write Floating Point

(* Write temperature setpoint *)
ok := S7_WRITE_DB_REAL('plc1', 10, 14, 72.5);

S7_WRITE_DB_BOOL — Write Single Bit

ParamTypeDescription
nameSTRINGConnection name
dbINTData block number
byteAddrINTByte offset
bitINTBit number (0-7)
valueBOOLTRUE or FALSE

Returns: BOOL — TRUE if write succeeded.

(* Set motor run command — DB10.DBX0.0 *)
ok := S7_WRITE_DB_BOOL('plc1', 10, 0, 0, TRUE);

(* Clear alarm acknowledge — DB10.DBX0.1 *)
ok := S7_WRITE_DB_BOOL('plc1', 10, 0, 1, FALSE);

Write atomicity: Each S7_WRITE_DB call is a separate S7 protocol transaction. If you need to write multiple values atomically (e.g., a setpoint and its enable bit), write them to a staging DB and have the PLC program copy them in a single scan.

Example: Writing Commands to a PLC

PROGRAM POU_WriteDB
VAR
ok : BOOL;
speed_cmd : INT := 1500;
temp_setpoint : REAL := 72.5;
start_cmd : BOOL := FALSE;
END_VAR

(* Write command values to DB20 *)
ok := S7_WRITE_DB_INT('plc1', 20, 0, speed_cmd);
ok := S7_WRITE_DB_REAL('plc1', 20, 2, temp_setpoint);
ok := S7_WRITE_DB_BOOL('plc1', 20, 6, 0, start_cmd);
END_PROGRAM

2.4 Area Read Functions (Inputs, Outputs, Markers)

These functions read directly from the PLC's process image areas — inputs (I), outputs (Q), and markers/flags (M). Byte-level reads return arrays for reading multiple consecutive bytes.

S7_READ_I — Read Input Bytes

ParamTypeDescription
nameSTRINGConnection name
byteAddrINTStarting byte offset
countINTNumber of bytes to read

Returns: []INT — Array of byte values (0-255 each).

(* Read 4 input bytes starting at IB0 *)
inputs := S7_READ_I('plc1', 0, 4);
(* inputs[0] = IB0, inputs[1] = IB1, inputs[2] = IB2, inputs[3] = IB3 *)

S7_READ_Q — Read Output Bytes

(* Read 2 output bytes starting at QB0 *)
outputs := S7_READ_Q('plc1', 0, 2);

S7_READ_M — Read Marker Bytes

(* Read 8 marker bytes starting at MB0 *)
markers := S7_READ_M('plc1', 0, 8);

S7_READ_I_BOOL — Read Input Bit

ParamTypeDescription
nameSTRINGConnection name
byteAddrINTByte offset
bitINTBit number (0-7)

Returns: BOOL — Input state.

(* Read I0.0 — first input bit *)
sensor := S7_READ_I_BOOL('plc1', 0, 0);

(* Read I1.5 — second byte, bit 5 *)
limit_sw := S7_READ_I_BOOL('plc1', 1, 5);

S7_READ_Q_BOOL — Read Output Bit

(* Read Q0.0 — first output bit *)
valve_state := S7_READ_Q_BOOL('plc1', 0, 0);

S7_READ_M_BOOL — Read Marker Bit

(* Read M0.0 — first marker bit *)
hmi_flag := S7_READ_M_BOOL('plc1', 0, 0);

(* Read M10.3 — commonly used for HMI handshake bits *)
ack_bit := S7_READ_M_BOOL('plc1', 10, 3);

Example: Reading Process Image

PROGRAM POU_ReadIO
VAR
(* Digital inputs *)
start_button : BOOL;
stop_button : BOOL;
e_stop : BOOL;
guard_door : BOOL;

(* Markers for HMI exchange *)
hmi_mode : INT;
marker_bytes : ARRAY[0..3] OF INT;
END_VAR

(* Read individual input bits *)
start_button := S7_READ_I_BOOL('plc1', 0, 0);
stop_button := S7_READ_I_BOOL('plc1', 0, 1);
e_stop := S7_READ_I_BOOL('plc1', 0, 2);
guard_door := S7_READ_I_BOOL('plc1', 0, 3);

(* Read marker area as bytes *)
marker_bytes := S7_READ_M('plc1', 0, 4);
END_PROGRAM

2.5 Area Write Functions (Outputs, Markers)

S7_WRITE_M — Write Marker Bytes

ParamTypeDescription
nameSTRINGConnection name
byteAddrINTStarting byte offset
values[]INTArray of byte values to write

Returns: BOOL — TRUE if write succeeded.

(* Write 2 bytes to MB0 and MB1 *)
ok := S7_WRITE_M('plc1', 0, [255, 128]);

S7_WRITE_Q — Write Output Bytes

(* Write 1 byte to QB0 *)
ok := S7_WRITE_Q('plc1', 0, [16#FF]);

Direct output writes: Writing to the output area (Q) directly overrides the PLC program's output image. This can cause unexpected actuator behavior. Use with extreme caution — in production, write to marker or DB areas and let the PLC program control the outputs.

S7_WRITE_M_BOOL — Write Marker Bit

ParamTypeDescription
nameSTRINGConnection name
byteAddrINTByte offset
bitINTBit number (0-7)
valueBOOLTRUE or FALSE

Returns: BOOL — TRUE if write succeeded.

(* Set M0.0 — handshake bit to PLC *)
ok := S7_WRITE_M_BOOL('plc1', 0, 0, TRUE);

(* Clear M10.7 — reset flag *)
ok := S7_WRITE_M_BOOL('plc1', 10, 7, FALSE);

S7_WRITE_Q_BOOL — Write Output Bit

(* Set Q0.0 — first output *)
ok := S7_WRITE_Q_BOOL('plc1', 0, 0, TRUE);

2.6 Background Polling

S7_ADD_POLL — Register a Polled Data Range

Adds a memory area to the background poll list. Polled data is refreshed automatically at the rate specified in S7_CLIENT_CREATE (the poll_rate_ms parameter), so subsequent reads return cached values without blocking.

ParamTypeDescription
nameSTRINGConnection name
areaSTRINGMemory area: 'DB', 'I', 'Q', or 'M'
dbNumberINTDB number (0 for I/Q/M areas)
byteAddrINTStarting byte offset
byteCountINTNumber of bytes to poll

Returns: BOOL — TRUE if the poll entry was added.

(* Poll DB10 bytes 0-19 in the background *)
ok := S7_ADD_POLL('plc1', 'DB', 10, 0, 20);

(* Poll input bytes 0-7 *)
ok := S7_ADD_POLL('plc1', 'I', 0, 0, 8);

(* Poll marker bytes 0-15 *)
ok := S7_ADD_POLL('plc1', 'M', 0, 0, 16);

Polling vs. on-demand reads: Without polling, each S7_READ_DB_* call generates a network request (5-20 ms round-trip). With polling, a background goroutine refreshes the data at the configured rate, and reads return instantly from the cache. Use polling for frequently-read data; use on-demand reads for infrequent or diagnostic access.

Example: Polled Data Access

PROGRAM POU_PolledRead
VAR
state : INT := 0;
ok : BOOL;
speed : REAL;
temp : REAL;
running : BOOL;
END_VAR

CASE state OF
0: (* Setup connection and polling *)
ok := S7_CLIENT_CREATE('plc1', '10.0.0.34', 0, 0, 102, 5000, 50);
IF ok THEN state := 1; END_IF;

1: (* Connect *)
ok := S7_CLIENT_CONNECT('plc1');
IF ok THEN state := 2; END_IF;

2: (* Register poll ranges *)
S7_ADD_POLL('plc1', 'DB', 10, 0, 20);
state := 10;

10: (* Running — reads return cached polled data, zero latency *)
speed := S7_READ_DB_REAL('plc1', 10, 0);
temp := S7_READ_DB_REAL('plc1', 10, 4);
running := S7_READ_DB_BOOL('plc1', 10, 8, 0);

IF NOT S7_CLIENT_IS_CONNECTED('plc1') THEN
state := 1;
END_IF;
END_CASE;
END_PROGRAM

3. Server Functions

The S7 server emulates a Siemens PLC, accepting incoming S7 connections and exposing DB blocks, inputs, outputs, and markers. This is invaluable for testing TIA Portal projects, developing HMI screens, or acting as a protocol bridge.

3.1 Server Management

S7_SERVER_CREATE — Create Named Server

ParamTypeRequiredDescription
nameSTRINGYesUnique server name
portINTYesTCP listen port

Returns: BOOL — TRUE if the server was created.

(* Create S7 server on the standard S7 port *)
ok := S7_SERVER_CREATE('sim', 102);

(* Create on a non-standard port to avoid permission issues *)
ok := S7_SERVER_CREATE('sim', 1102);

Port 102: The standard ISO-on-TCP port (102) may require root/admin privileges. During development, use a port above 1024. TIA Portal and WinCC can be configured to connect to non-standard ports.

S7_SERVER_START — Begin Listening

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if the server started listening.

ok := S7_SERVER_START('sim');

S7_SERVER_STOP — Stop Listening

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if the server stopped.

ok := S7_SERVER_STOP('sim');

S7_SERVER_IS_RUNNING — Check Server State

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if the server is actively listening.

IF NOT S7_SERVER_IS_RUNNING('sim') THEN
S7_SERVER_START('sim');
END_IF;

S7_SERVER_DELETE — Remove Server

ParamTypeDescription
nameSTRINGServer name

Returns: BOOL — TRUE if the server was deleted.

S7_SERVER_STOP('sim');
ok := S7_SERVER_DELETE('sim');

S7_SERVER_LIST — List All Servers

Returns: []STRING — Array of all server names.

servers := S7_SERVER_LIST();
(* Returns: ['sim'] *)

3.2 DB Area Access

S7_SERVER_SET_DB — Write to Server DB Area

ParamTypeDescription
nameSTRINGServer name
dbINTData block number
byteAddrINTStarting byte offset
values[]INTArray of byte values to write

Returns: BOOL — TRUE if the value was set.

(* Write 4 bytes to DB10 starting at offset 0 *)
ok := S7_SERVER_SET_DB('sim', 10, 0, [16#00, 16#FF, 16#12, 16#34]);

(* Pre-populate a REAL value (72.5 = 42 91 00 00 in IEEE 754 big-endian) *)
ok := S7_SERVER_SET_DB('sim', 10, 4, [16#42, 16#91, 16#00, 16#00]);

S7_SERVER_GET_DB — Read from Server DB Area

ParamTypeDescription
nameSTRINGServer name
dbINTData block number
byteAddrINTByte offset to read

Returns: INT — the single byte at byteAddr.

Reads are one byte at a time. Unlike S7_SERVER_SET_DB, which takes an array, the getters have no count argument — loop for a range. A block-read form returning an array is on the deferred list in TODO.md.

(* Read 10 bytes from DB10 *)
FOR i := 0 TO 9 DO
data[i] := S7_SERVER_GET_DB('sim', 10, i);
END_FOR;

Server DB storage: The server automatically allocates DB storage on first access. You don't need to pre-define DB sizes — just write to any DB number and offset.


3.3 Marker Area Access

S7_SERVER_SET_M — Write to Server Marker Area

ParamTypeDescription
nameSTRINGServer name
byteAddrINTStarting byte offset
values[]INTArray of byte values

Returns: BOOL — TRUE if the value was set.

(* Set MB0 = 0xFF, MB1 = 0x00 *)
ok := S7_SERVER_SET_M('sim', 0, [16#FF, 16#00]);

S7_SERVER_GET_M — Read from Server Marker Area

ParamTypeDescription
nameSTRINGServer name
byteAddrINTByte offset to read

Returns: INT — the single byte at byteAddr.

(* Read MB0..MB3 *)
FOR i := 0 TO 3 DO
markers[i] := S7_SERVER_GET_M('sim', i);
END_FOR;

3.4 Input/Output Area Access

S7_SERVER_SET_I — Write to Server Input Area

ParamTypeDescription
nameSTRINGServer name
byteAddrINTStarting byte offset
values[]INTArray of byte values

Returns: BOOL — TRUE if the value was set.

(* Simulate input byte IB0 with all bits set *)
ok := S7_SERVER_SET_I('sim', 0, [16#FF]);

S7_SERVER_GET_I — Read from Server Input Area

(* One byte per call — read IB0 *)
inputs := S7_SERVER_GET_I('sim', 0);

S7_SERVER_SET_Q — Write to Server Output Area

ok := S7_SERVER_SET_Q('sim', 0, [16#A5]);

S7_SERVER_GET_Q — Read from Server Output Area

(* One byte per call — read QB0 *)
outputs := S7_SERVER_GET_Q('sim', 0);

Server areas as simulation: Use S7_SERVER_SET_I to simulate sensor inputs for TIA Portal programs. Use S7_SERVER_GET_Q to verify that the PLC program drives the correct outputs. This creates a hardware-in-the-loop test environment without physical I/O.

Example: Server Lifecycle

PROGRAM POU_S7Server
VAR
state : INT := 0;
ok : BOOL;
scan_count : DINT := 0;
END_VAR

CASE state OF
0: (* Create server *)
ok := S7_SERVER_CREATE('sim', 1102);
IF ok THEN
state := 1;
END_IF;

1: (* Start listening *)
ok := S7_SERVER_START('sim');
IF ok THEN
state := 2;
END_IF;

2: (* Pre-populate data blocks *)
S7_SERVER_SET_DB('sim', 10, 0, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
state := 10;

10: (* Running — update simulated data *)
scan_count := scan_count + 1;

(* Simulate changing process values *)
S7_SERVER_SET_DB('sim', 10, 0, [DINT_TO_INT(scan_count MOD 256)]);

IF NOT S7_SERVER_IS_RUNNING('sim') THEN
state := 1;
END_IF;
END_CASE;
END_PROGRAM

4. Complete Example: Reading Process Data from an S7-1500

This example connects to an S7-1500 PLC running a packaging line, reads process data from DB10, and writes setpoints to DB20. The DB layout matches a typical TIA Portal project structure.

PLC DB Layout

DBOffsetTypeVariableDescription
DB100.0BOOLline_runningLine status
DB100.1BOOLfault_activeActive fault
DB100.2BOOLinfeed_readyInfeed conveyor ready
DB102INTline_speedActual speed (packages/min)
DB104INTreject_countRejected packages
DB106REALtemperatureSeal bar temperature (deg C)
DB1010REALpressureVacuum pressure (mbar)
DB1014DINTtotal_countTotal packages since reset
DB200.0BOOLstart_cmdStart command
DB200.1BOOLstop_cmdStop command
DB202INTspeed_setpointSpeed setpoint (packages/min)
DB204REALtemp_setpointTemperature setpoint (deg C)
PROGRAM POU_PackagingLine
VAR
(* Connection state *)
state : INT := 0;
ok : BOOL;
retry_count : INT := 0;

(* Process data from DB10 *)
line_running : BOOL;
fault_active : BOOL;
infeed_ready : BOOL;
line_speed : INT;
reject_count : INT;
temperature : REAL;
pressure : REAL;
total_count : DINT;

(* Commands to DB20 *)
start_cmd : BOOL := FALSE;
stop_cmd : BOOL := FALSE;
speed_setpoint : INT := 120;
temp_setpoint : REAL := 185.0;
END_VAR

CASE state OF
0: (* Create connection to S7-1500 *)
ok := S7_CLIENT_CREATE('pack_plc', '10.0.0.34', 0, 0);
IF ok THEN
state := 1;
END_IF;

1: (* Connect *)
ok := S7_CLIENT_CONNECT('pack_plc');
IF ok THEN
retry_count := 0;
state := 2;
ELSE
retry_count := retry_count + 1;
IF retry_count > 5 THEN
state := 99;
END_IF;
END_IF;

2: (* Register poll for fast reads *)
S7_ADD_POLL('pack_plc', 'DB', 10, 0, 18);
state := 10;

10: (* Running — read process data *)
IF NOT S7_CLIENT_IS_CONNECTED('pack_plc') THEN
state := 1;
END_IF;

(* Read DB10 — status and feedback *)
line_running := S7_READ_DB_BOOL('pack_plc', 10, 0, 0);
fault_active := S7_READ_DB_BOOL('pack_plc', 10, 0, 1);
infeed_ready := S7_READ_DB_BOOL('pack_plc', 10, 0, 2);
line_speed := S7_READ_DB_INT('pack_plc', 10, 2);
reject_count := S7_READ_DB_INT('pack_plc', 10, 4);
temperature := S7_READ_DB_REAL('pack_plc', 10, 6);
pressure := S7_READ_DB_REAL('pack_plc', 10, 10);
total_count := S7_READ_DB_DINT('pack_plc', 10, 14);

state := 11;

11: (* Write commands to DB20 *)
ok := S7_WRITE_DB_BOOL('pack_plc', 20, 0, 0, start_cmd);
ok := S7_WRITE_DB_BOOL('pack_plc', 20, 0, 1, stop_cmd);
ok := S7_WRITE_DB_INT('pack_plc', 20, 2, speed_setpoint);
ok := S7_WRITE_DB_REAL('pack_plc', 20, 4, temp_setpoint);

(* Auto-clear one-shot commands *)
start_cmd := FALSE;
stop_cmd := FALSE;

state := 10; (* Loop back to read *)

99: (* Fault *)
(* Log error, wait for intervention *)
END_CASE;
END_PROGRAM

DB access optimization: The S7 protocol allows reading up to 480 bytes per PDU (negotiated at connection time). Grouping reads within a single DB range via S7_ADD_POLL is far more efficient than individual typed reads. The background poll fetches all 18 bytes of DB10 in a single S7 transaction, and the typed read functions extract values from the cache.


5. Complete Example: S7 Server for TIA Portal Testing

This example creates an S7 server that simulates the packaging line PLC from Section 4. A TIA Portal project can connect to this server for HMI development and logic testing without the physical PLC.

PROGRAM POU_S7Simulator
VAR
(* Server state *)
state : INT := 0;
ok : BOOL;
scan_count : DINT := 0;

(* Simulated process values *)
sim_running : BOOL := FALSE;
sim_speed : INT := 0;
sim_temp : REAL := 25.0;
sim_pressure : REAL := 1013.0;
sim_total : DINT := 0;

(* Commands received from TIA Portal / HMI *)
cmd_byte : INT;
start_received : BOOL;
stop_received : BOOL;
END_VAR

CASE state OF
0: (* Create and start server *)
ok := S7_SERVER_CREATE('sim', 1102);
IF ok THEN state := 1; END_IF;

1:
ok := S7_SERVER_START('sim');
IF ok THEN state := 2; END_IF;

2: (* Initialize DB10 with zeros *)
S7_SERVER_SET_DB('sim', 10, 0, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]);
S7_SERVER_SET_DB('sim', 20, 0, [0, 0, 0, 0, 0, 0, 0, 0]);
state := 10;

10: (* Running — update simulation every scan *)
scan_count := scan_count + 1;

(* Read commands from DB20 (written by TIA Portal / HMI).
Only byte 0 carries the command bits, so read that one byte. *)
cmd_byte := S7_SERVER_GET_DB('sim', 20, 0);
start_received := (cmd_byte AND 1) = 1; (* Bit 0.0 *)
stop_received := (cmd_byte AND 2) = 2; (* Bit 0.1 *)

(* Process commands *)
IF start_received AND NOT sim_running THEN
sim_running := TRUE;
END_IF;
IF stop_received THEN
sim_running := FALSE;
END_IF;

(* Simulate process behavior *)
IF sim_running THEN
sim_speed := 120;
sim_temp := sim_temp + 0.1;
IF sim_temp > 190.0 THEN sim_temp := 185.0; END_IF;
sim_pressure := 950.0;
sim_total := sim_total + 1;
ELSE
sim_speed := 0;
sim_temp := sim_temp - 0.05;
IF sim_temp < 25.0 THEN sim_temp := 25.0; END_IF;
sim_pressure := 1013.0;
END_IF;

(* Write simulated status to DB10 *)
(* Byte 0: status bits *)
S7_SERVER_SET_DB('sim', 10, 0, [BOOL_TO_INT(sim_running)]);

(* Bytes 2-3: line speed as INT (big-endian) *)
S7_SERVER_SET_DB('sim', 10, 2, [sim_speed / 256, sim_speed MOD 256]);

(* Note: REAL and DINT values require IEEE 754 / big-endian
byte packing. In practice, use helper functions or
write complete byte arrays from your simulation model. *)

IF NOT S7_SERVER_IS_RUNNING('sim') THEN
state := 1;
END_IF;
END_CASE;
END_PROGRAM

TIA Portal connection: In TIA Portal, add an "Unspecified S7 300/400" as a connection partner and configure the IP address and port of the ControlForge server. The S7 server responds to standard S7comm read/write requests — TIA Portal and WinCC treat it like a real PLC.


6. Gateway Example: S7 to MQTT Bridge

This example reads data from an S7-1500 PLC and publishes it to an MQTT broker, bridging the OT (Siemens) and IT (MQTT/JSON) worlds.

PROGRAM POU_S7_MQTT_Gateway
VAR
(* State machine *)
state : INT := 0;
ok : BOOL;
scan_count : DINT := 0;
publish_interval : DINT := 10;

(* S7 data *)
speed : REAL;
temp : REAL;
pressure : REAL;
running : BOOL;

(* MQTT *)
payload : STRING;
END_VAR

CASE state OF
0: (* Initialize S7 client *)
ok := S7_CLIENT_CREATE('plc', '10.0.0.34', 0, 0);
IF ok THEN state := 1; END_IF;

1: (* Connect S7 *)
ok := S7_CLIENT_CONNECT('plc');
IF ok THEN state := 2; END_IF;

2: (* Initialize MQTT *)
ok := MQTT_CLIENT_CREATE('gw', 'tcp://10.0.0.144:1883', 'cf-gw');
IF ok THEN
ok := MQTT_CONNECT('gw');
IF ok THEN state := 3; END_IF;
END_IF;

3: (* Register polling *)
S7_ADD_POLL('plc', 'DB', 10, 0, 20);
state := 10;

10: (* Running — read and publish *)
scan_count := scan_count + 1;

IF NOT S7_CLIENT_IS_CONNECTED('plc') THEN
S7_CLIENT_CONNECT('plc');
END_IF;

(* Read from polled cache *)
speed := S7_READ_DB_REAL('plc', 10, 0);
temp := S7_READ_DB_REAL('plc', 10, 4);
pressure := S7_READ_DB_REAL('plc', 10, 8);
running := S7_READ_DB_BOOL('plc', 10, 12, 0);

(* Publish at reduced rate *)
IF (scan_count MOD publish_interval) = 0 THEN
payload := CONCAT('{"speed":', REAL_TO_STRING(speed),
',"temp":', REAL_TO_STRING(temp),
',"pressure":', REAL_TO_STRING(pressure),
',"running":', BOOL_TO_STRING(running), '}');

MQTT_PUBLISH('gw', 'plant/line1/data', payload);
END_IF;

(* Publish connection stats periodically *)
IF (scan_count MOD 100) = 0 THEN
MQTT_PUBLISH('gw', 'plant/line1/stats',
S7_CLIENT_GET_STATS('plc'));
END_IF;
END_CASE;
END_PROGRAM

7. Advanced Patterns

7.1 Multi-PLC Polling

PROGRAM POU_MultiPLC
VAR
poll_index : INT := 0;
plcs : ARRAY[0..2] OF STRING := ['plc_line1', 'plc_line2', 'plc_line3'];
speed : REAL;
END_VAR

(* Round-robin: read one PLC per scan to distribute load *)
speed := S7_READ_DB_REAL(plcs[poll_index], 10, 0);

poll_index := poll_index + 1;
IF poll_index > 2 THEN
poll_index := 0;
END_IF;
END_PROGRAM

Why round-robin? Each S7 read blocks for the round-trip time (5-30 ms on a local network). Reading from three PLCs sequentially every scan adds 15-90 ms. With background polling enabled via S7_ADD_POLL, the round-robin pattern is less critical since reads return from cache — but it remains useful for write operations that cannot be cached.

7.2 Dual Role: Client and Server Simultaneously

PROGRAM POU_DualRole
VAR
state : INT := 0;
ok : BOOL;
speed : REAL;
temp : REAL;
i : INT;
mirror_buf : ARRAY[0..19] OF INT;
END_VAR

CASE state OF
0: (* Initialize both roles *)
ok := S7_CLIENT_CREATE('plc', '10.0.0.34', 0, 0);
ok := S7_SERVER_CREATE('mirror', 1102);
IF ok THEN state := 1; END_IF;

1: (* Connect/Start *)
S7_CLIENT_CONNECT('plc');
S7_SERVER_START('mirror');
S7_ADD_POLL('plc', 'DB', 10, 0, 20);
state := 10;

10: (* Running — mirror PLC data to server *)
(* Read from real PLC *)
speed := S7_READ_DB_REAL('plc', 10, 0);
temp := S7_READ_DB_REAL('plc', 10, 4);

(* Mirror to server DB for HMI/SCADA access *)
(* Write raw bytes — REAL as IEEE 754 big-endian.
SET takes an array; GET returns one byte, so build the array. *)
FOR i := 0 TO 19 DO
mirror_buf[i] := S7_SERVER_GET_DB('mirror', 10, i);
END_FOR;
S7_SERVER_SET_DB('mirror', 10, 0, mirror_buf);

(* Alternatively, forward entire DB block from client reads *)
END_CASE;
END_PROGRAM

7.3 Cross-Protocol Bridge: Modbus TCP to S7

PROGRAM POU_Modbus_S7_Bridge
VAR
state : INT := 0;
ok : BOOL;
regs : ARRAY[0..9] OF INT;
END_VAR

CASE state OF
0: (* Initialize both protocols *)
ok := MB_CLIENT_CREATE('vfd', '10.0.0.50', 502, 1);
ok := S7_CLIENT_CREATE('plc', '10.0.0.34', 0, 0);
IF ok THEN state := 1; END_IF;

1: (* Connect both *)
MB_CLIENT_CONNECT('vfd');
S7_CLIENT_CONNECT('plc');
state := 10;

10: (* Running — bridge Modbus VFD data into S7 PLC DB *)
(* Read VFD status over Modbus *)
regs := MB_READ_HOLDING('vfd', 0, 5);

(* Write to S7 PLC DB30 as INT values *)
ok := S7_WRITE_DB_INT('plc', 30, 0, regs[0]); (* Output freq *)
ok := S7_WRITE_DB_INT('plc', 30, 2, regs[1]); (* Current *)
ok := S7_WRITE_DB_INT('plc', 30, 4, regs[2]); (* Voltage *)
ok := S7_WRITE_DB_INT('plc', 30, 6, regs[3]); (* Status word *)
ok := S7_WRITE_DB_INT('plc', 30, 8, regs[4]); (* Fault code *)

(* Read speed command from S7 PLC, forward to VFD *)
ok := MB_WRITE_REGISTER('vfd', 8192,
S7_READ_DB_INT('plc', 30, 10));
END_CASE;
END_PROGRAM

Protocol bridging: ControlForge acts as a universal translator. The S7 PLC program reads VFD data from its DB30 as if it were local data — it doesn't know or care that the values originate from a Modbus device. This decouples the PLC program from the field protocol.


8. PLC Family Notes

S7-300 / S7-400

  • Rack/Slot: Typically rack 0, slot 2. Check STEP 7 hardware configuration for multi-rack setups.
  • No special configuration required. PUT/GET is enabled by default on 300/400 series.
  • Connection limits: S7-300 supports 12-32 concurrent connections depending on CPU model. S7-400 supports up to 64.
  • DB access: Standard (non-optimized) DB blocks only. These PLCs do not support optimized block access.

S7-1200

  • Rack/Slot: Always rack 0, slot 0.
  • PUT/GET must be enabled in TIA Portal: Device configuration > Protection & Security > Connection mechanisms > Permit access with PUT/GET communication.
  • DB blocks must be non-optimized: In TIA Portal, open the DB properties and uncheck Optimized block access. Optimized DBs use internal addressing that external clients cannot access.
  • Connection limit: 8 concurrent connections (CPU firmware dependent).
  • Firmware: Requires firmware V4.0 or later for full PUT/GET support.

S7-1500

  • Rack/Slot: Always rack 0, slot 0.
  • PUT/GET must be enabled — same TIA Portal setting as S7-1200.
  • DB blocks must be non-optimized for external access. Alternatively, use a dedicated "interface DB" with optimized access disabled while keeping internal DBs optimized.
  • Connection limit: Up to 32 concurrent connections depending on CPU model.
  • Security: S7-1500 supports access levels (full access, read access, HMI access, no access). Ensure the access level permits PUT/GET.

Optimized vs. standard DB access: TIA Portal defaults to "optimized block access" for S7-1200/1500 DBs. This reorders variables internally for performance, breaking external byte-offset addressing. For any DB that ControlForge will read/write, disable optimized access. A common pattern is to create dedicated "exchange DBs" (e.g., DB10 for status, DB20 for commands) with optimized access disabled, and use optimized DBs for internal PLC logic.


Appendix A: S7 Address Mapping Quick Reference

TIA Portal AddressControlForge FunctionParameters
DB10.DBX0.0S7_READ_DB_BOOL('c', 10, 0, 0)db=10, byte=0, bit=0
DB10.DBX0.7S7_READ_DB_BOOL('c', 10, 0, 7)db=10, byte=0, bit=7
DB10.DBX1.0S7_READ_DB_BOOL('c', 10, 1, 0)db=10, byte=1, bit=0
DB10.DBB0S7_READ_DB_BYTE('c', 10, 0)db=10, byte=0
DB10.DBW2S7_READ_DB_WORD('c', 10, 2)db=10, byte=2
DB10.DBD4S7_READ_DB_DWORD('c', 10, 4)db=10, byte=4
DB10.DBD4 (REAL)S7_READ_DB_REAL('c', 10, 4)db=10, byte=4
I0.0S7_READ_I_BOOL('c', 0, 0)byte=0, bit=0
IB0 (byte)S7_READ_I('c', 0, 1)byte=0, count=1
Q0.0S7_READ_Q_BOOL('c', 0, 0)byte=0, bit=0
M10.3S7_READ_M_BOOL('c', 10, 3)byte=10, bit=3
MB0-MB7S7_READ_M('c', 0, 8)byte=0, count=8

Appendix B: Quick Reference — All Functions

Client Functions (~24)

FunctionReturnsDescription
S7_CLIENT_CREATE(name, host, rack, slot [, port] [, timeout_ms] [, poll_rate_ms])BOOLCreate named connection
S7_CLIENT_CONNECT(name)BOOLEstablish S7 connection
S7_CLIENT_DISCONNECT(name)BOOLClose S7 connection
S7_CLIENT_IS_CONNECTED(name)BOOLCheck connection state
S7_CLIENT_DELETE(name)BOOLRemove connection
S7_CLIENT_LIST()[]STRINGList all connections
S7_CLIENT_GET_STATS(name)MAPRequest/response/error/poll counts
S7_READ_DB_BYTE(name, db, byteAddr)INTRead unsigned byte (USINT)
S7_READ_DB_WORD(name, db, byteAddr)INTRead unsigned word (UINT)
S7_READ_DB_DWORD(name, db, byteAddr)DINTRead unsigned double word (UDINT)
S7_READ_DB_INT(name, db, byteAddr)INTRead signed 16-bit integer
S7_READ_DB_DINT(name, db, byteAddr)DINTRead signed 32-bit integer
S7_READ_DB_REAL(name, db, byteAddr)REALRead 32-bit float (IEEE 754)
S7_READ_DB_BOOL(name, db, byteAddr, bit)BOOLRead single bit from DB
S7_WRITE_DB_BYTE(name, db, byteAddr, value)BOOLWrite unsigned byte
S7_WRITE_DB_WORD(name, db, byteAddr, value)BOOLWrite unsigned word
S7_WRITE_DB_DWORD(name, db, byteAddr, value)BOOLWrite unsigned double word
S7_WRITE_DB_INT(name, db, byteAddr, value)BOOLWrite signed 16-bit integer
S7_WRITE_DB_DINT(name, db, byteAddr, value)BOOLWrite signed 32-bit integer
S7_WRITE_DB_REAL(name, db, byteAddr, value)BOOLWrite 32-bit float
S7_WRITE_DB_BOOL(name, db, byteAddr, bit, value)BOOLWrite single bit to DB
S7_READ_I(name, byteAddr, count)[]INTRead input bytes
S7_READ_Q(name, byteAddr, count)[]INTRead output bytes
S7_READ_M(name, byteAddr, count)[]INTRead marker bytes
S7_READ_I_BOOL(name, byteAddr, bit)BOOLRead input bit
S7_READ_Q_BOOL(name, byteAddr, bit)BOOLRead output bit
S7_READ_M_BOOL(name, byteAddr, bit)BOOLRead marker bit
S7_WRITE_M(name, byteAddr, values)BOOLWrite marker bytes
S7_WRITE_Q(name, byteAddr, values)BOOLWrite output bytes
S7_WRITE_M_BOOL(name, byteAddr, bit, value)BOOLWrite marker bit
S7_WRITE_Q_BOOL(name, byteAddr, bit, value)BOOLWrite output bit
S7_ADD_POLL(name, area, dbNumber, byteAddr, byteCount)BOOLRegister background poll range

Server Functions (~14)

FunctionReturnsDescription
S7_SERVER_CREATE(name, port)BOOLCreate named S7 server
S7_SERVER_START(name)BOOLBegin listening
S7_SERVER_STOP(name)BOOLStop listening
S7_SERVER_IS_RUNNING(name)BOOLCheck server state
S7_SERVER_SET_DB(name, db, byteAddr, values)BOOLWrite to server DB area
S7_SERVER_GET_DB(name, db, byteAddr, count)[]INTRead from server DB area
S7_SERVER_SET_M(name, byteAddr, values)BOOLWrite to server marker area
S7_SERVER_GET_M(name, byteAddr, count)[]INTRead from server marker area
S7_SERVER_SET_I(name, byteAddr, values)BOOLWrite to server input area
S7_SERVER_GET_I(name, byteAddr, count)[]INTRead from server input area
S7_SERVER_SET_Q(name, byteAddr, values)BOOLWrite to server output area
S7_SERVER_GET_Q(name, byteAddr, count)[]INTRead from server output area
S7_SERVER_DELETE(name)BOOLRemove server
S7_SERVER_LIST()[]STRINGList all servers

ControlForge v1.0.1064 | Siemens S7 Client + Server | IEC 61131-3 Structured Text

© 2026 JMB Technical Services LLC. All rights reserved. Back to All Guides