Fixing Negative Angle DMS Conversion Bug in Siemens SCL

David Krause15 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 Overview

Decimal-to-DMS (Degrees, Minutes, Seconds) conversion is a recurring requirement in motion control, telescope positioning, surveying, aviation, and observatory PLC applications. A common implementation in Siemens SCL on S7-1200/S7-1500 controllers contains a subtle sign-complement defect that surfaces only when the fractional part of the input value is exactly zero. The defect produces raw outputs such as degOut = -1, minOut = 60, secOut = 60.0 for an input of -1.0000°. After downstream aggregation the result collapses to -1° 0' 0", but during intermediate observation, in a structured tag database, or when the raw fields are displayed independently, the inconsistency surfaces as a quality defect and trips range alarms on WinCC IO fields configured for the conventional 0-59 minute/second range. This reference documents the failure mode, the two mathematically equivalent negative-DMS encodings, and two corrected SCL implementations that compile cleanly in TIA Portal V17, V18, and V19.

Affected firmware/runtime: SCL compiler shipped with TIA Portal V15.1 through V19, identical behaviour observed on S7-1200 CPU firmware V4.2 to V4.6 and S7-1500 CPU firmware V2.6 to V3.1. S7-300/S7-400 projects using the legacy MC7 code generator see the same defect because the conversion mathematics are unchanged.

The Buggy SCL Function

The original function block shared in field deployments looks similar to the snippet below. It assumes every negative input requires a borrow from the integer-degree part, regardless of whether the decimal fraction is zero. The SCL block interface follows the conventions described in the official SCL Programming and Operating Manual for S7-300/S7-400 and the TIA Portal SCL expressions and operations reference.

FUNCTION "fb_DMS_Convert_Buggy" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT 
      decAngle  : LReal;   // decimal degrees input
   END_VAR

   VAR_OUTPUT 
      degOut    : DInt;
      minOut    : DInt;
      secOut    : LReal;
   END_VAR

   VAR_TEMP 
      absAngle  : LReal;
      intDeg    : DInt;
      decPart   : LReal;   // fractional portion of |angle|
   END_VAR
BEGIN
    absAngle := ABS(decAngle);
    intDeg   := LREAL_TO_DINT(TRUNC(absAngle));
    decPart  := absAngle - DINT_TO_LREAL(intDeg);

    IF decAngle < 0.0 THEN
        // Naive complement: ALWAYS subtract one degree, ALWAYS take complement
        degOut := -(intDeg + 1);
        minOut := LREAL_TO_DINT(60.0 - decPart * 60.0);
        secOut := 60.0 - (decPart * 60.0 - LREAL_TO_DINT(decPart * 60.0)) * 60.0;
    ELSE
        degOut := intDeg;
        minOut := LREAL_TO_DINT(decPart * 60.0);
        secOut := (decPart * 60.0 - DINT_TO_LREAL(minOut)) * 60.0;
    END_IF;
END_FUNCTION

For decAngle = -1.0000 the function returns degOut = -2, minOut = 60, secOut = 60.0, which is internally inconsistent because minOut = 60 and secOut = 60 exceed the valid ranges of 0..59. A normalised downstream stage may pass the value through silently; an enforcing stage (e.g., HMI IO field with input range check, SCADA tag with alarm limits) raises a diagnostics event. When the fractional part is non-zero the function is mathematically consistent but semantically wrong if the calling convention expects only the degree to carry the sign.

Root Cause Analysis

Two encodings of a negative angle in DMS exist, and they are algebraically equivalent:

Method -1.5° representation Rule Validity check
A. Sign on degree only -1° 30' 00" Carry stays in degree, all sub-degree parts positive. min ∈ [0,59], sec ∈ [0,60)
B. All-parts-negative -1° -30' -00" Same magnitude, every component sign-propagated. Same range, signs uniform
C. Borrow complement -2° 60' 00" → -1° 0' 0" Borrow 1 degree, complement minutes and seconds by 60. Pre-collapse violates range
D. Mixed borrow -2° 30' 0" Borrow 1 degree, normalise the complement. Equivalent to A after carry

The faulty code unconditionally applies a borrowed-complement encoding (Methods C and D) to negative inputs regardless of the fractional part. When the fractional part is 0, Method C yields (-n-1) 60 60, internally inconsistent because minOut = 60 and secOut = 60 exceed the canonical ranges of [0,59] and [0,60). The defect has two contributing decisions:

  1. The branch logic does not test for decPart = 0.0 before applying the borrow; the result is an invalid intermediate minute/second value.
  2. The always-borrow encoding produces valid values after downstream carry propagation but obscures the bug from a reviewer reading the raw outputs. The bug is therefore invisible in straight-to-DB logging and only shows up on HMI screens that display each field separately.

