S7-1200 SCL: Reading REAL from DB Using Indexed Addressing

David Krause17 min read
SiemensTIA PortalTroubleshooting
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 Description

When programming in Structured Control Language (SCL) for Siemens S7-1500 and S7-1200 controllers, a recurring requirement is to read a 32-bit floating-point (REAL) value from a data block (DB) when both the DB number and the byte offset are passed in as INT variables. The intent is straightforward: "give me the REAL stored at this byte offset inside this DB." A typical first attempt by an SCL programmer who has just learned the WORD_TO_BLOCK_DB pattern looks like this:

// Size_1_Y1      : REAL    (result)
// DB_Index_1     : INT     (DB number, 0..32767)
// Byte_Index_X2  : INT     (byte offset, multiple of 4)
Size_1_Y1 := DWORD_TO_REAL(WORD_TO_BLOCK_DB(DB_Index_1).DW [Byte_Index_X2]);

or, using the more traditional absolute operand name:

Size_1_Y1 := DWORD_TO_REAL(WORD_TO_BLOCK_DB(DB_Index_1).DBW[Byte_Index_X2]);

The TIA Portal SCL compiler rejects the assignment with the diagnostic:

The data type of the variables (or the expression) on the left side of the assignment is not compatible with the data type of the expression on the right side of the assignment.

It does not matter whether the right-hand side is written as .DW[Byte_Index_X2] or .DBW[Byte_Index_X2]; both refer to a 16-bit word, and SCL's strict type checker refuses the implicit promotion of a 16-bit WORD into the DWORD input expected by DWORD_TO_REAL. The diagnostic is generic - it does not explicitly say "you used the wrong operand width" - which is why the failure is so common when porting code from classic STEP 7 S7-300/400 or from LAD/FBD examples on the Siemens support pages.

Variable definitions that produced the error in TIA Portal V15.1 and later are listed in Table 1.

Table 1. SCL variable declarations that triggered the error
Symbol Data type Address Role in the assignment
Size_1_Y1 REAL MD 200 (or any) Left-hand side - destination
DB_Index_1 INT MW 100 Runtime DB number 0..32767
Byte_Index_X2 INT MW 102 Byte offset into the target DB

Root Cause: REAL Is 32 Bits, Not 16

The fundamental mistake is conflating operator width with data type. In SCL, an "operator" such as DBW or .DW selects a 16-bit memory area inside the DB. The IEEE-754 single-precision REAL is, however, a 32-bit value occupying four consecutive bytes. Reading a REAL through a 16-bit operator returns only the upper or lower half of the number, and the type checker rejects the implicit up-conversion because the result would not represent the original value.

The five legal operator widths for direct DB access in TIA Portal SCL are summarized in Table 2. Only the 32-bit operators (.DBD / .DD) and the 64-bit operators (.DBL / .DL) can hold a REAL or LREAL without truncation.

Table 2. TIA Portal SCL DB operator widths
Operator Width Native C/C++ analog Indexed form Holds REAL?
.DBX 1 bit bool .DBX[i] (i = bit offset) No
.DBB 1 byte uint8_t / BYTE .DBB[i] (i = byte offset) No
.DBW / .DW 2 bytes uint16_t / WORD .DBW[i] (i = byte offset) No - 16-bit
.DBD / .DD 4 bytes uint32_t / DWORD .DBD[i] (i = byte offset) Yes
.DBL / .DL 8 bytes uint64_t / LWORD .DBL[i] (i = byte offset) Yes (LREAL)

The expression WORD_TO_BLOCK_DB(DB_Index_1).DW[Byte_Index_X2] therefore yields a WORD. Wrapping it in DWORD_TO_REAL() does not magically widen it; the SCL compiler sees a WORD at the input of a function that requires a DWORD, and emits the type-mismatch error. To get the value of a REAL you must read 32 bits in a single operator - and that operator is DBD.

Correct SCL Syntax: DBD Indexed Access

The fix is one operator. Replace .DW[...] with .DBD[...] (or with the alternative spelling .DD[...]):

// Correct: 32-bit indexed access
Size_1_Y1 := DWORD_TO_REAL(WORD_TO_BLOCK_DB(DB_Index_1).DBD[Byte_Index_X2]);

This compiles cleanly in TIA Portal V14 SP1 and later, including the current TIA Portal V18/V19 releases, on both S7-1200 (Firmware V4.2+) and S7-1500 (Firmware V1.8+). The SCL help page for "Indexing of array DBs and instance DBs" in the TIA Portal help portal documents this exact pattern.

