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.
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:
- Compute
nDigits = 1ifiLowDigits > 9; else 0; (CMP == 0) then add 1. - Repeat for thresholds 99, 999, 9999, 99999; the running total is the exponent k.
- Use a 6-position multiplexer whose inputs are constants 1, 10, 100, 1000, 10000, 100000 and whose selector is k.
- 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
- 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.
- Download the project to the S7-300 CPU in STOP mode, then switch to RUN-P.
- Open Monitor/Modify from the PLC menu and force test values into
MW100andMW102per the matrix above. - Confirm
MD104matches the "Expected" column - not the "Original (buggy)" column. - Check
BRbit: 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. - Force one negative value (e.g.
MW100 = -1) and confirm the FC returns 0 with ENO=0. - 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
iHighDigitsoriLowDigitsfalls into theerrbranch and forcesRET_VAL = 0withENO = 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 withTAKwhen 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/CADbefore 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.