The SYS_TIME function in ABB PLC / codesys programming returns a DWORD indicating the number of milliseconds since the PLC was turned on. (or perhaps hard reset / other event? Cannot find documentation of this.)
The max size of a DWORD in Codesys is 232-1 = 4,294,967,295.
This means SYS_TIME overflows after only 49.7 days!
Can anyone confirm exactly what the SYS_TIME function returns after 49.7 days has elapsed? Does it integer overflow and start counting from zero again?
This has important ramifications for using SYS_TIME for functions such as warning how long it is since some event occurred. (e.g. a read of a remote device via modbus).
Assuming it is just an integer overflow and SYS_TIME resets to zero, then the programmer can deal with this by e.g. resetting the variable they are using to record the last known event time:
(* Assuming now, last_event_time are suitably declared DWORDs *)
now := SYS_TIME(TRUE);
IF last_event_time > now THEN
last_event_time := 0;
END_IF
(* continue, performing check of how long since last event occurred etc.... *)
I'm hoping there is something I have missed that offers an alternative approach.
However - This is a GOTCHA that could trip up a PLC programmer who hadn't thought of this, causing an apparently fully functioning PLC program that has been tested extensively to fail after 49 days of use in the field.
Would be very helpful if there was an alternative to SYS_TIME that returns an LWORD, good for 5 billion years of uninterrupted service :-)
NB - I believe this function may be specific to the ABB AC500 range of PLCs, rather than a standard Codesys function, so this question is mostly directed at ABB & ABB PLC programmers.
There are few options, but I would use either one of the following
Read system date and time and keep it updated. You will get 1 second resolution when using DT. It's easy to do comparison between DTs when you convert them to DWORD first (epoch time). See my old answer for Codesys time update. Note that if you calculate something like DT1-DT2, you will get a result of TIME -> Same overflow problem is possible. That's why DT_TO_DWORD would be good idea.
Make your own time. Your PLC has a cycle time, that should always be precise the same. Use it for you calculation.
This is a simple example with different data types.
Note that it's also possible to read cycle time using some Codesys library if required, not sure which one though. See for example this.
PROGRAM PRG_Time
VAR CONSTANT
TASK_CYCLE_TIME_MS : WORD := 10; //Update this!
END_VAR
VAR_OUTPUT
Total : LREAL;
TotalMilliseconds : LWORD;
TotalSeconds : DWORD;
Milliseconds : WORD;
END_VAR
Total := Total + TASK_CYCLE_TIME_MS / 1000.0;
TotalMilliseconds := TotalMilliseconds + TASK_CYCLE_TIME_MS;
Milliseconds := Milliseconds + TASK_CYCLE_TIME_MS;
//Calculate seconds
IF Milliseconds >= 1000 THEN
TotalSeconds := TotalSeconds + 1;
Milliseconds := Milliseconds - 1000;
END_IF
The LREAL is precise enough for milliseconds. So basically here is a SYS_TIME as LWORD (the TotalMilliseconds)
Looking further into this, I have confirmed two things:
SYS_TIME is definitely an ABB AC500 specific function, included in library SysInt_AC500_V10.lib.
Quote from the online help for ABB AC500: "An overflow is reached after 49 days. After this, the counter restarts at 0.". So that answers the part about the behaviour of the function.
The second part of my question (what is the best way of handling this?) remains open...
Related
I have a program in TwinCat where 5 integer variables are being updated every 10 seconds to represent the state of 5 pumps. I want to save these values that are being generated into either a textfile or CSV file that I can later extract from the PLC. Code below:
IF toninPumpPulse.Q THEN
rinPump1RPM := (inPump1Count/6)*6; //Pulse sample for 10 seconds. 6 pulses = 1 round.
inPump1Count := 0; //Reset counter for next 10 second sample
rinPump2RPM := (inPump2Count/6)*6; //Pulse sample for 10 seconds. 6 pulses = 1 round.
inPump2Count := 0; //Reset counter for next 10 second sample
rinPump3RPM := (inPump3Count/6)*6; //Pulse sample for 10 seconds. 6 pulses = 1 round.
inPump3Count := 0; //Reset counter for next 10 second sample
rinPump4RPM := (inPump4Count/6)*6; //Pulse sample for 10 seconds. 6 pulses = 1 round.
inPump4Count := 0; //Reset counter for next 10 second sample
rinPump5RPM := (inPump5Count/6)*6; //Pulse sample for 10 seconds. 6 pulses = 1 round.
inPump5Count := 0; //Reset counter for next 10 second sample
I am looking to have a new CSV file be created and then populated with the variable values. I am pretty inexperienced in TwinCat and reading the Beckhoff website is not exactly helping as well.
You want to use a combination of several function blocks:
FB_FileOpen
FB_FilePuts
FB_FileClose
All documentation you need + example code is already available on the Beckhoff infosys for this purpose:
https://infosys.beckhoff.com/english.php?content=../content/1033/tcplclibutilities/html/tcplclibutilities_csv_sample.htm&id=
Also see this on how to write files in general:
TwinCAT 3: Write to File
The open source logging library TcLog may help you. It provides extended logging functionalities to the file system.
I wrote a blog post introducing it, you can also download a precompiled library there.
This is how you would use it in your case:
First, define a static logger instance of type TcLogCore that contains the configuration:
VAR
CoreLogger : TcLogCore;
END_VAR
CoreLogger
.MinimumLevel(E_LogLevel.Warning)
.WriteToFile('c:\logs\', 'pumpValues.csv')
.RunLogger();
Next, define a logger TcLog that actually logs the messages:
VAR
Logger : TcLog;
END_VAR
You can use that logger to generate log messages like this:
Logger
.OnCondition(toninPumpPulse.Q)
.AppendAny(rinPump1RPM)
.AppendString(',')
.AppendAny(rinPump2RPM)
.AppendString(',')
.AppendAny(rinPump3RPM)
.AppendString(',')
.AppendAny(rinPump4RPM)
.AppendString(',')
.AppendAny(rinPump5RPM)
.ToCustomFormat('');
Make sure, toninPumpPulse.Q is only true for one cycle, otherwise you will generate a load of messages. Have a look at logging on rising/falling edges as well.
Since the logger is implemented as Singleton, you can use it anywhere in your program and it will automatically use the configuration of TcLogCore.
There are more options in the library, like setting a rolling interval for the log files or automatic deletion of old files that may be useful to you.
Programming a raspberry pi with codesys, using mostly ladder, basically I need to write all data that is currently in a couple arrays to a csv file at midnight, so i'd like to be able to use a dt value as a trigger. I can't figure out how to use that value in ladder, however. I can display the local time on visualizer, but if i wanted something like "if localTime=#value" then coil 'Write' turns on, where is the actual variable for system time?
As far as I know, you need to read the clock from local system using function blocks, for example GetDateAndTime from CAA DTUtil Extern Library. Then you need to keep it up-to-date by using a function block, for example RTC from Standard libary
The following reads the system local time and then updates it with a RTC function block. Works at least on Windows, couldn't test with Raspberry. Please note that if the local time changes for some reason, this won't update it again. So you need to run the GetDateAndTime call every now and then, for example.
First, a program that updates and provides the local time:
PROGRAM PRG_UpdateSystemTime
VAR_OUTPUT
SystemDateTime : DT;
END_VAR
VAR
ReadLocalTime : DTU.GetDateAndTime; //Reads local time from system
RtcBlock : RTC; //Real-time clock - updates the previously received local time
END_VAR
//NOTE: Output is UTC time
//The block that reads local time. NOTE: Error handling is missing
ReadLocalTime(xExecute:= TRUE);
//Running real-time clock
RtcBlock(
EN := ReadLocalTime.xDone AND NOT ReadLocalTime.xError,
PDT := ReadLocalTime.dtDateAndTime,
CDT => SystemDateTime
);
And then one example for ladder. I think there are millions of ways. Note that the "DoSomething" will be TRUE for the whole second, so you should probably use rising edge detection.
I am trying to have a loop where it will start at 100 and drop until it hits to a point where the while condition no longer holds true.
I started with
While Solar_Power_House_W_Solar_PER <= OneHundred AND BatChargePercent < OneHundred DO
State_Dis_Charge := false
FOR PLC_SetLoopChargeValue:= 100 TO 0 By -1 DO
ConvertoReal := INT_TO_LREAL(PLC_SetLoopChargeValue);
Divide := ConvertoReal DIV(100);
PLC_SetCharge := Divide;
PLC_Charge := 1500 * PLC_SetCharge;
RB_Charge := PLC_Charge;
Visual_RBPower := 1500 * PLC_SetCharge; (*Charge *)
END_FOR;
The problem I believe I have with this is that it cycles too fast so the condition never gets out of the while loop because it takes a while for the system to update so I thought of adding a delay portion:
While Solar_Power_House_W_Solar_PER <= OneHundred AND BatChargePercent < OneHundred DO
State_Dis_Charge := false;
wait(IN:=not wait.Q , PT:=T#50ms);
if Wait.Q Then
FOR PLC_SetLoopChargeValue:= 100 TO 0 By -1 DO
ConvertoReal := INT_TO_LREAL(PLC_SetLoopChargeValue);
Divide := ConvertoReal DIV(100);
PLC_SetCharge := Divide;
PLC_Charge := 1500 * PLC_SetCharge;
RB_Charge := PLC_Charge;
Visual_RBPower := 1500 * PLC_SetCharge; (*Charge *)
END_FOR;
END_IF;
END_WHILE;
How I think it should work is every 50ms 1 for loop should run. Currently nothing happens every 50ms.
You have to consider that WHILE and FOR are executed synchronously. It means blocking. It means that interpreter do not execute next line, until previous line is finished.
This means that "running to fast" cannot apply here. It does not matter how fast it runs, the execution of the lines will be always in order.
The only thing I would change and loop not from 100 to 0 but vice versa from 0 to 100, because I am not sure this backward will work fine. And then all you have to change:
ConvertoReal := INT_TO_LREAL(100 - PLC_SetLoopChargeValue);
You do now show all code it is VERY HARD to judge but if FOR loom is complete it totally make no sense. You calculate some variables but you do not use them there. You know that you cannot use them outside of your FOR loop, right? Because outside of your FOR loop those variable will be always same value of last loop.
In your second example your FOR loop, although it might work, you should not use timer to run the loop inside the loop. Because loops are synchronous and times async.
As I understand you task you do not need WHILE at all. With this approach your program execution of other parts will be blocked until 100%. That might take a while as I can see. So you have to use IF.
IF Solar_Power_House_W_Solar_PER <= OneHundred AND BatChargePercent < OneHundred DO
// ....
END_IF;
The difference is significant. With WHILE it will block your program till WHILE finish and other parts will not be executed for this long, in the same PLC cycle FOR might be executed so many times.
With IF if will run FOR one time per one PLC cycle and actualy doe snot change your logic.
If you would share your full code or at least parts where variables you have here are used so that the whole picture is visible, you might get a better help. Edit your post and I'll edit my comment.
With this answer im only adressing your issue with the for loop not being executed every 50ms.
The other answers why the while loop cant be exited are correct unless the variables Solar_Power_House_W_Solar_PER and BatChargePercent aren't changed in a parrellel thread.
I suggest wait is a TON function block. Please mind that names of FBs are case sensitive: wait.Q is possibly unequal Wait.Q. I think that is the main reason your for loop is not executed, because you check the output of another FB. Maybe check your declaration list for doubles with higher or lower cases.
Another possibility is, that your condition for the while loop isn't met at all and you didn't notice. In this case the for loop wouldn't be executed too of course.
I'm using an ElNet energy&power meter that communicates with my processor via Modbus RTU protocol.
There are two 16-bit ElNet registers that contain information about the Date and Time (separately) in a Win Format (registers 85-86, page 6 of this document). I'm able to read these two registers. However, I'm unable to extract information about the Date and Time.
For example, Date register contains decimal value of 17841 for today's date (31/07/2015). Is there any person willing to explain me how to convert 17841 into 31/07/2015?
I have the same problem with the time. My time register contains a decimal value of 55296. Can you help me extract the time from this number?
This thread addresses the same problem:
HEX/Decimal to date and time from modbus
However, I'm not sure I understand the extraction algorithm applied there. My point of operation is processor with the code written in C or C++.
Thank you very much for your time and effort to help me.
Sincerely,
Bojan.
The MS-DOS date/time format is described here: https://archive.is/2bVlz (was http://proger.i-forge.net/MS-DOS_date_and_time_format/OFz but is gone)
It makes sense for the 17256 value mentioned in the other question, as it translates to 2013-11-08. See here how to do it:
Register bit description: 0bYYYYYYYMMMMDDDDD
Registervalue: 17256 0b0100001101101000
Yearmask: 0b1111111000000000
Yearpart: 0b0100001000000000
Yearpart rightshifted 9 steps: 0b0000000000100001 = 33 years after 1980
Monthmask: 0b0000000111100000
Monthpart: 0b0000000101100000
Monthpart rightshifted 5 steps: 0b0000000000001011 = 11
Daymask: 0b0000000000011111
Daypart: 0b0000000000001000 = 8
Unfortunately your register value 17841 does not make sense, as it translates to 2014-13-17 (That is month 13).
Are you sure that:
you read the correct register? (change the time setting in the instrument, and see what happens to the register value)
you do not mix up the two bytes in the register?
the time setting is correct?
During the reading of PLC documentation (Omron CP1L PLC and CX-Programmer), there are some missing explanation. For example, it defines "Flag" as "a bit that serves as an interface between in*struction", does that mean flag is some sort of conditional Power Flow?
It gets more confusing with the terms "Differential Up/Down", "Carry Flag"? What are flags and what do they do in the ladder logic? Are they a simple or instruction to use or just a concept that I don't really need to program in ladder?
[EDITED]
Where to add / modify / delete the flag in an instruction? I open up the edit but flag isn't there.
Ok, this is a better question.
PLCs are like any program - data is stored as different types. Think of flags as interchangeable with the term "bit", "boolean", etc. They are very important.
If you have CX-Programmer, a much better place to get information is the Instruction Reference (Help --> Instruction Reference --> yourPLC). These show time diagrams of most of the instructions and how each of the parameters and flags operate.
For example, a basic timer (TIM) works by assigning it a value. If you use a BCD type 100ms timer and assign its SV (setpoint value) a BCD value of 300 then you have created a timer with a 30 second limit (300 x 100ms). When the timer turns on it will begin counting and the PV (process value) will start from 300 and count down. When the value reaches zero the timer's flag turns ON to indicate that it has expired. If the timer's number is, say T100 then you can use T100 as a contact in another rung of logic - it will be true when the timer's execution conditions are TRUE and the timer has expired.
Differentials (UP/DOWN) are special flags which are true for only one PLC scan (ie: they are true for one execution cycle only) when their input conditions change from FALSE to TRUE (ie:OFF to ON) for UP differentials, and TRUE to FALSE (ie:ON to OFF) for DOWN differentials. You would use differentials in cases where you wanted to perform an action the moment a given condition changes.
Flags can be used for almost anything. You can use them as general booleans in your own programs, they can be parts of certain operations (ie: the CY (carry) flag is used on arithmetic operations which result in a carry - other flags are used to indicate overflows or div/0 errors, etc).
Edit again : (to answer extended question).
A basic timer's completion flag is a contact with its number. Say I have a 100ms timer, T100, which turns on when contact 10.00 is on:
10.00 ___
|-----| |---------------------------------------|TIM|
|100|
| |
|#20|
|___|
Now, once 10.00 has been ON for two seconds, the timer will elapse and the flag for timer 100, T100, will turn ON. If I had another rung where
| T100 W15.00
|-----| |-----------------------------------( )
Then work bit W15.00 would be turned on when the timer elapsed and would remain on so long as the timer's input condition remained satisfied (ie: so long as 10.00 remains ON). Flags work in different ways for different things, however. Each operation can use them in different ways.
The example from the Omron Instruction Reference (Help -> Instruction Reference -> [select PLC]) looks like this :
Well very good explanation with example and the flags value can be found in memory area it is pure binary either 0 or 1, as i was read the documentation work-bit memory location changes as per timer type e.g TIM/TIMX or TIMH or TIMHX, both are BCD timers but unit for the timer changes.