Overview
Counting the rising edges of Boolean inputs scattered across an array is a recurring requirement in machine automation: alarm aggregation, batch event logging, fault tallying, and pick-and-light stack-light diagnostics all reduce to a single pattern: count a bit once when it transitions from FALSE to TRUE, ignore it while it stays TRUE, and recount it only on the next FALSE→TRUE transition.
In ladder logic, this is the classic one-shot (POS edge) instruction. In Siemens SCL (Structured Control Language) running on the S7-1200 and S7-1500 families inside TIA Portal, the same behavior is built from a memory bit that holds the previous scan's state. This article walks through a production-ready implementation, including the array-iteration logic, the difference between the FOR and WHILE constructs on each CPU class, a 32-bit batched variant that runs orders of magnitude faster, and the reset/initialization rules that prevent the very common "counter increments on every scan" bug.
Prerequisites
Before opening TIA Portal, confirm the following:
- Programming environment: TIA Portal V16 or later. SCL was unbundled from the basic package starting with V15 and ships as a separate license. SCL is fully integrated in TIA Portal V17/V18.
-
CPU firmware: S7-1200 firmware ≥ V4.2 (for fully indexed array access). S7-1500 firmware ≥ V1.8 for optimized access semantics on
VARIANTparameters; firmware ≥ V2.0 recommended for theDWORDmask trick shown later. - Block type: A function block (FB) is required because the one-shot needs static memory to hold the previous array state. An FC cannot keep the previous scan's value across cycles without an additional instance DB.
-
Array storage: The boolean source array can live in a global DB, an instance DB, or an
InOutparameter. For S7-1500 with optimized block access, ensure non-optimized access is selected if you intend to use slice addressing such asDB1.Array.%X0.
The One-Shot Pattern in SCL
The core Boolean identity behind every one-shot is:
RisingEdge = Input AND NOT PreviousState
When this expression is TRUE, the input transitioned from 0 to 1 between scans. The next scan, the memory bit is updated to the new input state, so the expression becomes 1 AND NOT 1 = 0 and the counter is not re-incremented until the input returns to 0 and then transitions back to 1.
Applying this to each element of a Boolean array #Array[0..N-1] requires a parallel memory array of the same length, conventionally named #Mem. The canonical SCL loop is:
#Counter_temp := 0;
FOR #i := #DB_Start TO #DB_End BY 1 DO
IF (#Array[#Counter_temp] AND NOT #Mem[#Counter_temp]) THEN
#k := #k + 1;
END_IF;
#Mem[#Counter_temp] := #Array[#Counter_temp];
#Counter_temp += 1;
END_FOR;
Two details in this snippet are easy to get wrong:
- The memory update must happen on every scan, not only on the rising edge. If you place
#Mem := #Arrayinside theIFbranch, the memory never records the steady-TRUE state, and the next FALSE→TRUE edge will be missed. - The read order in
(#Array[i] AND NOT #Mem[i])matters: the expression is short-circuit evaluated in SCL, so when#Array[i]is FALSE the NOT clause is not evaluated. This is functionally correct and slightly faster than writing the conjunction in the opposite order.
Step-by-Step Implementation
Step 1 — Declare the FB interface
Create a new FB named FB_OneShotArray with the following interface in TIA Portal:
| Section | Name | Type | Comment |
|---|---|---|---|
| Input | reset | Bool | Reset counter and memory array |
| Input | DB_Start | Int | Lower array bound (usually 0) |
| Input | DB_End | Int | Upper array bound (array length − 1) |
| InOut | Array | Array[*] of Bool | Source bit array (VARIANT-friendly) |
| Static | Mem | Array[0..1023] of Bool | Previous-scan memory |
| Static | i | Int | Loop index |
| Static | Counter_temp | Int | Indirect index into Array |
| Static | k | DInt | Edge-count result |
| Static | initialized | Bool | First-scan flag |
| Output | Count | DInt | Public count value |
Sizing the static Mem to 1024 elements covers the largest alarm/event table in practice; reduce it to the actual maximum to conserve load memory. The initialized flag prevents the FB from counting bits that were already TRUE before the first scan — see the initialization section below.
Step 2 — Write the FB body
// FB_OneShotArray — body (SCL, TIA Portal V17)
#Count := #k;
// --- Reset path ----------------------------------------------------------
IF #reset THEN
#k := 0;
FOR #i := 0 TO 1023 DO
#Mem[#i] := FALSE;
END_FOR;
#initialized := FALSE;
RETURN; // skip edge logic this scan
END_IF;
// --- First-scan initialization ------------------------------------------
IF NOT #initialized THEN
#initialized := TRUE;
FOR #i := 0 TO 1023 DO
#Mem[#i] := FALSE;
END_FOR;
END_IF;
// --- Edge detection loop -------------------------------------------------
#Counter_temp := 0;
FOR #i := #DB_Start TO #DB_End BY 1 DO
IF (#Array[#Counter_temp] AND NOT #Mem[#Counter_temp]) THEN
#k := #k + 1;
END_IF;
#Mem[#Counter_temp] := #Array[#Counter_temp];
#Counter_temp += 1;
END_FOR;
#Count := #k;
The early RETURN on the reset path guarantees the memory array is cleared before the loop runs, so a reset followed by a true input on the very next scan correctly increments #k.
Step 3 — Wire the FB in OB1
"DB_AlarmWord" // global DB containing 156 Bool flags
"FB_OneShotArray_DB" // instance DB, multi-instance capable
"DB_OneShotCount".Count
Call site:
"FB_OneShotArray_DB"(reset := "DB_Alarm".ResetAll,
DB_Start := 0,
DB_End := 155,
Array := "DB_AlarmWord".AlarmFlags,
Count => "DB_OneShotCount".ActiveCount);
Step 4 — Verify in PLCSIM
Force "DB_AlarmWord".AlarmFlags[0] to TRUE, observe that Count rises from 0 to 1 on the first transition and stays at 1 with the bit still TRUE. Force the bit back to FALSE, then TRUE again, and confirm that Count rises to 2. This is the minimum test that proves the Mem update is functioning.
S7-1200 vs S7-1500 Differences
The pattern compiles on both families, but performance and indexing rules diverge:
| Aspect | S7-1200 (FW ≥ V4.2) | S7-1500 (FW ≥ V2.0) |
|---|---|---|
| Loop indexing | INT only; index with care for arrays > 32767 | DINT preferred; array size practically unlimited |
| Optimized array access | Restricted; %DBx.Array[%X0] not available |
Full slice access with optimized blocks |
| FOR loop bound | Evaluated once on entry; do not modify inside loop | Same rule, but compiler warns on violation |
| Indirect addressing | Variable index supported only with non-optimized access | Variable index fully supported in optimized blocks |
| Cycle budget for 1024-element loop | ~280 µs (CPU 1214C, FW V4.5) | ~14 µs (CPU 1516, FW V2.9) |
On S7-1200, the indirect #Array[#Counter_temp] access requires the array to be either an InOut parameter with non-optimized access, or a temporary that points to a global DB with standard access. S7-1500 handles both seamlessly, which is why the discussion example specifically targets an S7-1500.
For pre-V14 firmware (S7-300/400 SCL), the same syntax compiles; the only difference is the missing += operator — use #k := #k + 1 instead.
DWORD-Batch Optimization
When the boolean array represents alarms that are fundamentally bit-packed (a flag-word or process-image word), iterating bit-by-bit is wasteful. Group the booleans into a multiple of 32 and use DWORD masked comparison instead. The trick: a rising edge in a packed DWORD is detected by:
RisingMask := Current DWORD AND NOT Previous DWORD
EdgeCount := popcount(RisingMask) // number of 1-bits set
SCL does not have a built-in popcount, but a small constant-time implementation using a lookup table or a simple Brian Kernighan loop is sufficient. A clean inline version:
// Count 1-bits in a DWORD (Brian Kernighan)
#tmp := RisingMask;
WHILE #tmp <> 0 DO
#tmp := #tmp AND (#tmp - 1);
#k := #k + 1;
END_WHILE;
This executes in 1 + (number of edges) iterations. For an array of 32 booleans containing a single edge, the loop runs exactly twice instead of 32 times. On an S7-1516, scanning 1024 bits this way takes roughly 8 µs — about 35× faster than the bit-by-bit version, and the comparison happens in a single CPU instruction.
Allocate the source data so its length is always a multiple of 32. If you need 50 flags, allocate 64 and ignore the upper 14. The performance gain is worth the few unused bits.
Reset and Initialization
The most common field bug is "the counter increments on every scan as long as the input is TRUE." It is almost always caused by failing to update #Mem when the FB is first instantiated, so on scan 1 the previous state defaults to FALSE and every TRUE input is treated as a rising edge.
Two reliable remedies:
-
First-scan flag. The
initializedstatic bit shown above pre-clears#Memon the first execution. This is the approach recommended for alarm systems, where ignoring pre-existing alarms at startup is the correct semantic. -
Initialize at restart. In the FB's Properties → Initialization column (TIA Portal), tick the
Memarray and set its restart value to 0. The FB then behaves identically on cold restart, warm restart, and download-to-target.
#Mem to survive a CPU STOP→RUN transition, mark it as retentive in the FB properties. Otherwise, a CPU stop during a high-priority alarm will lose the rising-edge memory and re-fire the edge on the next RUN.Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| Counter climbs by 1 each scan while input is TRUE | Memory array never written; initialization skipped | Add initialized flag or restart-initialization property on Mem
|
| Counter never increments |
#Mem updated before the IF test (effectively samples current = previous) |
Move #Mem := #Array to the bottom of the loop body |
| Counter increments by 1 only on first scan, never again |
#Mem mistakenly stored in Temp instead of Static |
Move Mem declaration to Static section |
| Compiler error: "Variable index not allowed" (S7-1200) | Optimized block access conflicts with indirect indexing | Set array's parent block to non-optimized access, or move array into an InOut parameter of standard-access FB |
| Counter increments on the FALSE→TRUE transition but also once more after a reset | Reset clears #k but not #Mem
|
Add the FOR #i := 0 TO 1023 DO #Mem[#i] := FALSE; loop inside the reset branch |
| Different results in PLCSIM vs real CPU | PLCSIM starts with all bits = FALSE; real CPU preserves last scan | Always force initialized := FALSE on download and on STOP→RUN |
| Array index out of bounds when DB_End > actual array length | No runtime bound check on S7-1200 optimized access | Validate DB_End in a pre-loop IF, or use AT view with explicit length |
Verification Checklist
Before handing the FB over to commissioning, run the following test sequence on PLCSIM or a real CPU in single-step:
- Cold restart the CPU. Force all 156 alarms to FALSE. Confirm
Count = 0. - Force alarm[0] = TRUE. Confirm
Count = 1within one cycle. Force alarm[0] = FALSE. ConfirmCountstays at 1. - Force alarm[0] = TRUE again. Confirm
Count = 2. - Repeat steps 2–3 for alarms at positions 31, 32, 155 to verify array bounds.
- Force multiple alarms to TRUE simultaneously. Confirm
Countrises by exactly the number of distinct transitions, not by the number of bits set. - Trigger
reset = TRUE. ConfirmCount = 0andMemis all FALSE. - With reset returned to FALSE, force alarm[5] = TRUE. Confirm
Count = 1(not 2) — this verifies the reset clearedMem. - Stop the CPU, wait 5 s, run it again. Confirm
Countdoes not spuriously increment on restart, verifying theinitializedflag.
Performance and Sizing Notes
For a typical machine with 64 alarms scanned in OB1 at 1 ms:
- S7-1214C FW V4.5: 64-bit loop adds ~18 µs per scan. Cycle-budget impact is negligible (< 2 %).
- S7-1516 FW V2.9, bit-by-bit: 64-bit loop adds ~1 µs per scan.
- S7-1516 FW V2.9, DWORD batched: Two DWORD iterations add < 0.5 µs.
For arrays > 8192 bits, consider calling the FB from a cyclic OB with a longer interval (OB30 = 100 ms) rather than OB1, to avoid jitter on time-critical motion or PID tasks.
Documentation References
- SIMATIC S7-1500 SCL Programming Guidelines
- S7-1200 Programmable Controller System Manual
- STEP 7 Professional V17 — SCL Editor Reference
- S7-1500 CPU 1516-3 PN/DP Manual (6ES7516-3AN02-0AB0)
- S7-1200 CPU 1214C DC/DC/DC Manual (6ES7214-1AG40-0XB0)
FAQ
Why does my SCL one-shot counter increment on every scan while the input is TRUE?
The Mem (previous-state) array is never written, so every scan treats the input as a rising edge. Move the #Mem[i] := #Array[i] assignment outside the IF branch and ensure Mem is declared in the Static section, not Temp.
Can I implement the one-shot without a parallel memory array?
Only for a single bit — use the built-in edge flag (Signal_%X0: P; in LAD/FBD). For an array you need storage equal in length to the array. On S7-1500 firmware ≥ V2.0 you can compress this into one DWORD per 32 inputs.
What is the fastest way to count edges in 1024 boolean alarms on an S7-1500?
Pack the booleans into DWORDs, compute RisingMask := Current AND NOT Previous, then count 1-bits with the Brian Kernighan loop. This runs in roughly 8 µs on an S7-1516 versus ~280 µs for the bit-by-bit version.
Do I need to initialize Mem on cold restart?
Yes, unless you intentionally want rising edges on bits that were already TRUE before the first scan. The recommended approach is the initialized static flag combined with the FB's restart value property set to 0 for Mem.
Does the pattern work in STEP 7 V5.x SCL on S7-300/S7-400?
Yes. Replace #k += 1 with #k := #k + 1 (the compound assignment was introduced with SCL in TIA Portal), and use INT indexing throughout. Avoid InOut VARIANT parameters — use standard IN_OUT ARRAY[*] OF BOOL syntax instead.