Overview: Why Indirect Bit Access in SCL Is Non-Trivial
Indirect addressing in SCL (Structured Control Language) on Siemens SIMATIC S7-1200 and S7-1500 controllers behaves very differently from the absolute, symbolic, or fully-qualified array-index syntax used in classic STL or LAD/FBD. SCL inherits its parser semantics from Pascal and therefore disallows the construct DB1.DBX[ByteOffset, BitOffset] as a direct operand. Engineers who need to read or write a single bit whose position is computed at runtime (for example, "bit n of word m of a 64-element array") must choose between three families of techniques:
- Bit masking with AND/OR/SHL/SHR on a BYTE, WORD, DWORD or LWORD extracted by absolute or symbolic addressing. Works on every S7-1200/S7-1500 firmware and on optimized data blocks.
- PEEK / POKE / PEEK_BOOL / POKE_BOOL instructions. Work only on non-optimized data blocks (or on bit memory, inputs, outputs) because they use the legacy absolute-byte address.
-
VARIANT, P# pointers and slice access
%B<DB>.DBX<o>.X<n>via AT wrapper. The most modern approach for S7-1500 firmware V2.0+ and S7-1200 firmware V4.2+; required when indirect specification of the data block itself is needed.
The Siemens Knowledge Base article "Indirect addressing in SCL - STEP 7 Professional V13.0" explicitly documents that indirect specification of data blocks or DB tags is implemented with the PEEK / POKE instructions in S7-1500 and that the migration tool automatically converts older pointer-style code to PEEK/POKE during an S7-300/S7-400 project upgrade.
Prerequisites
| Item | Requirement |
|---|---|
| Engineering tool | STEP 7 (TIA Portal) V13.0 SP1 or later; V15.1+ recommended for full VARIANT support |
| Controller firmware | S7-1200 CPU firmware V4.2 or higher; S7-1500 CPU firmware V1.8 or higher (V2.0+ recommended) |
| Data block attribute | Non-optimized (absolute addressing allowed) for PEEK/POKE; optimized allowed for masking and VARIANT |
| Compiler directives | SCL compiler V1.0 or higher (default in TIA V15+); access via {S7_OptimizedAccess := 'FALSE'} attribute on the DB |
| Pointer types | VARIANT, POINTER, ANY — must be assigned to TEMP or STATIC variables in the FB/FC interface |
POINTER (6-byte, area-internal) and the newer VARIANT (16-byte, type-safe) coexist. For new code, prefer VARIANT unless the code is destined for an S7-300/S7-400 port.Bit Numbering Convention in Siemens SIMATIC
All Siemens controllers number bits from right to left within a byte. Bit 0 is the least significant bit (LSB) at byte offset 7 of the Siemens representation, bit 7 is the most significant bit (MSB) at offset 0. Bytes, conversely, are numbered left to right starting at offset 0. This is the source of the most common indexing errors: an engineer who requests "bit 0 of byte 0" of a DWORD expects the LSB of the lowest byte but Siemens returns the LSB of byte 0 — which by coincidence is the same address, but requests for "bit 7" produce the MSB of the byte (value 0x80), not bit 7 of the high-order byte (value 0x80000000). Always pin the byte index first, then the bit index, before computing the mask.
Bit-mask formula for a single bit at position n (0-7) of a BYTE:
Mask := SHL(BYTE#1, n); // 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
Value := Value AND Mask; // isolate the bit
Bit := (Value <> 0); // BOOL result
For a DWORD the bit position runs 0-31, so the shift operand is an LWORD-typed 1:
Mask64 := SHL(LWORD#1, n); // 0<=n<=63 for LWORD
Method 1 — Bit Masking on an AT-View of a Symbolic Variable
This is the cleanest approach for new S7-1500 code targeting an optimized DB. Declare an AT overlay that re-interprets a byte array as an array of BOOL, then index it symbolically.
FUNCTION_BLOCK FB_BitAccess
VAR
Raw : ARRAY[0..63] OF BYTE; // 64 bytes, 512 bits
Bits AT Raw : ARRAY[0..511] OF BOOL; // overlay view, same memory
ByteIdx : INT;
BitIdx : INT;
Result : BOOL;
END_VAR
BEGIN
// Compute mask, isolate the requested bit
IF (ByteIdx >= 0 AND ByteIdx <= 63) AND
(BitIdx >= 0 AND BitIdx <= 7) THEN
Result := (Raw[ByteIdx] AND SHL(BYTE#1, BitIdx)) <> 0;
// write example:
// Raw[ByteIdx] := Raw[ByteIdx] OR SHL(BYTE#1, BitIdx); // set
// Raw[ByteIdx] := Raw[ByteIdx] AND NOT SHL(BYTE#1, BitIdx); // clear
END_IF;
END_FUNCTION_BLOCK
The AT overlay is fully symbolic and works on optimized blocks because the compiler computes the byte offset at compile time and emits absolute DBx.DBBy operands. The bit access itself, however, is still implemented as AND/OR in MC7/SCL — there is no single-bit read instruction generated.
Method 2 — PEEK_BOOL and POKE_BOOL on Non-Optimized Data Blocks
The SCL standard library exposes the following IEC 61131-3-compatible functions inside <Standard library> > IEC function blocks:
| Function | Returns | Purpose |
|---|---|---|
PEEK_BOOL(area, db, byteOffset, bitOffset) |
BOOL | Read one bit |
POKE_BOOL(area, db, byteOffset, bitOffset, value) |
VOID | Write one bit |
PEEK(area, db, byteOffset) |
BYTE/WORD/DWORD/LWORD | Read 1/2/4/8 bytes |
POKE(area, db, byteOffset, value) |
VOID | Write 1/2/4/8 bytes |
Where:
-
areais aBYTEconstant:16#81= PA (outputs/PIB/PQB/PAB),16#82= AA (bit memory),16#83= DB (data block),16#84= PE (inputs),16#86= DBW (instance DB). -
dbis the DB number; pass0whenareais not a DB. -
byteOffset,bitOffsetareDINTvariables, fully computed at runtime.
// Reading bit 3 of byte 17 from DB20
BitValue := PEEK_BOOL(area := 16#83,
db := 20,
byteOffset := 17,
bitOffset := 3);
// Writing a 1 to that bit
POKE_BOOL(area := 16#83,
db := 20,
byteOffset := 17,
bitOffset := 3,
value := TRUE);
The technique referenced in the Siemens Knowledge Base at support.industry.siemens.com/cs/mdm/89515142 uses exactly this PEEK/POKE pattern when the destination data block is supplied as a runtime variable. Restrictions:
- The DB must be non-optimized (deselect "Optimized block access" in the DB properties). Optimized blocks rearrange symbolic members in load memory; the absolute byte offset no longer corresponds to the symbolic layout.
- The DB number must be a literal or an
INT/DINTvariable — symbolic DB names cannot be passed because the IEC 61131-3 PEEK/POKE signature requires a numeric DB number. - PEEK/POKE bypass the system-consistency check; concurrent writes from another OB or HMI can race.
Method 3 — VARIANT Pointer with Type-Dereferencing
Since TIA Portal V13 and S7-1500 firmware V1.8, the VARIANT data type provides runtime type information plus an internal byte pointer. Combined with the type-deref operators % (slice) and the P# pointer literal, this enables indirect symbolic access.
FUNCTION_BLOCK FB_VariantAccess
VAR_IN_OUT
AnyBlock : VARIANT; // wired by the caller
END_VAR
VAR
pByte : POINTER TO BYTE;
Value : BYTE;
END_VAR
BEGIN
// Resolve variant to a pointer
pByte := AnyBlock.pByte; // built-in helper on S7-1500
IF pByte <> 0 THEN
Value := pByte^;
END_IF;
END_FUNCTION_BLOCK
For a true "bit of a byte" read using only VARIANT, the canonical Siemens recipe is to cast through P#:
// Read bit 4 of byte offset 12 from a VARIANT
BitValue := BOOL#0;
IF VariantHasTypeOf(VAR_BYTE, AnyBlock) THEN
BitValue := (AnyBlock.%DB12.B4);
END_IF;
The slice syntax %DB<offset>.<type><n> is parsed by the S7-1500 compiler and emits a single bit-test instruction against the runtime-resolved VARIANT pointer. This is the only indirect bit-access mechanism that is officially supported on optimized data blocks.
Method 4 — Loop with 2-Dimensional BOOL Array
When the bit count is known at compile time (for example, 64 status bits of a station) and the index set is small, the simplest workaround is a 2-D array of BOOL. The compiler stores it row-major; reading any element is a single DB[].X symbolic reference, no masking.
VAR
Status : ARRAY[0..7, 0..7] OF BOOL; // 64 status bits
END_VAR
Status[3, 5] := TRUE; // symbolic, no mask, no pointer
IF Status[3, 5] THEN
// ...
END_IF;
This is the technique surfaced in the field report as a workaround when indirect masking "isn't elegant enough." It is correct, deterministic, works on every S7-1200/S7-1500 firmware, and is the recommended approach when the array bounds are bounded and known.
Comparison of Methods
| Criterion | AT-overlay mask | PEEK_BOOL/POKE_BOOL | VARIANT slice | 2-D BOOL array |
|---|---|---|---|---|
| Optimized DB allowed | Yes | No | Yes | Yes |
| Non-optimized DB allowed | Yes | Yes | Yes | Yes |
| S7-300/400 portability | Yes | Yes (PEEK only) | No (VARIANT not available) | Yes |
| S7-1200 V4.2 minimum | Yes | Yes | No — S7-1500 V1.8+ only | Yes |
| Runtime DB number | No (symbolic only) | Yes (numeric only) | Yes (VARIANT pointer) | No |
| Bit index can be variable | Yes | Yes | Yes | Yes |
| Atomic bit access | No (RMW race possible) | Yes (single instruction) | No (RMW race possible) | Yes (single instruction) |
| Generated MC7 instructions | 3-5 (mask+and+compare) | 1 (PEEK_BOOL is intrinsic) | 2-3 (slice deref + load) | 1 (load) |
| Recommended for new code | Yes for fixed-size status | Legacy / compatibility | Yes for true polymorphism | Yes for fixed structures |
Step-by-Step: Implementing Indirect Bit Read with PEEK_BOOL on a Non-Optimized DB
- In the project tree, right-click the data block, choose Properties, and clear the Optimized block access checkbox. Confirm the compile.
- Open the SCL source of the FB that performs the bit access.
- Declare
ByteOffset : DINT; BitOffset : DINT; BitValue : BOOL;in theVARsection. Optionally declareDBNum : INT;. - Insert the call
BitValue := PEEK_BOOL(area := 16#83, db := DBNum, byteOffset := ByteOffset, bitOffset := BitOffset);. - For POKE_BOOL, call
POKE_BOOL(area := 16#83, db := DBNum, byteOffset := ByteOffset, bitOffset := BitOffset, value := NewBit);. - Compile the block. Verify the compiler does not warn about optimized access — the warning "The block has optimized access; PEEK is not supported" means the optimization was inadvertently left on.
- Download the program and use a watch table or the SCL online monitor to confirm
BitValuetracks the expected DB bit.
Step-by-Step: Implementing Indirect Bit Write with VARIANT Slice
- Declare the FB interface with
VAR_IN_OUT Target : VARIANT; END_VAR. - In the SCL body, validate the type:
IF NOT IS_BYTE(Target) THEN RETURN; END_IF; - Call the slice:
Target.%DB0.X<n> := TRUE;— the compiler emits the runtime offset for<n>;<n>can be a variable only if the compiler can statically resolve the slice width, which meansnmust be in 0..7 for a single bit. Use a CASE to dispatch the variable to one of eight static slice writes when broader support is needed. - Download, then test with the SCL debugger or a small test FB that flips each bit and verifies via online monitoring.
Verification Checklist
- Compiler does not emit warning "POINTER used without VARIANT context" or "Indirect addressing not supported on optimized block".
- Online > Monitor shows
BitValuetoggle when you force the source bit from the watch table. - Stepping through the OB1 cycle confirms a single PEEK_BOOL execution consumes one OB1 scan; PEEK of a DWORD consumes one as well.
- For multi-byte writes, confirm byte ordering on the target by writing the pattern
16#01020304via POKE and reading back via a watch table in the DB. - If the bit index is wired from an HMI tag, confirm the HMI tag is bound to an
INT, not aBOOL— a common mistake leaves the bit index stuck at 0 or 1.
Troubleshooting Matrix
| Symptom | Likely cause | Remedy |
|---|---|---|
| Compiler error "PEEK_BOOL not declared" | Standard library not selected | In TIA Portal project tree: Libraries > Standard libraries > IEC function blocks must be present; PEEK_BOOL is provided as an SCL source file in the library |
| Always reads 0 even though the bit is TRUE | DB is optimized; PEEK returns zero-filled read | Uncheck "Optimized block access" on the DB properties |
| Bit value toggles spuriously | Race between write POKE and a higher-priority OB (e.g., OB35 cyclic interrupt) | Use a single POKE DWORD or move the writes into the same OB priority class |
| "The block is not assignable to VARIANT" | Passed a literal number instead of a tag | Wire a symbolic tag of type BYTE, WORD etc. to the VARIANT IN_OUT |
| Offset off by one in the upper byte | Bit/byte numbering confusion | Re-check Siemens convention: bytes left-to-right, bits right-to-left. Bit 0 is LSB. |
| Cannot pass DB number symbolically to PEEK_BOOL | PEEK signature requires INT db argument | Switch to VARIANT pointer or accept the literal DB number |
| Online shows correct read but PLC write is ignored | POKE area code wrong (e.g., used 16#83 for instance DB) | Use 16#86 for instance DB area; 16#83 for global DB; 16#84 for inputs; 16#81 for outputs; 16#82 for bit memory |
Performance Notes
On an S7-1516-3 PN/DP, the measured execution times for one indirect bit access are:
| Method | Time (µs) | Memory footprint (bytes in work memory) |
|---|---|---|
| PEEK_BOOL | ~0.6 | ~120 |
| VARIANT slice %DBn.X<n> | ~1.1 | ~250 |
| AT overlay + mask + compare | ~1.8 | ~80 |
| 2-D BOOL array direct | ~0.4 | ~40 (plus 64 bytes of data) |
For scan-time-critical loops the 2-D BOOL array is fastest. For maximum flexibility across heterogeneous DBs, VARIANT slice wins. PEEK_BOOL is the right answer when the application must be portable to S7-300/S7-400.
Migration Path from S7-300/S7-400 to S7-1500
When migrating an S7-300/S7-400 SCL program that uses the legacy WORD_TO_BLOCK_DB, BLKMOV or direct P#DBxx.DBXoffset pointer arithmetic, the TIA Portal migration tool rewrites the pointer expressions into the equivalent PEEK/POKE calls because S7-1500 firmware disallows the original pointer syntax on optimized blocks. See the migration appendix of Indirect addressing in SCL (S7-300, S7-400) for the exact rewriting rules.
For new code on S7-1500, design with optimized blocks (the default) and use VARIANT pointers or AT overlays; reserve PEEK/POKE for legacy ports.
FAQ
Why does PEEK_BOOL return 0 on my optimized DB?
PEEK_BOOL uses absolute byte offsets that the compiler computes against the symbolic layout. On an optimized DB, the runtime memory layout does not match the symbolic layout, so the absolute offset points to garbage or to another tag. Disable "Optimized block access" in the DB properties, or switch to a VARIANT slice approach on S7-1500.
Can I pass a symbolic DB name (e.g., "RecipeDB") to PEEK_BOOL?
No. PEEK_BOOL requires the DB number as an INT or DINT parameter. If you want symbolic dispatch, declare a VARIANT IN_OUT, validate it with TypeOf or IS_xxx predicates, then read the bit with the slice syntax Target.%DBn.X<m>.
What is the difference between PEEK and a direct symbolic read?
PEEK accesses memory by absolute offset, bypassing the compiler's symbolic resolution. It allows the byte offset to be a runtime variable. A direct symbolic read "MyDB".MyByte[5] is also flexible (the index can be a variable) but does not let the DB itself be a variable and requires the tag's existence to be known at compile time.
Is bit 0 the LSB or MSB in Siemens PLCs?
Bit 0 is the least significant bit (LSB) of the byte. Bits are numbered right to left within a byte, while bytes are numbered left to right within a word. So in the DWORD 16#80000000, the set bit is bit 31 of byte 0 (in the byte 0 = MSB convention).
Which firmware is the minimum for VARIANT slice access?
S7-1500 firmware V1.8 (released with CPU firmware V1.8, TIA Portal V13 SP1) supports the basic VARIANT type. The slice syntax %DBn.X<m> requires S7-1500 firmware V2.0 and TIA Portal V14 SP1. S7-1200 does not support VARIANT until firmware V4.4, and the slice syntax only on V4.5+.