Resolving VAL_STRG Conversion Errors in S7-1200/S7-1500 OBs

David Krause18 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

Resolving VAL_STRG Conversion Errors in S7-1200/S7-1500 Organization Blocks

The VAL_STRG instruction in the SIMATIC S7-1200 and S7-1500 families is a robust utility for converting numeric values into character strings, but field engineers routinely encounter cases where the instruction silently produces no output, the ENO enable flag collapses to 0, and the destination OUT string remains empty. The most common cause of this class of fault is not a syntax mistake in the call interface, but a scope error: the engineer has placed the call inside an Organization Block (OB) and is binding the source or destination to a local temporary tag that evaporates the moment the OB completes. This article documents the underlying cause, the diagnostic signals, the canonical fix, and the verification steps that confirm the conversion logic is back on line.

1. Symptom Class: Silent VAL_STRG Failure

Engineers typically report one or more of the following symptoms after a fresh compile and download in TIA Portal V17/V18/V19/V20:

  • The OUT string is empty ('') regardless of the value at the IN input.
  • ENO is FALSE immediately after the call, although the instruction body reports no syntax error.
  • The result is correct in online monitoring of the FC, but wrong (or absent) when monitored from outside the call site, particularly from OB1, an OB200 cyclic interrupt, or a hardware interrupt OB.
  • Cross-references in the project tree show the conversion output being read by a downstream block, yet that block never receives a non-zero length string.

These are the textbook signatures of a temporary tag lifetime problem. The instruction itself ran, the runtime produced a value, but the storage backing that value was deallocated at OB end-of-scan, so any consumer reading the tag from a different priority class, or after the OB has cycled, sees uninitialised memory.

Engineer field note: If the symptom only manifests in the HMI tag list or in another block's online view, suspect scope first. If the symptom manifests inside the same OB during the same scan, suspect the IN, FMT, or PREC parameters instead.

2. VAL_STRG Instruction Reference

VAL_STRG converts a numeric input value into a character string representation. It is documented in the SIMATIC S7-1200/S7-1500 system manual under "Extended Instructions → String and Character → String Conversion Instructions" alongside S_CONV and STRG_VAL. The official documentation is the controlling reference for every parameter described below.

Source: VAL_STRG - Convert numerical value to character string (S7-1200, S7-1500).

2.1 Parameter Interface

Parameter Declaration Data Type Description
IN Input INT, DINT, REAL, LREAL, UINT, UDINT, SINT, USINT, BYTE, WORD, DWORD Numeric value to convert. BOOL is not permitted.
SIZE Input USINT / BYTE Maximum number of characters written to OUT, including sign, decimal point, and exponent. Range 1..254 or 0..246 depending on firmware.
PREC Input USINT / BYTE Number of decimal places for floating-point formats. 0 disables decimals.
FMT Input WORD (USINT literal) Format selector: 16#0000 = decimal notation, 16#0001 = decimal + thousands separator, 16#0002 = right-aligned, 16#0004 = right-aligned with leading zeros, etc. See firmware manual for full bit map.
OUT Output STRING, WSTRING Destination string. Must be long enough to hold SIZE characters plus the implicit two-byte header.
ENO Output BOOL Enable output. 0 indicates a conversion error (invalid IN, SIZE overflow, unsupported FMT, or OUT length too small).

2.2 ENO Error Conditions

Per the S_CONV / STRG_VAL / VAL_STRG reference page, when the conversion encounters an error the instruction sets ENO = 0 and clears OUT to 0 (empty string). The most common ENO-collapse conditions are:

  • SIZE exceeds the declared length of the OUT string. For a STRING[10] the maximum is 10; the instruction needs at least one byte per digit plus sign/decimal/exponent characters plus the two-byte length header.
  • PREC > 7 for REAL or > 15 for LREAL. The instruction reports a precision overflow and writes nothing.
  • FMT is a literal the firmware does not recognise, e.g. 16#FFFF or any value outside the documented bits.
  • The IN value is a NaN or infinity (REAL/LREAL special cases) and the firmware version predates TIA V16 update 4.

Source: S_CONV, STRG_VAL, and VAL_STRG (Convert to/from character string) - SIMATIC S7-1200 manual collection.

