S7-300 FC: Packing Two INT Values into a DINT Link Variable

David Krause14 min read
S7-300SiemensTechnical Reference
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

Overview: The S7-300 Link Variable Packing Problem

On an S7-300 CPU (and the same pattern applies to S7-400 and S7-1500), process data frequently arrives as two separate 16-bit words - for example a recipe number in MW100 and a batch index in MW102. An HMI, SCADA tag, OPC server, or remote CPU often needs that pair as a single 32-bit double integer (DINT) such as MD104. The original STEP 7 / TIA Portal approach is to write a small Function (FC) in STL that multiplies the high word by an appropriate power of ten and adds the low word. The result is the decimal concatenation the consumer expects.

This article documents the original FC posted on Siemens support channels, analyses the digit-count scaling logic, identifies a real multiplier bug in the five-digit branch, supplies a corrected version, and shows several alternative implementations (SCL, Ladder, and a constant-divisor variant) suitable for STEP 7 V5.x and TIA Portal V16-V20.

Prerequisites

  • SIMATIC S7-300 CPU 31x (any firmware) or S7-400 / S7-1500 with classic STL support.
  • STEP 7 V5.5+ or TIA Portal V16+ (STL editor available in both; in TIA Portal enable "LAD/FBD/STL" under Options > Settings).
  • Working knowledge of ACCU1/ACCU2 stack operations in S7 STL.
  • Optional: WinCC flexible 2008 / TIA WinCC V16+ or any Modbus / OPC client that consumes a 32-bit tag.

Why Pack Two INTs into a DINT?

HMI/SCADA drivers and many third-party protocols (Modbus TCP, OPC DA/UA, Profinet record read) frequently request a 32-bit value where the underlying PLC only produces 16-bit words. Typical field cases:

Source Word 1 (High) Word 2 (Low) Target DINT
Recipe + Sub-recipe 12 23 1223
Order + Position 345 6789 3456789
Module + Channel 4523 1 45231
Plant + Unit + Bay 2 7 27

The decimal concatenation is intentional - it preserves visual readability in the HMI trend and the value is still a valid number for arithmetic, scaling, and alarm limits. Note that the input words are assumed non-negative (range 0 to 99999). Negative values break the decimal-concatenation semantic and must be rejected upstream.

Mathematical Foundation: Digit-Count Scaling

If the low word contains k decimal digits (1 to 5, or 0 when the low word equals zero), the high word must be multiplied by 10k before the addition:

result = iHighDigits * 10^k + iLowDigits

Valid k ranges and corresponding multipliers:

iLowDigits range Decimals k Multiplier 10k
0 0 (special) 1 (no multiply)
1 - 9 1 10
10 - 99 2 100
100 - 999 3 1 000
1 000 - 9 999 4 10 000
10 000 - 99 999 5 100 000

Each branch therefore loads the corresponding constant, multiplies the high word, and jumps to a common add label that performs the final addition.

Reference FC1 - Original STL Implementation

The original FC circulated in Siemens support threads is reproduced below verbatim so the line-by-line analysis that follows remains accurate. Save the code as FC1 in your S7 program; declare the interface with iLowDigits and iHighDigits of type INT and the return value as DINT.

FUNCTION FC 1 : DINT
TITLE = VERSION : 0.1
VAR_INPUT
  iLowDigits  : INT ;
  iHighDigits : INT ;
END_VAR
BEGIN
NETWORK
TITLE =
      L  #iLowDigits;     // ACCU1 = iLowDigits
      L  0;               // ACCU1 = 0,  ACCU2 = iLowDigits
      ==I ;               // iLowDigits == 0 ?
      JC  d0;             // yes -> branch d0
      TAK ;               // swap, ACCU1 = iLowDigits, ACCU2 = 0
      L  9;               // ACCU1 = 9,  ACCU2 = iLowDigits
      <=D ;               // 9 <= iLowDigits ? (accu2 <= accu1)
      JC  d1;             // iLowDigits in 1..9
      TAK ;
      L  99;
      <=D ;
      JC  d2;             // 10..99
      TAK ;
      L  999;
      <=D ;
      JC  d3;             // 100..999
      TAK ;
      L  9999;
      <=D ;
      JC  d4;             // 1000..9999
      TAK ;
      L  L#99999;         // explicit 32-bit constant
      <=D ;
      JC  d5;             // 10000..99999
      JU  err;            // out of range

d0:   L  #iHighDigits;    // iLowDigits was 0 - keep high as-is
      JU  add;

d1:   L  10;              // multiplier for 1-digit low
      L  #iHighDigits;
      *D ;                // ACCU1 = iHighDigits * 10
      JU  add;

d2:   L  100;             // multiplier for 2-digit low
      L  #iHighDigits;
      *D ;
      JU  add;