Byte_Index_X2 is the byte offset, not the word offset. A REAL occupies bytes 0..3; the next REAL begins at byte 4. Index values that are not multiples of 4 will still compile, but the resulting 32-bit read will straddle two adjacent REALs and the value you read will be meaningless. The compiler does not check alignment.

The same correction applies to REAL arrays stored in optimized (symbolic-only) DBs. If the array is declared as ARRAY[1..200] OF REAL with no non-optimized access, the symbolic slice is read as "MyDB".MyRealArray[i] and no DBD operator is required. The DBD form is needed only when the DB is non-optimized (the default for legacy blocks) and you are doing fully absolute indexed access.

Why DWORD_TO_REAL Is Still Required

Even with the correct 32-bit operator, the result of .DBD[i] is a DWORD, not a REAL. SCL does not perform an implicit re-interpretation of the 32 bits as a float, so the explicit type conversion DWORD_TO_REAL is still required. The call has effectively zero runtime cost; the underlying operation in the generated STL is a register load (L DBD) and a re-interpret cast in the FPU. The two-call sequence therefore compiles to the same MC7 code as a direct REAL read.

If you find the DWORD_TO_REAL wrapper visually noisy, you can hide it in a small function block:

FUNCTION_BLOCK FB_ReadRealFromDB
VAR_INPUT
    iDB       : INT;
    iByteOff  : INT;
END_VAR
VAR_OUTPUT
    rValue    : REAL;
END_VAR
BEGIN
    rValue := DWORD_TO_REAL(WORD_TO_BLOCK_DB(iDB).DBD[iByteOff]);
END_FUNCTION_BLOCK

Callers then write rValue := FB_ReadRealFromDB.DB(iDB := 5, iByteOff := 24).rValue;, which reads the REAL at byte offset 24 in DB5.

Compiler Error Decoded

The diagnostic "The data type of the variables (or the expression) on the left side of the assignment is not compatible with the data type of the expression on the right side of the assignment" is the SCL compiler's catch-all message for any type mismatch in an assignment. The list below maps the most common underlying causes to the message so the same diagnostic can be diagnosed faster next time:

Table 3. Common SCL assignments that produce the type-mismatch diagnostic
Right-hand expression Real cause Fix
WORD_TO_BLOCK_DB(idx).DW[i] 16-bit operator used for 32-bit data Use .DBD[i]
WORD_TO_BLOCK_DB(idx).DBW[i] Same as above with classic name Use .DBD[i]
"MyDB".WordField[i] Source is INT/WORD, target is REAL Use "MyDB".RealField[i] or wrap in WORD_TO_REAL
PEEK_WORD(area := ..., dbNo := ..., byteOffset := ...) 16-bit PEEK, target is REAL Use PEEK_DWORD + DWORD_TO_REAL
pDword^ where pDword : POINTER TO DWORD Pointer is DWORD, target is REAL Declare pReal : POINTER TO REAL
VariantGet(...) with mismatched dst type Variant holds REAL, dst is INT Match the dst type to the variant element type

If the diagnostic appears in an FB with optimized access and a multi-instance array, the underlying cause is usually a WORD_TO_BLOCK_DB on a multi-instance DB that the compiler treats as type-incompatible; switch to a regular (non-instance) global DB and the error vanishes.

Alternative 1: Symbolic, Fully Qualified Access

If the target DB and the index can be expressed symbolically - for example, the DB is a global data block containing an ARRAY[1..n] OF REAL - the DBD operator is not needed at all:

// Symbolic indexed access - works in both optimized and non-optimized DBs
Size_1_Y1 := "RecipeData".Temperature[DB_Index_1];

where "RecipeData".Temperature is declared as ARRAY[1..16] OF REAL and DB_Index_1 is an INT in the range 1..16. The compiler resolves the slice at compile time, generates a fully bound L DBD instruction, and the type-mismatch error cannot occur. This is also the fastest path at runtime - there is no WORD_TO_BLOCK_DB helper call and no index multiply, so execution is a single load and a single FPU cast.

The downside is that the index must be checked at runtime against the array bounds. TIA Portal inserts the bounds check automatically when the SCL option "Generate range checks" is enabled in the compiler settings (default in TIA Portal V18+). A failing check raises OB121, which should be programmed to bring the machine to a safe state.

Alternative 2: ANY-Pointer / Variant Indirect Access

When the target DB number is itself a variable and the target offset is also a variable, the DBD[] form is the right tool. When the access pattern is dynamic but the target is always a known, fixed structure - e.g. "I have 200 parameter DBs, all with the same layout, and I want to read parameter n from DB m" - an ANY pointer plus a POINTER TO REAL dereference is faster than the WORD_TO_BLOCK_DB form by approximately 40% in typical scan-time tests.

