Codesys Retentive Timer? (RTO/RTON)

Another approach would be to calibrate the mean scan time, and simply count scans (when IN is 1) and scale them to time based on the calibration. It will be a little noisy, but it might be accurate enough, and it would certainly be simpler.


If the scan times are short (a few ms), then counting scans might be as accurate as the proposed function block.
 
Last edited:
I think this does essentially the same thing i.e. back off any accumulated timing done by the TON from the PT, whenever IN is zero.

(* Usage: *)
(* rtovar : RTOpt; *)
(* rtoval(IN := IN *)
(* ,PT := RTOpt *)
(* ,PTin := PTremaining *)
(* ,PTout := PTremaining *)
(* ,ETin := ETsaved *)
(* ,ETout := ETsaved *)
(* ,Qin := RTOq *)
(* ,Qout := RTOq *)
(* ,TT := RTOtt *)
(* ); *)
FUNCTION_BLOCK RTO
VAR_INPUT
IN : BOOL; // Run (Hold)
PT : TIME; // RTO full Preset Time
PTin : TIME; // Remaining Preset Time at last IN rising edge
ETin : TIME; // Elapsed Time at end of previous block
Qin : BOOL; // Q state at end of previous block
RST : BOOL; // Reset
END_VAR
VAR_OUTPUT
PTout : TIME; // Preset time on exit from this block
ETout : TIME; // Elapsed Time on exit from this block
Qout : BOOL; // RTO Done
TT : BOOL; // RTO Timer Running
END_VAR
VAR
timer : TIMER;
END_VAR;

IF RST THEN // Note 1
PTout := PT;
ETin := 0;

ELSE // Note 2
PTout := PTin;
END_IF;

IF (RST OR NOT IN) AND NOT Qin THEN // Note 3
PTout := PTout - ETin;
END_IF

timer(IN := IN AND NOT RST // Note 4
,PT := PTout
,ET := ETout
,Q := Qout
);

TT := IN AND NOT (RST OR Qout); // Note 5

END_BLOCK

// Note 1: On RTO reset: use full RTO Preset Time; clear ETin

// Note 2: Otherwise copy PTin to PTout

// Note 3: If TON timer will not run on this scan, then ETin is the
// Elapsed Time the TON timer ran up until this event, so
// subtract ET from PTout, so PTout will be the remaining
// RTO time when the TON timer next runs on a later scan.
// Note that the TON timer will be reset on this scan, so
// ETout will be assigned to 0, and ETin will be 0 on all
// subsequent scans as long as the TON timer will not run
// (i.e. as long as RST is 1 or IN is 0)

// Note 4: A) Run the TON timer if BOTH [IN is 1] AND [RST is 0], or
// B) Reset the TON timer if EITHER [IN is 0] OR [RST is 0]
// B.1) N.B. ETout will be 0 if the TON timer is reset

// Note 5: Calculate Timer is Timing boolean

 
Here's my implementation from my TwinCAT library. Pretty similar to drbitboy's.

Code:
FUNCTION_BLOCK AB_RTO
VAR_INPUT
    EN: BOOL; // timer enable
    PRE: TIME; // preset time
    RES: BOOL; // reset signal
END_VAR
VAR_OUTPUT
    DN: BOOL; // timer done
    ACC: TIME; // accumulated time
    TT: BOOL; // timer timing
END_VAR
VAR
    Timer: TON;
    TempAcc: TIME;
END_VAR


ACC := TempAcc + Timer.ET;
DN := (ACC >= PRE);

IF NOT EN OR DN THEN
    TempAcc := TempAcc + Timer.ET;
END_IF

IF RES THEN
    ACC := T#0MS;
    TempAcc := T#0MS;
END_IF