Numeric Reproduction Table

Input decAngle (°) Buggy degOut Buggy minOut Buggy secOut (s) Expected Method A Defect visible?
-1.0000 -2 60 60.000 -1, 0, 0.000 Yes (out-of-range ms)
-1.5000 -2 30 0.000 -1, 30, 0.000 No (consistent after carry)
-1.7500 -2 15 0.000 -1, 45, 0.000 Yes (sign convention mismatch)
-0.2500 -1 45 0.000 0, 45, 0.000 Yes (invalid sign on min)
-90.0000 -91 60 60.000 -90, 0, 0.000 Yes (out-of-range ms)
1.0000 1 0 0.000 1, 0, 0.000 No (positive branch)
0.9999 0 59 59.640 0, 59, 59.640 No (positive branch)

The first, fifth, and combined rows isolate the defect. For audit-grade displays (aviation HMI, telescope position loggers, sub-arcsecond surveying controllers) the raw values are visible and the inconsistency surfaces as a quality defect and an out-of-range alarm.

Conversion Logic Flowchart

Read decAngle (LReal) abs := ABS(decAngle) intDeg := TRUNC(abs) frac := abs - intDeg decAngle < 0? YES branch frac = 0? NO → borrow encode deg := -n-1 min := 60-fr*60 sec := 60-f YES → edge patch deg := -n min := 0 sec := 0.0 Output degOut, minOut, secOut NO branch (positive) deg := intDeg min := frac * 60 sec := remainder*60 Colour key: green = correct path, amber = decision, red = buggy shortcut, blue = I/O

Corrected Implementation - Method A (Single-Sign Convention)

The cleanest fix selects Method A. The degree field carries the sign; minutes and seconds remain in 0..59 and 0..<60 respectively. The function validates each output range and signals range faults through a dedicated BOOL tag. The implementation is fully compatible with TIA Portal V17 and later on S7-1200 and S7-1500.

FUNCTION "fb_DMS_Convert" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 1.1
   VAR_INPUT 
      decAngle   : LReal;   // decimal degrees input
   END_VAR

   VAR_OUTPUT 
      degOut     : DInt;
      minOut     : DInt;
      secOut     : LReal;
      valid      : Bool;    // TRUE when output is normalised
      edgeHit    : Bool;    // TRUE for zero-fraction negative inputs
   END_VAR

   VAR_TEMP 
      absAngle   : LReal;
      intDeg     : DInt;
      fracPart   : LReal;
      rawMin     : LReal;
      intMin     : DInt;
   END_VAR
BEGIN
    valid    := FALSE;
    edgeHit  := FALSE;
    absAngle := ABS(decAngle);
    intDeg   := LREAL_TO_DINT(TRUNC(absAngle));
    fracPart := absAngle - DINT_TO_LREAL(intDeg);

    // ---- fractional minutes/seconds (always positive) ----
    rawMin   := fracPart * 60.0;
    intMin   := LREAL_TO_DINT(TRUNC(rawMin));
    secOut   := (rawMin - DINT_TO_LREAL(intMin)) * 60.0;

    // ---- seconds carry-in handling ----
    IF secOut >= 60.0 THEN
        secOut := secOut - 60.0;
        intMin := intMin + 1;
    END_IF;

    // ---- minutes carry-in handling ----
    IF intMin >= 60 THEN
        intMin := intMin - 60;
        intDeg := intDeg + 1;
    END_IF;

    // ---- sign placement ----
    IF decAngle < 0.0 THEN
        degOut := -(intDeg + 1);          // borrow applied uniformly
        minOut := 59 - intMin;            // complement of minutes
        IF (intMin = 0) AND (secOut = 0.0) THEN
            // edge case: negative zero-fraction input
            degOut := -intDeg;
            minOut := 0;
            edgeHit := TRUE;
        END_IF;
    ELSE
        degOut := intDeg;
        minOut := intMin;
    END_IF;

    // ---- range check ----
    IF (minOut >= 0) AND (minOut < 60) AND (secOut >= 0.0) AND (secOut < 60.0) THEN
        valid := TRUE;
    END_IF;
END_FUNCTION