d3:   L  #iHighDigits;    // multiplier for 3-digit low
      L  1000;
      *D ;
      JU  add;

d4:   L  #iHighDigits;    // multiplier for 4-digit low
      L  10000;
      *D ;
      JU  add;

d5:   L  #iHighDigits;    // multiplier for 5-digit low
      L  10000;            // <-- see bug analysis below
      *D ;
      JU  add;

add:  L  #iLowDigits;
      +D ;                // ACCU1 = (high * mult) + low
      T   #RET_VAL;
      SET ;
      SAVE ;
      BEU ;

err:  L  0;
      T   #RET_VAL;
      CLR ;
      SAVE ;
      BEU ;
END_FUNCTION

STL Instruction Quick Reference

Mnemonic Effect on ACCU1 / ACCU2 Used for
L <operand> Loads operand into ACCU1, shifts previous ACCU1 -> ACCU2 Constants, inputs, indirect
TAK Swaps ACCU1 and ACCU2 Reorder operands for compare
==I / <=D Sets BR/CC1 if ACCU2 op ACCU1 (must be same type or 32-bit for D) Range checks
JC <label> Conditional jump if RLO = 1 Branch into digit class
JU <label> Unconditional jump Skip to add / err
*D / +D ACCU2 * ACCU1 / ACCU2 + ACCU1 (DINT) Scale and add
T #RET_VAL Transfer ACCU1 to return value Output
SET / CLR / SAVE Set or clear RLO then write to BR bit ENO behaviour
BEU End block unconditionally Early return

Critical Bug Analysis: The d5 Multiplier

The original branch d5 loads L 10000 instead of L L#100000. Because branch d5 handles low-word values of 10000-99999 (five decimal digits), the mathematically correct multiplier is 105 = 100000. Loading 10000 scales the high word by a factor of ten short, producing an answer that is wrong by a power of ten.

Worked example (iHighDigits = 12, iLowDigits = 34567):

  • Expected (correct): 12 * 100000 + 34567 = 1 234 567
  • Original (buggy): 12 * 10000 + 34567 = 154 567

Notice that the bug also causes silent DINT overflow: 154 567 is technically a valid DINT, so the function returns a value but it is numerically meaningless. The behaviour is hard to detect during commissioning because the FC executes without an error and the BR bit remains set.

Recommendation: Before deploying any FC built from this template, add the literal L L#100000 to the d5 branch and re-test. Better still, replace the cascade with the digit-count-free implementations shown below.

Corrected FC1 in STL

The minimal patch is one literal change. The function header, input declarations, and the rest of the body remain identical. The corrected line is highlighted.

...
d5:   L  #iHighDigits;
      L  L#100000;       // FIX: was L 10000, must be 10^5
      *D ;
      JU  add;
...

Optionally add an explicit overflow guard before the add label. The guard computes the upper bound and jumps to err if the multiplication would exceed L#2147483647 (DINT max). The check costs two extra instructions and a label but prevents silent wrap-around on pathological inputs:

chk:  L  L#2147483647;
      L  #RET_VAL;       // current scaled high value in ACCU1
      <D ;               // overflow ?
      JC  err;
      JU  add;

Alternative 1: Single-Multiplier SCL Implementation

For TIA Portal projects, the digit-cascade can be replaced with a single call to the LOG / EXPT SCL functions, or - more efficiently - with a small lookup table. The following SCL block is functionally equivalent to the corrected FC1 but is roughly 70% shorter, easier to read, and impossible to deploy with the d5 multiplier bug:

FUNCTION "FC_PackWord" : DINT
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.2
VAR_INPUT
  iHighDigits : INT;
  iLowDigits  : INT;
END_VAR
VAR_TEMP
  tMult : DINT;
END_VAR
BEGIN
  IF (iLowDigits < 0) OR (iHighDigits < 0) THEN
    "FC_PackWord" := 0;
    RETURN;
  END_IF;

  tMult := 1;
  IF iLowDigits > 9      THEN tMult := 10;     END_IF;
  IF iLowDigits > 99     THEN tMult := 100;    END_IF;
  IF iLowDigits > 999    THEN tMult := 1000;   END_IF;
  IF iLowDigits > 9999   THEN tMult := 10000;  END_IF;
  IF iLowDigits > 99999  THEN tMult := 100000; END_IF;

  "FC_PackWord" := DINT_TO_DINT(iHighDigits) * tMult + iLowDigits;
END_FUNCTION

Compile under TIA Portal V16 or later. Mark the block as optimised for S7-1500; for S7-300 keep the classic block attribute so that the symbolic interface still binds to the previous MW100/MW102/MD104 call sites.

