Siemens WinCC CSV Real Values: Resolving Decimal Format Errors

David Krause16 min read
HMI / SCADASiemensTroubleshooting
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

1. Problem Overview

When logging Real (32-bit floating-point, IEEE 754) tag values from a Siemens WinCC HMI runtime to a CSV file, engineers frequently observe that the decimal separator rendered in the file does not match the expected engineering value. Common failure modes include:

  • A value of 1.2345 appears in the CSV as 1,2345 (decimal point converted to comma).
  • A value of 1.2345 appears as 12,345 (decimal point stripped, comma mistakenly used as thousands separator, or value multiplied by 1000).
  • A value of 1234.5678 appears as 12345678 (decimal separator dropped entirely, or formatted with German thousand-separator logic that confuses downstream Excel parsing).
  • Integer-magnitude values such as 9999 lose their decimal-place control because the script applies a single formatting path that assumes fractional content.

This defect corrupts the CSV file because downstream tools (Microsoft Excel, Power BI, MATLAB, Python pandas) interpret the comma as a column delimiter. The data becomes unusable for mathematical analysis, trend reporting, or audit logging without manual cleanup.

The original reference implementation described in the Siemens FAQ 26107211 works correctly for Integer tag types but breaks when the tag data type is changed to Real. The updated TIA Portal scripting pattern is documented in FAQ 59604194.

Critical constraint: CSV files generated for engineering or regulatory analysis must use the period (.) as the decimal separator regardless of the operator panel's regional locale. Excel and most data analysis stacks parse 1.2345 as a real number and 1,2345 as text.

2. Root Cause Analysis

The CSV decimal defect is rooted in locale-dependent string conversion performed by the Windows Script Host and VBScript runtime that WinCC uses for its file I/O operations.

2.1 Windows Regional Settings Inheritance

WinCC Runtime Professional and WinCC Comfort/Advanced inherit the decimal separator symbol from the operating system's Region configuration. The relevant Windows settings are:

Setting Path Effect on VBScript
Decimal symbol Settings → Time & Language → Region → Additional date, time, & regional settings → Change date, time, or number formats → Additional settings Determines the character produced by CStr(realValue) and string concatenation with the & operator.
Digit grouping symbol Same path Determines the character inserted every three digits when using FormatNumber.
List separator Same path Determines the delimiter used by Excel when saving as CSV.

When the runtime host is configured with German (Germany), French (France), Spanish (Spain), or any other locale that uses a comma as the decimal symbol, VBScript returns strings such as "1,2345" from numeric conversions.

2.2 VBScript Type Conversion Behavior

The default VBScript CStr() function and the & concatenation operator use the system locale, not the script's logical locale. This is unlike .NET's ToString(IFormatProvider) pattern. The relevant functions and their locale behavior are summarized below:

Function Locale-sensitive? Output for 1.2345 on German host
CStr(1.2345) Yes "1,2345"
1.2345 & "" Yes "1,2345"
FormatNumber(1.2345, 4) Yes "1,2345" (and adds thousand separator if grouping enabled)
FormatNumber(1.2345, 4, , , 0) Partial "1,2345" but no grouping
Format(1.2345, "0.0000") No "1.2345" (always period)
Replace(CStr(1.2345), ",", ".") Manual "1.2345"

2.3 Why the Multiply-Truncate Pattern Fails for Mixed-Magnitude Tags

A commonly suggested workaround in the engineering community is to multiply the real value by 10^n, truncate to DINT, and divide by 10^n in order to control decimal precision. The pseudocode is:

rValue = HMIRuntime.Tags("Tag1").Read
rScaled = rValue * 10000        ' assume 4 decimals
iTrunc = Fix(rScaled)           ' truncate toward zero
rFinal = iTrunc / 10000         ' produce real with controlled decimals

This pattern breaks in two scenarios:

  1. Integer-magnitude values (e.g. 9999). When rValue = 9999, rScaled = 99990000, which exceeds the DINT range (max 2,147,483,647) only at the 4-decimal scale; however, if the operator chooses 5 or 6 decimal places the value overflows and the result is corrupted. Even within range, dividing back yields 9999, but the trailing decimal zeros (9999.0000) cannot be recovered because DINT has no fractional component.
  2. Negative values. Fix() truncates toward zero, which is correct, but intermediate multiplication can still overflow with very negative inputs combined with high decimal scaling.

The root issue is that the multiply-truncate method is a precision-truncation algorithm, not a formatting fix. It modifies the data magnitude and is unsuitable for round-trip logging.

3. Diagnostic Checklist