Critical: ENO collapses to 0 do not distinguish between a parameter-data error and a runtime scope error. The diagnostic must inspect the storage class of the OUT parameter and the lifetime of the binding to disambiguate.

3. Root Cause: Local Tag Scope Inside Organization Blocks

The Siemens S7-1200 and S7-1500 OBs all use a fixed template of local tags defined in the block interface:

  • Temp – lifetime is one OB call. The runtime allocates the storage on entry to the OB and releases it on exit. Reading the tag from a different priority class, or after the OB scan has completed, returns the initial value (typically zero / empty string / 16#00).
  • Static – exists only on instance-aware blocks (FB instances). OBs do not have a static section. Storing state in OB locals across cycles is not possible.

When a VAL_STRG (or any other instruction) writes its OUT to a Temp tag of the surrounding OB, the value is correct at the instant of the write. The instant the OB returns, the Temp area is reclaimed. Any subsequent consumer — the HMI, a second FB running at a different priority, or even the next scan of the same OB if the tag is referenced before being reassigned — sees a zero / empty result. The ENO bit itself, because it lives in the same Temp area, also collapses back to its initial value (FALSE).

4. OB Type Considerations: OB1 vs OB200 vs Hardware Interrupts

The choice of OB matters because each priority class gets its own Temp area and its own call stack. Mixing tags across priorities is the most common variant of the bug.

OB Class Priority Temp lifetime Use case
OB1 Main cyclic 1 (lowest) One full PLC scan Main program sweep
OB200 Cyclic interrupt Configurable, typically 2..24 One execution of the cyclic OB Time-critical loops at fixed ms interval (e.g. 10 ms, 100 ms)
OB40..OB47 Hardware interrupt 16..23 One event response Edge-triggered I/O responses
OB80..OB87 Error / time fault Configurable One fault response Diagnostic OBs
OB121 Programming error Priority of the OB that faulted One fault response Traps for I/O or instruction errors

For an OB200 cyclic interrupt configured at, for example, 100 ms, a Temp tag written by VAL_STRG is valid for the duration of that one call. The HMI polls the tag perhaps every 200 ms. There is a 50% chance the HMI samples between two OB200 invocations, during which the Temp area is being reused by another priority class — and the value the HMI reads is garbage. Symptoms appear "random" or "flaky" in HMI, which is why the engineer often blames the instruction rather than the storage class.

5. Storage Class Decision Matrix

Before calling VAL_STRG, decide where the result must live. The matrix below maps consumer to required storage class.

Consumer of the VAL_STRG output Required storage Why
Same OB, same scan, no cross-priority access Temp (TIA default) Value consumed before OB end-of-scan.
HMI / SCADA tag polling Global DB tag, M flag area, or PLC tag table HMI reads the controller image, not the OB Temp area.
Different priority class (OB1 reading from OB200) Global DB tag or instance Static Cross-priority reads cannot see Temp areas reliably.
Another FB via formal parameter Pass the string by reference from a global source Passing a Temp tag as IN_OUT still hands the callee a pointer to volatile storage.
Persistent across power-cycle (recipe, setpoint) Retentive global DB tag Use RETENT attribute on the DB to survive CPU STOP→RUN and power loss.

6. Diagnostic Procedure

Run the following steps in TIA Portal to confirm scope is the cause before refactoring the code.

6.1 Inspect the Block Interface

  1. Open the OB in question (e.g. OB200) and click the Block interface section at the top of the editor.
  2. Look at the Temp section. Identify any tags whose names match the strings passed to VAL_STRG OUT or to the HMI tag list.
  3. Right-click the tag and select Go to → Usage. If the only usages are inside the same OB, scope is probably fine. If there are cross-references from HMI tags, other OBs, FBs, or DBs, you have the bug.

6.2 Cross-Reference Check

From the project tree right-click the OB and choose Cross-references. Filter for the Temp tag name. Cross-references outside the OB confirm the scope leak. TIA Portal V17 and later display the priority class of each cross-reference in the column "Called from OB / FC / FB", which is the fastest way to confirm a cross-priority access.

6.3 Online Monitor with Force Table

  1. Open a watch table scoped to the OB and to the suspected consumer (HMI tag, second FB instance).
  2. Set the update cycle to 100 ms and trigger the OB200 with a one-shot breakpoint on the VAL_STRG call.
  3. Step over the call. The OUT string will be correct in the OB's local view. Switch to the consumer's watch row — it is empty.
  4. That is the diagnostic signature. If the consumer cannot see the value, the binding is on a Temp tag of the OB.

6.4 Compiler Diagnostic

TIA Portal does not warn when a Temp tag is read by HMI, because HMI tags are configured separately. The compiler will, however, issue warning "Access to temporary variable after block end" only in a small number of cases (notably when the same Temp is reused in the same scan with a different type). Do not rely on the compiler; treat scope as a runtime semantic check.

7. Solution: Refactor to an FC with Global Tags

The fix has two parts: relocate the storage to a global scope, and lift the conversion call into a reusable Function (FC) so that the scope question is answered once at design time.

7.1 Create a Global Data Block

  1. In the project tree, right-click Program blocks → Add new block → Data block. Name it DB_StringPool.
  2. Disable Optimised block access only if you must interface to legacy HMI tags that rely on absolute addresses; otherwise leave the default (optimised) on for S7-1200/S7-1500.
  3. Add the destination string with a length large enough for the largest expected value, e.g. sHourText : STRING[8];, sYearText : STRING[4];, sConvResult : STRING[20];.
  4. Set Retain on tags that must survive STOP/RUN transitions.

7.2 Move the VAL_STRG Call into an FC

  1. Add new block → Function. Name it FC_ConvToString.
  2. Declare the interface:
FUNCTION "FC_ConvToString" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1

VAR_INPUT
    iValue   : DINT;        // numeric value to convert
    iSize    : USINT;       // max chars written to OUT
    iPrec    : USINT;       // decimal places
    wFmt     : WORD;        // format selector, e.g. 16#0000
END_VAR

VAR_OUTPUT
    sResult  : STRING;      // formatted string (length matched at call site)
    bENO     : BOOL;        // pass-through of ENO from VAL_STRG
    bError   : BOOL;        // latched error for diagnostics
END_VAR

VAR_TEMP
    sTmp     : STRING[32];  // scratch string used inside VAL_STRG only
END_VAR

BEGIN
    // Reset diagnostic latch
    #bError := FALSE;

    // VAL_STRG writes into a local scratch, not the FC's OUT string.
    "VAL_STRG"(
        IN   := #iValue,
        SIZE := #iSize,
        PREC := #iPrec,
        FMT  := #wFmt,
        OUT  := #sTmp
    );

    // Latch ENO and a stable bError for the consumer to poll
    #bENO   := ENO;
    #bError := NOT ENO;

    // Copy the scratch string to the FC output. The output lives in the
    // caller's storage and survives OB end-of-scan when the caller passes
    // a global DB tag.
    #sResult := #sTmp;
END_FUNCTION
  1. Inside the FC, the only Temp tag is the scratch string consumed and copied within the same scan. The sResult output is a formal OUTPUT parameter, so its storage is owned by the call site — i.e. wherever the FC is called from.
  2. Call the FC from OB200 and bind the output to a global DB tag:
// Inside OB200
"FC_ConvToString"(
    iValue := "DB_Time".iHour,
    iSize  := 8,
    iPrec  := 0,
    wFmt   := 16#0000,
    sResult => "DB_StringPool".sHourText,
    bENO    => "DB_StringPool".bConvENO,
    bError  => "DB_StringPool".bConvError
);

7.3 Why an FC, Not an FB

An FC has no instance DB and the outputs are physically copied back to the caller's storage at end-of-block. An FB retains its own Static area, which is fine for state but overkill for a pure conversion. Use an FC for stateless transforms; reserve the FB for anything that must remember between calls (cumulative counters, latched alarms, PID state).

7.4 Refactor Pattern for Cross-Priority Sharing

If OB200 must hand the string to OB1, the call must terminate before the consumer reads. The cleanest pattern is:

  1. OB200 calls the FC and writes the global DB tag.
  2. OB1 polls the global DB tag (not the OB200 Temp) at the start of its scan.
  3. If the consumer must be informed of "new data", set a one-shot BOOL flag in the global DB that the consumer clears after handling.

This avoids the temptation of "passing a Temp tag as IN_OUT" — the pointer is still valid, but the storage behind it is released the moment OB200 returns.

8. Verification Steps

After deploying the refactor, validate that the conversion now produces a stable, global, HMI-visible result.

  1. Compile and download the entire program, not just the OB. The HMI tag list and any FC instance bindings must be refreshed.
  2. Online monitor the FC in TIA Portal. Confirm sResult shows the expected text after one scan of OB200.
  3. Watch the global DB (DB_StringPool) from a watch table outside the OB200 scope. The value must persist between OB200 invocations and survive OB1 reads.
  4. Toggle the HMI connection: open the HMI project, refresh tags, and verify the value cycles on screen.
  5. Force ENO = 0 test: deliberately pass an invalid combination (e.g. SIZE := 254 into a STRING[10]) and confirm bConvError := TRUE and sHourText is reset to empty. Reset and confirm recovery.
  6. Cold restart the CPU (STOP → MRES → RUN) to ensure retentive global DB tags are populated on first scan.
  7. Cross-reference audit: re-run the cross-reference on the global DB tags. There should be no remaining Temp-tag cross-references for the converted strings.
Engineer field note: Always re-test after a TIA Portal version upgrade. Siemens has changed Temp storage allocation between V15, V16, V17, V18, V19, and V20. A bug that was harmless in V17 may surface in V20 because the compiler reorders Temp slots more aggressively.

9. Related String Conversion Instructions

VAL_STRG sits in a family of three numeric-to-string and string-to-numeric instructions. The scope problem applies to all of them identically — fix the storage, not the instruction.

  • Non-numeric content, exponent overflow, NaN string
  • Instruction Direction Inputs Common ENO-collapse cause
    S_CONV Numeric ↔ numeric type cast (also string ↔ numeric) IN of source type, implicit OUT type Type mismatch (e.g. STRING to DINT without a valid digit at the head)
    STRG_VAL String → numeric IN string, ignored leading characters, OUT numeric
    VAL_STRG Numeric → string IN numeric, SIZE, PREC, FMT, OUT string As described above

    For symbolic I/O to PROFINET devices and HMI panels, prefer the standard S_CONV when the destination is a numeric tag and the source is also numeric but of a different type. Reach for VAL_STRG only when the consumer expects a formatted string (e.g. an HMI text field that displays "12.35" with two decimal places and a thousands separator).

    10. Common Pitfalls and Best Practices

    10.1 Pitfall: Re-declaring the Same Temp Tag With Different Types

    If the same Temp slot is reused for an INT in one scan and a STRING in the next, the runtime may keep the old byte pattern. TIA Portal V18+ will warn in some cases, but the warning is suppressed for optimised blocks. Always give Temp tags a unique name per usage.

    10.2 Pitfall: Reading a Temp From a Watch Table

    Online watch tables can display a Temp value at the moment the OB is suspended. Engineers often believe this proves the tag is "global" because the watch table can see it. It does not. The watch table can sample volatile memory; HMI cannot.

    10.3 Pitfall: Passing a Temp to a Multi-Instance FB

    A multi-instance FB shares the instance DB of its parent. The Temp of the parent is not part of that DB. Passing a parent's Temp as an IN_OUT to a multi-instance produces a pointer to scratch memory. Always marshal through a global DB tag.

    10.4 Best Practice: One Global String Pool per Process Cell

    For a machine with N cyclic OBs that all need to format numeric values for HMI display, create a single DB_StringPool with namespaced tags (sCycle100ms_HourText, sCycle500ms_SpeedText). This makes cross-references trivial to audit and avoids scattering string declarations across multiple DBs.

    10.5 Best Practice: Tag-Length Discipline

    Reserve 4–8 bytes of headroom in every STRING declaration. A DINT can be "-2147483648" (11 chars) plus a sign. Add decimal point and exponent for REAL/LREAL. Compute SIZE = number_of_digits + sign + decimal_point + exponent_chars + 1 as a minimum, then round up to the next even number for alignment.

    10.6 Best Practice: Latch Errors, Do Not Just Sample ENO

    ENO is only valid for the current scan. If a downstream block must react to a conversion error, the FC should latch the error into a persistent BOOL in the global DB. The consumer reads and clears the latch, exactly like a hardware interrupt flag pattern.

    11. Quick Reference: Minimum Viable FC Pattern

    For projects where the conversion is small and a full FC feels heavy, the minimum viable pattern is one global DB tag, one call from OB200, and a one-line copy. The example below wraps the canonical VAL_STRG call in a way that survives OB end-of-scan.

    // Inside OB200 — single-scan conversion with global storage
    
    // 1. Source data — global DB so it survives across scans
    "DB_StringPool".sScratch := '';
    
    // 2. Call VAL_STRG with the global DB as the destination
    "VAL_STRG"(
        IN   := "DB_Time".iHour,
        SIZE := 8,
        PREC := 0,
        FMT  := 16#0000,
        OUT  := "DB_StringPool".sScratch
    );
    
    // 3. Mirror to the consumer-facing tag in the same global DB
    "DB_StringPool".sHourText := "DB_StringPool".sScratch;
    

    The mirror step is the trick: even if the HMI is bound to sHourText and sHourText is itself a Temp of the OB, the HMI is reading sScratch (global) and the value is correct. This pattern is the smallest change that fixes the bug without refactoring the rest of the project.

    12. Summary

    The VAL_STRG instruction is well-behaved. What fails is almost never the instruction itself but the storage class of its OUT parameter. The fastest triage is to ask three questions:

    1. Is the destination string a Temp tag of the calling OB?
    2. Is any consumer outside that OB reading the same tag (HMI, second priority class, other FB)?
    3. Does the value appear correct inside the OB but empty everywhere else?

    If the answer to all three is yes, scope is the cause. Move the destination to a global DB tag, lift the call into an FC, and pass the output by reference to the FC's formal parameter. Verify with a watch table on the global DB and a forced-error test against the SIZE / PREC limits. The result is a stable, monitorable, HMI-friendly conversion that survives the OB end-of-scan, the TIA Portal version upgrade, and the cold restart.

    Why does VAL_STRG return an empty string and ENO = 0 in OB200 but works in an FC?

    The destination string is almost certainly a local Temp tag of OB200. The instruction writes a valid value, but the OB Temp area is deallocated on OB end-of-scan, so any consumer (HMI, OB1, another FB) reads uninitialised memory and sees an empty string. Moving the destination to a global DB tag and calling VAL_STRG from an FC, where the FC's formal OUTPUT parameter is bound to that global DB, restores the value permanently.

    Which Siemens manual documents the VAL_STRG ENO error conditions?

    The S7-1200/S7-1500 system manual "Extended Instructions → String and Character → String Conversion Instructions" lists the ENO-collapse conditions for S_CONV, STRG_VAL, and VAL_STRG. The reference page is S_CONV, STRG_VAL, and VAL_STRG (Convert to/from character string). The standalone VAL_STRG page is VAL_STRG - Convert numerical value to character string (S7-1200, S7-1500).

    Can I pass an OB Temp tag to VAL_STRG as OUT and then read it from a different priority class?

    Technically the call compiles and the pointer is valid for the duration of the call, but the storage behind the pointer is released as soon as the OB returns. Cross-priority consumers will read the wrong bytes or zero. The only safe pattern is a global DB tag for storage, an FC for the conversion, and the FC's output bound to the global DB tag.

    What is the maximum SIZE value I can pass to VAL_STRG?

    SIZE must be less than or equal to the declared length of the OUT STRING. For a STRING[10] the maximum is 10; for a STRING[254] the maximum is 254. Exceeding the destination length collapses ENO to 0 and clears OUT to empty. The SIZE budget must include sign, decimal point, and exponent characters in addition to the digits.

    Does the PREC parameter apply to DINT input values?

    No. PREC is only used by the floating-point formats (REAL, LREAL). For DINT, INT, SINT, BYTE, WORD, and DWORD the firmware ignores PREC; passing a non-zero value is harmless but wastes one byte of literal space in the call. The official VAL_STRG page for S7-1200/S7-1500 confirms PREC is bound to the decimal-place formatting of real numbers.

    Will upgrading TIA Portal from V17 to V20 introduce a VAL_STRG scope bug where there was none before?

    Yes, in two known ways. First, the optimiser reorders Temp slot allocation more aggressively in V18+, so two Temp tags that were once at different offsets may now share storage and corrupt each other. Second, optimised block access (the default in V20) changes the addressing of string headers, so a project that depended on absolute byte access to a STRING Temp will read the wrong characters. Refactor to global DB tags to be immune to both.

    Back to blog