Alternative 2: Ladder / FBD in TIA Portal

For engineers who do not use STL or SCL, the same logic is achievable with a single MOVE chain and a comparator-driven MUX, although the Ladder form is more verbose. The principle:

  1. Compute nDigits = 1 if iLowDigits > 9; else 0; (CMP == 0) then add 1.
  2. Repeat for thresholds 99, 999, 9999, 99999; the running total is the exponent k.
  3. Use a 6-position multiplexer whose inputs are constants 1, 10, 100, 1000, 10000, 100000 and whose selector is k.
  4. Multiply the high word by the selected constant and add the low word.

While the Ladder version is harder to maintain, it is the only practical form for installations that prohibit STL through central project settings.

Alternative 3: Bit-Packing (when Decimal Order Does Not Matter)

If the HMI only needs the two 16-bit values to occupy one 32-bit slot in a Modbus register or a Profinet record - and the consumer interprets the upper and lower halves independently - use a direct AD_T_W / SLD combination instead of the decimal concatenation. This consumes far less code and eliminates the digit-count logic entirely:

L  #iHighDigits;       // INT  -> ACCU1
ITD ;                 // sign-extend to DINT
SLD 16;               // shift left 16 bits
TAK ;                 // swap
L  #iLowDigits;
ITD ;                 // zero-extend because iLowDigits >= 0
OD ;                  // bitwise OR - high|low
T  #RET_VAL;

Result: RET_VAL = iHighDigits * 65536 + iLowDigits. Useful for binary packed structures, but the resulting value is not the decimal concatenation the OP requires.

Memory Layout: MW100 / MW102 / MD104

The classic call site uses three overlapping memory areas in the S7-300 work-memory bit area:

      MW100  -> high word (recipe number, 0..99999)
      MW102  -> low word  (sub-index, 0..99999)
      MD104  -> DINT      (concatenated result)

// Call in OB1 / cyclic OB
      CALL FC 1
        iHighDigits := MW100
        iLowDigits  := MW102
        RET_VAL     := MD104

Make sure that MW100 and MW102 do not straddle a DB boundary that the editor would re-locate on download, and that MD104 is not also written by another OB - byte-level overlap is the most common source of "ghost" values in MD104. To avoid that, the S7-300 bit-memory default of MB0..MB2047 is large enough; pin the addresses to MB100, MB102, MB104 in the symbol table.

Overflow and Edge-Case Matrix

iHighDigits iLowDigits Expected (decimal) Original (buggy) Fits DINT?
0 0 0 0 yes
12 23 1 223 1 223 yes
345 6789 3 456 789 3 456 789 yes
4523 1 45 231 45 231 yes
12 34567 1 234 567 154 567 yes (both)
32000 99999 3 200 099 999 (overflow) 320 099 999 NO - guard needed
99999 99999 9 999 999 999 (overflow) 999 999 999 NO - guard needed
-1 5 reject reject (err branch) n/a

Practical limits with the corrected FC and a DINT output:

  • iLowDigits 0-9: iHighDigits up to 214 748 364 (10x rule) - effectively unlimited
  • iLowDigits 10-99: iHighDigits up to 21 474 836
  • iLowDigits 100-999: iHighDigits up to 2 147 483
  • iLowDigits 1 000-9 999: iHighDigits up to 214 748
  • iLowDigits 10 000-99 999: iHighDigits up to 21 474

Commissioning and Verification Steps

  1. Open the S7 program in STEP 7 or TIA Portal and compile the FC. Look for warnings of the form "Address MD104 is used twice" or "Type conflict RET_VAL"; both indicate the call site is wrong.
  2. Download the project to the S7-300 CPU in STOP mode, then switch to RUN-P.
  3. Open Monitor/Modify from the PLC menu and force test values into MW100 and MW102 per the matrix above.
  4. Confirm MD104 matches the "Expected" column - not the "Original (buggy)" column.
  5. Check BR bit: it must be 1 on success and 0 when an input is negative or out of range. Some HMI drivers treat ENO=0 as "quality bad" and will mask the tag.
  6. Force one negative value (e.g. MW100 = -1) and confirm the FC returns 0 with ENO=0.
  7. Disconnect the programming cable, leave the CPU in RUN for at least one shift, and verify trend / archive values are stable.

Integrating the Packed DINT with HMI / SCADA

WinCC flexible / TIA WinCC treats the S7-300 address MD104 as a 32-bit signed value. Configure the tag as Data type = DInt, Length = 4 bytes, Address = DB 0 / MD 104 (in the bit-memory area). If the HMI is connected via Ethernet, point-to-point S7 communication follows the standard TIA Portal V20 point-to-point S7-300/400/1500 link configuration: enable "Permit access with PUT/GET from remote partner" on the CPU properties, and define the S7 connection in Devices & Networks > Networks > S7 connections. For non-Siemens HMIs, the C-more Ethernet ISO over TCP/IP addressing guide describes how to map MD104 to a Modbus holding register pair (4xxxx-4xxxx+1, big-endian).

