Controlling a knife gate valve in structured text programming

I want to control a valve where I have a solenoid (when the HIGH valve opens and when the LOW valve closes).
It will control a few states, for example
-Opened
-Closed,
-Opening,
-Closing and
-ErrorState
There could be CommandOpen and CommandClose RPC. How can I implement it in structured text? It should have an enum that indicates the states and it will change the states when opens or closes. Thank you
 
Good question, and it is worth deciding a few things before writing any code — the state list is the easy part.

First, a note on enums: most IEC 61131-3 ST compilers do not give you real enums (TwinCAT and SCL in some versions do, classic ST generally does not). The portable way is named INT constants, and the discipline that matters is that no bare number ever appears in the CASE — always the constant name.

Second, the design decisions that will bite you if you leave them implicit:

1. Single solenoid means energise = open, de-energise = close. So "closed" is not the absence of a command, it is a command. Decide what the output does in ErrorState and write it down as a decision. If de-energise is your safe state, say so in a comment — otherwise the valve position in a fault is an accident, not a design.

2. Every transition needs a timeout. A stuck valve must be detected, not waited for. A timeout that lands in ErrorState is the whole point of having ErrorState.

3. Recovery from ErrorState must be explicit (require a Reset command). Do not auto-retry. If you auto-retry, a marginal valve starts cycling and you have converted a fault into a pulsing fault, which is much harder to diagnose.

4. Edge-detect the commands. A held HMI button is not a continuous command. Use a rising-edge on CommandOpen / CommandClose, otherwise the state machine keeps restarting transitions.

5. Decide what happens if the opposite command arrives mid-transition, and write it in a comment. My default is: ignore it, do not abort the current transition. Aborting a valve mid-stroke because an operator changed their mind is how you get mechanical damage.

6. Inconsistent feedback (both limits true, or neither true after the transition should have completed) is a fault, not a state. Only sample the limits at the end of a transition timeout window — not continuously — or transients will trip you.

Here is the shape I would start from (plain IEC ST, adjust names to your platform):

CONST
ST_CLOSED : INT := 0;
ST_OPENING : INT := 1;
ST_OPENED : INT := 2;
ST_CLOSING : INT := 3;
ST_ERROR : INT := 4;
T_STROKE : TIME := T#20S; (* per your valve data sheet *)
END_CONST

(* edge detect *)
CmdOpen_R := CommandOpen AND NOT CmdOpen_P;
CmdClose_R := CommandClose AND NOT CmdClose_P;
CmdOpen_P := CommandOpen;
CmdClose_P := CommandClose;

CASE State OF
ST_CLOSED:
Solenoid := FALSE; (* de-energised = closed *)
IF CmdOpen_R THEN
State := ST_OPENING;
StrokeTimer(IN := TRUE);
END_IF;

ST_OPENING:
Solenoid := TRUE;
IF LimitOpen THEN
StrokeTimer(IN := FALSE);
State := ST_OPENED;
ELSIF StrokeTimer.Q THEN (* timeout *)
StrokeTimer(IN := FALSE);
State := ST_ERROR;
END_IF;

ST_OPENED:
Solenoid := TRUE;
IF CmdClose_R THEN
State := ST_CLOSING;
StrokeTimer(IN := TRUE);
END_IF;

ST_CLOSING:
Solenoid := FALSE;
IF LimitClosed THEN
StrokeTimer(IN := FALSE);
State := ST_CLOSED;
ELSIF StrokeTimer.Q THEN
StrokeTimer(IN := FALSE);
State := ST_ERROR;
END_IF;

ST_ERROR:
Solenoid := FALSE; (* DECISION: fail closed *)
IF ResetCmd_R THEN
State := ST_CLOSED;
END_IF;
END_CASE;

Three things this deliberately does not do, and you should decide consciously:

- It does not debounce the limits. Mechanical limit switches bounce; add a short TON (100-200 ms) if yours do.
- It does not treat "no limit true and not moving" as a fault. If your process needs that, add a supervision timer that runs whenever State is CLOSED or OPENED.
- It does not queue commands. If you need a queued close that happens automatically after an open completes, that is a real requirement and should be visible, not implicit.

If you are on Siemens SCL, the code is essentially the same; use a timer instance per transition (or one shared instance, but then reset it on every state entry — a shared timer that is not reset is the most common bug in this pattern).

One last thing worth adding to your requirement list: what should happen on power-up. Read the actual limit switches and enter the matching state, rather than assuming closed. Assuming closed is how a valve that is actually open gets driven open again on the first command.
 
Top