PLC Programming Interview Questions: Scan Traces, Edge Detection, and Conflicting Task Writes
Quick Overview
Original PLC interview exercises trace held inputs, persistent R_TRIG state, skipped/shared calls and conflicting writes. A twelve-check Python model separates assumed scan behavior from CODESYS task, driver and bus contracts.
PLC interview questions are easier to reason about when you separate sampled inputs, function-block state, task execution, and physical I/O transfer. A button held through three calls can look like three activations to the counter even when the input stays steadily true. An edge detector can miss a transition it never observes. Two tasks can each contain plausible logic while jointly producing an undefined output arrangement.
This article uses original exercises and an executed Python host model, not a compiled PLC program, vendor simulator, controller test, or safety certification. Official references cover CODESYS 3 task and I/O behavior and its Standard library 3.5.18.0 rising-edge block. CODESYS 4 configuration has different supported settings. There are no candidate reports or claims that an employer asks these exact questions.
To practice defining state changes before coding, start with Implement a Multicore Overheat Prevention Controller on PracHub. Its specified tick order is useful preparation; its software simulation rules do not establish PLC scheduling or industrial safety behavior.

Which assumptions make a PLC scan trace meaningful?
A scan trace is a record of sampled inputs, stored state, and calculated results at explicitly chosen execution points. A simple input-read, program-execute, output-write loop is a useful teaching model. Real task configuration and I/O transfer rules determine whether that model fits the controller being discussed.
Our reference exercise takes one Boolean sample at the beginning of each logical call. It invokes one persistent edge-detector instance exactly once, updates counters, and records results afterward. Previous input starts false; counters start zero. There is no preemption, counter overflow, retained restart state, bus delay, or contact bounce in this model.
Those assumptions make a prediction possible. They are not a claim that every controller performs one global scan. Official CODESYS 3 task configuration supports cyclic, freewheeling, event-triggered, and status-triggered tasks, with configured program calls. Ask which task invokes the code and how it is scheduled before describing “the next scan.”
Keep the product generation visible. The current CODESYS 4 TaskConfiguration.json reference lists Cyclic and Freewheeling task types and currently rejects several other types. Check the CODESYS 4 reference before carrying a CODESYS 3 task configuration into that generation.
For an interview answer, state the target family, task type, call order, sampling point, and observation point. If any is unknown, give a conditional trace: “Under this synchronous model, the result is X; I would inspect the target task and driver configuration before predicting the physical output.”
Why does a held input increment more than once?
Suppose a diagnostic counter should increment when a button becomes true, once per observed activation. With IF Sample THEN Count := Count + 1, every call with a true sample increments it. The program is counting true observations rather than transitions.
CODESYS documents R_TRIG in Standard library 3.5.18.0 as a rising-edge function block with Boolean CLK and Q. For a transparent reference recurrence, ABB's official R_TRIG documentation shows the previous-input state and update logic. Our host model uses that recurrence with its initial previous value explicitly false.
Pulse := Sample AND NOT Previous;
Previous := Sample;
The order matters: calculate the pulse using the old previous value, then update previous. Reversing those statements would compare the input with itself and suppress the intended detection. This is a stateful instance, not merely a fresh Boolean expression reconstructed independently on each call.
The executed reference model produced the following six-call trace. Previous is recorded before the call; both counters are recorded after processing it.
| Call | Sample | Previous | Pulse | Level count | Edge count |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 | 0 |
| 2 | 1 | 0 | 1 | 1 | 1 |
| 3 | 1 | 1 | 0 | 2 | 1 |
| 4 | 1 | 1 | 0 | 3 | 1 |
| 5 | 0 | 1 | 0 | 3 | 1 |
| 6 | 1 | 0 | 1 | 4 | 2 |
Calls two through four represent one held activation. Edge counting adds one during that interval, while level counting adds three. The sampled false at call five prepares the instance to recognize the later rise at call six.
In a cyclic program with a persistent instance declaration, the illustrative Structured Text fragment calls Rise before consuming Q. InputImage represents the input value supplied by the exercise, not a portable physical address.
VAR
Rise : R_TRIG;
Sample : BOOL;
Count : UDINT := 0;
END_VAR
Sample := InputImage;
Rise(CLK := Sample);
IF Rise.Q THEN
Count := Count + 1;
END_IF;
This fragment was not compiled. Verify the target's declaration context, available library, startup and retention semantics. Our first-true-sample result follows the stated initial memory; it is not a universal warm-restart guarantee.
What happens if an edge instance is skipped or shared?
Change the five-call input sequence to zero, one, one, zero, one. Call the instance for the first two samples, skip the next two calls, then call it again. In our model, Q remains true during the skipped calls because nothing recomputes it. More importantly, previous remains true: the instance never observes the false sample.
On the final true sample, the new Q is false. The physical story might contain another activation, but the instance's observed history does not contain the intervening fall. “It produces a one-scan pulse” is incomplete unless one instance call corresponds to each relevant scan and you identify when Q is consumed.
This explains why wrapping the detector call inside IF Input THEN is suspect. The call may be suppressed exactly when it needs to observe a false input. Call it according to the intended sampling contract, then condition the downstream action separately. Also inspect whether downstream code reads the old true Q during skipped calls; that can repeat the action despite having an edge detector.
Sharing one instance between unrelated signals creates another failure. Let signal A remain false and signal B remain true. Call the same instance with A and then B on every cycle. A sets previous false; B then appears to rise again. In two cycles of A-then-B calls, the shared instance gives B pulses one, one. Separate instances give B pulses one, zero.
The repair is separate state for separate histories, with one deliberate call path per history. Renaming a shared instance does not separate its memory. If multiple tasks can access one function-block instance, investigate execution and synchronization rather than treating it as a stateless helper.
Can edge detection recover a pulse between samples?
Assume, only for this sampling exercise, that input observations occur at zero and ten milliseconds. A pulse exists from four to six milliseconds. Both observations are false, so the host model produces no rising edge. There is no true sample from which this detector can reconstruct the event.
That example does not establish a ten-millisecond controller's real capture limit. Input-module filters, hardware latches, interrupts, bus exchange, task timing and the device's specifications can change what reaches the application. Identify where the event was lost before proposing a shorter task period.
A useful investigation follows the signal through acquisition, transport, sampling and logic. If the input buffer already contains a captured event, inspect the consumer's state. If the application never receives it, editing the counter condition cannot restore the missing information. A pulse counter or capture facility may be appropriate when the target supports it; verify its actual interface and limits.
Contact bounce is a different question. Multiple observed false-to-true transitions can correctly produce multiple edge pulses even when the user intended one press. Define a filtering or debounce requirement separately; an edge detector alone does not infer which electrical transitions belong to one human action.
What if two tasks write the same output?
Use a logical application variable named DemoCommand, with no physical actuator attached. RunTask writes true when a demonstration run request is present. InhibitTask writes false when its demonstration inhibit is present. Assume both conditions are true and explicitly choose two serial schedules.
| Assumed host schedule | Writes to DemoCommand | Final application value |
|---|---|---|
| RunTask, then InhibitTask | 1, then 0 | 0 |
| InhibitTask, then RunTask | 0, then 1 | 1 |
These are deterministic results of our chosen statement order. They are not a portable guarantee that a physical output follows “the last task to finish.” CODESYS 3 I/O mapping documentation warns that an output written by multiple tasks has undefined status. Application assignments, output buffers, driver transfer and bus transmission are separate observation points.
The same documentation explains that task-level transfers interact with the I/O driver and bus-cycle task. Driver implementation determines further physical transfer timing. Do not infer a terminal voltage from a debugger's final Boolean value without checking the configured device and transfer path.
The Task Deployment view can show I/O assignments across tasks after code generation. Use it to locate unintended shared writers. Static Analysis rule SA0006 detects variables written by several tasks. These are concrete investigation tools, not evidence that our article executed a CODESYS build.
Raising one task's priority is an incomplete repair. It leaves two authorities for the same decision and can make behavior depend on scheduling details. Even serialized access needs a rule for deciding which conflicting intent should prevail, or task order still decides whether the demonstration command is on.
How does a single writer make the decision explicit?
Give each producer its own intent slot. RunTask alone publishes RunRequest; InhibitTask alone publishes InhibitRequest. One owner task acquires a coherent, sufficiently fresh view of those intents and alone assigns DemoCommand. For this exercise, inhibit dominates: run is allowed only when requested and not inhibited.
(* Owner task only; these are already acquired local snapshots. *)
DemoCommand := RunFresh AND InhibitFresh
AND RunSnapshot AND NOT InhibitSnapshot;
With fresh inputs, RunRequest one and InhibitRequest one produce command zero. RunRequest one and InhibitRequest zero produce command one. All other combinations produce zero. Given either freshness flag false, the host model returns false; it does not implement or measure a target timestamp-transfer mechanism. Those are authored application policies for a demonstration command, not a certified safe-state strategy.