Timer(IN:=EN AND NOT DN AND NOT RES, PT:=T#2147483647MS);

TT := Timer.IN;
 
If RST is 1, then [ACTION Reset] executes, and IN will be 0, so the [AND NOT RST] is not necessary, because IN can only be 1 at that point if RST is 0, so [NOT RST] will always be 1 if IN is 1 at that point.

If [Remaining] is an output, how can it have a value in the FUNCTION_BLOCK before that function block assigns it a value? Where does the value come from?

It is initialized to 0 and first assigned when IN goes true farther down the code.

This could be replaced with
Code:
// Timer Done Rising Edge
IF timer.Q and NOT Q THEN
     ET := timer.PT + previousElapsed;
     timer.PT := T#0MS;
END_IF
Q := timer.Q;

If IN_F.Q is 1, then previousElapsed will be incremented by timer.ET, and then it will be incremented by timer.ET again to write the value of ET. Is that double-dipping?

Again, comments would be useful to understand the intent.

Also, if scan time is short, i.e. on the order of a few ms, the internals of the timer might be keeping track of fractional ms (more resolution than can be carried by the value timer.ET as integer ms), and those fractional ms might get lost in this approach that re-assigns a new integer value for timer.PT, resulting in a loss of accuracy. That said, any approach that uses whole ms might lose those fractional ms (if they exist),


Ok, using your advice and also me going back through it again and wondering WTF :unsure: on some of my own code. This is what I have now.

Code:
FUNCTION_BLOCK RTO
VAR_INPUT
    IN : BOOL; // Run (Hold) Stops Timing when released
    PT : TIME; // Run Timer Duration/Setpoint
    RST : BOOL := TRUE; // Reset Self Resetting (Holding is allowed)
END_VAR
VAR_OUTPUT
    Q : BOOL; // Done, stays true until timer is reset, regardless of state of IN
    ET : TIME; // Accumulated time that the timer has been running
    TT : BOOL; // Timer Running, goes False when Q is True
    Remaining : TIME; // Time Remaining
END_VAR
VAR
    hasBeenStart : BOOL; //** Timer is not in default state
    previousElapsed : TIME; //** Elapsed time when IN last went False
    IN_R : R_TRIG; // IN Rising Edge
    IN_F : F_TRIG; // IN Falling Edge
    timer : TON;
END_VAR
Code:
FUNCTION_BLOCK RTO
VAR_INPUT
    IN : BOOL; // Run (Hold) Stops Timing when released
    PT : TIME; // Run Timer Duration/Setpoint
    RST : BOOL := TRUE; // Reset Self Resetting (Holding is allowed)
END_VAR
VAR_OUTPUT
    Q : BOOL; // Done, stays true until timer is reset, regardless of state of IN
    ET : TIME; // Accumulated time that the timer has been running
    TT : BOOL; // Timer Running, goes False when Q is True
    Remaining : TIME; // Time Remaining
END_VAR
VAR
    hasBeenStart : BOOL; //** Timer is not in default state
    previousElapsed : TIME; //** Elapsed time when IN last went False
    IN_R : R_TRIG; // IN Rising Edge
    IN_F : F_TRIG; // IN Falling Edge
    timer : TON;
END_VAR
//** Changed from action to Method

Code:
METHOD PRIVATE Reset : BOOL
VAR_INPUT
END_VAR


//** Body

IN := FALSE;
previousElapsed := T#0MS;
Remaining := PT;
ET := T#0MS;
Q := FALSE;
hasBeenStart := FALSE;
 
Last edited:
@TheColonel26: you have copied your variable declarations twice and missed out your actual code.
Note a TIME can only hold 49days, then it rolls over. You can use LTIME and Standard64.LTON. You get nanosecond resolution and something like 500 years before it rolls over.
 
It is initialized to 0 and first assigned when IN goes true farther down the code.


[Remaining] is a VAR_OUTPUT to the function. As such, at the beginning of the function call, it has no defined value (other than perhaps being initialized to 0?). Even if it writes a value to its external argument in the caller, it will be un-initialized (or 0) at the start of the function on the next scan.

The caller's external variable, which the function-internal [Remaining] value is written to when the function exits, is not, I am pretty sure*, written to the function-internal value of [Remaining] to initialize it when the function starts.

I think* the only way to do that would be to make the same external variable both a VAR_INPUT and a VAR_OUTPUT to the function, and then assign the input var to the output var at the start of the function.


* I might be wrong about that for Codesys, and a VAR_OUTPUT is actually both input and output, but I know functions do act the way I describe above (input-only and output-only) for some PLC brands; some also have a special VAR_IN/OUT type that does both.
 
Last edited:
* I might be wrong about that for Codesys, and a VAR_OUTPUT is actually both input and output, but I know functions do act the way I describe above (input-only and output-only) for some PLC brands; some also have a special VAR_IN/OUT type that does both.

For a FUNCTION_BLOCK the inputs and and outputs and vars hold their value between calls.
Inputs can be changed by the calling code, either before hand:
instance1.input1 := 6;
instance1();
or during the call
instance1(input1 := 6);
 
@TheColonel26: you have copied your variable declarations twice and missed out your actual code.
oops :oops:

Here is the main body code
Code:
IN_R(CLK := IN);
IN_F(CLK := IN);

IF RST THEN
    Reset();
    RST := FALSE; //Self Reset the Reset Bit (LOL)
END_IF

IF NOT hasBeenStart THEN
    timer.PT := PT;
    Remaining := PT;    
END_IF

IF IN_R.Q THEN
    hasBeenStart := TRUE;
    IF NOT Q THEN
        timer.PT := Remaining;
    END_IF
END_IF



timer.IN := IN;

timer();

//** Keep after Timer execution incase IN goes False and RST goes True at same time.
//** This ensures that timer.ET is 0 because timer was reset before executing this.
// When IN goes false, update and save the Elapsed time, to be used later
IF IN_F.Q THEN
    previousElapsed := timer.ET + previousElapsed;
    //Timer.PT := PT- Remaining;
END_IF

Q := timer.Q;
// Continously update the Elapsed Time output using the Timer ET + previous Elapsed time.
ET := timer.ET + previousElapsed;

IF timer.IN AND NOT Q THEN
    Remaining := Timer.PT - timer.ET;
END_IF

TT := IN AND NOT Q;


Note a TIME can only hold 49days, then it rolls over. You can use LTIME and Standard64.LTON. You get nanosecond resolution and something like 500 years before it rolls over.
True but I only need a couple of hours at most, most of the use will be a few minutes.
 

timer.IN := IN;

timer();

//** Keep after Timer execution incase IN goes False and RST goes True at same time.
//** This ensures that timer.ET is 0 because timer was reset before executing this.
// When IN goes false, update and save the Elapsed time, to be used later
IF IN_F.Q THEN
previousElapsed := timer.ET + previousElapsed;
//Timer.PT := PT- Remaining;
END_IF

huh. If IN_F.Q is 1, then IN is False, so timer.ET should be 0, no? If yes, i.e. if timer.ET is 0, then that IF-END_IF does nothing?
 
For a FUNCTION_BLOCK the inputs and and outputs and vars hold their value between calls.
Inputs can be changed by the calling code, either before hand:
instance1.input1 := 6;
instance1();
or during the call
instance1(input1 := 6);




I was more concerned about the outputs.


E.g. If I have this function:

FUNCTION_BLOCK BLAH
VAR_OUTPUT
out_arg : INT;
END_VAR

out_arg := out_arg + 1;

END_BLOCK


and I call it like this:
outer_out := 0;

BLAH(out_arg:=outer_out);


BLAH(out_arg:=outer_out);
Then what will outer_out be after the second call to BLAH? If the [out_arg] argument is both an output and an implicit input, then it will be 2. If not, it will be 1 (or 1 more than whatever it is initialized to inside BLAH).
 
[Remaining] is a VAR_OUTPUT to the function. As such, at the beginning of the function call, it has no defined value (other than perhaps being initialized to 0?). Even if it writes a value to its external argument in the caller, it will be un-initialized (or 0) at the start of the function on the next scan.

The caller's external variable, which the function-internal [Remaining] value is written to when the function exits, is not, I am pretty sure*, written to the function-internal value of [Remaining] to initialize it when the function starts.

I think* the only way to do that would be to make the same external variable both a VAR_INPUT and a VAR_OUTPUT to the function, and then assign the input var to the output var at the start of the function.


* I might be wrong about that for Codesys, and a VAR_OUTPUT is actually both input and output, but I know functions do act the way I describe above (input-only and output-only) for some PLC brands; some also have a special VAR_IN/OUT type that does both.
It is a Function Block not a function.

Inputs and outputs retain their values between calls, and you can even write back to the inputs from inside the FB, or write to the outputs from outside.

The only thing that is really different from them being a "Public" which doesn't exist on FBs, is that if you reference an Input in the Function Block execution call it will copy from the source to the Input, and if you reference an OUTPUT it will copy to the destination.

So if you do


Code:
dummyFB(TempIn := FALSE, TempOut := SomeVariable);
When it hits that line it will copy false to, TempIn, and it will copy TempOut to SomeVariable.

You can all do this

Code:
dummyFB.TempIn := false;

dummyFB(); 



SomeVariable := dummyDB.TempOut
In rockwell you would have to decide where each on of those variables is "Required" or not, you couldn't change the form that they are assigned for each instance, but in Codesys you can. Both of those forms are valid for the exact same FB defintion.


Further

Let say you have a FB with following code (pseudo code because I am lazy)


TempIn : INT; // In
TempOut : INT; // Out


Code:
dummyB_FB(){
TempIn := TempIn;


TempOut := TempIn; //** Edit Fixed oops



TempIn := 50;

}
Lets call it

Code:
InstOfDummyB_FB.TempIn : 25;


//** InstOfDummyB_FB.TempIn is = 25


InstOfDummyB_FB();

//** InstOfDummyB_FB.TempOut is = 25

//** InstOfDummyB_FB.TempIn is = 50

InstOfDummyB_FB.TempIn := 75

//** InstOfDummyB_FB.TempIn is = 75

InstOfDummyB_FB.TempOut := 100

//** InstOfDummyB_FB.TempOut is = 100


//** lets call the same instance again, normally you wouldn't but it's just for demo purposes

InstOfDummyB_FB();

//** InstOfDummyB_FB.TempIn is = 50

//** InstOfDummyB_FB.TempOut is = 75
EDIT:

Lets do another, but this time lets only Write to input once;

TempIn : INT; // In
TempOut : INT; // Out


Code:
dummyB_FB()
{
TempOut := TempIn;
}
Lets call it

Code:
//** init InstOfDummyC_FB.TempIn = 0


IF InstOfDummyC_FB.TempIn < 10 THEN
    InstOfDummyC_FB.TempIn : 25;
End If

InstOfDummyC_FB();


//** InstOfDummyC_FB.TempOut = 25
//** InstOfDummyC_FB.TempIn = 25


InstOfDummyC_FB();


//** InstOfDummyC_FB.TempOut = 25
//** InstOfDummyC_FB.TempIn = 25




next scan

Code:
//** InstOfDummyC_FB.TempIn = 25


IF InstOfDummyC_FB.TempIn < 10 THEN // ** Skipped
    InstOfDummyC_FB.TempIn : 25;
End If

InstOfDummyC_FB();


//** InstOfDummyC_FB.TempOut = 25
//** InstOfDummyC_FB.TempIn = 25


InstOfDummyC_FB();


//** InstOfDummyC_FB.TempOut = 25
//** InstOfDummyC_FB.TempIn = 25
 
Last edited:
Thanks!


So is there any difference between an argument in the VAR_INPUT block and an argument in the VAR_OUPUT block?
 
Last edited:
Working the other side i.e. ET, just four statements, plus one for TT. Untested.

(****************************************************************)
(* Usage: *)
(* VAR *)
(* rtovar : RTO; *)
(* END_VAR *)
(* rtovar(IN:=RTOin, PT:=RTOpt, RST:= RTOrst // Inputs *)
(* , Q:=RTOq, TT:=RTOtt); // Outputs *)
(****************************************************************)
FUNCTION_BLOCK RTO
VAR_INPUT
IN : BOOL; // Run (Advance the accumulated RTO ET)
PT : TIME; // RTO full Preset Time
RST : BOOL; // Reset
END_VAR
VAR_OUTPUT
Q : BOOL; // RTO Done
TT : BOOL; // RTO Timer Running
END_VAR
VAR
timer : TIMER;
ET : TIME; // Accumulated RTO ET from previous scan
END_VAR;

// Reset will assign 0 to accumulated RTO ET
IF RST THEN ET:=0; END_IF;

// Run TON if RTO is not being reset
timer(IN:=NOT RST, PT:=PT, Q:=Q);

// If RTO is running, then advance accumulated ET
IF IN AND NOT RST THEN ET:=timer.ET; END_IF;

// If RTO is running, this does nothing (ET already equals timer.ET)
// If RTO is not running, then this keeps accumulated ET in timer.ET
timer.ET = ET;

TT := IN AND NOT (RST OR Q);

END_BLOCK


 

Similar Topics

Been a long time since I've posted, but here we go. Started a new role in august and progressively learning about Codesys programming in...
Replies
2
Views
1,157
Hello, I am using a Hitachi Micro EHV+ for a small project, and I wanted to have a Web visu, done with Codesys V3.5 SP13 Patch 2. I test the...
Replies
6
Views
296
Hello, I have a requirement to manage the text alignment dynamically. So, for example: 1. English Texts should be displayed from Left in...
Replies
0
Views
90
Hello, I am new to Codesys, and am trying to learn about it for a project we're developing. I've got a couple questions, but first a little...
Replies
1
Views
160
Hi everyone, as this is my first experience with Rockwell Software i would like to know what's the best way to make Enumerations?
Replies
10
Views
509
Back
Top Bottom