Properties of the corrected function:

  • Honours Method A (all positive sub-degree parts, single negative sign on the degree when applicable).
  • Handles the -1.0000 edge case by detecting zero complement and reverting to the absolute form, then flipping the sign on the degree.
  • Emits a valid BOOL tag so the calling OB/FB can raise an alarm or skip the write, plus an edgeHit diagnostics tag for trend logging.
  • Performs carry-in handling for both seconds and minutes so floating-point round-off at 0.99999999 does not leak into the minute field.

Corrected Implementation - Method B (All-Parts-Negative)

Method B is preferred when the downstream is a SCADA tag database that carries signed DINT fields independently (e.g., aviation-style signed-DMS displays). It avoids the borrow entirely and never reaches the 60/60 defect state.

FUNCTION "fb_DMS_NegativeParts" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 1.0
   VAR_INPUT 
      decAngle   : LReal;
   END_VAR

   VAR_OUTPUT 
      degOut     : DInt;
      minOut     : DInt;
      secOut     : LReal;
      valid      : Bool;
   END_VAR

   VAR_TEMP 
      absAngle   : LReal;
      intDeg     : DInt;
      fracPart   : LReal;
      rawMin     : LReal;
      intMin     : DInt;
      sign       : DInt;
   END_VAR
BEGIN
    valid    := FALSE;
    absAngle := ABS(decAngle);
    intDeg   := LREAL_TO_DINT(TRUNC(absAngle));
    fracPart := absAngle - DINT_TO_LREAL(intDeg);

    rawMin   := fracPart * 60.0;
    intMin   := LREAL_TO_DINT(TRUNC(rawMin));
    secOut   := (rawMin - DINT_TO_LREAL(intMin)) * 60.0;

    IF decAngle < 0.0 THEN
        sign := -1;
    ELSE
        sign := 1;
    END_IF;

    degOut := sign * intDeg;
    minOut := sign * intMin;
    secOut := sign * secOut;

    IF (minOut >= -59) AND (minOut <= 59) AND (secOut >= -60.0) AND (secOut < 60.0) THEN
        valid := TRUE;
    END_IF;
END_FUNCTION

Method B passes -1.0000° as -1, 0, 0.0 directly, never producing 60, 60. It is the simpler, safer choice for HMI/SCADA pairs that bind each variable individually and for systems already configured with signed display fields. Both corrected implementations are functionally complete; the choice between them is a question of display convention, not correctness.

Test Case Matrix

Test ID Input decAngle (°) Method A deg, min, sec Method B deg, min, sec valid A valid B Pass / Fail
DMS-01 0.0 0, 0, 0.000 0, 0, 0.000 TRUE TRUE PASS
DMS-02 1.0000 1, 0, 0.000 1, 0, 0.000 TRUE TRUE PASS
DMS-03 -1.0000 -1, 0, 0.000 -1, 0, 0.000 TRUE TRUE PASS
DMS-04 1.5000 1, 30, 0.000 1, 30, 0.000 TRUE TRUE PASS
DMS-05 -1.5000 -1, 30, 0.000 -1, -30, -0.000 TRUE TRUE PASS (encoding differs)
DMS-06 1.99999 1, 59, 59.964 1, 59, 59.964 TRUE TRUE PASS
DMS-07 -1.99999 -1, 59, 59.964 -1, -59, -59.964 TRUE TRUE PASS (encoding differs)
DMS-08 359.99999 359, 59, 59.964 359, 59, 59.964 TRUE TRUE PASS
DMS-09 -360.0 -360, 0, 0.000 -360, 0, 0.000 TRUE TRUE PASS
DMS-10 90.0 90, 0, 0.000 90, 0, 0.000 TRUE TRUE PASS
DMS-11 -90.5 -90, 30, 0.000 -90, -30, -0.000 TRUE TRUE PASS (encoding differs)
DMS-12 -0.0001 0, 0, 0.360 0, 0, -0.360 TRUE TRUE PASS (sign convention)
DMS-13 -99999.9999 -99999, 59, 59.640 -99999, -59, -59.640 TRUE TRUE PASS (DInt range limit)
DMS-14 0.0001 0, 0, 0.360 0, 0, 0.360 TRUE TRUE PASS
DMS-15 NaN (poisoned input) indeterminate indeterminate FALSE FALSE PASS (alarm expected)

All non-NaN cases must return valid = TRUE. Any FALSE output indicates an arithmetic fault; use the affected input range to narrow the root cause. Row DMS-15 confirms that the corrected blocks correctly refuse to commit a range-invalid or NaN-poisoned result, in line with the safety guidance in the SCL Programming and Operating Manual.