Run the following diagnostics on the WinCC runtime host before applying any fix. Document each result.

  1. Confirm tag data type. Open the WinCC tag management and verify the tag used in the script is declared as Real (32-bit float) or LReal (64-bit float). Mixed-type scripts that read DInt tags do not exhibit the decimal separator problem.
  2. Inspect the OS locale. On the runtime PC, run control.exe /name Microsoft.RegionAndLanguage and note the Decimal symbol and Digit grouping symbol values.
  3. Capture the raw VBScript output. Insert a temporary MsgBox CStr(rValue) line in the script and observe whether the dialog displays a comma or period.
  4. Inspect the CSV in a hex editor. Open the generated CSV in a tool that displays raw bytes (Notepad++ with View → Show Symbol → Show All Characters, or any hex viewer). This separates encoding issues (UTF-8 BOM, OEM codepage) from separator issues.
  5. Test with a controlled value. Force the tag to 1.2345 via the PLCSIM or HMI tag simulator, run the logging cycle once, and inspect the CSV row.

4. Solution 1: VBScript Format() Function with Fixed-Pattern Mask

The cleanest solution is to use VBScript's Format() function with an explicit numeric picture string. Unlike FormatNumber(), Format() is not affected by the system locale; it always interprets the picture string literally.

' Force four decimal places regardless of host locale
Dim rValue, sValue
rValue = HMIRuntime.Tags("AnalogTag_1").Read
sValue = Format(rValue, "0.0000")

The picture string "0.0000" always produces a period as the decimal separator. The leading 0 before the period guarantees at least one digit to the left of the decimal, even for values between -1 and 1. The trailing zeros guarantee a fixed width of four decimal digits, which is useful for column-aligned CSV output.

If variable decimal precision is required, build the picture string dynamically:

