Problem Statement: Why Validate an External I/O Matrix at Runtime?
In safety-relevant plants (Burner Management Systems, ESD, turbine governors, machine tools with Safe Stop), the wiring matrix is a controlled document. It records which physical terminal at a sensor or actuator maps to which PLC input or output. When a matrix is generated by an external tool (Microsoft Excel, a CSV export, an EPLAN macro, or a Safety Lifecycle Tool such as Siemens SIMATIC Safety Matrix), and the resulting tag list is imported into the project, three parallel representations must stay synchronized:
- Baseline — the original matrix file (controlled, signed-off, often PDF).
- As-built — the compiled S7 program after the import (one mapping per FB call, one absolute address per BOOL tag).
- As-running — the live process image in OB1 at runtime (the bit actually energized by the field wiring).
Manual cross-checking with eyes on a Watch table works for ten points. It collapses for two hundred. The runtime check described here closes the loop programmatically: it walks the matrix, calls PEEK_BOOL for every declared absolute address, compares the read level to the matrix entry's expectedValue, and raises a mismatch flag when the two diverge. The technique also catches the inverse case — where the program has been edited but the matrix was not — because the matrix DB now stores the row index and the absolute pointer, so a renamed or moved tag still resolves to its real hardware terminal.
The approach is SCL/STL-driven and runs in OB1 or a periodic OB (OB30–OB38). It is independent of the symbol table: the addresses are stored as raw BYTE/DINT/UINT fields, not as symbolic tags. That is what makes the check survive a recompile of the surrounding program.
Prerequisites
| Item | Required |
|---|---|
| Engineering tool | TIA Portal V18 (6AV2100-0AA08-0AA0) or V19 (6AV2100-0AA09-0AA0); STEP 7 V5.7 for legacy STL |
| Controller family | S7-1200 (CPU 1211C through 1518), S7-1500 (CPU 1510SP through 1518), or ET 200SP CPU |
| Firmware (S7-1200) | V4.5 or V4.6 — PEEK/POKE added in V4.0 (2015); V4.6 enables optimized-block PEEK with full BOOL support |
| Firmware (S7-1500) | V2.9 or V3.1 — PEEK/POKE available since V2.0; V3.0+ recommended for SCL syntax |
| Library block | None — PEEK/POKE are basic instructions in TIA Portal V13 SP1+ |
| Memory | ~600 bytes of DB per 100 matrix rows (optimized); ~1 kB non-optimized |
| Source matrix file | CSV or TXT with rows of I200.7,Q43.6,1 (point A, point B, expected value) |
Confirm PEEK availability in your CPU by opening TIA Portal, dragging Instructions > Basic instructions > Extended instructions > PEEK. If the entry is greyed out the CPU firmware is below the minimum — perform a firmware update via the Siemens support portal before continuing.
S7 Memory Model and Address Encoding
Every absolute address on an S7 CPU decomposes into three integer coordinates: a memory area (input process image, output process image, bit memory), a byte offset, and a bit offset. The S7-1200/1500 reference manual formalizes this with the area codes listed below; the same codes are the parameter area passed to PEEK.
| Area code (hex) | Symbol | Description | Bit-addressable? |
|---|---|---|---|
| 16#81 | I / PE | Inputs (process image of inputs, OB1-PI) | Yes |
| 16#82 | Q / PA | Outputs (process image of outputs) | Yes |
| 16#83 | M | Bit memory (flags) | Yes |
| 16#84 | DB | Data block contents | No (DB number required) |
The S7-300/400 layout is identical, although there the PEW / PAW / MW width prefixes (input word, output word, memory word) coexist with the bit form. For matrix validation you want the bit form, because the matrix carries wire-level granularity.
An address such as I200.7 resolves to:
area = 16#81byteOffset = 200bitOffset = 7
Note that byteOffset is a DINT in the PEEK instruction, not a WORD. With S7-1500 firmware 3.0+, the maximum byte offset for inputs on a CPU 1518 is 8192 bytes; for outputs 8192 bytes; for memory 16384 bytes. These limits are documented in the CPU data sheet, not in the instruction help.
PEEK and POKE in TIA Portal
The PEEK family reads process data without going through the symbol table. POKE writes. Both are available for S7-1200/1500 in TIA Portal V13 SP1+; the BOOL variants PEEK_BOOL and POKE_BOOL arrived with S7-1200 FW 4.0 (2015) and S7-1500 FW 2.0 (2017). They live in the “Basic instructions > Extended instructions” folder.
| Instruction | Width | Use case |
|---|---|---|
| PEEK / POKE | BYTE / WORD / DWORD | Bulk read of a byte-aligned chunk (e.g., read I200..I203 as a DWORD) |
| PEEK_BOOL / POKE_BOOL | 1 bit | Matrix validation — read or force a single I/Q/M bit |
| PEEK_I / POKE_I / PEEK_R / POKE_R | 16-bit int / 32-bit real | Process value read of a scaled signal |
For matrix validation PEEK_BOOL is the correct instruction. Its signature is:
PEEK_BOOL(area := _byte_in_,
dbNumber := _uint_in_,
byteOffset := _dint_in_,
bitOffset := _uint_in_,
RET_VAL => _bool_out_);
Where:
-
area=16#81(I),16#82(Q),16#83(M), or16#84(DB). -
dbNumber= 0 for I/Q/M, otherwise the DB number (1–65535). -
byteOffset= byte index from the start of the area. -
bitOffset= 0–7 inside that byte. -
RET_VAL= the live level at that bit.
The instruction returns no error code in the BOOL variant — if the address is out of range, the CPU goes to STOP with a Parameter assignment error in the diagnostic buffer. Always wrap the call in a try/catch via the GET_ERR instruction when the matrix holds dynamic indices.
Optimized vs Non-Optimized Block Constraints
This is the single largest source of confusion when working with PEEK. S7-1200/1500 default to optimized blocks for any new DB or FB created since TIA Portal V14. Optimized means the compiler assigns storage addresses privately; the symbol name is the only stable handle. As a result:
- Direct absolute indexing with
DBx.arr[i].fieldwhereiis a runtime variable does not work on optimized blocks for S7-1200 and fails unpredictably on S7-1500. - Symbolic I/O slices like
%I200.7still work, but they require a literal constant in the address, not a variable. -
PEEKandPOKEare the legal escape hatch — they read the absolute byte directly, bypassing the symbol table.
For the matrix DB, the choice is application-specific:
| Aspect | Optimized DB | Non-optimized DB |
|---|---|---|
| Default since TIA V14 | Yes | No (must explicitly disable) |
| Indexing with variable | Not allowed — use PEEK/POKE or ATTRIB/GETIO | Allowed directly |
| Memory layout | Compiler-controlled, can change on recompile | Fixed, deterministic byte order |
| Read via PUT/GET from another CPU | Trivial (symbolic) | Requires the absolute DB number |
| Recommended for this task | Yes — the validation FB reads via PEEK, the DB contents are just data | Optional, only if HMI must show raw bytes |
To force a non-optimized DB right-click the DB in the project tree, choose Properties > Attributes, and clear Optimized block access. The same toggle exists at FB/FC level — the validation FB itself can stay optimized because it does not index the matrix DB by variable.
Designing the Matrix Data Block
One DB per matrix is the cleanest layout. Each row stores both the absolute pointer (for the runtime check) and the human-readable tag (for diagnostics on the HMI). The following structure covers 256 rows; scale as needed.
DATA_BLOCK "DB_Matrix"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
STRUCT
rowCount : INT; // active rows (1..n)
lastRun : DTL; // timestamp of last check
row : ARRAY[0..255] OF STRUCT
area : BYTE; // 16#81, 16#82, 16#83, 16#84
byteOffset : DINT;
bitOffset : UINT; // 0..7
expectedValue : BOOL; // what the matrix says
actualValue : BOOL; // what PEEK_BOOL returned
mismatch : BOOL; // 1 if expected <> actual
lastUpdate : DTL;
sourceTag : STRING[32]; // e.g. "I200.7"
sinkTag : STRING[32]; // e.g. "Q43.6"
comment : STRING[64];
END_STRUCT;
mismatchCount : INT; // running tally
END_STRUCT;
END_DATA_BLOCK
Each row is roughly 184 bytes. 256 rows is ~47 kB — comfortably inside the DB limit of every S7-1200/1500 CPU. Use NON_RETAIN if the matrix is re-imported at every restart from a recipe; leave the default RETAIN if you want the last comparison state to survive a power cycle.
Import the CSV with the Importing a data block wizard (TIA Portal > right-click the DB > Import from file). The CSV must have a header line; map the columns to the row fields explicitly. For CSV inputs from a non-Siemens tool, pre-process with a Python or PowerShell script that converts I200.7 into the three numerical fields (area, byteOffset, bitOffset) so the runtime code never has to parse strings.
Address Conversion Routines
Most matrix files arrive as text. The conversion from I200.7 to three numeric fields should happen off-line, not inside the SCL block — string parsing in an OB30 task on a 1510 will burn cycles. A small Python preprocessor (or Excel macro) is sufficient.
# Pre-convert a CSV column into (area, byteOffset, bitOffset)
def split_addr(text):
text = text.strip()
area_codes = {'I': 0x81, 'Q': 0x82, 'M': 0x83, 'DB': 0x84}
area_char = text[0]
area = area_codes[area_char]
byte_bit = text[1:].split('.')
byte_offset = int(byte_bit[0])
bit_offset = int(byte_bit[1]) if len(byte_bit) > 1 else 0
return area, byte_offset, bit_offset
print(split_addr("I200.7")) # (129, 200, 7)
print(split_addr("Q43.6")) # (130, 43, 6)
print(split_addr("M0.0")) # (131, 0, 0)
Save the resulting triplets as new CSV columns. They are what TIA imports into DB_Matrix.row[i].area / byteOffset / bitOffset. Keep the original text column too — it makes the HMI diagnostic screen legible.
If you must convert inside the PLC (e.g., matrix entered by an operator panel), use the following SCL snippet. It accepts a STRING such as I200.7 and writes the three numeric outputs:
FUNCTION "FC_ParseAddr" : VOID
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
sAddr : STRING[12];
END_VAR
VAR_OUTPUT
bArea : BYTE;
diByte : DINT;
uiBit : UINT;
bError : BOOL;
END_VAR
VAR_TEMP
iPosDot : INT;
cFirst : CHAR;
END_VAR
BEGIN
bError := FALSE;
bArea := 0;
diByte := 0;
uiBit := 0;
IF LEN(sAddr) < 2 THEN bError := TRUE; RETURN; END_IF;
cFirst := LEFT(IN := sAddr, L := 1);
CASE cFirst OF
'I': bArea := 16#81;
'Q': bArea := 16#82;
'M': bArea := 16#83;
'D': bArea := 16#84;
ELSE bError := TRUE; RETURN;
END_CASE;
// Split on the dot
iPosDot := FIND(IN1 := sAddr, IN2 := '.');
IF iPosDot = 0 THEN
// No bit part; default bit 0
diByte := STRING_TO_INT(MID(IN := sAddr, L := 99, P := 2));
ELSE
diByte := STRING_TO_INT(MID(IN := sAddr,
L := iPosDot - 2,
P := 2));
uiBit := STRING_TO_INT(MID(IN := sAddr,
L := 99,
P := iPosDot + 1));
END_IF;
IF uiBit > 7 THEN bError := TRUE; END_IF;
END_FUNCTION
Test with the F1 help on each string instruction — LEFT, FIND, MID have off-by-one quirks in older TIA Portal versions.
SCL Implementation: Runtime Comparison
The runtime FB reads every active row, calls PEEK_BOOL once, and compares. It is called from OB1 or a cyclic OB. Each invocation validates one row; the loop runs rowCount times per scan, distributed across scans if you want to limit cycle impact.
FUNCTION_BLOCK "FB_MatrixCheck"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
iStartRow : INT; // first row to process this cycle
iEndRow : INT; // last row (inclusive)
END_VAR
VAR_OUTPUT
iMismatchCount : INT;
bAnyMismatch : BOOL;
END_VAR
VAR
sArea : BYTE;
diByte : DINT;
uiBit : UINT;
bActual : BOOL;
iRow : INT;
END_VAR
BEGIN
iMismatchCount := 0;
bAnyMismatch := FALSE;
FOR iRow := iStartRow TO iEndRow DO
// 1. Decode the matrix row
sArea := "DB_Matrix".row[iRow].area;
diByte := "DB_Matrix".row[iRow].byteOffset;
uiBit := "DB_Matrix".row[iRow].bitOffset;
// 2. Read the live bit
"PEEK_BOOL"(area := sArea,
dbNumber := 0,
byteOffset := diByte,
bitOffset := uiBit,
RET_VAL => bActual);
// 3. Compare against the expected value
"DB_Matrix".row[iRow].actualValue := bActual;
"DB_Matrix".row[iRow].mismatch :=
(bActual <> "DB_Matrix".row[iRow].expectedValue);
// 4. Bookkeeping
IF "DB_Matrix".row[iRow].mismatch THEN
iMismatchCount := iMismatchCount + 1;
bAnyMismatch := TRUE;
END_IF;
"DB_Matrix".row[iRow].lastUpdate := SYSTEM_TIMESTAMP();
END_FOR;
"DB_Matrix".mismatchCount := iMismatchCount;
"DB_Matrix".lastRun := SYSTEM_TIMESTAMP();
END_FUNCTION_BLOCK
Calling FB_MatrixCheck from OB1 with iStartRow := 0, iEndRow := 255 checks every row each scan — about 1 ms per 100 rows on a 1516. To stretch over multiple scans, store a persistent iCursor in a static DB and advance it by a step size each OB1 pass.
For best results call the FB from OB30 (default 100 ms) and process only the rows the HMI is currently displaying — the operator can pin one row at a time and force a single-bit refresh, which is invaluable for commissioning.
DB_Matrix.row[0].expectedValue := TRUE, force the I/O bit off via POKE_BOOL, and confirm the mismatch flag raises. This validates the wiring of the comparison logic without touching field hardware.
STL Alternative for S7-300/400
For S7-300/400 in STEP 7 V5.7 or TIA Portal, the same result is achievable with the legacy library block PEEK_BOOL from the standard library. STL example for a single matrix row at I200.7:
NETWORK 1
TITLE = Read I200.7 via PEEK_BOOL
CALL "PEEK_BOOL"
area := B#16#81
dbNumber := 0
byteOffset := L#200
bitOffset := 7
RET_VAL := #bActualValue
NETWORK 2
TITLE = Compare against matrix expectation
// Load the matrix-expected bit from the DB
A DB 100.DBX0.0 // DB100 = DB_Matrix; row[0].expectedValue
// XOR with the just-read value
XOR #bActualValue
// Result is 1 if mismatch
= DB 100.DBX0.4 // row[0].mismatch
On S7-300/400 with classic STEP 7 V5.7 the PEEK_BOOL FB is part of the “Standard Library > System Function Blocks” folder; on S7-300/400 in TIA Portal the same instructions are available from V13 onwards. CPU firmware does not gate PEEK availability on this family — it is in the system firmware since the original SIMATIC S7 line.
To loop over multiple rows in STL, use a loop counter and indirect addressing:
NETWORK 3
TITLE = Loop over rows 0..255
L 0
T #iRow
LOOP1: L #iRow
L 255
>I
JC DONE
// Compute the byte offset of the row struct in DB100
L #iRow
L 184 // sizeof(row struct)
*D
SLD 3 // convert DINT to POINTER
LAR1
// Read the row.area, .byteOffset, .bitOffset into locals
L DBW [AR1,P#0.0] // row.area (BYTE packed in WORD)
T #sArea
L DBD [AR1,P#2.0] // row.byteOffset
T #diByte
L DBW [AR1,P#6.0] // row.bitOffset
T #uiBit
CALL "PEEK_BOOL"
area := #sArea
dbNumber := 0
byteOffset := #diByte
bitOffset := #uiBit
RET_VAL := #bActual
// Compare and store mismatch back into the row
A #bActual
XAB DBX [AR1,P#12.0] // row.expectedValue
= DBX [AR1,P#14.0] // row.mismatch
L #iRow
L 1
+I
T #iRow
JU LOOP1
DONE: NOP 0
This STL skeleton works on every S7-300/400 CPU regardless of firmware and avoids the optimized-block restriction entirely, because non-optimized DBs were the only option on this generation.
Verification and Commissioning Procedure
-
Static address review. Open DB_Matrix > Watch all and confirm every
row[i].sourceTagparses to a unique triple ofarea / byteOffset / bitOffset. Duplicates indicate a copy-paste error in the source CSV. -
Force one mismatch. With the PLC in RUN, force row 0: set
expectedValue := TRUE, then drive the input at I200.7 low. Confirmmismatchrises within one scan. Reset both, confirm it clears. -
Force one no-mismatch. With the input at its known resting state, set
expectedValueequal toactualValue. ConfirmmismatchCountstays at zero. -
Drive every row. Using the HMI screen, walk down the matrix and toggle each input via a hand-held calibrator (mA source, dry contact simulator). Compare the HMI
actualValuewith the calibrator’s setpoint; record any divergence. -
Loop-bound test. With the maximum
rowCountloaded, time the scan. On a 1516 with 256 rows the FB should complete in < 5 ms. If it exceeds 20 ms, move the FB from OB1 to a cyclic OB. -
Stop/restore test. Power-cycle the CPU. Confirm
lastRunupdates on the first OB1 after restart, andmismatchCountreflects the new scan. -
Diagnostic buffer scan. Force a deliberately bad address (e.g.,
area := 16#84withdbNumber := 9999). The CPU should raise a diagnostic event — if it does not, your version of PEEK_BOOL has been replaced by a vendor block that does not validate; revert to the standard library version.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Compiler error “Variable cannot be used as array index” | DB is optimized, SCL is indexing with a runtime INT | Either disable optimization on DB_Matrix, or use PEEK_BOOL with runtime address (as in FB_MatrixCheck above) |
| PEEK_BOOL returns 0 even when input is hot | Process image not updated for that address; PI configured too small | Increase process image in PLC properties, or move the read out of the OB1 PI via direct I/O access (PEEK defaults to direct for areas outside the configured PI on S7-1500 FW 2.9+) |
| CPU goes to STOP with SF LED on after PEEK_BOOL runs once | Address out of range, or DB number 0 with area 16#84 | Validate area codes; clamp byteOffset against the CPU datasheet limits |
| Mismatch flag always 1 regardless of input | Bit offset or byte offset swapped with row index | Re-verify CSV preprocessing; print one row’s triple and compare with a manual PEEK in the Watch table |
| First scan after restart shows 0 for all rows | Watch table pulled before OB1 completed | Add SYSTEM_TIMESTAMP() capture and read lastRun to confirm the FB has executed |
| Force via POKE_BOOL has no effect on output | Output is wired to a safety output group, or output is forced by another FB | Confirm the output is in standard (not safety) runtime group, and remove conflicting assignment |
| Performance: cycle time doubles | FB called from OB1 with 1000+ rows | Distribute rows across cyclic OBs (OB30–OB38) or use PE_W (16-bit) and decode the bit manually for 16 rows per call |
| HMI shows stale actualValue | HMI polling too slow or DB in non-optimized layout with byte alignment surprises | Set HMI polling cycle to 200 ms, confirm DB attribute “Accessible from HMI” is set, and that the HMI connection uses absolute addressing |
For additional background, the SIMATIC S7-1500 system manual (entry ID 109767220) documents the PEEK/POKE instruction family and the optimized-block rules; the S7-1200 system manual (entry ID 109751705) covers the same topics for the smaller CPU family. Both are available on the Siemens Industry Online Support portal.
Does PEEK_BOOL work on S7-1200 firmware below V4.0?
No. PEEK and POKE were added to S7-1200 in firmware V4.0 (release 2015) and BOOL variants in the same update. Earlier firmware (V1.0–V3.0) supports only direct symbolic access; upgrade the CPU or use an S7-1500/ET 200SP CPU for PEEK-based matrix validation.
Can I use this technique on S7-300 or S7-400 CPUs?
Yes. The PEEK/POKE block family is part of the STEP 7 V5.7 standard library and has been on S7-300/400 since their release. Optimized blocks do not exist on this family, so all DBs are non-optimized by default; direct variable-index access works without PEEK when the matrix DB is non-optimized.
Why does my optimized DB refuse to compile with DBx.arr[var].field?
Optimized blocks have no fixed byte layout. The compiler cannot resolve a variable index to a known offset. Two solutions: disable optimization for the matrix DB only, or store the address components as plain numeric fields and call PEEK_BOOL with them — which is what this article does.
How large can a PEEK_BOOL byteOffset be on S7-1500?
Up to the CPU-specific input/output/memory limit, which is 8192 bytes for I/O and 16384 bytes for M memory on CPU 1515–1518 (Firmware V3.1). Exceeding the limit puts the CPU into STOP with diagnostic event ID 16#3582. Always clamp the matrix byte offset against the CPU datasheet before calling PEEK_BOOL.
Does the matrix DB count against the S7-1500 data-work memory limit?
Yes. A 256-row matrix DB of roughly 47 kB is negligible for an S7-1500 (work memory is typically 150 kB to 6 MB depending on CPU). For S7-1214C with 100 kB work memory, keep the matrix under 256 rows, or split across multiple DBs.