Dynamic I/O Access in S7-1500 with PEEK, POKE, and :P Suffix

David Krause15 min read
S7-1200SiemensTechnical Reference
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

Overview: Why Dynamic I/O Access Matters on S7-1500

The S7-1500 CPU family (firmware V1.0 through V20) does not allow the I/O address operands of a compiled statement to be modified while the user program is executing. The compiler resolves %IB0, %QW4, %MW10, %DB5.DBX2.0 and similar fully qualified addresses at build time and emits absolute memory references in the MC7 code. If the application needs to read or write a module whose slot address is not known until the machine is commissioned (or worse, is selected by the operator at runtime from an HMI recipe), the program must use one of three indirect-access mechanisms supplied by STEP 7 / TIA Portal:

  • Direct I/O access with the :P suffix — bypasses the process image and addresses the module's I/O area immediately.
  • SCL PEEK / POKE commands — read and write any memory area (including process image input I, process image output Q, bit memory M, and data blocks) using an integer offset that can be a variable.
  • ANY-pointer-based block moves — MOVE_BLK, FILL_BLK, VariantGet, and DPRD_RAW/DPWR_RAW for PROFINET slot-level reads.

Because the original question on dynamic I/O access is a recurring thread on industrial automation forums, the remainder of this reference consolidates the official Siemens documentation paths, SCL syntax, pointer variants, and a runtime-configurable "virtual I/O" pattern that engineers have shipped to production on S7-1500 systems.

S7-1500 I/O Access Fundamentals

An S7-1500 CPU maintains two parallel views of the physical I/O:

View Storage Update Operand syntax
Process image of the inputs (PII) Internal RAM, mirrored per scan Updated at the start of the OB1 cycle (or assigned OB) %I, %IB, %IW, %ID
Process image of the outputs (PIQ) Internal RAM, mirrored per scan Written to the module at the end of the OB1 cycle %Q, %QB, %QW, %QD
Direct I/O (bypass) No copy — accesses module register directly Immediate, on demand %I:P, %Q:P
Bit memory Internal RAM only No hardware I/O backing %M, %MB, %MW, %MD

According to the Siemens S7-1500 functional description (TIA Portal V20), appending the suffix :P to an I/O operand forces the program to access the I/O module directly. The address component (slot byte/bit) is still required; what changes is whether the access goes through the process image or directly to the module's peripheral bus (backplane or PROFINET).

Important: Direct :P access on inputs is read-only on the CPU side but causes immediate bus traffic. Use it sparingly in time-critical interrupt OBs (OB40–OB47) where deterministic I/O latency matters more than scan-time uniformity. Outputs accessed with :P are written immediately and are not held in the PIQ; reading back the same %Q:P returns the value previously written, not the actual module pin state.

Direct I/O Access with the :P Suffix

The :P suffix is available for all elementary I/O data types in SCL, LAD, FBD, and STL on S7-1500 CPUs (and S7-1200 V4.0 and later). It can be applied to:

  • Inputs: %IB0:P, %IW4:P, %ID8:P, %I0.0:P–%I65535.7:P
  • Outputs: %QB0:P, %QW4:P, %QD8:P, %Q0.0:P–%Q65535.7:P

Because the CPU still resolves the slot address at compile time, :P by itself does not make the address dynamic. To make the address runtime-variable you must combine :P access with an ANY-pointer or with the SCL slice/offset operators described in the next sections.

When to use :P instead of process-image I/O

Scenario Recommended access Reason
Standard cyclic I/O at OB1 priority Process image (%I, %Q) Single read/write per scan, deterministic and consistent
Hardware interrupt OB (OB40) reacting to a sub-millisecond event %I:P on the same byte PII snapshot may be 1–2 ms stale
Module diagnostics (channel status, value status) Direct RDREC / WRREC (not :P) :P only addresses process data, not record sets
PROFINET IRT high-priority output switching %Q:P with provider slot PIQ update may miss IRT cycle boundary

SCL PEEK and POKE Commands

The SCL commands PEEK and POKE are the S7-1500 equivalent of the older S7-300/400 PEEK/POKE legacy blocks. They were reintroduced as native SCL statements with TIA Portal V13 SP1 for S7-1500 and are documented in the SCL programming manual. They support four addressing modes selected by the area identifier parameter.