Dim iDecimals, sPattern
iDecimals = 4
sPattern = "0." & String(iDecimals, "0")    ' result: "0.0000"
sValue = Format(rValue, sPattern)
Note: The Format() function in VBScript only supports a fixed subset of picture tokens (0, #, ., ,, %, scientific notation E+/-). Locale-aware tokens such as the system thousand separator are not honored; the period and comma in the picture string are always emitted literally.

5. Solution 2: Replace Comma with Period After Conversion

If preserving maximum significant digits is the priority (no trailing zeros, no rounding), apply the conversion with the default CStr() and then rewrite the separator using Replace():

Dim rValue, sValue
rValue = HMIRuntime.Tags("AnalogTag_1").Read
sValue = Replace(CStr(rValue), ",", ".")

This pattern is robust against locale changes because Replace() is purely string-based. The output preserves every significant digit the IEEE-754 representation can hold (typically 6-7 digits for 32-bit Real, 15-17 digits for 64-bit LReal).

Edge case: If the system locale uses a period for the decimal symbol and a comma for digit grouping, Replace() will incorrectly convert thousand separators. To guard against this, use a more selective approach that only replaces a single comma followed by 1-4 digits at the end of the string:

Function NormalizeDecimal(sRaw)
    Dim oRegex, sResult
    Set oRegex = CreateObject("VBScript.RegExp")
    oRegex.Pattern = "(\d),(\d{1,4}$)"
    oRegex.Global = False
    sResult = oRegex.Replace(sRaw, "$1.$2")
    NormalizeDecimal = sResult
End Function

This regex matches a digit, a comma, and 1-4 trailing digits, and rewrites the comma as a period. It correctly handles values like 1,23451.2345 but leaves values like 1,234,567 unchanged (because the trailing capture is only 4 digits).

6. Solution 3: Multiply-Truncate-Divide for Fixed Decimal Precision

When the application requires deterministic decimal precision (for example, regulatory logging at exactly four decimals, no scientific notation), the multiply-truncate-divide pattern is appropriate if the magnitude is bounded:

Function FixedDecimal(rValue, iDecimals)
    Dim rScale, iScaled, rResult
    rScale = 10 ^ iDecimals
    iScaled = CLng(rValue * rScale)    ' CLng for 32-bit signed range
    If iScaled > 2147483647 Or iScaled < -2147483648 Then
        ' Overflow: fall back to Format()
        FixedDecimal = Format(rValue, "0." & String(iDecimals, "0"))
        Exit Function
    End If
    rResult = iScaled / rScale
    FixedDecimal = Format(rResult, "0." & String(iDecimals, "0"))
End Function

Use this function when you need guaranteed fixed-width columns in the CSV, for example 1.2345 or 9999.0000 rather than 1.2345 or 9999.

7. Solution 4: Configure the Windows Regional Settings

If the entire plant is standardized on English (United States) or any locale that uses a period as the decimal symbol, configure the runtime host consistently. This is the lowest-effort fix but has the highest operational risk because it affects every application on that PC.

  1. Open Settings → Time & Language → Region.
  2. Click Additional date, time, & regional settings.
  3. Click Region → Change date, time, or number formats.
  4. Click Additional settings....
  5. Set Decimal symbol to . and Digit grouping symbol to ,.
  6. Set List separator to , (do not set it to ; unless you also rewrite your CSV delimiter).
  7. Click Apply and restart the WinCC Runtime.

For multi-station deployments, propagate the setting through Group Policy: Computer Configuration → Administrative Templates → Control Panel → Regional and Language Options. The relevant GPO keys are described in Microsoft documentation for Windows 10/11 region GPO templates.

Warning: Changing the Windows decimal symbol affects every VBScript, .NET, and Win32 application on the host. Third-party SCADA add-ons, printer drivers, and OPC servers may break. Test thoroughly before deploying to production.

8. Solution 5: Updated TIA Portal WinCC Script (FAQ 59604194)

Siemens published an updated FAQ specifically for TIA Portal WinCC Runtime that demonstrates a locale-independent VBScript pattern for CSV export. The pattern uses a wrapper function that normalizes all numeric outputs before writing. The reference implementation is documented at FAQ 59604194.

The recommended pattern is:

Function ToCsvNumber(vValue, iDecimals)
    Dim sPic
    sPic = "0." & String(iDecimals, "0")
    ToCsvNumber = Format(vValue, sPic)
End Function

' Usage in the scheduled VBScript action
Dim sLine, r1, r2, r3
r1 = HMIRuntime.Tags("Temperature").Read
r2 = HMIRuntime.Tags("Pressure").Read
r3 = HMIRuntime.Tags("FlowRate").Read

sLine = _
    Year(Now) & "-" & Right("0" & Month(Now), 2) & "-" & Right("0" & Day(Now), 2) & "," & _
    Right("0" & Hour(Now), 2) & ":" & Right("0" & Minute(Now), 2) & ":" & Right("0" & Second(Now), 2) & "," & _
    ToCsvNumber(r1, 2) & "," & _
    ToCsvNumber(r2, 4) & "," & _
    ToCsvNumber(r3, 1)

WriteLine sLine

This pattern guarantees a period decimal separator regardless of the runtime host's region settings.

9. Complete Working VBScript Template

The following complete script implements a robust CSV logger suitable for scheduled execution (every 1 s, 500 ms, or event-triggered). It demonstrates all recommended practices.

' ---------- Configuration ----------
Const CSV_PATH = "C:\Logs\ProcessData.csv"
Const CSV_HEADER = "Timestamp,Temperature_C,Pressure_bar,FlowRate_Lpm,Setpoint_C"

' ---------- Helpers ----------
Function ToCsvNumber(vValue, iDecimals)
    ' Locale-independent fixed-decimal formatter
    ToCsvNumber = Format(vValue, "0." & String(iDecimals, "0"))
End Function

Function Pad2(iN)
    If iN < 10 Then
        Pad2 = "0" & iN
    Else
        Pad2 = CStr(iN)
    End If
End Function

Function IsoTimestamp(d)
    IsoTimestamp = _
        Year(d) & "-" & Pad2(Month(d)) & "-" & Pad2(Day(d)) & " " & _
        Pad2(Hour(d)) & ":" & Pad2(Minute(d)) & ":" & Pad2(Second(d))
End Function

Function EnsureHeader(sPath, sHeader)
    Dim oFSO, oFile
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    If Not oFSO.FileExists(sPath) Then
        Set oFile = oFSO.CreateTextFile(sPath, True, False)   ' ASCII, no BOM
        oFile.WriteLine sHeader
        oFile.Close
    End If
End Function

Sub WriteCsvLine(sPath, sLine)
    Dim oFSO, oFile
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    Set oFile = oFSO.OpenTextFile(sPath, 8, True, 0)         ' 8 = ForAppending, 0 = ASCII
    oFile.WriteLine sLine
    oFile.Close
End Sub

' ---------- Main ----------
EnsureHeader CSV_PATH, CSV_HEADER

Dim rTemp, rPres, rFlow, rSet
rTemp = HMIRuntime.Tags("Temperature").Read
rPres = HMIRuntime.Tags("Pressure").Read
rFlow = HMIRuntime.Tags("FlowRate").Read
rSet  = HMIRuntime.Tags("Setpoint").Read

Dim sLine
sLine = _
    IsoTimestamp(Now) & "," & _
    ToCsvNumber(rTemp, 2) & "," & _
    ToCsvNumber(rPres, 4) & "," & _
    ToCsvNumber(rFlow, 1) & "," & _
    ToCsvNumber(rSet, 2)

WriteCsvLine CSV_PATH, sLine

Key design decisions:

  • The header is written only when the file does not exist, preventing duplicate headers after restarts.
  • OpenTextFile is opened with mode 8 (ForAppending) and tristate 0 (ASCII) to match the Excel default codepage and avoid BOM issues.
  • All numeric values pass through ToCsvNumber(), which guarantees locale-independent formatting.
  • The timestamp uses ISO-8601 format, which sorts correctly and is unambiguous across regional Excel installations.

10. Edge Cases and Field-Proven Caveats

10.1 Negative Values

VBScript's Format() correctly emits the leading minus sign (-1.2345) regardless of locale. FormatNumber() on some hosts wraps negatives in parentheses if the locale uses accounting style; avoid it.

10.2 Scientific Notation

VBScript Format() switches to scientific notation automatically when the magnitude exceeds 10^15 or drops below 10^-4. To force decimal notation, scale the value before formatting, or post-process to convert the exponent. A common pattern is to flag out-of-range values with "OVER" / "UNDER" strings instead of attempting to coerce them.

10.3 Very Small Magnitudes

A value of 1.2345e-7 formatted with picture "0.0000" produces "0.0000", which loses the information. Use scientific notation (Format(v, "0.00E+00")) when sub-normal values are expected.

10.4 NaN and Infinity

WinCC Real tags can return NaN (Not-a-Number) when the underlying PLC reports a bad quality. VBScript's Format(NaN, "0.0000") returns the locale-dependent string "ungültige Zahl" or "Invalid Number". Guard against this with an explicit check:

If IsNumeric(rValue) Then
    If rValue >= -1.79769313486231E+308 And rValue <= 1.79769313486231E+308 Then
        sValue = ToCsvNumber(rValue, 4)
    Else
        sValue = "NaN"
    End If
Else
    sValue = "NaN"
End If

10.5 Mixed-Locale SCADA Networks

In plants where some panels are German-locale and others are English-locale, enforce a single CSV format by using Format() everywhere and avoid relying on the OS regional settings at all. Document the format in the plant's data-exchange standard.

10.6 Excel Codepage and BOM

If the runtime host is configured for a non-ASCII code page (Windows-1252 Eastern European variant, etc.) and the script writes non-ASCII characters (e.g. operator names), open the file in Unicode (UTF-8) tristate when creating it to avoid mojibake:

Set oFile = oFSO.CreateTextFile(sPath, True, True)    ' True = Unicode (UTF-16 LE)

For Excel compatibility, prefer UTF-8 with BOM. The Scripting.FileSystemObject does not expose UTF-8 with BOM directly; use an ADODB.Stream object instead:

Function WriteUtf8Bom(sPath, sContent)
    Dim oStream
    Set oStream = CreateObject("ADODB.Stream")
    oStream.Type = 2                      ' adTypeText
    oStream.Charset = "utf-8"
    oStream.Open
    oStream.WriteText sContent
    oStream.Position = 0
    oStream.Type = 1                      ' adTypeBinary
    oStream.Position = 3                  ' skip BOM written by WriteText
    Dim oBin
    Set oBin = CreateObject("ADODB.Stream")
    oBin.Type = 1
    oBin.Open
    oBin.Write oStream.Read
    Dim aBOM
    aBOM = Array(&HEF, &HBB, &HBF)
    oBin.Position = 0
    oBin.Write ChrB(&HEF) & ChrB(&HBB) & ChrB(&HBF)
    oBin.Position = 0
    oBin.Write oStream.Read
    oBin.SaveToFile sPath, 2
    oBin.Close
    oStream.Close
End Function

For numeric-only CSVs (no operator names, no ° symbols), this is unnecessary; ASCII output is sufficient.

11. Verification Procedure

After applying any of the above solutions, execute the following verification sequence:

  1. Force test values. In PLCSIM or the HMI tag simulator, force the source tag to five canonical values: 0.0, 1.2345, -1.2345, 9999, and 0.0001.
  2. Trigger one logging cycle. Use the WinCC Runtime's "Execute VBScript" diagnostic command or wait for the scheduled trigger.
  3. Open the CSV in a hex editor or Notepad++. Confirm each numeric column contains exactly one period character and no comma characters within numeric fields. The timestamp and ID fields may contain colons and dashes, which are expected.
  4. Open the CSV in Excel. Verify that Excel parses each numeric column as a number (right-aligned by default) rather than as text (left-aligned). If Excel displays green triangles indicating "number stored as text", the locale is still inverted.
  5. Run a Python pandas parse test. From a development workstation, run import pandas as pd; df = pd.read_csv(r'C:\Logs\ProcessData.csv'); print(df.dtypes). Every numeric column should be reported as float64, not object.
  6. Test locale override. Change the Windows region to German (Germany), restart the WinCC Runtime, and repeat steps 2-5. The CSV output must remain identical.

12. Troubleshooting Matrix

Symptom in CSV Likely Cause Recommended Fix
1,2345 instead of 1.2345 System locale uses comma decimal Apply Solution 1 (Format()) or Solution 2 (Replace())
12,345 instead of 1.2345 Tag declared as Int but reads as Real via cast; or script logic strips decimal Verify tag data type in tag management; avoid implicit integer conversion in VBScript
1234,5678 (no thousand sep in original) FormatNumber() inserting thousand separator Replace FormatNumber() with Format()
1,234,567 (comma used as thousand sep) FormatNumber() with default grouping on Use FormatNumber(v, n, -1, -1, 0) or switch to Format()
ungültige Zahl / Invalid Number Tag value is NaN or out-of-range Guard with IsNumeric() check; emit "NaN" literal
1.2345e+00 when expecting 1.2345 Magnitude triggered scientific notation Use fixed-decimal picture "0.0000"
All values identical across runs Tag not updated; cached read Call HMIRuntime.Tags(...).Write or use SmartTags read pattern
CSV empty after every cycle File handle not closed; locking Use Close after every WriteLine; avoid persistent handles
Mojibake characters in non-numeric columns Codepage mismatch Write file as UTF-8 with BOM (see Section 10.6)
Excel treats columns as text List separator set to semicolon Change list separator to comma in OS region settings, or use semicolon as CSV delimiter and document it

13. Related Siemens Documentation

  • FAQ 26107211 — Original WinCC flexible / WinCC V7.x CSV export pattern for Integer tags.
  • FAQ 59604194 — Updated VBScript pattern for WinCC Runtime in TIA Portal with locale-independent numeric formatting.
  • WinCC V7.x Scripting Reference — Documentation of HMIRuntime object model and VBScript function reference.

Why does my Siemens WinCC CSV show a comma where I expect a period in real values like 1.2345?

VBScript's default numeric-to-string conversion (CStr(), concatenation with &, FormatNumber()) uses the host operating system's decimal symbol from the Region settings. On a German-locale PC, CStr(1.2345) returns "1,2345". Use Format(vValue, "0.0000") with an explicit picture string to force a period regardless of locale.

My CSV writes 12,345 when the tag is 1.2345. What causes the decimal to disappear and a thousand separator to appear?

Two failures typically combine: the tag is being cast to an integer somewhere in the script (losing the fraction), or the script applies FormatNumber() with the default thousand-grouping enabled, which inserts a comma every three digits. Verify the tag data type in WinCC tag management and replace FormatNumber() with Format(v, "0.0000").

How do I write 4 decimals for some tags and 2 decimals for others in the same CSV row?

Wrap each numeric value in a helper function that takes the decimal count as a parameter: Format(vValue, "0." & String(iDecimals, "0")). This produces 1.2345 for 4 decimals and 99.99 for 2 decimals from the same row.

Can I keep the Windows locale in German and still produce valid engineering CSVs?

Yes. Use Format(vValue, "0.0000") for every numeric write. The Format() function ignores the OS decimal symbol and emits a literal period from the picture string. This is the recommended approach for multi-locale SCADA deployments.

What is the maximum number of decimal digits Format() can handle without scientific notation?

VBScript's Format() switches to scientific notation once the picture's decimal count exceeds about 15 digits. For a 32-bit Real tag (IEEE-754 single precision), the meaningful precision is 6-7 significant digits; for a 64-bit LReal tag it is 15-17. Use 4-6 decimals for process values and rely on LReal tags if higher precision is needed.

My WinCC Runtime is on a PC that must stay in German locale for operator HMI. Will changing the region break the operator screens?

Changing the decimal symbol to a period and the list separator to a comma affects WinCC faceplates that contain numeric text fields bound via dynamic dialog. Test thoroughly in a staging environment. A safer approach is to keep the OS locale in German but use Format(vValue, "0.0000") exclusively in the VBScript layer; this isolates the CSV export from the operator screen formatting.

Back to blog