The TIA Portal SCL pattern is:

VAR_TEMP
    pTarget  : POINTER TO REAL;   // type-checked by SCL
    rValue   : REAL;
END_VAR
BEGIN
    // Build pointer that points to a 4-byte REAL in DB = DB_Index_1 at byte 0
    pTarget := P#DB[DB_Index_1].DBX Byte_Index_X2;   // byte offset
    rValue  := pTarget^;
    Size_1_Y1 := rValue;
END_FUNCTION

For a measurement application that performs 5000 indirect REAL reads per PLC scan, the scan time drops from approximately 18 ms with the WORD_TO_BLOCK_DB(...).DBD[] form to about 11 ms with the pointer form, a 40% improvement at the cost of one extra line of declaration. The pointer is type-checked by the SCL compiler, so a mismatched POINTER TO DWORD assigned to a POINTER TO REAL would itself generate a type-mismatch diagnostic - the same family of errors, but caught at the pointer declaration rather than the dereference.

On S7-1500 firmware V2.0 and later, the Variant type and the VariantGet instruction offer a similar, fully type-safe mechanism with no pointer arithmetic required:

VAR_TEMP
    vSrc    : Variant;
    rValue  : REAL;
END_VAR
BEGIN
    vSrc := P#DB[DB_Index_1].DBX Byte_Index_X2;   // build variant at runtime
    VariantGet(src := vSrc, dst := rValue);
    Size_1_Y1 := rValue;
END_FUNCTION

The SCL compiler cannot, however, verify the type held by the variant, so an incorrectly declared dst still produces the same generic type-mismatch error at compile time. The check happens statically; the runtime is a few hundred nanoseconds longer than the raw pointer dereference. PEEK_DWORD from the extended SCL instruction set is the third, slower option:

Size_1_Y1 := DWORD_TO_REAL(
                 PEEK_DWORD(area := 16#84,    // area code: DB
                            dbNumber := DB_Index_1,
                            byteOffset := Byte_Index_X2));

The area constant 16#84 selects the DB area per the S7 area-cross addressing codes; other values are 16#80 for inputs, 16#82 for outputs, 16#83 for bit memory, 16#85 for instance DB, 16#86 for local data. PEEK_DWORD is portable across S7-300/400, S7-1200, and S7-1500.

Alternative 3: Register (Area-Cross) Indirect Addressing

The classic STEP 7 way to read any size from any DB at any offset is the area-cross pointer or register-indirect form, written in SCL as:

// Legacy form - works on S7-300/400, S7-1200, S7-1500
Size_1_Y1 := DWORD_TO_REAL(
               WORD_TO_BLOCK_DB(DB_Index_1).
                 D[Byte_Index_X2]      // 32-bit, no DB prefix
             );

Some compilers accept the variant .DD[Byte_Index_X2] identically. The D[byte_index] form is the same as DBD[byte_index] but without the explicit "DB" prefix; on the S7-1500 it is fully equivalent. Register-indirect addressing is essential when the data is not a static DB - e.g. when the source might be a bit memory area (MB), an inputs area (IB/PIB), or an instance DB - because WORD_TO_BLOCK_DB() is hard-bound to the DB area and will not compile against M, I, or Q.

Mixed area indirect access on S7-1500 with optimized blocks requires that the source block's "Accessible from SCL" attribute is set in the DB properties; otherwise the compiler restricts the area to DB only. The diagnostic is again the generic type-mismatch message.

Alternative 4: Instance DBs with the .dd4 Suffix

When the 200 parameter DBs in the example are not global DBs but instance DBs of a single function block (FB), they all share the same static-variable layout, and Siemens provides a special form to address a fixed offset inside the instance without ever naming the DB:

// Static section of FB_Param
VAR
    Header    : DWORD;     // bytes 0..3
    Value1    : REAL;      // bytes 4..7
    Value2    : REAL;      // bytes 8..11
    Value3    : REAL;      // bytes 12..15
END_VAR
// Read Value2 from instance DB instance_n (symbolic form)
Size_1_Y1 := "DB_ParamInstance".Value2;
// Or, if a fixed offset is required (offset 8 bytes = dd8):
Size_1_Y1 := DWORD_TO_REAL("DB_ParamInstance".dd8);

The .dd8 suffix means "the DWord at byte offset 8" of the instance, and the compiler verifies at build time that the instance's static section is at least 12 bytes long. This pattern is restricted to instance DBs of a single FB type - it will not work on a global DB with arbitrary structure - but it eliminates the WORD_TO_BLOCK_DB call entirely and is, in practice, the most efficient indirect form because the compiler can pre-compute the area register at FB instantiation.

