SFC64 Pulse Duration: Siemens S7-300/400 Time Overflow Reference

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

Siemens SFC64 (TIME_TCK) is a system function that reads the CPU system time tick and returns it as a TIME data type. Because TIME on the S7-300/400 is a 32-bit signed integer with millisecond resolution, the counter wraps at T#24D_20H_31M_23S_647MS (2 147 483 647 ms, the positive limit of a 32-bit signed integer). Any code that uses SFC64 to time an external event must therefore handle the boundary condition. This reference walks through a real program fragment that measures the duration of a pulse on a boolean memory bit, and explains the accumulator (ACCU) mechanics, the boundary bug in the original logic, and the corrected implementation. The examples are valid for STEP 7 V5.x on S7-300 CPUs (CPU 312, 314, 315, 316, 317, 319) and the discussion notes the S7-400 differences where they matter.

The reference is intended for engineers who already understand STL (Statement List) loading, transfer, and comparison instructions, but need clarification on the dual-accumulator model, the TAK instruction, and the way SFC64 is integrated with rising-edge (FP) and falling-edge (FN) detection.

Platform note: The accumulator behavior described below is documented for S7-300 CPUs. S7-400 CPUs may produce different results in the edge case of arithmetic operations, particularly the +D instruction in the M002 block. Always validate the corrected code in the target CPU's PLCSIM before deployment.

SFC64 Return Value and Data Type

SFC64 is declared with one OUT parameter, RET_VAL, of data type TIME. Internally the value is a 32-bit signed integer in milliseconds counted from CPU power-on. The call is:

CALL  SFC 64
RET_VAL := DBD 0

After this call, the double-word at DBD 0 contains a millisecond timestamp. Two important characteristics drive the program logic below:

Property Value Engineering impact
Resolution 1 ms (0.001 s) Any pulse shorter than 1 ms may be missed or aliased to zero.
Width 32-bit signed (DINT) Maximum value: 2 147 483 647 ms = T#24D_20H_31M_23S_647MS.
Wrap behavior Returns to T#0ms after overflow Falling-edge timestamp can be numerically less than rising-edge timestamp.
Origin CPU power-on or restart Not synchronized to wall clock. Use SFC1 (READ_CLK) for absolute time.

The official SFC64 help page in the Siemens Industry Online Support documents the parameter interface. The wrap behavior is a property of the TIME data type and is described in the STEP 7 reference manual "System Software for S7-300/400 System and Standard Functions" (entry ID 18652056).

Pulse Measurement Concept

The goal of the program is to measure how long a boolean signal M160.0 stays TRUE. The technique uses two calls to SFC64:

  1. On the rising edge of M160.0, capture timestamp A in DBD 0.
  2. On the falling edge of M160.0, capture timestamp B in DBD 4.
  3. Compute duration = B - A in milliseconds and store in DBD 8.
  4. Accumulate the duration into DBD 12 as a running total of pulse time.

The PLC scan cycle is irrelevant as long as the scan rate is faster than the pulse rate. Edge bits M160.1 (rising) and M160.2 (falling) hold the previous state of M160.0 across scans so that each SFC64 call fires exactly once per edge.

Original Program Listing

The complete STL segment as it appeared in the field is reproduced below. Comments are added for clarity.

// Rising-edge block: capture timestamp on 0 -> 1 transition of M160.0
A   M 160.0
FP  M 160.1
JCN M001                // If not rising edge, jump to falling-edge block
CALL SFC 64
RET_VAL := DBD 0        // DBD0 = rising-edge time

M001: // Falling-edge block: capture timestamp on 1 -> 0 transition of M160.0
AN  M 160.0
FN  M 160.2
JCN M004                // If not falling edge, jump to output/reset block
CALL SFC 64
RET_VAL := DBD 4        // DBD4 = falling-edge time

// Compare and compute pulse duration
L   DBD 4               // ACCU1 = DBD4 (falling-edge time)
L   DBD 0               // ACCU1 = DBD0 (rising-edge time), ACCU2 = DBD4
<D                       // ACCU1 = (DBD0 < DBD4) ? 1 : 0   i.e. normal case
JC  M002                // If DBD0 < DBD4 then normal case, jump to M002
-D                       // Else: ACCU1 = ACCU2 - ACCU1 = DBD4 - DBD0
JU  M003                // Jump to store result

M002: // Overflow / boundary correction (BUG: see analysis below)
L   TIME#24D_20H_31M_23S_647MS  // ACCU1 = T_MAX
TAK                          // Swap: ACCU1 = DBD0, ACCU2 = T_MAX
-D                           // ACCU1 = ACCU2 - ACCU1 = T_MAX - DBD0
L   DBD 0                   // ACCU1 = DBD0, ACCU2 = (T_MAX - DBD0)
+D                           // ACCU1 = ACCU2 + ACCU1

