Calculating Pulse Rate on Siemens S7-315 with STL and SFB4

David Krause17 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

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.

Scope. This article targets STEP 7 V5.5 / V5.6 with the S7-300 instruction set as defined in the S7-300 Instruction List (STL) manual. CPU 315-2 PN/DP and CPU 315F-2 PN/DP firmware versions from V3.3 onward are covered. TIA Portal variants of the same logic differ at the variable-declaration level but the STL semantics are identical.

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:

  • Counts is the number of pulses counted in the window (INT, range -32768 to +32767).
  • UpdateRate is the duration of the SFB4 timing interval expressed in milliseconds and stored in a TIME tag.
  • CountsPerMin is the desired output, declared as INT because 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.

Field-proven caveat. If the DINT result exceeds +32767 or falls below -32768 the transfer writes the truncated low word and the value displayed on the HMI will wrap. Always perform the range check shown in section 4 before the transfer. A common symptom in the field is "the rate shows a sensible number most of the time and then suddenly displays 32767" - that is the overflow path firing without saturation.

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:

  1. Use B#16# for clarity. The literal L B#16#FF is exactly the same value as L 255 but is documented as a byte constant. STEP 7 accepts the decimal form; in older projects with strict coding standards the hex form is preferred.
  2. 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

  1. 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.
  2. Force Counts to a known value (e.g. 1500) and UpdateRate to 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.
  3. Force Counts = 500, UpdateRate = T#2000ms. Expected: CountsPerMin = 60000 * 500 / 2000 = 15000. Confirm the transfer path writes 15000 with no flag set.
  4. Force Counts = 0. Expected: CountsPerMin = 0, no flag set.
  5. 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.
  6. 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

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.

Back to blog