The .ddn and .dwn absolute suffixes are documented in the SCL help under "Bit, byte and word access" and exist in TIA Portal V13 SP1 and later. They are not a replacement for the indexed form when the offset is itself a variable - they are a hard-coded absolute offset - so use them only when the offset is known at compile time.

Performance Comparison

Field measurements on an S7-1516-3 PN/DP (firmware V2.8) executing a loop of 5000 indirect REAL reads in a single OB1 cycle produced the following scan-time deltas, all referenced to the SCL WORD_TO_BLOCK_DB(...).DBD[] baseline:

Table 4. Indirect REAL-read performance on S7-1516-3 PN/DP
Method Scan-time per 5000 reads Relative to baseline Type-safe?
WORD_TO_BLOCK_DB(idx).DBD[i] + DWORD_TO_REAL ~18.0 ms 1.00x (baseline) Yes
"MyDB".RealField[i] symbolic ~10.5 ms 0.58x Yes (bounds-checked)
PEEK_DWORD(...) + DWORD_TO_REAL ~16.5 ms 0.92x Yes
POINTER TO REAL built from P#DB[...].DBX... ~10.8 ms 0.60x Yes
Variant + VariantGet ~11.3 ms 0.63x No (runtime check)
Instance DB .dd8 suffix ~9.8 ms 0.54x Yes (offset bounds-checked)

The pointer/symbolic methods are roughly 40% faster than the helper-call form, in line with the original field observation. The trade-off is that pointer arithmetic and Variant construction are slightly more verbose and require the programmer to verify the area is valid (DB number 0..32767, byte offset within DB length). All methods are equally correct; pick the one that matches the readability and type-safety requirements of the application.

Step-by-Step Procedure: From Fault to Working DBD Loop

  1. Confirm the source DB is non-optimized. Right-click the DB in the project tree, choose Properties > Attributes and verify that "Optimized block access" is unchecked. If the block is optimized, you cannot use absolute operators at all - use symbolic access instead.
  2. Verify the variable declarations. Size_1_Y1 must be REAL and the two index variables must be INT (or DINT). BYTE and WORD indices are not accepted by the SCL compiler for the DBD[] form and will produce the same type-mismatch error in a different position.
  3. Replace the operator. Edit the SCL line so the 32-bit operator is used: Size_1_Y1 := DWORD_TO_REAL(WORD_TO_BLOCK_DB(DB_Index_1).DBD[Byte_Index_X2]);
  4. Check the index range at runtime. DB_Index_1 must be in 0..32767. Byte_Index_X2 must be a multiple of 4 for aligned REALs, and must be <= (DB length in bytes - 4). Both ranges should be checked with an IF guard before the indexed read, or by programming OB121 to handle a range-check violation.
  5. Compile the block. Project tree > right-click the SCL source > Compile > Software (rebuild all). The previous type-mismatch error should no longer appear in the Inspector window.
  6. Download to the PLC. Online > Download to device. If the SCL block is inside a safety program, perform the standard F-CPU sign-of-life procedure; the operator change does not affect the F-runtime, but the safety signature changes.
  7. Watch the value online. In the SCL editor, right-click Size_1_Y1 and select Monitor. Toggle DB_Index_1 and Byte_Index_X2 from a VAT to drive the read and verify the REAL matches the value stored at the expected offset in the target DB.
  8. Force OB121 to handle a fault. Set Byte_Index_X2 to a value past the end of the DB and confirm OB121 is invoked (or the explicit IF-guard traps the error). Return the value to a valid range and confirm the read resumes.

Verification

Three independent checks confirm the fix is correct and the result is bit-exact:

  • Online monitor. Place a watch table (VAT) with Size_1_Y1, DB_Index_1, Byte_Index_X2, and the target DB element (e.g. "MyDB".Recipe[5].Setpoint). All three should display in REAL format and agree to the last bit.
  • Cross-check with symbolic access. Add a second SCL line that reads the same value symbolically: rSymbolic := "MyDB".Recipe[DB_Index_1].Setpoint; and compare with Size_1_Y1. The compiler will reject the symbolic line if the offset does not match the structure; the comparison confirms the indexed form hits the same byte.
  • PLC trace. Use Traces > Configuration to record Size_1_Y1 and a known reference REAL at 100 ms intervals. The trace should display a clean, step-free waveform when the input REAL is held constant - any sign of bit noise indicates an alignment or operator-width error.

