Fixing FN Negative Edge Detection in Siemens FC Blocks
The Siemens FN (negative/falling edge) instruction is one of the most-used bit-logic operations in STEP 7 and TIA Portal, but it is also one of the most common sources of intermittent control faults. Engineers frequently report that the edge bit "never fires," that the output Q is pulsed only once during commissioning but never again in RUN, or that the falling-edge behavior disappears the moment the function is called from a different OB. In nearly every case, the root cause is the same: the edge memory bit has been placed in the TEMP area of an FC (Function), where Siemens reinitializes it on every call and the previous state is therefore lost between scan cycles.
This reference covers the underlying memory model, three production-ready fixes, the S7-1200/1500 TIA Portal behavior that is different from S7-300/400, and a complete worked example for a 3-phase motor forward/reverse starter with a 5-second off-delay between direction changes. The procedures below are aligned with the SIMATIC S7-1200 manual collection on positive/negative edge instructions and standard STEP 7 programming guides.
R_TRIG/F_TRIG IEC instances when they are placed inside an FC without persistent backing. Treat both edges as a single class of problem.1. Problem Statement
Symptom: A falling-edge evaluation inside a Function (FC) behaves as if the input never changes, or the output Q flickers on for one scan after every cold restart but never again during steady-state operation.
Typical user code that fails:
// INSIDE an FC - this is the broken pattern
VAR_TEMP
prev_dir_cmd : Bool; // <-- WRONG: reinitialized every call
END_VAR
IF dir_cmd XOR prev_dir_cmd THEN // always FALSE on second call
off_delay_timer(IN := TRUE, PT := T#5s);
END_IF;
prev_dir_cmd := dir_cmd;
The author observes that the FN instruction "does not work inside the function" and that "the memory bit loses its value between FC calls." This is correct: the FC's TEMP area is a scratchpad, not memory.
2. Root Cause Analysis
Siemens S7 PLCs segment the variable memory into three distinct lifetimes:
| Variable Class | Location | Lifetime | Available In |
|---|---|---|---|
TEMP |
Local stack (L stack) | One call only; undefined on entry | FC, FB, OB |
STAT |
Instance DB (iDB) | Persistent across calls and power-cycle (retain attribute) | FB only |
| Global (M, DB, I, Q, PIW, PQW) | Process image / global DB / Merker area | Persistent for the configured retention time | Everywhere |
Edge detection in a Boolean sense is a two-point memory: you must remember the input's state from the previous execution to compare against the current state. The FN instruction's Q output is high for exactly one scan when current_input = 0 and previous_state = 1. After Q is set, the instruction latches previous_state := current_input for the next scan.
If previous_state is a TEMP variable, it is reallocated on the L stack on every OB1 cycle. The compiler may even reuse the same L-stack byte for unrelated TEMPs in different code sections, so the value visible at the start of the FN evaluation is essentially random. Result: Q is never asserted during normal RUN because the comparison always sees previous_state = current_input.
current_input = 1 at first call, the FN sees previous = 0 and current = 1, which is the rising-edge case, not the falling edge. Q is asserted only on the inverse transition, which often never occurs because the FC's "memory" has been clobbered by then. The single observed pulse is the result of a lucky alignment between L-stack init and the first real input change.3. FC vs FB: Why Multi-Instance Programming Exists
The Siemens "multi-instance" model was introduced in STEP 7 V5 to eliminate the need for one instance DB per FB. Instead of DB_Motor1, DB_Motor2, DB_Motor3, a single parent FB holds a STAT array of motor FBs that share one instance DB. This is the architecture Siemens documents as the default for new code, and it is the recommended structure for any program that reuses the same logic on multiple instances (valves, drives, conveyors, pumps).
| Property | FC (Function) | FB (Function Block) |
|---|---|---|
| Has its own instance DB | No | Yes (single or multi-instance) |
| STAT variables allowed | No | Yes |
| TEMP lifetime | One call | One call |
| Multi-instance (one DB holds many FBs) | No | Yes (since STEP 7 V5) |
| Suitable for edge / counter / timer memory | No (use In/Out or global) | Yes (use STAT) |
| Reusable as "function library" | Yes (with In/Out discipline) | Yes (with multi-instance) |
Siemens' own recommendation, repeated in the SIMATIC programming style guide, is to use an FB with STAT variables for any block that needs internal memory. FBs are intended for "stateful" code, FCs for "pure" calculations. The user question in the source thread is therefore a classic case of using the wrong block type for a stateful requirement.
4. Solution 1: Convert the FC to a Multi-Instance FB
The cleanest fix is to migrate the FC to an FB and place the edge memory in a STAT variable. If the FC is called from OB1, simply change the block type and the editor will prompt to create an instance DB (single-instance) or to nest it as a multi-instance under an existing parent FB.
SCL for the motor-direction FB:
FUNCTION_BLOCK "Motor_DirChange_FB"
VAR
EdgeMemoryFwd : Bool; // STAT - holds previous forward cmd
EdgeMemoryRev : Bool; // STAT - holds previous reverse cmd
OffDelay : TON; // STAT - timer instance (also persistent)
END_VAR
VAR_INPUT
CmdFwd : Bool;
CmdRev : Bool;
Enable : Bool;
END_VAR
VAR_OUTPUT
RunFwd : Bool;
RunRev : Bool;
TimerDone : Bool;
END_VAR
VAR_TEMP
ti : SInt;
END_VAR
BEGIN
// Falling edge on the active direction command
IF Enable THEN
// Detect 1 -> 0 transition on the command input
IF (NOT CmdFwd) AND EdgeMemoryFwd THEN
OffDelay(IN := TRUE, PT := T#5s);
ELSIF (NOT CmdRev) AND EdgeMemoryRev THEN
OffDelay(IN := TRUE, PT := T#5s);
END_IF;
// Latch current state for next scan
EdgeMemoryFwd := CmdFwd;
EdgeMemoryRev := CmdRev;
// Direction outputs only when timer is done and command is stable
TimerDone := OffDelay.Q;
RunFwd := CmdFwd AND (NOT CmdRev) AND TimerDone;
RunRev := CmdRev AND (NOT CmdFwd) AND TimerDone;
ELSE
RunFwd := FALSE;
RunRev := FALSE;
OffDelay(IN := FALSE);
END_IF;
END_FUNCTION_BLOCK
Because EdgeMemoryFwd, EdgeMemoryRev, and the OffDelay instance are all STAT, they live in the instance DB and survive between calls. Marking the FB with {S7_Optimized_Access := 'TRUE'} (S7-1500/1200) or with retain flags on the relevant STATs makes the data survive a power cycle.
5. Solution 2: Keep the FC and Pass an In/Out Edge Bit
If a full FB migration is not feasible (e.g., the FC is shared across many programs and you cannot change its interface), pass the edge bit in as an VAR_IN_OUT parameter and back it with a STAT in the calling block, or with a global Merker byte.
FUNCTION "Motor_DirChange_FC" : Void
VAR_INPUT
CmdFwd : Bool;
CmdRev : Bool;
Enable : Bool;
END_VAR
VAR_IN_OUT
EdgeMemoryFwd : Bool; // <-- persistent caller-owned memory
EdgeMemoryRev : Bool; // <-- persistent caller-owned memory
TimerAccum : Time; // <-- elapsed-time accumulator
END_VAR
VAR_TEMP
FallFwd : Bool;
FallRev : Bool;
END_VAR
BEGIN
FallFwd := (NOT CmdFwd) AND EdgeMemoryFwd;
FallRev := (NOT CmdRev) AND EdgeMemoryRev;
IF (FallFwd OR FallRev) AND Enable THEN
TimerAccum := T#0s; // arm the 5 s gap
END_IF;
IF TimerAccum < T#5s THEN
TimerAccum := TimerAccum + T#10ms; // OB1 cycle = 10 ms
END_IF;
// Caller FB links its own STATs to these In/Out
EdgeMemoryFwd := CmdFwd;
EdgeMemoryRev := CmdRev;
END_FUNCTION
The caller wires the In/Out pins to its own STATs:
// In the parent FB, e.g. "Machine_OB1"
VAR
statEdgeFwd : Bool;
statEdgeRev : Bool;
statTimerAcc : Time;
instMotor1 : Motor_DirChange_FB; // FB instance (preferred)
END_VAR
// OR, for FCs:
"Motor_DirChange_FC"(
CmdFwd := i_CmdFwd,
CmdRev := i_CmdRev,
Enable := i_Enable,
EdgeMemoryFwd := statEdgeFwd, // wire to STAT
EdgeMemoryRev := statEdgeRev,
TimerAccum := statTimerAcc
);
6. Solution 3: Use Global Memory (M Area or Shared DB)
The simplest patch, suitable for small standalone projects where you can accept the loss of multi-instance discipline, is to use a Merker (M) bit or a global data block bit. STEP 7 reserves the M area (flags) in CPU work memory and persists it across OB1 cycles.
// In the FC body
VAR_TEMP
FallFwd : Bool;
END_VAR
BEGIN
FallFwd := (NOT "i_CmdFwd") AND "M_cmd_prev_fwd";
IF FallFwd THEN
"i_OffDelayStart" := TRUE;
END_IF;
"M_cmd_prev_fwd" := "i_CmdFwd"; // M bit retains across calls
END_FUNCTION
Mapping table for the global-memory approach:
| Signal | Address (S7-300/400) | Address (S7-1200/1500) |
|---|---|---|
| Prev forward cmd | M 100.0 | %M100.0 |
| Prev reverse cmd | M 100.1 | %M100.1 |
| Direction-change latch | M 100.2 | %M100.2 |
| 5 s off-delay accumulator | MW 102 (TIME format) | %MW102 |
| 5 s timer done flag | M 100.3 | %M100.3 |
For larger systems, replace the M area with a dedicated global DB (e.g., DB100 "Motor_Internal") so that the variables are documented in the symbol table and can be HMI-tagged without polluting the Merker area.
7. S7-1200/1500 Behavior in TIA Portal
On S7-1200 firmware V4.x and S7-1500, the FP/FN instructions have been modernized. According to the SIMATIC S7-1200 manual collection, bit-logic operations, the edge instructions in TIA Portal:
- Automatically create an implicit instance DB when the M_BIT operand is omitted, removing the need to declare a STAT variable manually.
- Expose an
ENOoutput and standardized error handling consistent with other IEC 61131-3 instructions. - Can be used inside optimized-access FBs (the default on S7-1200/1500), where the M_BIT is allocated symbolically and is retentive by default.
- For an FC, the implicit instance DB is still created, but the editor warns that the instance DB lifetime is tied to the FC's calling context. If the FC is called from multiple OBs (e.g., OB1 and OB35), the implicit instance DB can be created in each calling OB's context, leading to inconsistent state.
Best practice on S7-1200/1500 is therefore identical to the S7-300/400 recommendation: use an FB with STAT for any stateful code, including edge bits. The TIA Portal improvements reduce the verbosity, not the underlying memory model.
| Platform | FN / FP Old Style | FN / FP TIA Style | Edge in FC works? |
|---|---|---|---|
| S7-300 / S7-400 (STEP 7 V5) | FN <in> <M_BIT> = Q | Not applicable | Only if M_BIT is STAT or global |
| S7-1200 (TIA V13+) | Supported | FN (with implicit IDB) | Implicit IDB created; not portable |
| S7-1500 (TIA V13+) | Supported | FN (with implicit IDB) | Implicit IDB created; not portable |
| S7-1500 Software Controller / ET 200SP CPU | Supported | FN (with implicit IDB) | Same as S7-1500 |
8. Worked Example: 3-Phase Motor Direction Change with 5 s Off-Delay
A common application of negative edge detection is the forward/reverse motor starter. The control philosophy is:
- Operator presses FORWARD (CmdFwd = 1). Contactor K1 closes, motor spins clockwise.
- Operator releases FORWARD. CmdFwd falls 1 -> 0. The FC must detect the falling edge and start a 5 s off-delay.
- During the 5 s window, neither K1 nor K2 may close, so the motor is allowed to stop and the back-EMF on the DC bus decays. This protects the contactors and any solid-state switching.
- After 5 s, the operator may press REVERSE (CmdRev = 1). Contactor K2 closes, motor spins counter-clockwise.
- Mechanical and electrical interlocks (e.g.,
RunFwd XOR RunRev) prevent both contactors being closed simultaneously.
Using a multi-instance FB for the motor block gives a clean, scalable architecture:
// Parent FB: "Line_Control"
VAR
MotorConveyor1 : "Motor_Direction_FB"; // multi-instance
MotorConveyor2 : "Motor_Direction_FB"; // multi-instance
EdgeMemFwd1 : Bool; // only needed if using the FC fix
END_VAR
Wiring diagram (textual):
+24 V ----[PB_Fwd]---+---- %I0.0 (CmdFwd)
|
+24 V ----[PB_Rev]---+---- %I0.1 (CmdRev)
Motor FB outputs:
%Q0.0 (RunFwd) --> K1 forward contactor coil
%Q0.1 (RunRev) --> K2 reverse contactor coil
Interlock:
K1 NC aux contact in series with K2 coil
K2 NC aux contact in series with K1 coil
The 5 s off-delay must be measured using a real-time source. The simplest implementation uses a TON instance inside the FB, with PT = T#5s and a STAT edge bit that retriggers the timer on every falling edge of either command.
9. Step-by-Step Implementation in TIA Portal V18
-
Create a new FB. In the project tree, right-click "Program blocks > Add new block > Function Block." Name it
Motor_Direction_FB. Choose SCL or LAD/FBD. -
Declare the interface. In the FB interface:
- Inputs:
CmdFwd : Bool,CmdRev : Bool,Enable : Bool. - Outputs:
RunFwd : Bool,RunRev : Bool,TimerDone : Bool. - Static:
EdgeMemFwd : Bool,EdgeMemRev : Bool,OffDelay : TONwith PT preloaded to T#5s. - Temp: any local calculation scratchpad.
- Inputs:
-
Write the edge logic. Use the FBD/LAD FN instruction, or in SCL code the manual comparison:
IF (NOT #CmdFwd) AND #EdgeMemFwd THEN #OffDelay(IN := TRUE); END_IF; #EdgeMemFwd := #CmdFwd; -
Compile and download. The TIA Portal will auto-generate an instance DB (e.g.,
DB_Motor1). Download both the FB and the DB to the PLC. -
Call the FB from OB1. Add a call box for
Motor_Direction_FBin OB1 and wire the inputs to your I/O tags. -
Mark retentive STATs. In the FB interface, right-click the
EdgeMemFwd,EdgeMemRevSTATs and check "Set in IDB" retain so the edge state survives a CPU stop/start or power cycle. -
Test in single-step. Use the TIA Portal online monitor to step through OB1 and confirm that
EdgeMemFwdlatches the input state across multiple calls.
10. Verification Procedure
-
Online monitor the edge bit. Open the FB instance DB online and watch the
EdgeMemFwdboolean. With CmdFwd held high, the bit must remain 1; the moment CmdFwd goes to 0, the bit stays 0 only for the cycle in which the FN fires. -
Force a known pattern. Use the watch table to force
CmdFwd = TRUE, observe the output Q assertion only on the 1->0 transition. If Q fires immediately, the FC's implicit instance DB is being re-initialized and you have a TEMP leak. - Cold-restart test. Perform a CPU STOP then RUN. The off-delay timer must start from a defined state, not from a random L-stack residue.
- Multi-call test. If two motor FBs are instantiated, force both CmdFwd inputs high, drop them sequentially, and confirm each motor's off-delay starts at the correct instance's falling edge only.
- Cycle-time check. Verify the OB1 cycle time is stable (e.g., 10 ms). If the cycle time varies >20%, the implicit TON instance in an FC may be skipped or coalesced. The standard practice is to use TON inside an FB with a defined OB1 cycle period.
11. Best Practices and Program Structure
- Default to FBs with STATs. Treat every block that holds counters, edges, or timers as an FB. Treat pure calculations (scaling, scaling-to-physical-unit conversions) as FCs.
- Use multi-instance FBs for repeated equipment. A single instance DB holds the STAT for every valve, motor, or conveyor in the machine. Avoid creating one DB per FB instance.
-
Naming convention for STATs. Prefix every STAT that holds edge memory with
Edge_orMem_so that reviewers can identify it during code review (e.g.,Edge_CmdFwd). -
HMI / SCADA segregation. Place HMI-visible tags in a dedicated DB (e.g.,
DB_HMI) so that the operator panel never references FB instance DBs directly. This decouples the HMI from internal STAT structure. -
Alarm DB. Use a separate
DB_Alarmswith structured alarm records. Edge-derived alarm triggers should latch into a STAT in the FB, not into a global M bit. - Avoid global Merker for multi-instance code. M bits have no instance context. If the same FC is used for three motors, three M bits must be hand-allocated and reviewed at every change, which is a maintenance burden. FBs eliminate this.
- Set retain on STATs that must survive restart. In TIA Portal, the default retain setting is "non-retain." For edge memory and timer instances that need restart stability, explicitly set them as retain.
12. Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| FN output Q never asserts in RUN | M_BIT is a TEMP inside an FC | Move M_BIT to a STAT (FB) or VAR_IN_OUT backed by caller STAT |
| FN output Q asserts once at cold start, then never | L-stack zero-initialization gives the rising-edge appearance | Same as above: use persistent memory |
| FN output Q fires on the wrong motor / instance | Shared M bit across multiple FC calls | Use multi-instance FB with separate STAT per instance |
| FN output Q is intermittent, correlates with CPU scan time jitter | FC called from OB1 and OB35; implicit IDB is recreated | Call FC only from one OB, or convert to FB with multi-instance |
| Off-delay timer never reaches 5 s | TON is a TEMP inside the FC; it is re-initialized each call | Place TON in STAT of an FB |
| After STOP->RUN, motor runs immediately without off-delay | Retain bit set on the edge memory but not on the timer | Set retain on the TON instance STAT as well |
| Edge works in simulation but not on real CPU | PLCSIM uses different memory model; FC TEMP behaves as if STAT | Trust the real CPU; fix the FC -> FB migration |
13. Alternative-Platform Note: TI Programmable Logic Devices (TPLD)
Outside the Siemens world, similar edge-detection semantics appear on TI Programmable Logic Devices (TPLD). The TI SCLA083 application brief on edge and frequency detection describes rising/falling-edge detectors and frequency comparators implemented in TPLD configurable logic. The TPLD approach differs from S7 in that the edge memory is a dedicated flip-flop inside the device, not a software variable, so the issue of "FC calls losing state" is structurally impossible. This is a useful mental model when justifying the Siemens FB migration to colleagues: a rising-edge detector is a one-bit memory cell, and the same physical property applies regardless of whether the memory is a flip-flop, an instance DB, or a global Merker byte.
14. Summary of the Three Fixes
| Fix | Effort | Scalability | Recommended For |
|---|---|---|---|
| Convert FC to multi-instance FB (STAT) | Medium (interface migration) | Best | All new code; refactor of existing code |
| Keep FC, pass edge bit as In/Out | Low | OK with discipline | Shared FC across many programs |
| Use M area / global DB | Lowest | Poor for multi-instance | Single-instance, small projects, quick patch |
For the motor-direction use case described in the source thread, the recommended path is a multi-instance FB per motor. This gives a documented STAT set per instance, supports retain, and is the pattern documented in the SIMATIC S7-1200 manual on positive and negative edge instructions.
Why does my FN edge instruction never trigger inside an FC?
The FN instruction compares the current input to its memory bit (M_BIT). Inside an FC, any TEMP variable is allocated on the L stack and reinitialized to undefined values on every call, so the previous input state is lost. Move the M_BIT into a STAT variable of an FB, into a VAR_IN_OUT backed by the caller's STAT, or into a global Merker (M) bit. See the S7-1200 manual entry on positive and negative edge instructions for the full semantics.
What is the difference between a Siemens FC and an FB?
An FC (Function) has only TEMP, IN, OUT, and IN_OUT variables. It has no instance DB and no STAT, so any value it calculates is discarded at the end of the call. An FB (Function Block) has a STAT section that is stored in a dedicated instance DB and persists across calls. Since STEP 7 V5, FBs can be nested as multi-instances inside a parent FB, so a single DB can hold the state of dozens of motor or valve blocks.
How do I detect a falling edge on a forward/reverse motor command with a 5 s off-delay?
Build an FB with two STAT boolean edge bits (one for forward, one for reverse), a TON instance with PT = T#5s, and an output that is asserted only when the timer has elapsed. On the 1->0 transition of either command, the FN-equivalent comparison (NOT CmdFwd AND EdgeMemFwd) arms the timer. While the timer is running, neither forward nor reverse output is allowed. After 5 s, the new direction is released.
Can I use a TON timer inside an FC?
Yes, but the TON instance will be reinitialized every call and the accumulated time will be lost. The timer will never reach its preset. Place the TON in a STAT of an FB to give it persistent instance data, or use a global DB to hold an IEC_TIMER variable that the FC reads and writes via In/Out.
Does the TIA Portal V18 FP/FN instruction fix the FC memory problem automatically?
On S7-1200 (firmware V4.x) and S7-1500, the modern FP/FN box creates an implicit instance DB when no M_BIT is wired, which removes the need to manually declare a STAT. However, if the FC is called from multiple OBs, the implicit IDB is created per call context, which can still produce inconsistent state. The clean solution is still to migrate the logic to an FB with multi-instance and use the explicit M_BIT for the edge bit.