A shutter with only UP and DOWN relays and no end switches follows a slider when the program integrates motor run time into a position estimate: 3.33 % per second on a 30 s shutter, clamped 0 to 100, with a deadband larger than one step driving the relays. Measure both travel times with a stopwatch first.
A shutter driven by one UP relay and one DOWN relay, with no end switches, can still follow a slider in CODESYS. The program estimates the position by counting how long each relay has been energized, then runs the relays until the estimate matches the slider value. On a 30 s shutter, one second of UP adds 100/30 = 3.33 % to the estimate and one second of DOWN subtracts the same. The estimate clamps at 0 for fully closed and 100 for fully open. A comparator with a deadband starts and stops the relays so the shutter never chatters around the target. The job is about fifty lines of Structured Text plus one slider element, and it stands on one field measurement: the full-travel time in each direction, timed with a stopwatch.
What you need before the code
- The full-travel time from closed to open, timed three times and averaged, in seconds.
- The full-travel time from open to closed, timed the same way. Two numbers, two constants: the motor works against the shutter weight in one direction, so the two times usually differ.
- Two relay outputs, jogged one at a time from the forced-output view, so you know which physical direction each output drives before any logic runs.
- A hardware interlock on the relays: the contacts of each relay wired in series with the other relay's coil, or a reversing contactor pair with a mechanical interlock.
- A cyclic task with a fixed interval. 100 ms is a workable start, and the same number goes into the program as rCycle.
How run time becomes a position
Fix the convention first: 0 is fully closed, 100 is fully open, a higher slider value energizes UP and a lower one energizes DOWN. One relay runs at a time.
With no encoder and no end switches, the run time of the motor is the only measurement you have. Each scan of the task adds one step to the estimate while UP runs and subtracts one step while DOWN runs, in percent:
step = 100 / travel time x cycle time
Clamp the result to 0 and 100. Two properties follow, and both shape the rest of the design:
- The ends are exact. When the estimate clamps at 0 or 100 and the motor has actually seated on its mechanical stop, every error accumulated earlier is discarded. That clamp is the recalibration, so the program must give the motor time to reach the stop.
- The middle drifts. Each partial move adds timing error from relay pull-in, motor start and stop, and scan quantization. Nothing removes it except a run to one of the ends.
Recommendation: build the run-time estimator whenever the shutter has no end switches and you can time a full travel with a stopwatch. When the shutter must hold an exact mid position after weeks of partial moves, fit end switches or a position sensor instead, because this estimate only re-syncs at the ends.
Worked example: a 30 s shutter in a 100 ms task
Inputs:
- Full travel, closed to open: 30 s, from three stopwatch runs averaged. Put your own number in rTravelUp.
- Task interval: 100 ms, set when the cyclic task is created. The program carries it as rCycle = 0.1 s.
- Test move: 20 s of UP from fully closed.
The rule, in words: each scan adds 100 divided by the travel time, multiplied by the cycle time, all in percent.
The arithmetic:
- Per second of UP: 100 / 30 = 3.33 %.
- Per scan: (100 / 30) x 0.1 = 0.33 %.
- The 20 s test move: 20 x (100 / 30) = 66.7 %, so the estimate reads 66.7.
- Deadband: 1.5 x 0.33 % = 0.5 %, rounded up to 1.0 % in rDeadband.
- Overrun: 30 / 10 = 3 s in tOverrun.
Decision: with rDeadband at 1.0 %, a slider change smaller than 1 % starts no relay, and the usable resolution of the whole scheme is the 0.33 % step. Keep the deadband above 1.5 steps and the chatter stays out. At or below one step, the estimate can jump clean across the slider value in a single scan and start the opposite relay. If 1 % is too coarse, shorten the task interval: at 20 ms the step is 0.066 % and a 0.1 % deadband holds.
Build the program in Structured Text
Structured Text is the IEC 61131-3 text language, and the whole control fits in one POU.
- Create the program POU and declare the variables below. rPos is RETAIN so the estimate survives a power cycle.
- Place a call of the POU in a cyclic task with a fixed interval, 100 ms to start, and enter the same interval in rCycle. A free-running task makes the step size wrong, and the estimate drifts by the mismatch between the two numbers.
- Integrate: while xUp is on, add a step; while xDown is on, subtract one; clamp to 0 and 100.
- Start logic: with both relays off and the reversal delay elapsed, start a relay only when the slider sits outside the deadband.
- Stop logic: a mid-range move stops when the estimate reaches the slider. A target within one deadband of an end is treated as that end and runs onto the mechanical stop for tOverrun.
- Close with the software interlock, then wire the hardware interlock from the first section.
PROGRAM PRG_Shutter
VAR
rSlider : REAL; // 0 closed to 100 open, set by the slider
xUp : BOOL; // UP relay output
xDown : BOOL; // DOWN relay output
rTravelUp : REAL := 30.0; // measured seconds, closed to open
rTravelDown : REAL := 30.0; // measured seconds, open to closed
rCycle : REAL := 0.1; // seconds, equal to the task interval
rDeadband : REAL := 1.0; // percent, at least 1.5 x one step
tOverrun : TIME := T#3S; // one tenth of the measured travel
tRevDelay : TIME := T#0S; // off-time before a reversal, from the motor data
tonEndUp : TON;
tonEndDown : TON;
tonRev : TON;
END_VAR
VAR RETAIN
rPos : REAL := 0.0; // the estimate
END_VAR
IF xUp THEN
rPos := rPos + 100.0 / rTravelUp * rCycle;
ELSIF xDown THEN
rPos := rPos - 100.0 / rTravelDown * rCycle;
END_IF
rPos := LIMIT(0.0, rPos, 100.0);
tonRev(IN := NOT xUp AND NOT xDown, PT := tRevDelay);
IF NOT xUp AND NOT xDown AND tonRev.Q THEN
IF rSlider - rPos > rDeadband THEN
xUp := TRUE;
ELSIF rPos - rSlider > rDeadband THEN
xDown := TRUE;
END_IF
END_IF
tonEndUp(IN := xUp AND rPos >= 100.0, PT := tOverrun);
tonEndDown(IN := xDown AND rPos <= 0.0, PT := tOverrun);
IF xUp THEN
IF rSlider >= 100.0 - rDeadband THEN
IF tonEndUp.Q THEN
xUp := FALSE;
END_IF
ELSIF rPos >= rSlider THEN
xUp := FALSE;
END_IF
END_IF
IF xDown THEN
IF rSlider <= rDeadband THEN
IF tonEndDown.Q THEN
xDown := FALSE;
END_IF
ELSIF rPos <= rSlider THEN
xDown := FALSE;
END_IF
END_IF
IF xUp AND xDown THEN
xUp := FALSE; // the hardware interlock is the real protection
xDown := FALSE;
END_IF
Both end timers are TON on-delay instances; where a timer instance belongs, local or shared, is the question in local timer versus shared timer in control software.
Keep the ends and the reversal safe
Energizing UP and DOWN together shorts the motor through both contact sets, so back the software rule with hardware. Break the coil of each relay with the opposite relay's contacts, or use a reversing contactor pair with a mechanical interlock. Keep that wiring even though the program never asks for both.
The reversal delay tRevDelay holds both relays off after a stop before the opposite one may pull in, giving the motor time to come to rest. Take the value from the motor data sheet or from watching one direction change, and set it before the first automatic reversal.
The overrun tOverrun is what makes the ends trustworthy. Without it, a deadband of 1 % means a slider at 99.5 is treated as full open while the shutter stops half a slat short, and the error stays in the estimate. Three seconds on a 30 s shutter seats the end and clears the accumulated error.
Bind the slider in the visualization
- Add a slider element with its variable set to PRG_Shutter.rSlider and its range 0 to 100.
- Add a bar or numeric display bound to PRG_Shutter.rPos, so the operator sees the estimate next to the setpoint.
- Add two buttons that write 100.0 and 0.0 to rSlider. Full open and full close become one-touch commands, and each one is a recalibration.
- Leave rPos to the program. The slider writes the setpoint only.
Commission against the stopwatch
- Jog both outputs from the forced-output view and confirm the direction each one drives. Reversed outputs make every symptom below look like a logic bug.
- Close the shutter fully. Time the run to fully open three times, average, and enter rTravelUp.
- Open the shutter fully. Time the run to fully closed three times, average, and enter rTravelDown.
- Enter the task interval in rCycle, to the millisecond.
- Set rDeadband to 1.5 x one step, rounded up; 1.0 % for the 100 ms, 30 s case above.
- Set tOverrun to one tenth of the measured travel.
- Set tRevDelay from the motor data sheet, then watch one automatic reversal.
How to check it worked
- Watch rPos online during a full open. It rises linearly and reaches 100 within the measured travel time; the relay drops tOverrun later. If rPos never moves, settle whether the task is executing before touching the logic: check if a CODESYS runtime is running or stopped.
- Chatter test: set the slider to 50, let the move finish, then nudge the slider by 1 %. Both relays stay off until the difference passes rDeadband.
- Interlock test: force both outputs on from the watch list. The hardware interlock keeps one relay from pulling in.
- Drift test: run ten partial moves, 25, 75, 40, 90, 10 and similar, then command 0. The shutter should seat on the mechanical end within tOverrun of rPos reaching 0; a visible gap before seating is accumulated drift.
- Recalibration test: after the drift test, run a full close, then command 50. It lands on the same physical mark as the first 50 taken from closed.
When it misbehaves, match the symptom
| What you see | Likely cause | Fix |
|---|---|---|
| Chatters up and down around the target | Deadband at or below one step | Raise rDeadband above 1.5 x one step |
| Every move lands off by the same percentage | rCycle and the task interval disagree | Enter the interval to the millisecond |
| Stops short or long in one direction only | One travel constant used for both directions | Measure both, load rTravelUp and rTravelDown |
| Accuracy fades over many partial moves | Drift between the ends, no feedback to remove it | Run to a full end, or press the full-open button |
| Slider reads 100 with the shutter not open | Estimate clamped before the motor reached the stop | Raise tOverrun toward one tenth of travel |
Questions
What happens to the position value when the controller loses power?
Declare rPos as a RETAIN variable and the estimate survives the outage at its last value. If the shutter could have moved while the power was off, by hand or from a local pushbutton, the saved value is stale: command a full close on power-up and let the overrun seat it on the mechanical end before trusting the slider again.
The error grows the further the shutter moves in one direction. What is that?
One travel constant is wrong, and the error grows with the distance moved in that direction. Measure both directions and load rTravelUp and rTravelDown separately. The motor works against the shutter weight in one direction, so unequal times are normal; a single constant shared by both directions produces the fixed-percentage offset you are seeing.
The shutter stops past the slider value every time it moves in one direction. How do I correct it?
The motor coasts after the relay drops. Lengthen that direction's travel constant by the observed error as a percentage, or stop the relay one coast early with an offset subtracted from the target, then retest with the stopwatch. Correct one direction at a time; fixing both at once lets the two corrections hide each other.
Can the same code run several shutters?
Yes. Turn the POU into a function block and instantiate it once per shutter. Each instance carries its own rPos, its own travel constants and its own two outputs, and the visualization binds one slider to each instance's rSlider. Commission every shutter against its own stopwatch runs; the constants belong to the individual motor.
Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.