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.
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:
- The branch logic does not test for
decPart = 0.0before applying the borrow; the result is an invalid intermediate minute/second value. - 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
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.0000edge case by detecting zero complement and reverting to the absolute form, then flipping the sign on the degree. - Emits a
validBOOL tag so the calling OB/FB can raise an alarm or skip the write, plus anedgeHitdiagnostics tag for trend logging. - Performs carry-in handling for both seconds and minutes so floating-point round-off at
0.99999999does 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
- 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 toFALSEfor legacy panels. - 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. - Insert a watch table named
DMS_Testwith the fifteen test inputs from the matrix; forcedecAnglein incremental steps from+360.0down to-99999.999. Use the "Force" permission level only with the safety key in RUN position and on a local test rack. - Online > Monitor All forces: confirm each
validbit reaches TRUE and the encoded output matches the expected column. Capture the screenshot for the FAT/SAT folder. - 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 tos999to allow the negative sign. - 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.
- For S7-300/S7-400 targets with firmware V3.x, change
LRealtoRealand reduce the precision spec to centisecond resolution; the carry-in logic remains identical. - 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
decAngleandsecOutasLRealto preserve arcsecond resolution beyond 9 decimal digits;Real(32-bit IEEE 754) loses precision below ~0.000001° once the magnitude exceeds 10 000. - Use
TRUNCrather thanFLOORfor the integer-degree truncation:FLOORreturnsLRealin TIA Portal V17+ and forces an extra cast. - Avoid
MODonLReal; the SCL compiler emits an implicit-conversion warning. Implement the 60/60 carry with explicitIFblocks as shown above. - Tag the FB with
{ S7_Optimized_Access := 'TRUE' }for S7-1500 and unset it (or set it explicitly toFALSE) 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_FUNCTIONblock; 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
- All fifteen test IDs return
valid = TRUEfor the selected method (A or B). - The
secOutvalue stays strictly below 60.0 seconds (or above -60.0 for Method B). -
minOutstays strictly between 0 and 59 for Method A, or in [-59, 59] for Method B. - For NaN-poisoned or Inf-saturated inputs the
validtag stays FALSE and thesecOuttag 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.