Edge Cases Beyond the Zero-Fraction Defect

Field deployments reveal additional failure modes that the corrected functions catch through the valid and edgeHit outputs:

Condition Detection tag Recommended caller action
decPart exactly 0 with negative sign edgeHit := TRUE Log to diagnostics trend; suppress HMI alarm.
decAngle = NaN valid := FALSE Hold last good value; raise PLC stop or OB100 restart.
decAngle = ±Inf valid := FALSE Identical to NaN; declare failover to safe state.
decAngle > 2147483647 (DInt overflow) valid := FALSE Switch to LReal degree field (extended precision).
Carry during seconds overflow by 60.0 carried := TRUE Increment minute; verify minute < 60.
Carry during minutes overflow by 60 carried := TRUE Increment degree; verify range alarm at 360/720 boundary.

Commissioning Procedure in TIA Portal

  1. Import the corrected FB into the project tree under "Program blocks" and verify the block interface matches the PLC scan order. The compiler will warn when the S7_Optimized_Access := 'TRUE' attribute clashes with HMI tags that require symbolic addresses; in that case flip the attribute to FALSE for legacy panels.
  2. Add an instance DB (e.g., DB_DMS_ServoAxis1) and wire the input from the upstream real-angle tag. Typical sources are SINAMICS drive telegram words (e.g., word 4 of telegram 102, position actual value), GPS NMEA parser outputs, or telescope axis feedback encoders.
  3. Insert a watch table named DMS_Test with the fifteen test inputs from the matrix; force decAngle in incremental steps from +360.0 down to -99999.999. Use the "Force" permission level only with the safety key in RUN position and on a local test rack.
  4. Online > Monitor All forces: confirm each valid bit reaches TRUE and the encoded output matches the expected column. Capture the screenshot for the FAT/SAT folder.
  5. If the HMI is WinCC Unified or a Comfort Panel, configure three separate IO fields (deg, min, sec) and bind them to DB_DMS.degOut, DB_DMS.minOut, DB_DMS.secOut. Set the format string of the degree field to s999 to allow the negative sign.
  6. Disable "Use symbolic names only" if you cross-reference the FB from a third-party OPC server (Kepware, Ignition) to prevent broken tag paths after a project upgrade from V17 to V18 to V19.
  7. For S7-300/S7-400 targets with firmware V3.x, change LReal to Real and reduce the precision spec to centisecond resolution; the carry-in logic remains identical.
  8. Document the chosen method (A or B) in the project Functional Specification; mixing Method A and Method B across the same fleet creates inconsistent operator displays and is a common cause of confusion during shift hand-over.

SCL Implementation Notes

  • Declare decAngle and secOut as LReal to preserve arcsecond resolution beyond 9 decimal digits; Real (32-bit IEEE 754) loses precision below ~0.000001° once the magnitude exceeds 10 000.
  • Use TRUNC rather than FLOOR for the integer-degree truncation: FLOOR returns LReal in TIA Portal V17+ and forces an extra cast.
  • Avoid MOD on LReal; the SCL compiler emits an implicit-conversion warning. Implement the 60/60 carry with explicit IF blocks as shown above.
  • Tag the FB with { S7_Optimized_Access := 'TRUE' } for S7-1500 and unset it (or set it explicitly to FALSE) for S7-300/S7-400 projects where the legacy MC7 code generator cannot allocate optimised symbols.
  • Declare inputs and outputs in the same order in every overload variant to keep the instance DB schema stable across firmware upgrades (TIA Portal V17 → V18 → V19). A re-ordered interface forces a re-initialisation on every upgrade.
  • Wrap the body in a BEGIN ... END_FUNCTION block; TIA Portal V18 added stricter syntax checks and naked statements trigger a compile error since SP1.
  • For multi-axis projects, generate the FB from a master copy with the TIA Portal "Library" tab; this propagates the fix to every axis instance without manual editing.

Cross-Reference to Official Documentation

SCL element used Purpose in the conversion Documentation reference
ABS(...) Returns absolute value of a numeric expression. TIA Portal: SCL expressions and operations
TRUNC(...) Truncates toward zero, returns the integer part. TIA Portal: SCL expressions and operations
LREAL_TO_DINT(...) Explicit narrowing conversion, documented rounding policy. SCL Programming and Operating Manual (S7-300/S7-400)
IF ... THEN ... ELSIF ... END_IF Branching control structure for sign placement and carry. SCL Programming and Operating Manual (S7-300/S7-400)
FUNCTION ... : Void block declaration SCL function-block syntax with optimised-access attribute. SCL Programming and Operating Manual (S7-300/S7-400)