PEEK syntax

// Read byte / word / dword from any memory area
out := PEEK(area := <area>, byteOffset := <INT>);       // byte
out := PEEK_WORD(area := <area>, byteOffset := <INT>);   // word
out := PEEK_DWORD(area := <area>, byteOffset := <INT>);  // dword
out := PEEK_BOOL(area := <area>, byteOffset := <INT>, bit := <INT>);

POKE syntax

// Write byte / word / dword to any memory area
POKE(area := <area>, byteOffset := <INT>, value := <BYTE>);
POKE_WORD(area := <area>, byteOffset := <INT>, value := <WORD>);
POKE_DWORD(area := <area>, byteOffset := <INT>, value := <DWORD>);
POKE_BOOL(area := <area>, byteOffset := <INT>, bit := <INT>, value := <BOOL>);

Area identifier constants

Constant Numeric value Targeted memory Bit access?
PEEK_AREA_I / 16#81 Process image input (PII) Yes (via PEEK_BOOL)
PEEK_AREA_Q / 16#82 Process image output (PIQ) Yes
PEEK_AREA_M / 16#83 Bit memory Yes
PEEK_AREA_DB / 16#84 * Instantaneous DB; DB number must be loaded separately Yes

* For PEEK_AREA_DB the active DB is selected with SET_DB / SET_DI or by an OPN DB before the call. The byte offset is always relative to the start of the active DB and the resulting bit address follows the DB boundary rules.

Bit access: Only PEEK_BOOL and POKE_BOOL exist; the byte/word/dword variants always address whole data widths and cannot be masked. If you need to set a single bit inside a byte, use PEEK → WordLogic → POKE, or call the underlying IEC bit functions on a sliced byte.

Numeric constants accepted by the area parameter

If you prefer the legacy 16#xx numeric form rather than the symbolic constants, the following mapping is valid across all S7-1500 firmware versions that support PEEK/POKE:

  • 16#81 — Inputs (PII)
  • 16#82 — Outputs (PIQ)
  • 16#83 — Bit memory (M)
  • 16#84 — Data block of the currently opened DB

Making PEEK / POKE Runtime-Dynamic

The whole point of the forum question is that the address itself must be a variable, not a literal. PEEK and POKE accept INT or DINT for the byte offset, so the address can be the result of any SCL expression — an HMI tag, a recipe value, or a computed index. The next four SCL samples cover the typical patterns.

Example 1 — Read a runtime-selected input byte

// FB "dynIn" - read one byte from PII at runtime address
FUNCTION_BLOCK "dynIn"
VAR_INPUT
  i_Addr  : INT;     // 0..65535 - byte offset in PII
END_VAR
VAR_OUTPUT
  o_Value : BYTE;
  o_Sts   : WORD;    // 16#0000 = OK
