Periodic tasks with TimerSwitch
Every now and then in automation systems there will be a problem where something needs to happen at a specified time and for some predetermined duration. To make things more interesting, that something should happen only on certain weekday. Like triggering the mixing of the irrigation solution every workday, at 17:00 that runs for 15 minutes, except on weekends.
Obviously my first idea on solving this was using the TON
, TOFF
and TP
function blocks (turn on after a period, turn off after a period and pulse timer). Until I realized that having more than one bit to drive and more than one period to take care of makes this approach quite unfeasible. So after digging through the library documentation of Codesys I found a really nice construct TimerSwitch
.
On the first look, it looks really promising. It can drive the output of DWORD (32) bits, supports scheduling using the idea of first on time and last on time, setting the days (including weekdays/weekends), can accommodate the time-zone switching and so forth. But getting it to work actually has been quite a ride, so I thought I'll document the nuances up here, for the reference.
TimerSwitch
aslSchedule
input parameter
So first key thing is that contrary to the documentation IT IS NOT pointer to anything. The input is actually ARRAY [*] OF UTIL.Schedule
. Yes. This is pretty wild, because the documentation clearly states it should be pointer to schedule. So fear not and make it an array and pass it directly to the function block.
A sample of that array can be seen here, where a declaration of 32 schedule items is made and first 2 are initialised.
// Ofc. it would be nice to have ST syntax colouring here as well :P
(* Create an array with 32 schedule items, e.g. for every bit
and initialize the first two items there *)
aslMySwitchingSchedule: ARRAY[0..31] OF UTIL.Schedule :=
[(usiSwitch := 1,
todFirstOn := TOD#08:00:00, todLastOn := TOD#09:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY),
(usiSwitch := 1,
todFirstOn := TOD#20:00:00, todLastOn := TOD#21:00:00,
byDayFlags := DAY_FLAGS.WEEKEND)];
I have not tested out, if the array has any fixed maximum size limits, so far the schedule with automatically created 512 items seemed to work correctly, but rather not make assumption on this. The documentation has no clear information on this neither.
Continuous array with usiSwitch > 0
Second very key thing is that the schedule items have to be on sequential indices. To make it more clear and understandable let me make a quick example, which would not work as expected (at least, as I was expecting).
aslMySwitchingSchedule: ARRAY [0..2] OF Util.Schedule :=
[(usiSwitch := 1,
todFirstOn := TOD#20:00:00, todLastOn := TOD#21:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY),
(* NBNB! THE FOLLOWING ITEM BREAKS WHOLE SCHEDULE
AND THE ITEM(S) AFTER WILL NOT BE EXECUTED *)
(usiSwitch := 0,
todFirstOn := TOD#00:00:00, todLastOn := TOD#00:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY),
(usiSwitch := 4,
todFirstOn := TOD#20:00:00, todLastOn := TOD#21:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY)];
Based on this schedule the educated assumption would be that between 20:00
and 21:00
the bits set in the dwSwitches
output are 0
and 4
(B#1001
or making the value H#09
). Well it turns out that my educated guess was not really that educated. It seems that the internal logic uses the fact that usiSwitch = 0
and after that does not process any following array items.
In general this means that one can't have any kind of fancy logic where the certain indexes are allocated/modified so that the usiSwitch
might be eventually 0. E.g. you update some parts of the array to contain structures where the usiSwitch
for any reason is set to 0. This will render the whole schedule after this item as non-triggered.
No Rollovers
Another thing which I ran into and ... probably depends on your viewpoint ... didn't make any sense to me (might make to you). This is the case where the schedule kicks off at some point before midnight and runs into some other point during next day. To get it working two schedule items are needed (ordering is irrelevant). Again an example
(* Will always output 0 in the dwSwitches *)
aslMySwitchingSchedule: ARRAY [0..1] OF Util.Schedule :=
[(usiSwitch := 1,
todFirstOn := TOD#23:00:00, todLastOn := TOD#01:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY)];
And what didn't make any sense to me as well, was that if there is an idea that the rollover should not happen, then at least it could run until T#23:59:59.999
. But it won't. The correct way to get it working would be to create a following array structure which runs until midnight and then runs from midnight to the specified time.
(* Will output 1 in the dwSwitches between 23:00 and 01:00 *)
aslMySwitchingSchedule: ARRAY [0..1] OF Util.Schedule :=
[(usiSwitch := 1,
todFirstOn := TOD#23:00:00, todLastOn := TOD#23:59:59.999,
byDayFlags := DAY_FLAGS.EVERY_DAY),
(usiSwitch := 1,
todFirstOn := TOD#00:00:00, todLastOn := TOD#01:00:00,
byDayFlags := DAY_FLAGS.EVERY_DAY)];
In general, if there is any kind of automation involved in the schedule calculations, this makes it a bit annoying. In general there is a need for additional check that the duration doesn't roll over to the next day, and if it does, additional schedule item needs to be created.
Time and duration calculations
The final a most annoying thing which triggered me to create this whole blog are the time and duration calculations. Codesys help/reference states that:
The data types
DATE
,DATE_AND_TIME
(DT
), andTIME_OF_DAY
(TOD
) are handled internally like aDWORD
(32-bit value)....
Data Type Lower Limit Upper Limit Memory Resolution TIME_OF_DAY
TIME_OF_DAY#0:0:0
TIME_OF_DAY#23:59:59.999
32-bit Milliseconds
So obviously if you need to translate a business logic into schedule item (e.g. based on some criteria), which says to start at 20:00 and run for 6 hours, then a sensible calculation that one would do is to take TOD#20:00:00 + T#06:00:00 and according to all Codesys UI logic one will get TOD#04:00:00 as result.
But it clearly did not work. So even if I created a new schedule item and added it to array, it resulted in trigger not turning off at all. Which was a bit weird, as it clearly should've had done so. After a small investigation and using TOD_TO_DWORD()
it became clear that although the TOD field is showing the correct TOD, the actual DWORD value used, is not as expected. And honestly at this point, I will not make any assumptions as how the whole system interprets this value.
Again an example and how I solved it, keeping in mind that I'm an average developer.
aslMySchedule: ARRAY[0..1] OF UTIL.Schedule;
todStart: TIME_OF_DAY := TOD#20:00:00;
tDuration: TIME := T#6h;
todEnd: TIME_OF_DAY;
(* Codesys tooling shows (correctly|incorrectly)? that the result
is T#04:00:00, but no idea how this really is interpreted *)
todEnd := todStart + tDuration;
(* So to solve it, and get correct result I check if the
value is higher than TOD#23:59:59.999 and if it is then
subtract 24h from it, which seems to give the right result *)
todEnd := todStart + tDuration;
IF todEnd > TOD_TO_DWORD(TOD#23:59:59.999) THEN
todEnd := todEnd - TIME_TO_DWORD(T#24h);
END_IF