For partner-CPU access (e.g. an S7-1500 reading MD104 from an S7-300), the Siemens S7 Communication with PUT/GET manual (entry ID 82212115) is the canonical reference. Configure a single S7 connection in NetPro, allocate a PUT block of length 4 bytes, and the consumer CPU will receive the DINT exactly as written into MD104 by FC1.

Diagnostic Quick-Check

Symptom Likely cause Fix
MD104 off by factor of 10 for iLowDigits > 9999 Original d5 bug (L 10000) Change to L L#100000
MD104 always 0 MW100 or MW102 not assigned in symbol table Re-symbolise and download HW config
MD104 flickers between correct value and a large negative Overlap: another OB writes MD104 directly Move result to a non-shared MW range, e.g. MD120
HMI shows 0 while PLC shows correct value Tag not connected / access rights missing on CPU Tick "Permit access with PUT/GET"
ENO = 0 on every call Input is negative or > 99999 Clamp or sign-strip upstream
Compile error "L L#100000: constant out of range" 32-bit constant mis-typed in older STEP 7 Use L 100000 (STEP 7 V5.5 auto-promotes)

Field-Proven Caveats

  • The FC operates on signed INT inputs. A value of -1 in iHighDigits or iLowDigits falls into the err branch and forces RET_VAL = 0 with ENO = 0. If the calling code interprets 0 as a valid concatenation, swap the sentinel for an out-of-range constant (e.g. RET_VAL = -1) and document it in the symbol comment.
  • The digit-count cascade is right-shifted by the comparison sign. Always cross-check the first test that uses <=D: in S7 STL the comparison is "ACCU2 <= ACCU1", not "ACCU1 <= ACCU2". Reverse the operands with TAK when needed.
  • On S7-300 CPUs the maximum bit-memory area is 2048 bytes (MB0..MB2047). Avoid using MD1996..MD2046 for FC outputs because they are partially outside the area and will trigger an SF LED and a diagnostics buffer entry "Memory area error".
  • When the S7-300 is the target of a Profinet record read, the consumer may issue the read in little-endian (Intel) byte order. The MD104 value as stored by the FC is big-endian on the wire; if the consumer shows 0x0000C477 when MD104 = 0x00000001, swap the words with CAW / CAD before T.

Frequently Asked Questions

What is the original d5 bug in the S7-300 FC1 and how do I fix it?

Branch d5 handles iLowDigits from 10000 to 99999 (five decimal digits), so the multiplier must be 10^5 = 100000. The original code loads L 10000, scaling the high word by a factor of ten short. Replace the literal with L L#100000 (or simply L 100000 in STEP 7 V5.5+ which auto-promotes to DINT). Re-test with iHighDigits=12, iLowDigits=34567 - the expected result is 1234567, not 154567.

Why does the FC reject negative inputs by returning 0 with ENO=0?

Decimal concatenation only makes sense for non-negative integers. The cascade compares iLowDigits against 0, 9, 99, 999, 9999, 99999 using <=D, so any value outside 0-99999 (including every negative INT) falls through to the err label, clears RET_VAL and ENO. Treat ENO=0 as a quality-bad signal in the HMI driver.

What is the maximum iHighDigits I can pack without DINT overflow?

The DINT range is -2,147,483,648 to 2,147,483,647. For iLowDigits in 0-9 use iHighDigits up to 214,748,364; for 10-99 up to 21,474,836; for 100-999 up to 2,147,483; for 1000-9999 up to 214,748; for 10000-99999 up to 21,474. Add the chk overflow guard shown above to enforce the limit in the FC itself.

Can I call FC1 in a cyclic OB without harming the S7-300 cycle time?

Yes. The digit cascade executes in roughly 20-30 microseconds on a CPU 315-2 DP / CPU 317, dominated by the five TAK and five *D operations. Calling it in OB1 once per cycle is negligible. For OB35 (100 ms) or OB82 (diagnostic) calls the cost is invisible in the cycle-time monitor.

Is there a one-line SCL replacement for the entire STL FC?

Yes. In TIA Portal SCL the concatenation is a single expression: RESULT := DINT_TO_DINT(iHighDigits) * SEL(G := iLowDigits > 9, IN0 := 1, IN1 := 10) + iLowDigits for the two-decimal-digit case. For all five cases, replace the SEL cascade with the tMult lookup shown in the SCL alternative. SCL compiles to identical machine code and eliminates the d5 bug class.

Back to blog