M003: // Store result
T   DBD 8                   // DBD8 = pulse duration in ms

// Add to running total
L   DBD 8
L   DBD 12
+D
T   DBD 12                  // DBD12 = cumulative ON time

M004: // Output and reset
L   DBD 12
T   MD 71                   // Mirror to MD 71 for HMI / other code
A   M 160.3
JCN M005
L   L#0
T   DBD 0
T   DBD 4
T   DBD 8
T   DBD 12                  // Manual reset on M160.3
M005: NOP 0

Accumulator Mechanics Step by Step

The S7-300 has two 32-bit accumulators, ACCU1 and ACCU2. Most arithmetic and load instructions shift ACCU1 into ACCU2 before loading the new value into ACCU1. The TAK (Tausche / swap) instruction exchanges the two.

The execution of the M002 block on an S7-300 is traced in the table below. Assume the rising edge fired at DBD0 = 2 147 000 000 ms and the falling edge fired at DBD4 = 100 000 ms (boundary case).

Step Instruction ACCU2 ACCU1 Comment
0 Entry to M002 (jump from <D) (don't care) DBD0 = 2 147 000 000 From L DBD0 two instructions earlier; ACCU2 still holds the old DBD4, but S7-300 documentation permits the JC to leave ACCU2 in a defined state. Treat ACCU2 as undefined on entry.
1 L TIME#24D_20H_31M_23S_647MS undefined 2 147 483 647 Old ACCU1 shifted to ACCU2; T_MAX loaded into ACCU1.
2 TAK 2 147 483 647 undefined Swap. ACCU1 now holds whatever was in ACCU2.
3 -D 2 147 483 647 2 147 483 647 - undefined ACCU1 = ACCU2 - ACCU1. ACCU2 = T_MAX. ACCU1 = T_MAX - ACCU2_old.
4 L DBD 0 2 147 483 647 - undefined 2 147 000 000 Old ACCU1 shifted to ACCU2; DBD0 loaded.
5 +D 2 147 483 647 - undefined 2 147 483 647 - undefined + 2 147 000 000 ACCU1 = ACCU2 + ACCU1.

The result in ACCU1 is a meaningless number because step 3 used an undefined ACCU1. The original code's intent was:

duration = (T_MAX - DBD0) + DBD4

That is, the time from the rising edge up to wrap, plus the time from wrap to the falling edge. With the operand order in the original code, step 3 actually computes T_MAX - undefined and step 5 adds DBD0 instead of DBD4. The final duration is wrong in the boundary case.

The Overflow Boundary Problem

To understand the math, consider a system that has been running for 24 days. The CPU tick is at 2 147 000 000 ms. A pulse begins, then ends just after wrap, with SFC64 now reading 100 000 ms. The "real" duration is:

(T_MAX - 2 147 000 000) + 100 000 = 483 647 + 100 000 = 583 647 ms

Without correction, the naive DBD4 - DBD0 = 100 000 - 2 147 000 000 = -2 146 900 000 ms (a huge negative number). The <D comparison detects this because DBD0 < DBD4 is FALSE when DBD0 is near the top of the range and DBD4 is near zero. The program jumps to M002 to apply the boundary fix.

The correct closed-form is:

duration = (T_MAX - DBD0) + DBD4

The corrected STL is:

M002: L   TIME#24D_20H_31M_23S_647MS
TAK
-D
L   DBD 4
+D
M003: T   DBD 8

Walking through with the same values (DBD0 = 2 147 000 000, DBD4 = 100 000):

Step Instruction ACCU2 ACCU1
1 L T_MAX 2 147 000 000 (DBD0, stale) 2 147 483 647 (T_MAX)
2 TAK 2 147 483 647 2 147 000 000 (DBD0)
3 -D 2 147 483 647 2 147 483 647 - 2 147 000 000 = 483 647
4 L DBD 4 483 647 100 000
5 +D 483 647 483 647 + 100 000 = 583 647

The result 583 647 ms matches the expected duration. Note that the stale value of ACCU2 at step 1 does not affect the result because TAK and the subsequent -D only consume the value that was loaded by L T_MAX.

Critical bug fix: Replace L DBD 0 with L DBD 4 on the second load in M002. The original code loaded DBD0 (the rising-edge time) again instead of DBD4 (the falling-edge time), producing duration = (T_MAX - DBD0) + DBD0 = T_MAX rather than the correct sum.

Why the Constant T#24D_20H_31M_23S_647MS

Several engineers mistake this constant for a "reset" of the system clock. It is not. SFC64 is read-only; no call can modify the tick. The constant is simply the largest positive value that fits in a 32-bit signed integer, expressed in the TIME literal syntax:

2 147 483 647 ms = 24 days + 20 hours + 31 minutes + 23 seconds + 647 ms

The expression (T_MAX - DBD0) gives the milliseconds remaining until the next wrap, which is the time elapsed from the rising edge to the wrap instant. Adding DBD4 (which is now measured from the wrap) completes the pulse duration.

S7-300 vs S7-400 Arithmetic Differences

The S7-300 and S7-400 have different status-word behavior after overflow in 32-bit signed addition. The +D instruction in M002 computes (T_MAX - DBD0) + DBD4. The maximum value of the first term is T_MAX (when DBD0 = 0) and the maximum value of the second term is also T_MAX, so the sum can in principle reach 2 * T_MAX = 2 * 2 147 483 647 = 4 294 967 294. This is greater than 32-bit signed max and would overflow.

For pulses shorter than T_MAX - DBD4 (i.e. (T_MAX - DBD0) + DBD4 < 2 147 483 647) the addition fits in 32 bits and there is no overflow. Practically, this is always true because the rising edge must have occurred less than T_MAX ago for the boundary to be detected. The maximum value of (T_MAX - DBD0) is just under T_MAX, and the maximum value of DBD4 is just under T_MAX, so the sum is bounded by just under 2 * T_MAX. If the pulse is shorter than DBD4 (i.e. the rising-edge was within the same wrap cycle as the falling edge, which would have been caught by the <D check), we exit through the non-boundary path. Therefore the only case that reaches M002 has (T_MAX - DBD0) + DBD4 strictly less than 2 * T_MAX, but the actual maximum is only reached when DBD0 is at 0 ms, which is the wrap instant itself and impossible to be the rising edge of a finite pulse.

Conclusion: for any pulse that physically completes after a wrap, (T_MAX - DBD0) + DBD4 fits in a 32-bit signed integer. The result is always positive and no status word issue arises on either S7-300 or S7-400.

CPU class ACCU width Status word after +D Behavior in M002
S7-300 (CPU 31x) 32-bit OV, OS set on signed overflow No overflow in normal boundary case; logic is safe.
S7-400 (CPU 41x/416/417) 32-bit Same as S7-300 Same; some 400 series may have different JC after <D status flag treatment in the STATUS word. Validate with PLCSIM.
S7-1200/1500 (for reference) 32-bit (1200), 64-bit (1500 with extended ACCU) Different S7-1500 has a 64-bit accumulator for +D/-D, so intermediate values are immune to overflow. The boundary code still works but is unnecessary on a 1500.

Comparison: Normal vs Boundary Path

Condition Path taken Formula Sign of result
DBD0 < DBD4 (no wrap) Non-boundary (after <D) DBD4 - DBD0 Positive
DBD0 >= DBD4 (wrap occurred) M002 boundary (T_MAX - DBD0) + DBD4 Positive
DBD0 = DBD4 (zero-length pulse, no wrap) Non-boundary (equal, <D false, goes to M002) (T_MAX - DBD0) + DBD0 = T_MAX False positive: zero pulse reports as 24.8 days

The third row reveals a subtle edge case: a zero-length pulse (rising and falling edges on the same scan, or the bit never went high but edge bits were set by some glitch) produces a duration of T_MAX because the boundary code fires. The original <D check uses strict less-than, so DBD0 = DBD4 also goes to M002. If a zero-length pulse is a concern, change the comparison to <=D and add a separate zero-test on DBD4 before the M002 jump, or hold the last valid duration in DBD 8 and ignore new results that exceed a sanity threshold (e.g. one hour).

Cumulative Total and Reset Semantics

After M003, the code accumulates the pulse duration into DBD 12 with a simple add:

L   DBD 8
L   DBD 12
+D
T   DBD 12

This is a running total of "milliseconds the input has been TRUE." It does not account for the wrap in DBD 12 itself, but since each individual pulse duration is at most ~24.8 days, the cumulative value is bounded by the time the system has been running divided by the pulse repetition rate. If pulses occur frequently, monitor DBD 12 for overflow and consider saturating it at T_MAX, or use the OV status bit to detect overflow and reset to zero.

The reset block (M004) clears DBD0, DBD4, DBD8, and DBD12 to zero when M160.3 goes high. This is a manual operator reset. A reasonable enhancement is to also clear the edge bits M160.1 and M160.2 in the reset block to force the next scan to re-arm edge detection.

Verification and Commissioning

Before deploying the corrected code, validate it in PLCSIM with these checks:

  1. No-boundary test: Force DBD0 = 1000, DBD4 = 6000, clear M160.0, then set and clear M160.0 in sequence. Verify DBD8 contains 5000.
  2. Boundary test (simulated): Force DBD0 = 2 147 000 000, DBD4 = 100 000, set M160.3 to trigger the math path directly. Verify DBD8 contains 583 647.
  3. Zero-length test: Force DBD0 = 50 000, DBD4 = 50 000. Expected DBD8 = T_MAX (false positive). If unacceptable, switch the comparator to <=D and route the equal case to a zero-handler.
  4. Long-running test: Leave the CPU running for 25 days with periodic pulses on M160.0 (use a clock generator or test OB). Verify DBD8 values across the wrap instant are correct on both sides of T#24D_20H_31M_23S_647MS.
  5. Status-word check: After each M002 execution on a real CPU (not PLCSIM), read STW and confirm OV is not set. If OV is set, the add overflowed and the cumulative logic in DBD12 is corrupted.

Alternative Implementations

For new projects on S7-1200 or S7-1500, prefer:

  • IEC timer (TP / TON): A TP (pulse timer) on M160.0 with PT = T#5s gives an instant-duration value in ET that is already corrected for any wrap because it is internally managed.
  • Time difference via RD_SYS_T: On S7-1500, RD_SYS_T returns a DTL (date-and-time) structure. Subtract two DTL values to get a TIME directly without wrap concerns, as long as both timestamps come from the same CPU within the same power cycle.
  • Hardware counter on a fast digital input: If the pulse is from a digital input, configure the input as a counter and read the count. This is immune to CPU wrap entirely.

For legacy S7-300/400 systems that must keep the SFC64 approach, the corrected M002 block is sufficient. A more robust version that also handles the zero-pulse edge case is:

L   DBD 4
L   DBD 0
-D                          // ACCU1 = DBD4 - DBD0 (signed)
JP  POS                     // If positive, store and exit
L   TIME#24D_20H_31M_23S_647MS
TAK
-D                          // ACCU1 = T_MAX - DBD0 (positive)
L   DBD 4
+D                          // ACCU1 = (T_MAX - DBD0) + DBD4
POS: T DBD 8

Using JP (jump if positive) on the unsigned-signed result after -D eliminates the ambiguous equal-zero case from the boundary path.

Frequently Asked Questions

What is the maximum value SFC64 can return on an S7-300?

2 147 483 647 ms, expressed as TIME#24D_20H_31M_23S_647MS. It is the positive limit of a 32-bit signed integer, since the TIME data type is encoded as milliseconds in a DINT.

How long can a CPU run before SFC64 wraps?

Approximately 24 days, 20 hours, 31 minutes, 23 seconds, and 647 milliseconds (~24.8 days) from the last CPU restart or power-on. The wrap is silent: the next call returns a small positive number near T#0ms.

Can the boundary code in M002 overflow a 32-bit accumulator?

No, for any pulse that physically completes after a wrap, the expression (T_MAX - DBD0) + DBD4 is strictly less than 2 * T_MAX. The S7-300 will not set the OV status bit in this case. Always verify in PLCSIM with the worst-case values for your process.

Why does the original code load DBD0 a second time in M002?

It is a bug. The intent was to load DBD4 (the falling-edge time) so that the final +D adds (T_MAX - DBD0) to DBD4. Loading DBD0 instead produces (T_MAX - DBD0) + DBD0, which equals T_MAX regardless of the actual pulse duration. The fix is to replace L DBD 0 with L DBD 4 on the second load.

Does the same SFC64 boundary logic apply to SFC1 (READ_CLK)?

No. SFC1 returns a DATE_AND_TIME structure with year, month, day, hour, minute, second, and millisecond fields. It is decoded by FC3/FC6 for subtraction and is not subject to the 32-bit wrap. Use SFC1 when you need absolute wall-clock timestamps; use SFC64 only for high-resolution relative time differences where wrap can be handled explicitly.

What happens if a pulse straddles two power cycles?

SFC64 resets to T#0ms on every restart because the tick starts at power-on. If a rising edge is captured before power loss, the corresponding falling edge will appear to have a smaller timestamp and the boundary code will compute a duration of T_MAX - DBD0 + DBD4, which is not the true pulse length. Persist the timestamps in a retentive DB (use SFC 51 or DB properties with RETAIN) if the application must survive power cycles.

Back to blog