1. Problem Definition and Signal Path
A pressure transmitter scaled to engineering units delivers a 16-bit signed integer (INT) to the PLC: raw value 10353, representing 10353 mbar or 10.353 bar. The user wants the CPU's webserver to render the value as 10.35 bar with exactly two decimal places. The raw integer carries no display metadata, so the formatting work must be performed explicitly by either the PLC program or the webserver's rendering layer.
The end-to-end chain on a Siemens S7-1500 (or S7-1200) platform is:
- Acquire the raw INT from the analog input module's process image (for example, an ET 200SP
AI 2xU/I 2-/4-wire HS). - Convert INT to REAL with the standard IEC conversion
INT_TO_REAL(SCL) or its LAD equivalentDI_R/I_DIcascade. - Scale to bar: divide by 1000.0 using
DIV_Rin LAD or the/operator in SCL. - Round to two decimal places with
ROUNDin SCL or its LAD equivalentRND(round to DINT), followed by a division by 100.0 to restore the engineering scale. - Format the result with
VAL_STRGand parameterPREC = 2, or send the REAL to a custom webserver page and let JavaScript format it viaNumber.prototype.toFixed(2).
Steps 1 to 4 produce a numerically correct floating-point result. Step 5 is the cosmetic formatting step that produces the trailing zero the user wants. Skipping step 5 and displaying the REAL as-is in the webserver yields 10.35 (the underlying value), but the user-perceived "missing zero" comes from the page rendering, not the value itself.
1.1 Target Platform and Firmware
All code below is valid for:
- S7-1500 CPU 1511-1 PN / 1515-2 PN / 1518-4 PN/DP with firmware V2.9 or later (TIA Portal V17 or V18).
- S7-1200 CPU 1214C DC/DC/DC, 1215C DC/DC/DC, 1217C DC/DC/DC with firmware V4.4 or later.
- ET 200SP analog input modules with standard scaling (0 to 27648 for unipolar signals).
For full parameter definitions of every instruction used (DI_R, DIV_R, RND, VAL_STRG), consult the S7-1500 programming and operating manual on Siemens Industry Online Support or the TIA Portal help (F1 on the selected instruction). VAL_STRG is documented in the "String + Char" instruction set available in TIA Portal V14 SP1 and later.
2. Numerical Foundation: Scaling, Truncation, and Rounding
Three distinct numeric operations appear in the conversion chain and must not be confused.
2.1 Scaling
Scaling is the linear rescaling of an integer to engineering units. For a 4 mA to 20 mA pressure transmitter with 0 to 25 bar range, the Siemens standard scaling formula is:
Pressure_bar = (Raw_INT - 0) * (25.0 - 0.0) / (27648 - 0) = Raw_INT * 25.0 / 27648
For the user's case, scaling is simpler: 1 bar equals 1000 mbar by definition, so:
Pressure_bar = Pressure_mbar / 1000.0
For raw value 10353: 10353 / 1000.0 = 10.353 bar.
2.2 Truncation
Truncation drops the fractional part without rounding. 10.353 truncated to two decimals is 10.35 (the third decimal, 3, is below 5, so the second decimal is preserved). Truncation is implemented in S7 as TRUNC in SCL or the LAD instruction TRUNC. Truncation is acceptable for display only when the underlying process never exhibits the round-half-to-even edge case (see Section 8).
2.3 Rounding
Rounding adjusts the least-significant retained digit based on the first dropped digit. The S7-1500 / S7-1200 instruction RND rounds to the nearest integer using round-half-to-even (banker's rounding), the IEEE 754 default rounding mode. This means:
- 0.5 rounds to 0 (even).
- 1.5 rounds to 2 (even).
- 2.5 rounds to 2 (even).
- 3.5 rounds to 4 (even).
- 10.355 rounds to 10.36 because 1035.5 / 100 maps to the nearest even integer 1036.
The IEC 61131-3 third edition specifies that ROUND shall produce the nearest integer, with ties resolved in a way compatible with the platform. Siemens implements banker's rounding. If your customer specification demands round-half-away-from-zero (the "schoolbook" rule where 2.5 always rounds up to 3), you must implement a manual correction (see Section 6.3).
3. Method 1: Integer Scaling with DIV in Ladder Logic
The simplest approach is to perform all arithmetic in INT (or DINT) to avoid floating-point execution time. The trade-off is loss of precision when scaling by a non-power-of-two factor.
3.1 LAD Network Example
Network 1: Scale 10353 mbar (INT) to 10.35 bar (INT, hundredths).
MW100 (mbar, INT = 10353)
|---[ MOVE ]---> MW110 // duplicate for clarity
|---[ I_DI ]---> MD120 // sign-extend INT to DINT
|---[ DI_R ]---> MD130 // DINT to REAL (10353.0)
|---[ /R 1000.0 ]---> MD140 // 10.353 bar (REAL)
|---[ *R 100.0 ]---> MD150 // 1035.3 (REAL)
|---[ RND ]---> MD160 // 1035 (DINT, banker's round)
|---[ DI_R ]---> MD170 // 1035.0 (REAL)
|---[ /R 100.0 ]---> MD180 // 10.35 (REAL)
|---[ ROUND ]---> MW190 // 10 (DINT, integer part)
|---[ MOD 100 ]---> MW200 // 35 (DINT, fractional part as 0.35 × 100)
For raw value 10353 the integer-result sequence is 1035 (hundredths) and 35 (two-decimal integer remainder). The webserver page can format MW190 + "." + MW200 as "10.35". This avoids VAL_STRG but requires string concatenation logic on the SCL side (Section 6.4).
3.2 Limitations
The INT-scaling path fails when the result exceeds the INT range (-32768 to +32767). For a 25 bar transmitter at 27648 raw, the integer-scaled value 25 * 100 = 2500 hundredths fits in INT, but a 350 bar transmitter at the same scaling would overflow. Always check the worst-case value against DINT#32767 before choosing this method.
4. Method 2: Real Division with INT_TO_REAL
The standard IEC conversion path uses REAL arithmetic throughout. It is slower than the INT path on the S7-1200 (firmware V4.4) but negligible on the S7-1500.
4.1 SCL Implementation
FUNCTION_BLOCK FB_PressureScale
VAR
raw_mbar : INT; // input, raw integer from AI module
bar_value : REAL; // output, scaled value in bar
bar_rounded: REAL; // output, value rounded to 0.01 bar
END_VAR
BEGIN
// Step 1: INT to REAL conversion
bar_value := INT_TO_REAL(raw_mbar) / 1000.0;
// Step 2: round to two decimal places
bar_rounded := REAL_TO_DINT(bar_value * 100.0 + 0.5 * SGN(bar_value));
// Note: SGN returns -1, 0, or +1; the +0.5 / -0.5 correction
// implements round-half-away-from-zero, not banker's rounding.
bar_rounded := DINT_TO_REAL(bar_rounded) / 100.0;
END_FUNCTION_BLOCK
The + 0.5 * SGN() correction is the explicit round-half-away-from-zero formula. If the S7-1500 RND behavior (round-half-to-even) is acceptable, replace the body of Step 2 with bar_rounded := ROUND(bar_value * 100.0) / 100.0;.
4.2 LAD Equivalent
MW100 (raw_mbar INT)
|---[ I_DI ]---> MD200 // sign-extend
|---[ DI_R ]---> MD204 // 10353.0
|---[ /R 1000.0 ]---> MD208 // 10.353 bar
|---[ *R 100.0 ]---> MD212 // 1035.3
|---[ RND ]---> MD216 // 1035 (DINT)
|---[ DI_R ]---> MD220 // 1035.0
|---[ /R 100.0 ]---> MD224 // 10.35 bar
Round-trip for raw value 10353: MD224 = 10.35. For raw value 10359, MD224 = 10.36 (because 1035.9 rounds to 1036).
4.3 Performance Notes
On the S7-1500, a single REAL divide takes approximately 18 ns (CPU 1515-2 PN), and a RND takes 12 ns. The total method-2 cost is well under 200 ns per call, and the FB can be invoked from the OB1 (or OB35 cyclic interrupt) without measurable CPU load. On the S7-1200 CPU 1214C, the same sequence takes approximately 18 microseconds, still negligible for cyclic 100 ms scan rates.
5. Method 3: VAL_STRG with PREC = 2 (Siemens-Recommended)
For webserver display, the most compact solution is to use Siemens' VAL_STRG instruction. It converts a numeric value to a STRING with controlled precision, sign, and exponent formatting in a single call.
5.1 VAL_STRG Interface
| Parameter | Direction | Type | Description |
|---|---|---|---|
| IN | IN | REAL, LREAL, DINT, INT | Value to format. |
| FORMAT | IN | WORD | Bit 8: exponent notation; Bits 0-3: number of digits left of decimal. |
| PREC | IN | SINT | Number of decimal places (0 to 7 for REAL; 0 to 15 for LREAL). |
| OUT | OUT | STRING, WSTRING | Result string. |
For two decimal places, set PREC = 2 and FORMAT = 0 (no exponent, default left-of-decimal count).
5.2 SCL Call Example
// Variant 1: 2 decimals, no exponent, fixed-point notation
VAL_STRG(
IN := bar_rounded, // REAL input 10.35
FORMAT := 0, // fixed-point, default width
PREC := 2, // 2 decimals
OUT := "DB_Web".PressureText // STRING(20) output
);
The output string for bar_rounded = 10.35 is exactly '10.35' (six characters). For 10.3 the output is '10.30', with the trailing zero preserved. This is the missing behavior the user wants from the webserver.
5.3 LAD Call Example
MD224 (bar_rounded REAL = 10.35)
|---[ VAL_STRG ]---> DB100.PressureText (STRING[20])
FORMAT = W#16#0000
PREC = 2
In LAD, drop the VAL_STRG block from the "String + Char" instruction palette onto the network, then wire the REAL input, set FORMAT to 0 (or W#16#0 in hexadecimal), and set PREC to 2. The OUT pin writes directly to the STRING tag in the data block.
5.4 Webserver Binding
To expose DB_Web.PressureText on the S7 webserver, the variable must be in a data block with the "Webserver" attribute enabled (in TIA Portal: DB properties → Attributes → "Accessible from HMI/OPC UA/Webserver"). The webserver page then references the variable as := "DB_Web".PressureText in the HTML AWP syntax. Refer to the S7-1500 Webserver programming manual on Siemens Industry Online Support for AWP syntax details.
5.5 Why PREC = 2 is Safe for Banker's Rounding
VAL_STRG with PREC = 2 performs IEEE 754 round-half-to-even to two decimal places, then prints the result as fixed-point ASCII. The intermediate rounding prevents visual artifacts such as '10.3499999...' from leaking through (a known issue when displaying raw REALs in JavaScript). The behavior is consistent with Section 2.3: 10.355 → 10.36, 10.345 → 10.34.
6. Method 4: Reusable SCL Function Block
For projects with dozens of scaled values (pressure, flow, temperature, level), a generic SCL function block avoids code duplication and centralizes the rounding policy.
6.1 FB Definition
FUNCTION_BLOCK FB_RealFormatter
VAR_INPUT
rawValue : REAL; // scaled REAL input
decimals : SINT; // 0..7
truncate : BOOL; // FALSE = ROUND, TRUE = TRUNC
END_VAR
VAR_OUTPUT
text : STRING[32]; // formatted output
rawResult : REAL; // rounded/truncated REAL
END_VAR
VAR
scaledDint : DINT;
scaleFactor : REAL;
END_VAR
BEGIN
// Compute scale factor (10^decimals)
IF decimals = 0 THEN
scaleFactor := 1.0;
ELSIF decimals = 1 THEN scaleFactor := 10.0;
ELSIF decimals = 2 THEN scaleFactor := 100.0;
ELSIF decimals = 3 THEN scaleFactor := 1000.0;
ELSIF decimals = 4 THEN scaleFactor := 10000.0;
ELSIF decimals = 5 THEN scaleFactor := 100000.0;
ELSIF decimals = 6 THEN scaleFactor := 1000000.0;
ELSE scaleFactor := 10000000.0;
END_IF;
// Apply round or truncate
IF truncate THEN
scaledDint := REAL_TO_DINT(rawValue * scaleFactor);
ELSE
scaledDint := ROUND(rawValue * scaleFactor);
END_IF;
rawResult := DINT_TO_REAL(scaledDint) / scaleFactor;
// Format to string
VAL_STRG(
IN := rawResult,
FORMAT := 0,
PREC := decimals,
OUT := text
);
END_FUNCTION_BLOCK
6.2 Call Example
// In OB35 every 100 ms
FB_RealFormatter_1(
rawValue := bar_value, // 10.353
decimals := 2, // two decimals
truncate := FALSE // use round-half-to-even
);
// FB_RealFormatter_1.text = '10.35'
// FB_RealFormatter_1.rawResult = 10.35
6.3 Manual Round-Half-Away-from-Zero Patch
Replace the line:
scaledDint := ROUND(rawValue * scaleFactor);
with the explicit correction:
// Add a small bias toward zero then round.
// For positive values: +0.5 * (1/scaleFactor).
// For negative values: -0.5 * (1/scaleFactor).
// This breaks the round-half-to-even tie by always pushing
// ties away from zero (schoolbook rounding).
IF rawValue >= 0.0 THEN
scaledDint := REAL_TO_DINT(rawValue * scaleFactor + 0.5);
ELSE
scaledDint := REAL_TO_DINT(rawValue * scaleFactor - 0.5);
END_IF;
For rawValue = 10.355 and scaleFactor = 100.0, the positive branch yields REAL_TO_DINT(1035.5 + 0.5) = REAL_TO_DINT(1036.0) = 1036, then 10.36. For rawValue = -10.355 the negative branch yields REAL_TO_DINT(-1035.5 - 0.5) = -1036, then -10.36. This is round-half-away-from-zero as specified by ASTM E29 for industrial measurement reporting.
6.4 Manual String Concatenation (No VAL_STRG)
On the S7-1200 firmware V4.2 and earlier, VAL_STRG may not be available. In that case, build the string manually:
// Integer part and fractional part separated by '.'
integerPart := REAL_TO_DINT(rawResult);
fractionalPart := REAL_TO_DINT((rawResult - DINT_TO_REAL(integerPart)) * 100.0);
// Build string: "10" + "." + "35"
INT_TO_STRING(integerPart, 0, text); // produces "10"
CONCAT(IN1 := text, IN2 := '.', OUT := text); // "10."
INT_TO_STRING(fractionalPart, 0, tempStr); // produces "35"
CONCAT(IN1 := text, IN2 := tempStr, OUT := text); // "10.35"
This works but loses the leading zero for single-digit fractions (0.5 would render as "0.5" correctly because REAL_TO_DINT(0.5 * 100) = 50, but 0.05 would render as "0.5" due to REAL_TO_DINT(5.0) = 5). To force two-digit fractions, pad with a leading zero check:
IF fractionalPart < 10 THEN
CONCAT(IN1 := text, IN2 := '0', OUT := text);
END_IF;
7. Webserver JavaScript Alternative (toFixed)
If the S7 webserver serves the raw REAL via a custom HTML page, the formatting can move to the browser. The PLC stays integer- or REAL-only; the page renders two decimals with JavaScript.
7.1 Why This is Often Cleaner
- The PLC uses less memory (no STRING tags, no VAL_STRG calls).
- Multiple consumers (HMI, SCADA, third-party dashboards) can each format the value differently without burdening the PLC.
- JavaScript
toFixedis well-supported and renders trailing zeros natively.
7.2 HTML AWP Example
<!-- AWP variable declaration -->
<!-- AWP_Out_Variable Name='PressureBar' Use='"DB_Web".PressureBar' -->
<p>Pressure: <span id="pressure">--</span> bar</p>
<script>
// Poll the PLC every 2 seconds
setInterval(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/awp/DB_Web/PressureBar', true);
xhr.onload = function() {
if (xhr.status === 200) {
// xhr.responseText is a string representation of the REAL
var v = parseFloat(xhr.responseText);
if (!isNaN(v)) {
document.getElementById('pressure').innerText = v.toFixed(2);
}
}
};
xhr.send();
}, 2000);
</script>
The v.toFixed(2) call returns a STRING with exactly two decimal places, with trailing zeros preserved. For v = 10.35 the result is "10.35". For v = 9.9 the result is "9.90", which is the cosmetic behavior the user is asking for.
7.3 Caveats of toFixed
JavaScript Number.prototype.toFixed is defined by ECMA-262 to return a string with the requested number of digits, but the rounding rule is implementation-defined. V8 (Chrome, Node.js) uses round-half-to-even. SpiderMonkey (Firefox) uses round-half-away-from-zero on some versions. If the rounding policy matters, format the value on the PLC with VAL_STRG (Section 5) before sending it to the page as a STRING, so the rule is consistent across browsers.
7.4 Direct Webserver JSON Access
The S7-1500 webserver also supports a JSON-style interface through the AWP command :='DB_Web'.PressureBar: embedded in the page. The browser then parses the value as a number and calls toFixed(2) on it. Reference the "S7-1500 Webserver" manual section on user-defined pages for the full AWP syntax.
8. Edge Cases: Negative Numbers, Round-Half, and Overflow
Industrial measurement is rarely all positive. Differential pressure, vacuum, and bidirectional flow can produce negative raw values. Each edge case must be handled.
8.1 Edge Case Matrix
| Raw INT | REAL after /1000 | ROUND(*100)/100 (banker's) | TRUNC(*100)/100 | VAL_STRG PREC=2 |
|---|---|---|---|---|
| 10353 | 10.353 | 10.35 | 10.35 | '10.35' |
| 10359 | 10.359 | 10.36 | 10.35 | '10.36' |
| 10355 | 10.355 | 10.36 (tie → 1036 even) | 10.35 | '10.36' |
| 10345 | 10.345 | 10.34 (tie → 1034 even) | 10.34 | '10.34' |
| -10353 | -10.353 | -10.35 | -10.35 | '-10.35' |
| -10355 | -10.355 | -10.36 (tie → -1036 even) | -10.35 | '-10.36' |
| 0 | 0.0 | 0.0 | 0.0 | '0.00' |
| 5 | 0.005 | 0.00 (banker's) or 0.01 (away) | 0.00 | '0.00' or '0.01' |
8.2 Negative Numbers and DI_R / I_DI
Siemens LAD instruction I_DI sign-extends a 16-bit INT to a 32-bit DINT. Without this step, DI_R would interpret the high word as zero, turning -10353 (INT) into +55183 (DINT after zero-extension) and producing a positive scaled value. Always include the I_DI pre-step for negative-capable signals.
8.3 Overflow on the INT Path
For a -1 bar to 25 bar transmitter with 4 to 20 mA scaling, the raw INT range is approximately 0 to 27648. The integer-scaled value (hundredths) is 0 to 2500, which fits in INT. For a 0 to 100 bar transmitter at the same scaling, the integer-scaled value is 0 to 10000, still fits. For a 0 to 400 bar transmitter, the integer-scaled value is 0 to 40000, which overflows INT (max 32767) and must be held in DINT. Use DINT_TO_REAL and the DINT variants of DIV and RND in that case.
8.4 NaN and Infinity from a Disconnected Sensor
An open-circuit 4 to 20 mA loop typically drives the analog input to 0 (or the overrange value 32767, depending on module configuration). The PLC sees a valid INT, not NaN. The conversion chain produces a valid REAL. The webserver displays the resulting bar value. If the customer wants the webserver to show "--.--" on sensor fault, wrap the conversion with a quality-code check from the AI module's diagnostic interrupt (OB82) and replace the STRING with a placeholder before VAL_STRG runs.
8.5 Floating-Point Precision
REAL (32-bit IEEE 754) has approximately 7 significant decimal digits. The conversion 10353.0 / 1000.0 cannot be represented exactly; the actual stored value is 10.3530006408691406... When the user reads 10.353 in the watch table, TIA Portal truncates the display to a few digits. The round-to-2-decimals step then snaps it to 10.35. This is why the round-then-format approach is essential; formatting the raw REAL directly yields '10.35300064' in some implementations.
9. Verification and Commissioning Checklist
Before the webserver goes live, run the following verification steps in the TIA Portal PLCSIM or on the real CPU.
- Watch table test: Create a watch table with the raw INT, the bar_value REAL, the bar_rounded REAL, and the formatted STRING. Force the raw INT to 10353. Verify bar_rounded = 10.35 and the STRING = '10.35'.
- Edge case sweep: Force the raw INT to 0, 1, 5, 10353, 10355, 10359, 20000, 27648, -100, and -27648 (the last only for bidirectional scaling). Verify each output against the table in Section 8.1.
-
Browser test: Open the S7 webserver URL (default
http://<cpu-ip>) from Chrome, Firefox, and Edge. Verify the rendered text shows two decimals with trailing zeros. - Round-trip test: Click the "Update" button on the custom web page (if implemented) and confirm the displayed value matches the watch table value to two decimal places.
- CPU load: In TIA Portal online & diagnostics, monitor OB1 cycle time. The conversion FB should add less than 0.5 ms to the cycle on a CPU 1515-2 PN, well below the 1% warning threshold.
- String length: Confirm the output STRING is dimensioned to hold the worst-case value, including sign, integer digits, decimal point, and two decimals. For 5-digit integer values with sign: STRING[9] is sufficient ('-12345.67' = 9 chars).
- Webserver access rights: Configure the user authentication in the CPU's webserver properties so the page is not anonymously writable. Refer to the S7-1500 Webserver manual for the access level definitions.
- Documentation: Annotate the FB with the rounding policy (banker's vs. away-from-zero) in the block comment. This avoids the next maintainer guessing why 0.5 sometimes rounds down.
10. Frequently Asked Questions
How do I round a REAL to 2 decimal places in S7 LAD?
Multiply the REAL by 100.0, apply the LAD RND block to convert to DINT, then convert back to REAL and divide by 100.0. The sequence REAL → *R 100.0 → RND → DI_R → /R 100.0 produces the rounded value on the S7-1500. The RND instruction uses round-half-to-even (banker's rounding) per IEEE 754.
Why does 9.90 display as 9.9 on the S7 webserver?
REAL values in the S7 webserver are stored as 32-bit floats. The number 9.90 and 9.9 are mathematically identical at that precision. The webserver page renders the value as-is, dropping the trailing zero. To force the trailing zero, format the value as a STRING on the PLC using VAL_STRG with PREC = 2, or use JavaScript toFixed(2) on the browser side.
Should I use VAL_STRG with PREC = 2 or manual rounding in SCL?
Use VAL_STRG with PREC = 2 for webserver display, because it does the rounding and the string formatting in a single instruction and preserves trailing zeros. Use manual rounding (multiply, RND, divide) when the rounded REAL itself is consumed by further math, because the STRING output of VAL_STRG cannot be used in arithmetic operations.
What is the difference between RND, TRUNC, CEIL, and FLOOR in S7?
RND rounds to the nearest integer using round-half-to-even. TRUNC drops the fractional part (toward zero). CEIL rounds toward positive infinity (10.1 → 11, -10.1 → -10). FLOOR rounds toward negative infinity (10.9 → 10, -10.1 → -11). For two-decimal display formatting, RND is the correct choice; TRUNC causes systematic downward bias.
How do I show trailing zeros in a Siemens webserver variable?
Bind the webserver to a STRING tag, not a REAL tag. Set the STRING from VAL_STRG with PREC = 2 on every PLC cycle. The webserver renders the STRING verbatim, including the trailing zero. If the page uses JavaScript toFixed(2), the browser renders the trailing zero regardless of the underlying REAL value.
Can I use DINT instead of REAL to avoid floating-point errors?
Yes. Hold the value in hundredths (or tenths) of bar as a DINT, for example pressure_cbar : DINT; pressure_cbar := raw_mbar / 10; gives the value in centibar. The webserver page then formats it as (pressure_cbar / 100) + '.' + (pressure_cbar MOD 100) in JavaScript, or via INT_TO_STRING concatenation in SCL. This avoids IEEE 754 round-off but requires manual decimal-point placement in the display layer.