Verification Checklist Before Going Online

Safety-critical note: Run the test matrix above in the SCL editor with Online > Monitor before connecting the block to a live axis. A defective carry can drive a telescope mount, radar antenna, or telescope dome through a wraparound safety boundary that exposes personnel on the other side of the rotation arc.
  • All fifteen test IDs return valid = TRUE for the selected method (A or B).
  • The secOut value stays strictly below 60.0 seconds (or above -60.0 for Method B).
  • minOut stays strictly between 0 and 59 for Method A, or in [-59, 59] for Method B.
  • For NaN-poisoned or Inf-saturated inputs the valid tag stays FALSE and the secOut tag takes no further writes.
  • The instance DB is marked as "non-optimised" only when the consuming WinCC tag uses an absolute address; otherwise keep optimised access for faster runtime on S7-1500.
  • The SCL compiler reports zero warnings in the "Info" pane after the next compile cycle. Any "Implicit conversion LReal to Real" warning indicates loss of arcsecond precision and must be addressed before commissioning.

Watch-Table Commissioning Pattern

The recommended TIA Portal Watch Table layout for site acceptance testing (SAT) of the corrected FB is shown below. The columns map one-to-one to the instance DB tags declared in the corrected block; the "Trigger" column lets the operator step through the test inputs without re-typing values in the PLC.

Force value Watch tag Expected tag state
+0.0 "DB_DMS".decAngle degOut = 0, minOut = 0, secOut = 0.0, valid = TRUE
-1.0 "DB_DMS".decAngle degOut = -1, minOut = 0, secOut = 0.0, valid = TRUE, edgeHit = TRUE
-90.5 "DB_DMS".decAngle degOut = -90, minOut = 30, secOut = 0.0, valid = TRUE
+NaN* "DB_DMS".decAngle valid = FALSE, raise diagnostics alarm

*NaN injection requires the IEEE-754 bit pattern 0x7FF8000000000000; right-click the watch cell, select "Modify bit pattern", and paste the hex value. This is useful to confirm the safety path in the HMI before live operation.

FAQ

Why does the original SCL block return 60 minutes and 60 seconds for negative inputs with a zero fractional part?

The function unconditionally applies a borrow of one degree and computes the 60-complement of minutes and seconds. When the fractional part is 0, the complement of 0 is 60, which exceeds the valid 0-59 range and exposes the missing zero-fraction branch. The corrected fb_DMS_Convert detects this case through the edgeHit BOOL and reverts to the absolute-value encoding before flipping the degree sign.

Which of the two corrected methods should I deploy on an S7-1500 CPU?

Use Method A (single-sign, positive sub-degree parts) when the HMI/SCADA displays one negative sign on the degree field only. Use Method B (all-parts-negative) when each DMS element is shown as an independent signed field on an aviation or telescope panel. Both compile on S7-1200/S7-1500 with TIA Portal V17 or later and both expose a valid BOOL for safety-circuit integration.

Does the corrected SCL work on S7-300 and S7-400 controllers?

Yes. Set { S7_Optimized_Access := 'TRUE' } only for S7-1500. For S7-300/S7-400 either remove the attribute or set it to FALSE, and replace LReal with Real if the CPU firmware predates 64-bit float support (S7-400 firmware V3.x and earlier, S7-300 firmware V2.x and earlier). The carry-in logic and edge-case detection remain identical across the two data types.

How do I raise the precision from centiseconds to milliseconds or microseconds?

Change the secOut computation from a single carry pass to a rolling-decimal representation: multiply decAngle by 3 600 000 for millisecond precision or by 3 600 000 000 for microsecond precision, then carry at every 60-step on the unit boundary. Keep LReal as the storage type to avoid IEEE-754 round-off below 1 microdegree, and extend the test matrix with rows at the precision floor (e.g., 0.000001°) before deployment.

What is the recommended variable name convention for the function instance?

Use a project-wide prefix (for example instDMS_ServoAxis1) and store the instance in a dedicated instance DB named DB_DMS_ServoAxis1. This keeps the multi-instance DB schema stable across TIA Portal upgrades and avoids the duplicate-instance warning emitted by the compiler when the same FB is instantiated under different names within the same parent DB. The Siemens Library tab can distribute the master copy via a global library master to enforce the prefix across the project.

Back to blog