END_VAR
BEGIN
  IF (i_Addr < 0) OR (i_Addr > 65535) THEN
    o_Sts := 16#8001;          // out-of-range
    o_Value := 16#00;
    RETURN;
  END_IF;
  o_Value := PEEK(area := 16#81, byteOffset := i_Addr);
  o_Sts   := 16#0000;
END_FUNCTION_BLOCK

Example 2 — Write a runtime-selected output byte

// FB "dynOut" - write one byte to PIQ at runtime address
FUNCTION_BLOCK "dynOut"
VAR_INPUT
  i_Addr  : INT;     // 0..65535 - byte offset in PIQ
  i_Value : BYTE;
END_VAR
VAR_OUTPUT
  o_Sts   : WORD;
END_VAR
BEGIN
  IF (i_Addr < 0) OR (i_Addr > 65535) THEN
    o_Sts := 16#8002;
    RETURN;
  END_IF;
  POKE(area := 16#82, byteOffset := i_Addr, value := i_Value);
  o_Sts := 16#0000;
END_FUNCTION_BLOCK

Example 3 — Read / write a bit in PII / PIQ

// FB "dynBit" - manipulate one bit anywhere in the PII / PIQ
FUNCTION_BLOCK "dynBit"
VAR_INPUT
  i_ByteAddr : INT;  // 0..65535
  i_BitAddr  : INT;  // 0..7
  i_Value    : BOOL; // for write
  i_Write    : BOOL; // TRUE = POKE_BOOL, FALSE = PEEK_BOOL
END_VAR
VAR_OUTPUT
  o_Value : BOOL;
  o_Sts   : WORD;
END_VAR
BEGIN
  IF (i_ByteAddr < 0) OR (i_ByteAddr > 65535) OR
     (i_BitAddr  < 0) OR (i_BitAddr  > 7) THEN
    o_Sts := 16#8003;
    RETURN;
  END_IF;
  IF i_Write THEN
    POKE_BOOL(area := 16#82, byteOffset := i_ByteAddr,
              bit := i_BitAddr, value := i_Value);
  ELSE
    o_Value := PEEK_BOOL(area := 16#81, byteOffset := i_ByteAddr,
                         bit := i_BitAddr);
  END_IF;
  o_Sts := 16#0000;
END_FUNCTION_BLOCK

Example 4 — Cycle through eight inputs at runtime

// Used in a recipe where the active module index is HMI-driven
FUNCTION_BLOCK "RecipeInput"
VAR_INPUT
  i_Recipe : INT;     // 0..7 selects which input byte is the "live" one
END_VAR
VAR_OUTPUT
  o_Value  : BYTE;
END_VAR
VAR
  s_Addr   : INT;
END_VAR
BEGIN
  // clamp input to 0..7 and multiply by 2 for two-byte slot pattern
  IF (i_Recipe < 0) OR (i_Recipe > 7) THEN
    s_Addr := 0;
  ELSE
    s_Addr := i_Recipe * 2;
  END_IF;
  o_Value := PEEK(area := 16#81, byteOffset := s_Addr);
END_FUNCTION_BLOCK

Virtual I/O Pattern (Data Block as I/O Mirror)

DB definition

DATA_BLOCK "VIO"
  STRUCT
    v_IN  : BYTE;   // mirror of the live input byte
    v_OUT : BYTE;   // mirror of the live output byte
    sel   : INT;    // 0..N - selected slot
  END_STRUCT;
END_DATA_BLOCK

OB1 cyclic body (SCL)

// 1. Refresh v_IN from the selected slot
CASE "VIO".sel OF
  0: "VIO".v_IN  := PEEK(area := 16#81, byteOffset := 0);
  1: "VIO".v_IN  := PEEK(area := 16#81, byteOffset := 2);
  2: "VIO".v_IN  := PEEK(area := 16#81, byteOffset := 4);
  3: "VIO".v_IN  := PEEK(area := 16#81, byteOffset := 6);
ELSE
  "VIO".v_IN  := 16#00;
END_CASE;

// 2. Run all logic against "VIO".v_IN / "VIO".v_OUT
//    (any LAD/FBD/SCL statement that references %I / %Q
//     is replaced by "VIO".v_IN / "VIO".v_OUT)
"VIO".v_OUT := "VIO".v_IN;     // identity copy for the example

// 3. Push v_OUT back to the selected slot
CASE "VIO".sel OF
  0: POKE(area := 16#82, byteOffset := 0, value := "VIO".v_OUT);
  1: POKE(area := 16#82, byteOffset := 2, value := "VIO".v_OUT);
  2: POKE(area := 16#82, byteOffset := 4, value := "VIO".v_OUT);
  3: POKE(area := 16#82, byteOffset := 6, value := "VIO".v_OUT);
ELSE
  ; // ignore invalid slot
END_CASE;
Why a CASE rather than PEEK with a computed offset? The virtual I/O pattern lets you mix in derived bits (for example, a software-debounced version of v_IN, or a watchdog bit) that no PEEK statement can supply. It also makes the program self-documenting because the slot-to-byte mapping appears in a single block that can be edited from one place when the I/O list changes.

Pointer-Based Indirect Access (VARIANT, ANY, POINTER)

Beyond the byte-granular PEEK/POKE commands, S7-1500 SCL offers three higher-level pointer types:

Type Size Lifetime Typical use
POINTER 6 bytes Static Legacy, compile-time area + offset
ANY 10 bytes Static / runtime Block move, indirect DB access
VARIANT 0 bytes (handle) Static / runtime Type-safe generic block parameters

For the dynamic I/O problem specifically, MOVE_BLK with a runtime-built ANY source is the most flexible alternative when the source and destination areas differ in type.

Runtime-built ANY source

// Copy 1 byte from PII[recipeAddr] to "VIO".v_IN
VAR_TEMP
  t_SrcAny  : ANY;
  t_Rc      : INT;
END_VAR

t_SrcAny :=  // build manually using the ANY byte layout
// byte 0..5 : header 16#1002 + DB / area / byte offset
// (see SCL help for the exact construct)

Building an ANY byte-by-byte is verbose; the modern approach is to declare a tag of type POINTER TO BYTE and pass ADR(tag) to a block expecting an ANY, or use MOVE_BLK_VARIANT with VARIANT#"VIO".v_IN. For process-image access the cleaner path remains PEEK/POKE because the CPU exposes those areas as flat byte offsets.

Indirect Access via Slice Syntax

S7-1500 SCL supports slicing on tags of structured type, but not on process-image I/O directly. For an array tag, you can read arrayVar[i].byte0 with i runtime. The same pattern applied to the PII / PIQ is not legal — which is why PEEK/POKE is the official workaround.

Slice on a structured DB tag

// Equivalent of array indexing on a DB tag
"RecipeDB".slot[i_Recipe].word0 := PEEK_WORD(area := 16#81,
                                              byteOffset := i_Recipe * 2);

PROFINET I/O Considerations

When the dynamic I/O address refers to a PROFINET device (not a central rack module), two extra layers apply:

  1. Slot / sub-slot mapping. The PROFINET device is configured in the device view of TIA Portal. Each submodule occupies a slot, and the I/O area is assigned in the device properties. PROFINET I/O on S7-1500 is addressed identically to central I/O from the user program ("I/Q + offset"), so PEEK/POKE works without modification.
  2. Replacement-port / device-swap behaviour. When a device is replaced, the slot index is preserved; the same byte offset remains valid. If you re-map the device to a different slot, you must also change the address calculation in your program.

The walkthrough on adding PROFINET I/O and drives to an S7-1500 in TIA Portal (see the Automation Show episode S24 PROFINET I/O & Drives) is a useful field reference for confirming slot assignments after commissioning.

Direct Peripheral Read with DPRD_RAW and DPWR_RAW

For PROFINET and PROFIBUS DP, the system function blocks DPRD_RAW (read raw data record) and DPWR_RAW (write raw data record) provide access to module I/O when PEEK/POKE is not sufficient — for example, when you need to read beyond the configured process-image length or want to access the module's view of its own I/O area. These blocks live in the "Communication" folder of the Instructions task card.

// Read 4 bytes from logical address 256 (PROFINET slot 1)
#status := DPRD_RAW(LADDR := 256,  // I/O start address from HW config
                    AREA   := 0,   // 0 = I, 1 = Q
                    NUMBER := 4,   // length in bytes
                    RECORD := #buffer);
// #buffer is an ARRAY[0..3] OF BYTE

Performance and Cycle-Time Impact

PEEK and POKE execute as standard MC7 instructions and cost roughly the same as a single memory-register move — typically under 1 µs on an S7-1516 and a few µs on an S7-1511. The cost that matters is the side effect of bypassing the process image:

  • PEEK on 16#81 / POKE on 16#82: still goes to the PII/PIQ in RAM, no bus traffic, near-zero overhead.
  • %I:P / %Q:P: forces a peripheral-bus transaction every access. On PROFINET this can trigger a real-time frame; on the backplane it is a single read/write. Use inside time-critical OBs only.

For most control loops the process-image access is sufficient and the dynamic-PEEK pattern is the better trade-off.

Edge Cases and Common Pitfalls

Pitfall Symptom Mitigation
Reading past the configured input range PEEK returns 16#00 but no diagnostic — silent failure Clamp the offset to (configured input length − 1)
Writing to an output that is not assigned to a module POKE succeeds but the LED does not change; later module insertion repaints the bit Use IOCCS / device status to confirm the slot is present
Mixing :P and PII access on the same byte within one scan Non-deterministic state if an OB40 fires between the two reads Pick one access path per byte; document it in the I/O list
PEEK_AREA_DB with the wrong active DB Reads from the previously opened DB Always issue SET_DI("VIO") before PEEK
Bit offset > 7 passed to PEEK_BOOL No error, undefined return value Validate the input with 0..7 before the call
Using PEEK on safety-relevant I/O Bypasses the F-CPU's safety signature checks Never read/write F-I/O with PEEK/POKE — use the F-runtime block library

Verification Procedure

After implementing dynamic I/O access, run the following checks before the line is signed off:

  1. Static check: Compile with "All warnings as errors" in TIA Portal. Warnings about indirect access (information 1135/1136) are expected and confirm the code uses PEEK/POKE correctly.
  2. Watch table sweep: Open the watch table on the v_IN / v_OUT tags and step through every recipe value. Verify each PII byte and PIQ byte matches the expected source.
  3. Online PII view: Force i_Recipe to each value and confirm in "Online & Diagnostics → I/O" that the monitored byte changes accordingly.
  4. Cycle-time check: Open the "Cycle Time" measurement in the online view and confirm the OB1 scan time did not regress (typical increase < 0.5 ms for 100 PEEK calls).
  5. Edge cases: Force the offset to 65534, 65535, −1, and a value 1 above the configured range to verify the FB returns the expected status word and does not crash the CPU.
  6. Field test: Disconnect the addressed module — the diagnostic OB82 must fire if you monitor IOCC state, and the FB must continue to return the documented error status without dropping the CPU to STOP.

Diagnostic Tags to Expose on HMI

To make runtime troubleshooting fast, expose the following tags on the operator panel:

  • v_IN (BYTE) — current mirror value
  • v_OUT (BYTE) — pending output value
  • sel (INT) — currently selected slot
  • o_Sts (WORD) — status word from the FB
  • ob82_fault (BOOL) — rack / station fault latched from OB82

Comparison: Access Method by Use Case

Use case Best method Why
Cyclic OB1 scan, address known at compile time Process image (%I, %Q) Fastest, consistent
Cyclic OB1 scan, address supplied at runtime (recipe, HMI) PEEK / POKE on 16#81/16#82 Process-image speed, address can be a variable
Time-critical interrupt, address fixed %I:P, %Q:P Bypasses the PII/PIQ snapshot
Time-critical interrupt, address variable PEEK / POKE on %I:P semantics + OB-based selection Direct access with computed offset
Mass block copy across 200+ bytes MOVE_BLK with runtime-built ANY Single CPU call, no per-byte PEEK loop
PROFINET device raw record access DPRD_RAW / DPWR_RAW Reads beyond process-image length
Safety I/O (F-CPU) F-runtime library only Safety certification prohibits indirect access

Frequently Asked Questions

Can an S7-1500 program change the I/O address of a statement at runtime?

No — the compiler resolves fully qualified I/O addresses (%IB0, %QW4, %I0.0) at build time. To read or write an I/O byte whose offset is only known at runtime, use SCL PEEK(area := 16#81, byteOffset := i_Addr) for inputs and POKE(area := 16#82, byteOffset := i_Addr, value := b) for outputs.

What is the difference between %IB0 and %IB0:P on an S7-1500?

%IB0 reads from the process image of the inputs (PII), a snapshot updated once per OB1 cycle. %IB0:P reads the byte directly from the module register, bypassing the PII, so the value is current at the moment of the instruction. Use :P for sub-millisecond response in hardware-interrupt OBs (OB40–OB47); use the standard process image for cyclic logic.

Which numeric area constants does PEEK accept on S7-1500?

The constants are 16#81 (process image inputs), 16#82 (process image outputs), 16#83 (bit memory), and 16#84 (current DB). All four are documented in the SCL programming and operating manual for S7-1500 and are valid from TIA Portal V13 SP1 onward.

Can PEEK and POKE be used on safety-related I/O on an F-CPU?

No. Indirect access to F-I/O violates the safety signature protocol and breaks the F-CPU's certificate. F-I/O must be accessed exclusively through the certified F-runtime block library (F-channel drivers in the F-program).

Is there a runtime-cost difference between PEEK/POKE and direct :P access?

Yes. PEEK/POKE on 16#81/16#82 reads/writes the PII/PIQ in RAM and costs roughly the same as a register move. %I:P/%Q:P triggers a peripheral-bus read/write on the backplane or PROFINET every time it executes, so it should be used only where up-to-date module state within the same scan is required.

Back to blog