S7 I/O Validation: PEEK/POKE for External Matrix Tables in TIA

David Krause17 min read
SiemensTIA PortalTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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:

  1. Baseline — the original matrix file (controlled, signed-off, often PDF).
  2. As-built — the compiled S7 program after the import (one mapping per FB call, one absolute address per BOOL tag).
  3. 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.

Scope warning. Runtime matrix validation is a diagnostic, not a substitute for FAT/SAT, a wiring loop test, or a formal SIL verification. In SIL 2/3 applications, route the matrix data only into the standard program (non-F-runtime group); the F-runtime group must still be validated per IEC 61508/61511 by its own certified means.

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#81
  • byteOffset = 200
  • bitOffset = 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.

Address range caveat. The default process-image size on S7-1500 is set under PLC properties > Process image. Anything outside the configured process image is still addressable via PEEK (the instruction reads directly from the I/O backplane), but it is not visible in the standard symbol browser and the OB1 process-image update will not refresh it. For matrix validation this is actually an advantage — PEEK always reads the live value, regardless of the OB1 PI configuration.

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), or 16#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].field where i is a runtime variable does not work on optimized blocks for S7-1200 and fails unpredictably on S7-1500.
  • Symbolic I/O slices like %I200.7 still work, but they require a literal constant in the address, not a variable.
  • PEEK and POKE are 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.

Migration gotcha. If the matrix DB was generated as optimized and later opened in TIA Portal V17 SP1 or older, the internal byte offsets may shift. Always export the DB again from the source tool and compare total bytes with a CSV diff. PEEK-based reading is robust against this because it consumes area/byte/bit, not internal offsets.

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.

Force test. Before relying on the mismatch output, force a known row. Use a Watch table to set 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

  1. Static address review. Open DB_Matrix > Watch all and confirm every row[i].sourceTag parses to a unique triple of area / byteOffset / bitOffset. Duplicates indicate a copy-paste error in the source CSV.
  2. Force one mismatch. With the PLC in RUN, force row 0: set expectedValue := TRUE, then drive the input at I200.7 low. Confirm mismatch rises within one scan. Reset both, confirm it clears.
  3. Force one no-mismatch. With the input at its known resting state, set expectedValue equal to actualValue. Confirm mismatchCount stays at zero.
  4. 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 actualValue with the calibrator’s setpoint; record any divergence.
  5. Loop-bound test. With the maximum rowCount loaded, 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.
  6. Stop/restore test. Power-cycle the CPU. Confirm lastRun updates on the first OB1 after restart, and mismatchCount reflects the new scan.
  7. Diagnostic buffer scan. Force a deliberately bad address (e.g., area := 16#84 with dbNumber := 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.

Back to blog