Calculating Pulse Rate on Siemens S7-315 with STL and SFB4
A pulse rate calculation - converting a pulse count over a fixed sample window into counts per minute - is a recurring engineering task on the Siemens S7-300 platform. The CPU 315 (order number 6ES7315-2EH14-0AB0 and related variants) executes the math cleanly in STL, but the interaction between the IEC timer type TIME, 16-bit INT counters, 32-bit DINT accumulators, and the implicit truncation that occurs when you transfer a DINT result into an INT tag is the source of most field callbacks. This reference documents the formula, the exact STL sequence, the safe DINT-to-INT handling, and the byte-comparison idiom requested alongside it.
1. Problem Definition
A rotating shaft carries a sensor that emits one pulse per revolution. An IEC on-delay timer (SFB4 "TON" or pulse timer SFB3 "TP") generates a fixed sample window declared as TIME with a millisecond base. Within that window, a high-speed counter (or a regular input scanned by OB1) accumulates pulse edges. At the end of each window the engineering target is:
CountsPerMin = 60000 * Counts / UpdateRate_ms
Where:
-
Countsis the number of pulses counted in the window (INT, range -32768 to +32767). -
UpdateRateis the duration of the SFB4 timing interval expressed in milliseconds and stored in aTIMEtag. -
CountsPerMinis the desired output, declared asINTbecause the HMI faceplate expects a 16-bit word. - 60000 converts the per-millisecond rate into a per-minute rate.
Mathematically the formula is a rate in the rate (quotient) sense: the dependent quantity (pulses per minute) expressed as a ratio of two measured quantities. The constant 60000 is dimensionally ms/min and balances the units so the result is pulses per minute.
2. Prerequisites
| Item | Specification |
|---|---|
| CPU | 6ES7315-2EH14-0AB0 (CPU 315-2 PN/DP), firmware V3.3 or later. CPU 315-2 AF04 and 315F-2 PN/DP share the same instruction set. |
| Programming software | STEP 7 V5.5 + SP2 / V5.6 or TIA Portal V16+ with S7-300 plug-in. |
| Organization block | OB1 (cyclic), OB35 (cyclic interrupt at 100 ms is typical when UpdateRate is small), or OB40 (hardware interrupt driven by the counter overflow). |
| Counter source | Integrated DI (≤ 20 kHz on CPU 315), FM350-1/2 counter module, or ET200S 1Count24V. |
| IEC timer block | SFB3 (TP), SFB4 (TON), or SFB5 (TOF) from the standard library, instantiated as a DB instance so the ET (elapsed time) tag is preserved. |
Reference the S7-300 Instruction List manual (entry ID 109751423) for the complete set of STL operations used below. The SFB4 block interface is documented in the STEP 7 Standard Functions reference manual (entry ID 1215404).
3. Variable Declaration in the FC
The recommended pattern is to encapsulate the calculation in a function (FC) with the following interface. This keeps the formula reusable and lets the compiler handle the input/output wiring.
| Name | Type | Direction | Initial value / comment |
|---|---|---|---|
| Counts | INT | INPUT | Pulses in the current window, range -32768 to +32767. |
| UpdateRate | TIME | INPUT | Sample window, e.g. T#2s. Underlying DINT value is in milliseconds. |
| UpdateRateMs | DINT | INPUT | Optional explicit millisecond value if the caller prefers to pre-convert. |
| CountsPerMin | INT | OUTPUT | Final 16-bit result, scaled counts/minute. |
| Status | WORD | OUTPUT | Bit 0 = overflow (result > 32767), bit 1 = divisor zero. |
| RawResult | DINT | TEMP | 32-bit intermediate result before truncation. |
The conversion of TIME to a raw millisecond DINT is automatic in STEP 7 because TIME is internally stored as a signed 32-bit value with units of milliseconds. A direct L "UpdateRate" loads that DINT into accumulator 1, which is the behaviour exploited below.
4. STL Implementation - Step by Step
// FC "CalcRate"
// Counts (INT) * 60000 / UpdateRate (TIME, ms) -> CountsPerMin (INT)
L 0 // clear status
T #Status
L #UpdateRate // TIME -> DINT (ms) loaded into ACCU1
L 0
==D // divisor zero?
JC DIV0
L #Counts // INT pulses -> ACCU1
ITD // sign-extend to DINT (ACCU1)
L L#60000 // constant 60000 (DINT literal)
*D // ACCU2 * ACCU1 -> DINT product
L #UpdateRate // load TIME ms again
/D // divide, quotient remains in ACCU1
T #RawResult // keep full-precision DINT
// Range check before INT transfer
L #RawResult
L L#32767
>D
JC OVERFLOW
L #RawResult
L L#-32768
<D
JC UNDERFLOW
// In range: transfer low word to INT
L #RawResult // DINT in ACCU1
T #CountsPerMin // STEP 7 truncates to destination width
JU DONE
OVERFLOW: L 32767
T #CountsPerMin
SET
S #Status // bit 0 = overflow
JU DONE
UNDERFLOW: L -32768
T #CountsPerMin
SET
S #Status
JU DONE
DIV0: L 0
T #CountsPerMin
SET
S #Status // bit 1 = divide by zero
DONE: NOP 0
4.1 What each line does
| STL line | Effect on accumulators | Why it matters |
|---|---|---|
L "UpdateRate" |
ACCU1 := DINT (ms) | Loads the IEC TIME millisecond value directly. TIME in S7-300 is a 32-bit signed millisecond count, not a BCD-coded S5TIME. |
ITD |
ACCU1 := DINT(sign-extended INT) | Required because Counts is INT. Without ITD the subsequent *D operates on the 16-bit value and the upper word is undefined. See S7-300 Instruction List, section on accumulator / arithmetic operations. |
L L#60000 |
ACCU1 := 60000, ACCU2 := previous ACCU1 | The L# prefix is the explicit DINT literal marker. Without it, the literal could be loaded as INT and overflow during *D. |
*D |
ACCU1 := ACCU2 * ACCU1 (DINT) | Full 32-bit multiplication. Watch the V flag (overflow) on the STATUS word if you need to detect intermediate overflow. |
L "UpdateRate" |
ACCU1 := DINT, ACCU2 := product | Loads divisor on top of the product. |
/D |
ACCU1 := ACCU2 / ACCU1 (DINT), ACCU2 := remainder | Integer division; remainder is discarded. If you need the fractional part, capture ACCU2 before the next load. |
T "CountsPerMin" |
ACCU1 low word -> destination | Transfer width is dictated by the destination declaration. The upper 16 bits of ACCU1 are silently discarded. This is the explicit behaviour requested in the original question. |
5. DINT to INT Conversion - The Definitive Answer
The STEP 7 T (Transfer) instruction is width-aware: it copies the number of bytes corresponding to the data type of the destination operand. When the source is a 32-bit DINT loaded into ACCU1 and the destination is a 16-bit INT, only the low word (bits 0-15) is written. The upper word is discarded without any status bit being set in the standard status word unless the result lies outside INT range and the overflow check you wrote explicitly catches it.
The recommended sequence is therefore:
L #RawResult // DINT in ACCU1
T #CountsPerMin // destination INT, low word copied
This is functionally equivalent to the manual idiom L "DINT_WORD" / T "INT_WORD" you may have seen in older code, because both rely on the same low-word copy. There is no signed-extension side effect: when the DINT is negative, the low word already carries the two's-complement sign bit, so a properly bounded DINT will round-trip cleanly.
5.1 Using the truncation for positive-only quantities
When the rate is known to be non-negative - which is the typical case for a one-direction shaft sensor - the upper range check is sufficient:
L #RawResult
L L#32767
>D
JC SAT_POS // saturate instead of wrapping
L #RawResult
T #CountsPerMin
JU DONE
SAT_POS:
L 32767
T #CountsPerMin
This eliminates the need to test the lower bound. Combine it with a clamp in the HMI for defence in depth.
5.2 Alternative: use a DINT tag and convert at the HMI
If you have any doubt about range, declare CountsPerMin as DINT in the FC and let the WinCC flexible / TIA Portal HMI tag perform the 32-bit display. You avoid the truncation entirely and can still present 16-bit-compatible values by scaling in the faceplate.
6. Comparing a Byte with a Constant
The byte-comparison question that followed the rate question has a clean STL answer. The pattern is: load the byte, load the constant as INT, compare with the 16-bit integer comparator <>I, ==I, >I, <I, >=I, <=I, then jump on the result.
6.1 Idiomatic byte / constant comparison
L MB20 // load byte into ACCU1 low byte
L 255 // B#16#FF as decimal 255
<>I // ACCU2 <> ACCU1 ? -> RLO = 1 if unequal
JC NOT_FF // jump if RLO = 1
// Code that runs only when MB20 == 255
...
NOT_FF:
...
Two important details:
-
Use
B#16#for clarity. The literalL B#16#FFis exactly the same value asL 255but is documented as a byte constant. STEP 7 accepts the decimal form; in older projects with strict coding standards the hex form is preferred. - Always load the constant second. The compare operators pull from ACCU1 and ACCU2, so the byte must be loaded first and the constant loaded into ACCU1 on top of it. Reversing the order produces a different comparison direction and is a frequent bug source.
6.2 Example: mask a single bit within the byte
If the intent is to check a single bit inside MB20 (e.g. bit 7 = "alarm"), use the word-AND idiom:
L MB20
L B#16#80 // mask bit 7
AW // ACCU1 := ACCU2 AND ACCU1 (word)
L 0
<>I
JC ALARM_ON // jump if bit 7 was set
This pattern - mask, compare against zero, conditional jump - is more efficient than multiple load / compare sequences when you need to test several bits in the same byte. The same idea extends to OW (OR word) and XOW (XOR word).
6.3 Byte range compare with limits
L MB20
L 10 // lower bound
<I // MB20 < 10 ?
JC LOW
L MB20
L 100 // upper bound
>I // MB20 > 100 ?
JC HIGH
// MB20 in [10..100]
LOW:
...
HIGH:
...
For more complex range checks, prefer the standard library FCs (FC16 "EQ_DW", FC18 "GE_DW", etc.) which provide direct DINT comparisons. Byte values can be widened with ITB (INT-to-BCD) only when the target is BCD; for integer compare no widening is needed because the byte is automatically placed in the low byte of ACCU1 and the upper bits remain zero.
7. LAD/FBD Equivalent for Engineers Who Avoid STL
STEP 7 will translate the STL above to a ladder network with several MOVE blocks. The advantage of STL is the single accumulator pipeline; the disadvantage is readability. The following LAD sequence performs the same task:
Network 1: Convert TIME to milliseconds (already in ms internally)
MOVE "UpdateRate" -> MD100 // TIME > DINT via MOVE
Network 2: Multiply Counts by 60000
MOVE "Counts" -> MD104
*D (CONST=60000) MD104 // result in MD108
Network 3: Divide by UpdateRate
/D MD108 MD100 // result in MD112
Network 4: Range check + saturate
GE_D MD112 L#32767 -> sat_high
LE_D MD112 L#-32768 -> sat_low
MOVE MD112 -> "CountsPerMin"
JU done
sat_high: MOVE L#32767 -> "CountsPerMin"
S "Status".overflow
sat_low: MOVE L#-32768 -> "CountsPerMin"
S "Status".underflow
done: NOP 0
In LAD/FBD the IDE inserts the same ITD / accumulator load / T sequence behind the scenes, but you lose visibility on which intermediate value is in ACCU1. Use LAD for clarity in commissioning; use STL for compactness and for cases where you need access to ACCU2 (e.g. to retrieve the division remainder).
8. Edge Cases and Field-Proven Caveats
8.1 UpdateRate declared as S5TIME instead of TIME
If the original project used S5TIME (the legacy BCD-coded 16-bit timing format), the line L "UpdateRate" loads a 16-bit value with the time base encoded in bits 12-13. The result of *D will be garbage. Convert S5TIME to TIME first with FC33 "S5TI_TIM" or by manually shifting the time base. This is documented in the STEP 7 Standard Functions reference (entry ID 1215404).
8.2 Zero-length window
If UpdateRate is T#0ms (legal in declaration but useless), the /D raises the CPU's division-by-zero error (OB121 if enabled, otherwise the CPU goes to STOP). The pre-check in section 4 catches this before the division and routes to the DIV0 label.
8.3 Negative counts from a quadrature decoder in reverse
When the input is a quadrature counter that reports negative counts for reverse motion, the ITD sign extension preserves the sign, the *D produces a negative product, and the /D yields a negative rate. The HMI faceplate must accept negative values; otherwise add an absolute-value step with ABS (FC24) before saturation.
8.4 60000 ms / 60 s / 1 min - units check
The constant 60000 is correct only when UpdateRate is in milliseconds. If you sample with SFB4 at T#1s the divisor is 1000 (not 60000) for a per-second rate, or 60000 with the factor for per-minute. A common bug is to reuse the same FC with UpdateRate in seconds and forget to change the constant.
8.5 60000 multiplication overflow
If Counts approaches 32767 and the multiplication result exceeds the DINT range (+/- 2 147 483 647), the V (overflow) flag is set in the status word but /D still uses the wrapped value. Check the V flag after *D if you expect very large counts:
L #Counts
ITD
L L#60000
*D
JOV HARD_OVF // jump if overflow
...
HARD_OVF:
L L#32767
T #CountsPerMin
SET
S #Status
8.6 OB35 jitter when UpdateRate is small
If the FC runs in OB35 and the OB35 cycle time is greater than UpdateRate, the same count can be read twice. Two options: (a) call the FC inside the SFB4 alarm block (instance DB of SFB4 can be associated with an OB40 alarm), or (b) ensure UpdateRate > OB35 cycle time and that the counter is reset only on the SFB4 rising edge.
9. Commissioning and Verification Steps
- Open the FC online in STEP 7, right-click on the STL line and select Monitor. Confirm ACCU1 / ACCU2 values at each line match the expected intermediate math.
- Force
Countsto a known value (e.g. 1500) andUpdateRateto T#1000ms. Expected: CountsPerMin = 60000 * 1500 / 1000 = 90000. Verify the range-check path saturates to 32767 and that the Status word bit 0 is set. - Force
Counts= 500,UpdateRate= T#2000ms. Expected: CountsPerMin = 60000 * 500 / 2000 = 15000. Confirm the transfer path writes 15000 with no flag set. - Force
Counts= 0. Expected: CountsPerMin = 0, no flag set. - Force
UpdateRate= T#0ms. Expected: CountsPerMin = 0, Status bit 1 (divide by zero) set. CPU does NOT go to STOP because the pre-check intercepted the call. - Spin the shaft at a known RPM with the sensor connected; cross-check CountsPerMin against a handheld tachometer. Tolerate +/- 1 count due to integer truncation.
10. Troubleshooting Matrix
| Symptom | Likely cause | Diagnostic | Corrective action |
|---|---|---|---|
| CountsPerMin always 0 | Counts never read before reset; or UpdateRate is in S5TIME format | Monitor MD location of raw product; check data type of UpdateRate tag | Convert S5TIME to TIME, or move the reset to OB40 / SFB4 instance DB |
| CountsPerMin stuck at 32767 | Range saturation firing constantly | Monitor RawResult (DINT) | Reduce sample window, increase counter range, or output as DINT to HMI |
| CountsPerMin oscillates +/- 32768 | Negative counts, no sign check | Monitor Counts tag live; check encoder direction | Apply ABS or allow negative INT output and adjust HMI |
| CPU STOP with OB121 / SF light on | Division by zero on the STL /D line | Check diagnostic buffer in STEP 7 | Add the DIV0 pre-check from section 4 |
| CountsPerMin correct, but HMI shows wrong value | HMI tag declared BYTE or WORD instead of INT | Inspect WinCC flexible / TIA tag data type | Change tag to INT (16-bit signed) on the HMI side |
| CountsPerMin half of expected | UpdateRate expressed in seconds (T#2s vs T#2ms) | Force UpdateRate and monitor | Adjust the 60000 constant or scale UpdateRate to ms |
| Byte compare always false | Constants loaded in wrong order | Watch ACCU1 vs ACCU2 in monitor | Load byte first, constant second |
| Byte compare true at unexpected times | Upper byte of ACCU1 contains garbage from previous load | Monitor ACCU1 fully (32-bit view) | Use L B#16#0 then L MB20 to clear upper byte before compare |
11. Performance Notes
The full STL sequence in section 4 runs in roughly 9-12 microseconds on a CPU 315-2 PN/DP at firmware V3.3. Calling it from OB35 at 100 ms gives a CPU utilisation impact well below 1 %. The dominant cost is the two memory loads (L "UpdateRate" twice) and the explicit range-check branches. If the FC is called from a fast OB (OB40 hardware interrupt at the SFB4 instance event), the same logic fits comfortably inside a 100 microsecond budget.
For projects where the FC must run on a smaller CPU (CPU 312 / 314), the same code is supported but the ITD / *D / /D sequence should be verified for any pre-existing accumulator state if it is reused inside a larger FC. A defensive pattern is to begin the FC with L 0 / T #TempAccu to guarantee accumulator cleanliness, even though STEP 7 does not formally guarantee accumulator persistence across FC boundaries.
12. Related Siemens References
- S7-300 Instruction List (STL) manual - complete instruction reference, entry ID 109751423.
- STEP 7 Standard Functions reference manual - SFB3/4/5 IEC timers and time conversion FCs, entry ID 1215404.
- S7-300 CPU 315-2 PN/DP manual - hardware datasheet and wiring, entry ID 12996906.
- Programming with STEP 7 manual - accumulator model and data type handling, entry ID 15218017.
- IEC 61131-3 standard summary - reference for the data type semantics used by STEP 7.
For project-specific questions about the S7-300 platform, the Siemens Industry Online Support portal at support.industry.siemens.com is the canonical reference.
FAQ
Does the T instruction really truncate a DINT to INT on S7-300?
Yes. The transfer width is determined by the destination operand's declared data type. When the destination is a 16-bit INT tag, only the low word (bits 0-15) of the DINT in ACCU1 is written; the upper word is discarded. No implicit range check is performed by T, so the rate calculation FC must include an explicit range test before the transfer to avoid wraparound.
Can I do the 60000 * Counts / UpdateRate calculation in LAD without STL?
Yes. STEP 7's LAD compiler produces the same ITD / *D / /D sequence behind MOVE and arithmetic boxes. The accumulator pipeline is hidden but the result is identical. LAD is preferred for code that is reviewed by maintenance engineers unfamiliar with STL; STL is preferred when you need direct access to ACCU2 (for the division remainder) or want the smallest possible network.
Why does the byte compare MB20 = 255 fail when MB20 really equals 255?
The most common cause is loading the constant in the wrong accumulator order. Load MB20 first (into ACCU1), then load 255 (which pushes MB20 into ACCU2 and puts 255 in ACCU1). The <>I / ==I comparator operates on ACCU2 vs ACCU1, so reversing the loads changes the comparison direction and the jump never fires when expected.
Is L#60000 different from 60000 in STL?
Yes. The L# prefix forces the literal to be loaded as a 32-bit signed DINT (range +/- 2 147 483 647). Without the prefix, 60000 is still in INT range so it loads as INT, but in older STEP 7 versions and in certain contexts the compiler may complain or generate a warning. Using L#60000 is the explicit, portable form for any constant that participates in a DINT multiply.
What UpdateRate value should I choose for a shaft tachometer application?
A common engineering rule is to make UpdateRate roughly equal to 60 / (max RPM * pulses per revolution / 1000) milliseconds. For a shaft that spins at 3000 RPM with one pulse per revolution, the minimum UpdateRate for 0.5 % resolution is 40 ms. Below that, the integer-truncation error in the division becomes visible. For slower shafts (below 200 RPM) extend the window to 500-1000 ms to keep the count magnitude manageable and the rate reading smooth.