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:
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.
| 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.
| 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.
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:
| 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.
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.
.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:
| 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
- 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.
-
Verify the variable declarations.
Size_1_Y1must beREALand the two index variables must beINT(orDINT).BYTEandWORDindices are not accepted by the SCL compiler for theDBD[]form and will produce the same type-mismatch error in a different position. -
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]); -
Check the index range at runtime.
DB_Index_1must be in 0..32767.Byte_Index_X2must be a multiple of 4 for aligned REALs, and must be <= (DB length in bytes - 4). Both ranges should be checked with anIFguard before the indexed read, or by programming OB121 to handle a range-check violation. - 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.
- 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.
-
Watch the value online. In the SCL editor, right-click
Size_1_Y1and select Monitor. ToggleDB_Index_1andByte_Index_X2from a VAT to drive the read and verify the REAL matches the value stored at the expected offset in the target DB. -
Force OB121 to handle a fault. Set
Byte_Index_X2to 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 withSize_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_Y1and 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
| 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.