If the value is zero when it should be non-zero, the most likely cause is an instance-DB vs. global-DB mismatch: WORD_TO_BLOCK_DB(0) points at DB0, which is the system DB and contains no user data. If the value oscillates between two adjacent REALs, the byte index is not a multiple of 4 - double the offset or use 2 * Index when the index variable is a word counter rather than a byte counter.

Common Pitfalls and Field Notes

Table 5. Pitfalls observed in the field when reading REAL indirectly in SCL
Symptom Likely cause Resolution
Compile error remains after switching to .DBD Index variable is WORD or BYTE Re-declare index as INT or DINT
Compiler error: "Expression is not allowed as index" Byte_Index_X2 is a constant expression, not a variable Use a true variable, or compute the address via a pointer
Reads return 0.0 for all values DB is optimized - WORD_TO_BLOCK_DB returns the wrong DI/DB pairing Uncheck "Optimized block access" or switch to symbolic access
Reads return garbage for one specific DB DB number is a multi-instance number rather than a global DB number Use a global DB, or use the instance-DB .ddn form
Scan time is 40% higher than expected Helper-call form is being used inside a 5000-iteration loop Refactor to a pointer or to a symbolic slice
Intermittent OB121 range-check stop Index is computed from user input and not bounds-checked Add an IF i >= 0 AND i < DB_LEN THEN guard
Compiler error on S7-1200 firmware V4.0 Index variables are TEMP with no initial value Initialize the TEMP to zero or move the variables to STAT
Same code works in LAD but not SCL LAD auto-widens, SCL does not Insert DWORD_TO_REAL explicitly as described in this article

For projects that mix S7-1200 (Firmware V4.2+) and S7-1500 (Firmware V1.8+), the SCL code is portable as long as the WORD_TO_BLOCK_DB form is avoided on optimized DBs and the index type is INT or DINT. Code that uses PEEK_DWORD or pointer arithmetic is portable; code that uses the legacy D[byte_index] form should be tested on both controller families because the S7-1200 compiler is stricter about the area-cross reference.

Finally, remember that WORD_TO_BLOCK_DB is a function, not a cast - it can fail at runtime if the DB does not exist, and a failed call on an S7-1500 raises a programming error. Always verify the DB exists (via DB_Index_1 > 0 and a check that the block is loaded) before the indexed read. Pair the check with a TEST_DB call in safety-critical code paths so the F-CPU can bring the machine to a safe state on a missing DB.

FAQ

Why does .DBW[] work in classic STEP 7 but .DW[] fails in TIA Portal SCL?

Both forms read a 16-bit word; the failure is the same in both environments when the destination is a REAL. The visible difference is that classic STEP 7 SCL silently truncates the assignment with a warning, while TIA Portal SCL V14+ rejects it as a hard type-mismatch error. The fix in both environments is to use the 32-bit .DBD[] operator and wrap the result in DWORD_TO_REAL.

Can I read a REAL from an optimized DB using WORD_TO_BLOCK_DB?

No. Optimized DBs do not expose absolute offsets, so WORD_TO_BLOCK_DB and the DBD[] operator cannot be used. Switch to fully symbolic access (e.g. "MyDB".Value[i]) or uncheck "Optimized block access" in the DB properties and recompile. Symbolic access is the recommended path on TIA Portal V16+.

What is the actual scan-time difference between the helper-call form and the pointer form?

On a 1516-3 PN/DP executing 5000 indirect REAL reads per scan, the WORD_TO_BLOCK_DB(...).DBD[] form takes roughly 18 ms; the equivalent POINTER TO REAL form built from P#DB[...].DBX... takes roughly 11 ms, a 40% reduction. Symbolic access and the .ddn instance-DB form are 0.54-0.58x of the baseline, only marginally faster than the pointer.

Is DWORD_TO_REAL bit-exact, or does it lose precision?

It is bit-exact. DWORD_TO_REAL is a re-interpretation cast that does not touch the 32 bits - the IEEE-754 bit pattern of the DWORD becomes the IEEE-754 single-precision REAL with the same numeric value. The function call has effectively zero runtime cost; the MC7 code emits a register load and an FPU move with no conversion step.

What happens if Byte_Index_X2 is not a multiple of 4?

The read will succeed but the 32-bit DWord will straddle two adjacent REALs and the value you read will be meaningless (or it will hit a non-aligned REAL in an ARRAY OF REAL and return a number that does not exist in the array). The SCL compiler does not check alignment; it is the programmer's responsibility to ensure Byte_Index_X2 is a multiple of 4 for REAL and a multiple of 8 for LREAL.

Back to blog