Separate slots do not automatically provide coherent cross-task publication. On the target, specify how values, sequence numbers and timestamps are transferred together, which synchronization mechanism applies, and what age is acceptable. A Boolean copied by Python does not establish atomicity of a target structure or prevent reading fields from different updates.
Input consistency deserves the same care. The CODESYS 3 I/O documentation describes cases where another task's input update changes the process image during a task. Its recommended local-copy approach helps code reason from selected samples. A local copy of several fields still needs an appropriate coherence contract; it is not a universal multi-field transaction.
Finally, distinguish synchronization from arbitration. A semaphore can prevent an inconsistent concurrent access when used correctly, but it does not decide whether run or inhibit should win. State the policy, the data-transfer mechanism and the physical-output verification separately. Real machinery protection requires its own qualified design; this demonstration establishes none of those safety properties.
What can the local model actually prove?
The original host model ran under Python 3.9.6 and completed twelve checks with no failures and exit status zero. It checked the held-input trace, stated initial memory, skipped calls, shared versus separate histories, the between-sample pulse example, two explicit write schedules, the arbitration truth table and stale-intent rejection. These results verify our model's declared transitions and calculations.
They do not run a PLC scheduler, compile Structured Text, emulate a fieldbus, measure jitter, validate input filtering or actuate hardware. In a target investigation, first compare the application's task configuration, function-block call sites, initialization and I/O mapping with the model's premises. A disagreement may identify a different contract rather than an arithmetic mistake.
For a useful regression, record task identity, invocation sequence, captured input, previous state, Q, count and the application's assigned command. Then identify the separate observation that confirms device transfer. Compare the target configuration with the teaching assumptions before changing the logic to match the table.
Test startup while the input is already true, release and repress, disabled call paths, separate inputs, stale intent and conflicting requests. When the count looks wrong, find the first call where expected and observed state diverge before replacing the block or blaming the sensor. That gives the interviewer a concrete diagnostic argument rather than a list of PLC terminology.
Practice the surrounding engineering reasoning
The checked PracHub search did not return dedicated PLC questions. These five verified records offer adjacent simulation, firmware, scheduling, concurrency and sensing practice. They are not PLC-native programs or predictions of an automation employer's interview.
| Complete PracHub question title | What to practice and keep distinct |
|---|---|
| Implement a Multicore Overheat Prevention Controller | Trace state changes in the specified tick order; a software thermal simulation is not machinery protection. |
| Reason About Endianness, Ring Buffers, and Stack Behavior in Firmware | Name execution contexts, producer/consumer ownership and bounded state; firmware assumptions are target-specific. |
| Order Processes on One CPU by Shortest Ready Job | Follow an explicit scheduling policy; its non-preemptive shortest-job rule is not a CODESYS task rule. |
| Identify and handle race conditions | Separate shared-state consistency from the application's conflict-resolution policy. |
| Design a smartwatch sensor subsystem | Explain acquisition and validation limits before attributing missed events solely to application logic. |
For your next PLC interview practice, change one premise: skip a detector call, hold the input through startup, or let an intent become stale. Predict the altered trace before running the model. Explain which evidence would make you revise the answer.
Open Implement a Multicore Overheat Prevention Controller, read its overview and state-update rules, and trace one tick by hand before consulting an available solution. Bring that discipline back to PLC code: identify the sample, preserve the intended instance history, assign one output decision owner, and state what remains unverified at the driver and hardware boundary.
Sources and Further Reading
- CODESYS 3 Development System: Task Configuration
- CODESYS 4: TaskConfiguration.json
- CODESYS Standard 3.5.18.0: R_TRIG
- ABB PLC documentation: R_TRIG state equation
- CODESYS 3 Development System: I/O Mapping and bus-cycle behavior
- CODESYS 3 Development System: Task Deployment
- CODESYS Static Analysis: SA0006, write access from several tasks
Research checked November 3, 2026. Trace values are from an original Python host model under stated assumptions. No PLC compilation, vendor simulation, hardware test, candidate experience or safety certification is claimed.
Comments (0)