1. Problem Overview
Multi-CPU plants that use one S7-1500 (or S7-1500SP) as a head CPU and several S7-1200 / S7-300 stations as slaves waste a large amount of Profinet bandwidth when the head polls every station on a fixed cycle. The standard remedy is event-driven PUT/GET: a slave only transmits when its local process data actually changed. The challenge is the block compare stage that decides whether a change has occurred.
The reference architecture used throughout this article is:
- 1 × CPU 1510SP-1 PN (software controller, ET200SP) acting as head.
- 9 × CPU 1215C DC/DC/DC (firmware V4.x) acting as substations.
- 1 × CPU 315-2 PN/DP (legacy station, S7-300, used only to demonstrate the STL alternative).
- TIA Portal V13 SP1 (engineering).
Each slave holds a process image in DB101. The slave keeps DB102 as the previous-state shadow. On every PLC cycle the slave compares DB101 vs DB102, and only when a mismatch exists does it arm a PUT that ships DB101 to the head CPU. After the PUT completes, DB102 is refreshed with the new DB101 contents.
2. Architecture and Communication Topology
Figure 1 — 1 head + 9 slaves, PUT/GET over Profinet, event-driven only.
3. Prerequisites
- TIA Portal V13 SP1 (V13.0 + SP1) or higher; the SCL syntax used here is valid up to V17.
- Firmware ≥ V4.0 on the S7-1215 CPUs to support
PEEK_DWORD/POKE_DWORD. - S7-1500 CPU 1510SP-1 PN firmware ≥ V1.8 (for software controller PUT/GET limits).
- Configured S7 connections between head and each slave (PUT/GET permission enabled in the connection properties).
- Profinet network with the slaves visible in the device view (no DNS, fixed IP or DHCP reservation).
4. DB Configuration — Non-Optimized Access
Both DB101 (live) and DB102 (shadow) must be created with the "Optimized block access" attribute disabled. Optimized blocks hide the absolute byte layout, so PEEK/POKE and any %DBx.DBDy style address would not compile.
- In the project tree, right-click
DB101→ Properties → Attributes. - Uncheck Optimized block access.
- Confirm that Setpoint by user is selected so the block starts at offset 0.
- Repeat for
DB102with the identical structure (use Update block type if you keep one as the type).
| Property | DB101 (live) | DB102 (shadow) |
|---|---|---|
| Optimized access | Disabled | Disabled |
| Accessible from HMI/OPC UA | Enabled | Disabled |
| Data block number | 101 | 102 |
| Size (initial) | 20 bytes (5 DWORD) | 20 bytes (5 DWORD) |
| Retain | No | No |
Keep symbolic naming on each tag for HMI / TIA visibility, but the address space stays stable for the compare function.
5. SCL Block Compare Function Block
The reusable FB walks the source and shadow DBs in DWORD chunks and produces both a boolean change flag and a bitmask identifying which DWORD changed. Using DWORD granularity halves the loop iterations versus a byte compare and aligns naturally with the 32-bit Profinet payload typical of PUT/GET.
FUNCTION_BLOCK "FB_BlockCompare_Diff"
{ S7_Optimized_Access := 'FALSE' }
VERSION : 0.1
VAR_INPUT
i_dbSource : INT; // source DB number (e.g. 101)
i_dbTarget : INT; // target DB number (e.g. 102)
i_numDword : INT; // number of DWORDs to compare (5 = 20 bytes)
END_VAR
VAR_OUTPUT
o_changed : BOOL; // TRUE if any DWORD differs
o_diffMask : DWORD; // bit n = 1 → DWORD n differs
END_VAR
VAR
s_srcDword : DWORD;
s_tgtDword : DWORD;
s_offset : DINT;
s_i : INT;
END_VAR
BEGIN
o_changed := FALSE;
o_diffMask := DWORD#16#0;
// Guard against out-of-range inputs
IF (i_dbSource < 1) OR (i_dbTarget < 1) THEN
RETURN;
END_IF;
IF (i_numDword < 1) OR (i_numDword > 64) THEN
RETURN; // 64 DWORD = 256 bytes max here
END_IF;
FOR s_i := 0 TO i_numDword - 1 DO
s_offset := INT_TO_DINT(s_i) * 4;
s_srcDword := PEEK_DWORD(area := 16#84,
dbNumber := i_dbSource,
byteOffset := s_offset);
s_tgtDword := PEEK_DWORD(area := 16#84,
dbNumber := i_dbTarget,
byteOffset := s_offset);
IF s_srcDword <> s_tgtDword THEN
o_diffMask := o_diffMask OR SHL(IN := DWORD#1, N := s_i);
o_changed := TRUE;
END_IF;
END_FOR;
END_FUNCTION_BLOCK;
The two PEEK calls use the area code 16#84, which is the standard "DB area, byte access" selector used internally by PEEK/POKE on S7-1200/1500. A complete table is maintained in the SCL PEEK/POKE reference (entry 57374718).
6. Change Detection and Copy-to-Shadow Logic
Instantiate the FB in OB1 (or in a cyclic interrupt OB if the slave has heavy math) and use the boolean change signal to drive a rising-edge trigger for the PUT and for the DB101 → DB102 refresh. The refresh must occur after the PUT completes — otherwise the next cycle's compare sees the same value as the sent one and never arms again.
// --- Input tag DB ---
DATA_BLOCK "iDB_Compare"
{ S7_Optimized_Access := 'FALSE' }
VERSION : 0.1
NON_RETAIN
i_dbSource : INT := 101;
i_dbTarget : INT := 102;
i_numDword : INT := 5;
END_DATA_BLOCK
// --- Static tags ---
DATA_BLOCK "sDB_CompareState"
{ S7_Optimized_Access := 'FALSE' }
VERSION : 0.1
NON_RETAIN
stat_changedPrev : BOOL;
stat_changePulse : BOOL;
stat_diffMask : DWORD;
stat_putReq : BOOL;
stat_copyDone : BOOL;
END_DATA_BLOCK
// In OB1 (SCL segment)
"inst_FB_BlockCompare_Diff"(i_dbSource := "iDB_Compare".i_dbSource,
i_dbTarget := "iDB_Compare".i_dbTarget,
i_numDword := "iDB_Compare".i_numDword);
"sDB_CompareState".stat_diffMask := "inst_FB_BlockCompare_Diff".o_diffMask;
"sDB_CompareState".stat_changePulse :=
"inst_FB_BlockCompare_Diff".o_changed
AND NOT "sDB_CompareState".stat_changedPrev;
"sDB_CompareState".stat_changedPrev :=
"inst_FB_BlockCompare_Diff".o_changed;
// Arm PUT on rising edge of the change flag
IF "sDB_CompareState".stat_changePulse THEN
"sDB_CompareState".stat_putReq := TRUE;
END_IF;
// Copy DB101 → DB102 only after PUT finished OK
IF "sDB_CompareState".stat_copyDone THEN
FOR #s_i := 0 TO "iDB_Compare".i_numDword - 1 DO
#s_offset := INT_TO_DINT(#s_i) * 4;
#s_srcDword := PEEK_DWORD(area := 16#84,
dbNumber := "iDB_Compare".i_dbSource,
byteOffset := #s_offset);
POKE_DWORD(area := 16#84,
dbNumber := "iDB_Compare".i_dbTarget,
byteOffset := #s_offset,
value := #s_srcDword);
END_FOR;
"sDB_CompareState".stat_copyDone := FALSE;
END_IF;
7. PUT Instruction Setup
Drop the standard PUT instruction (Instructions → Communication → S7 Communication) in OB1. The minimum parameter wiring is shown below; refer to the SIMATIC S7-1500 Communication function manual for full descriptions.
| Parameter | Direction | Data type | Wiring | Note |
|---|---|---|---|---|
REQ |
IN | BOOL | stat_putReq |
Rising edge starts the transfer. |
ID |
IN | WORD | Configured connection ID | 1–255 per connection. |
DONE |
OUT | BOOL | stat_putDone |
TRUE for one cycle on success. |
ERROR |
OUT | BOOL | stat_putErr |
TRUE on failure. |
STATUS |
OUT | WORD | stat_putStatus |
Hex error code (see §11). |
ADDR_1 |
IN_OUT | ANY | P#DB101.DBX0.0 BYTE 20 |
Source on the slave. |
SD_1 |
IN_OUT | ANY | P#DB201.DBX0.0 BYTE 20 |
Destination on the head. |
LEN |
IN_OUT | INT | 20 | Bytes; max 400 with S7-1200, 8192 with S7-1500. |
On DONE rising edge, set stat_copyDone := TRUE and stat_putReq := FALSE. On ERROR, clear stat_putReq, latch stat_putStatus and raise an HMI bit so the operator can clear the alarm manually after root-cause analysis.
8. S7-300 STL Alternative (Peek / Poke with ANY Pointer)
S7-300 does not expose PEEK_DWORD/POKE_DWORD in SCL. Replace the loop with STL in a dedicated FB:
FUNCTION FC 100 : VOID
// Inputs in TEMP: srcDB (INT), tgtDB (INT), numDW (INT), idx (INT)
// Output flag: stat_changed (BOOL), stat_diffMask (DWORD)
L #srcDB
T #tDB // current DB number
L P#DBX 0.0 // start pointer
T #pSrc // AR1 later
L #tgtDB
T #tDB
L P#DBX 0.0
T #pTgt
L 0
T #idx
L #numDW
T #limit
LOOP: NOP 0
L #idx
L #limit
>=I // idx >= numDW → done
JC ENDLP
OPN DB [#srcDB] // open source
L DBW [AR1,P#0.0] // read 16-bit at AR1+0
T #wSrc // (use DBW; for DWORD, do two reads)
OPN DB [#tgtDB]
L DBW [AR1,P#0.0]
T #wTgt
L #wSrc
L #wTgt
<>I
JC SETBIT
NEXT: L #idx
+ 1
T #idx
L P#4.0 // next DWORD
+AR1 #pSrc // advance pointer
+AR1 #pTgt
JU LOOP
SETBIT: SET
= #stat_changed
L #idx
SLW 1 // bit per DWORD
L #stat_diffMask
OW
T #stat_diffMask
JU NEXT
ENDLP: NOP 0
BE
Use AUF DB [#srcDB] with the variable DB number from a static tag. Note that on S7-300, the DB must be loaded with AUF DB [...] or compiled to a fixed DB; an ANY pointer built dynamically is the standard workaround when the DB number is parameterised.
LAR1 P#DBX 0.0 on S7-300 if you intend to address bytes and words. Use separate area-cross pointers (P# + +AR1) and remember that AR1 / AR2 must be saved/restored at FB entry/exit to avoid corrupting the calling environment.9. 1510SP Software Controller Limitations
The S7-1500 Software Controller inherits the S7-1500 instruction set but the runtime is shared with Windows / the host. Several field-confirmed caveats:
- The application example 40556214 (
s7-komm_sync) is not compatible with 1510SP as a head CPU; it was designed for S7-1500 hardware controllers. Re-implement the comm logic in the user program as shown above. - The Any-Pointer variant of the PUT/GET instructions (
POKE_BLK,BLKMOVwith dynamicANY) can return STATUS80A1("DB does not exist on partner") during warm restart while the WinAC service re-attaches. Wrap every PUT/GET call with a retry counter (max 3, 200 ms apart). - Software controller cycle times are jittery under heavy host load; place the compare FB in OB1 (not OB35) to keep it consistent with PUT/GET ordering.
- The 1510SP will reset all active S7 connections when its host enters standby or hibernation; head-side tags must tolerate gaps (use a watchdog in the head DB).
10. Network Traffic Optimization
Even after event-driven PUT, several tuning levers remain. The Profinet update budget on a 100 Mbit/s Profinet segment is dominated by the PUT/GET packet overhead (~80 bytes per call) when payloads are tiny; merging 9 slaves' payloads into a single 160-byte PUT (slave 1 = PUT, slave 2 = GET into shared buffer, etc.) is not supported by the protocol — each S7 connection remains independent.
| Optimization | Mechanism | Typical gain |
|---|---|---|
| Event-driven PUT | Compare + rising-edge trigger (this article) | ~200× vs 100 ms polling |
| Coalesce multiple DBs | Combine DB101 + DB103 into DB110; resize PUT LEN to 40 bytes | ~40% fewer packets |
| Rising-edge filter on bits | Use a per-bit "dirty" bit set when a value changes; compare only when the dirty bit is set | ~10× CPU reduction on noisy data |
| Update-time tuning | Profinet IO update 1 ms (head) vs 4 ms (slaves) lowers burst impact | Stable under load |
| Suppress redundant bits | Hysteresis / dead-band before writing into DB101 | Eliminates chatter-induced PUTs |
A per-bit dirty mask is built exactly the same way as the compare FB but on the producer side — every time you write a value into DB101, also set the corresponding bit in a DWORD mask. The compare loop short-circuits: if dirtyMask = 0, skip the loop entirely.
11. Verification and Commissioning
-
Online → Monitor / Modify: Open DB101, change a bit, observe
stat_changedgoing TRUE for one cycle,stat_putReqarming, then PUT DONE firing. -
Trace: In TIA Portal, configure a trace on
stat_changed,stat_putReq,stat_putDone,stat_diffMask. Run for 60 s; you must see edges only on state changes, not on every cycle. -
Wireshark on the Profinet segment (SPAN port): the count of
S7 Comm PUTpackets should match the count ofstat_putReqrising edges within ±1. -
Head-side validation: In the head CPU, create
DB201(non-optimized, 20 bytes) and bind it to the receiving SD_1 slot. Cross-check DB201 against the slave's DB101 via a separate GET (debug only, disable in production). -
Latency budget: end-to-end (slave write → head read) typically ≤ 30 ms with 9 slaves and 100 Mbit Profinet. If higher, check
OB1scan time and 1510SP host CPU usage.
12. Troubleshooting Matrix
| Symptom | Likely cause | Diagnostic | Fix |
|---|---|---|---|
| PUT STATUS = 80A1 | Destination DB on partner missing | Head DB201 not present or optimized | Create non-optimized DB201 of same length |
| PUT STATUS = 80B1 | LEN exceeds partner bounds | LEN > 400 on S7-1200 | Cap LEN at 400 for S7-1200 destinations |
| PUT STATUS = 80A4 | Connection not configured | Devices & Networks → Connections empty | Add S7 connection with PUT/GET permission |
| Compare always TRUE | DB102 never written | Online monitor DB102 vs DB101 | Ensure stat_copyDone branch executes after PUT DONE |
| Compare never TRUE | DB101 never changes | Force a tag, watch trace | Confirm the HMI/write logic actually updates DB101 |
| Compile error "PEEK not allowed" | DB optimized | DB properties | Disable optimized access on DB101/102 |
| STL: AREA OVERFLOW | AR1 not saved at FB start | First lines of FB | Add TAR1/LAR1 save/restore |
| 1510SP resets connections hourly | Host standby or WinAC restart | Windows event log | Disable host sleep, watchdog on head DB |
| High CPU on 1215 | Loop runs every OB1 cycle | Task runtime > 5 ms | Add dirtyMask pre-check |
13. FAQ
Do I really need to disable optimized block access on DB101 and DB102?
Yes. PEEK/POKE on S7-1200/1500 and any indirect STL addressing require non-optimized blocks; otherwise the compiler rejects the call with "PEEK not allowed" or "Area length error". Keep the symbolic view for HMI; only the storage layout changes.
Why not just use GET from the head instead of PUT from every slave?
GET-from-head forces 9 slaves × N Hz polling traffic, regardless of whether data changed. With 9 slaves at 100 ms, the head generates roughly 90 GET/s; event-driven PUT reduces this to a few per minute per slave — a >200× reduction. The slave is also a better owner of "what just changed" than the head.
Can the same pattern run on an S7-1500 CPU that is not the 1510SP software controller?
Yes — the SCL compare, PEEK/POKE and PUT logic are identical on S7-1511, S7-1515 and S7-1518 hardware controllers. The 1510SP-specific caveats in §9 are the only differences (host jitter, warm-restart STATUS 80A1).
What is the maximum payload per PUT between an S7-1200 and S7-1500?
400 bytes when an S7-1200 is either side; 8192 bytes when both endpoints are S7-1500. For 9 slaves each pushing ≤ 256 bytes, a single PUT is well within the limit. Reference: SIMATIC S7-1500 Communication manual.
How do I handle the case where DB101 grows from 20 to 80 bytes after commissioning?
Either change i_numDword from 5 to 20 and update both DBs and the PUT LEN (16 → 80), or — preferred — wire the FB's i_numDword to a tag that you compute at startup from the DB header length. Read the DB length via DB_GET_LENGTH in SCL (S7-1500 only) or by PEEK_WORD at offset 0 on non-optimized DBs (each DB header stores its own length in bytes at offset 0 on S7-300/400).
Does the PUT trigger need a rising-edge or is level-sensitive triggering acceptable?
Always use a rising-edge trigger (REQ = TRUE for one cycle). Level-sensitive REQ on the SIMATIC PUT keeps the connection occupied and prevents other connections from being serviced. Use a R_TRIG on stat_changed if you prefer FBD over SCL.