Change system time of Beckhoff controller by programming - plc

For the last couple of hours, I am facing problems with changing system date and time by structured-text programming. I have used function block FB_LocalSystemTime where I can read the system time. But I could not find any function or function block to write new system time. I have checked NT_SetLocalTime, that also did not work. Do you have any idea how can I do that?
For further information: I have included the sample code like:
/**
Declaration Part
**/
fbLocalSystemTime:FB_localSystemTime;
fbSetLocalTime:NT_SetLocalTime;
newTime:TIMESTRUCT:=(wHour:=5);
/**
DEFINITION PART
**/
fbLocalSystemTime(); /*This gives system time */
fbSetLocalTime.TIMESTR:=newTimne; /* New time to set */
fbSetLocalTime.START:=TRUE;
fbSetLocalTime(); /** This does NOT set the system time which I guess should set **/

I understand the question, but unfortunately I don't have much experience with beckhoff plcs. Have you tried calling their support line? This should be a non application specific question that should be easy for them to help you with.

You may consider using FB_LocalSystemTime in the way indicated below. This will synchronize the local PLC time with the system with the given AMS ID passed to the parameter sNetID. If you do not pass sNetID parameter local OS system will be used as reference for setting the local PLC time. The time will be synchronized on rising edge of the signal bEnable and then in the interval given by the parameter dwCycle
VAR
{attribute 'hide'}
LocalSysTime : FB_LocalSystemTime;
SynchNodeAmsId : STRING := '10.10.10.1.1.1';
END_VAR
LocalSysTime(
sNetID:= SynchNodeAmsId,
bEnable:= TRUE,
dwCycle:= 60,
dwOpt:= ,
tTimeout:= ,
bValid=> ,
systemTime=> ,
tzID=> );

You are correct. You should be using NT_SetLocalTime.
If you open the function block fbSetLocalTime(), you will realize your function block returns an error with error ID 1862.
The definition of the errors can be found here: https://infosys.beckhoff.com/english.php?content=../content/1033/tcplclib_tc2_utilities/18014398544490635.html&id=
1862 means error in win32 system.
This is because TIMESTRUCT consists of Year, Month, Week, etc, but you only initialize the Hour as 5. This means that other things will become 0. The Year needs to be between 1970 and 2106, and there are many things to follow, shown below:
After you use a valid TIMESTRUCT variable, your code should be able to execute without problem and your computer system will be changed.

Had a similiar problem, using TwinCat3. Sometimes it works to change local system time, sometimes not.
I use a small state machine to solve that (also there could be a more advanced solution) - just write 3 times the new time....
Here is an code example:
VAR
nState : BYTE := 0; //local state machine
ntSetLocalTime : Tc2_Utilities.NT_SetLocalTime;
tTimestructSet : Tc2_Utilities.TIMESTRUCT; // time to set
nErrId : UDINT;
nRetryCnt : BYTE := 0;
and the state machine:
CASE nState OF
0: //wait for change
bBusy := FALSE;
nRetryCnt := 0;
1: //trigger change
bBusy := TRUE;
ntSetLocalTime(
NETID:= '',
TIMESTR:= tTimestructSet,
START:= TRUE,
TMOUT:= ,
BUSY=> ,
ERR=> ,
ERRID=> );
nState := 2; //wait for writing
2: //wait till written
ntSetLocalTime(
START:= FALSE,
ERRID => nErrId);
IF NOT ntSetLocalTime.BUSY THEN
nState := 3;
END_IF
3: //retry and after some retries go back to init
nRetryCnt := nRetryCnt + 1;
IF nRetryCnt >=3 THEN
nState := 0;
ELSE
nState := 1;
END_IF
END_CASE
Otherwise you could call Beckhoff hotline, in most cases they have a really good support.

Related

Time and number counter in one?

I want to check how long a motor is working since the last check up and how many times the motor startet up since then.
#xSekundenzaehler := "Clock_1Hz";
REGION Allgemein
#Betriebszeit_Anlage(CU:="eHauptschuetz" AND #xSekundenzaehler,
R:="Lebensdauer".Allgemein.xResetBetriebszeit,
PV:=0,
CV=>#rSecondsAnlage);
"Lebensdauer".Allgemein.rLaufzeitAnlage := #rSecondsAnlage / 3600;
END_REGION
This is the code i used. Do i have to use another "CTU" again or is there a smarter way to do this in the already existing one that i don't know of?
Usually storing important values on CTU is not recommendable because you have no control on whether TiaPortal may restore the CTU instance.
It is preferable to build a counter by yourself so its value is kept on a non volatile db var:
"R_TRIG_1Hz"(CLK := "Clock_1Hz");
// Condition to count up running time
IF "eHauptschuetz" THEN
// Only at rising edges of seconds
IF "R_TRIG_1Hz".Q THEN
"DBZeit".LebensdauerSeconds := "DBZeit".LebensdauerSeconds + 1;
END_IF;
END_IF;
// Reset prevails over increment
IF #xReset THEN
#xReset := false;
"DBZeit".LebensdauerSeconds := 0;
END_IF;
// Hour total
"DBZeit".LebensdauerHours := "DBZeit".LebensdauerSeconds / 3600;
// Hour total with a decimal place
"DBZeit".LebensdauerDeciHours := "DBZeit".LebensdauerSeconds / 360;
This is the definition of the DB
Please note that only seconds are remanent, as hours and decihours are calculated on every cycle.
It would be better to store hours and decihours in other DB, so this one is dedicated to remanent values. When you modify the DB variables their values are reset to the start values.
Please remember to make a snapshot and copy them to start values prior to any changes.

Count machine runtime in PLC

I am working on a simple machine project in PLC using structured text programming language. Now, I want to count the runtime (hours, mins and seconds) of a machine after Start Command is pressed. As I am new, I struggle to develop an idea. I tried to do it by using 3 counter for each seconds, minutes and hours. But could not achieve solid result. Can you please share the idea how to count runtime hours of a machine on PLC (ST or FB) ?
There are two issues here:
Correctly handling delta time
Using data types to your advantage
Since you want the time since the Start Command, you need to first store the time when the start command is pressed. Then you calculate the delta time (current time - start time) to obtain the seconds elapsed. Then take the seconds elapsed and get your hours, minutes, seconds. You can even simply use a TON FUNCTION_BLOCK for this - the ET output is in the TIME data type.
There are a number of good built-in data times for date and time. TOD (or TIME_OF_DAY) automatically is in the HH:MM:SS.mmm format for you. If your max elapsed time is less than 24 hours, you can use this format. If it could be longer, then use the TIME will go to 49 and some days, otherwise DT (DATE_AND_TIME) will count until the year 2106. You will get YYYY-MM-DD-HH:MM:SS in that format.
The following is a TON trick you can use, assuming 49 days or less is true.
PROGRAM PLC_PRG
VAR
Running : BOOL := FALSE;
Start : BOOL := FALSE;
MyTimer : TON;
Elapsed : TOD;
MyEt : TIME;
END_VAR
//calculate Elapsed time only if running
IF Running THEN
//ET is in milliseconds, so can easily convert to TOD
Elapsed := TIME_TO_TOD(MyEt);
ELSE
Elapsed := TOD#0:0:0.000;
END_IF
//run a TON timer, but set the PT to the absolute limit
//this trick is only valid for 49 days or less!
MyTimer(IN:=Running,PT:=DWORD_TO_TIME(16#FFFFFFFF),ET=>MyEt);
//only start once
IF Start AND NOT Running THEN
Running := TRUE;
Start := FALSE;
END_IF
If you are adverse to the trick, you can also do something like below (requires the SysTime library). In your online debugger, you will see Elapsed in HH:MM:SS format. Of course you should create a FUNCTION_BLOCK for this.
PROGRAM PLC_PRG
VAR
Running : BOOL := FALSE;
Start : BOOL := FALSE;
Now : ULINT;
StartTime : ULINT;
Elapsed : TOD;
pResult: SysTimeRtc.RTS_IEC_RESULT;
END_VAR
//get current time (result is in seconds)
Now := DWORD_TO_ULINT(SysTimeRtcGet(pResult));
//calculate Elapsed time only if running
IF Running THEN
//TOD is in milliseconds, so we x1000
Elapsed := ULINT_TO_TOD((Now - StartTime) * 1000);
ELSE
Elapsed := TOD#0:0:0.000;
END_IF
//only start once
IF Start AND NOT Running THEN
StartTime := Now;
Running := TRUE;
Start := FALSE;
END_IF

Why does this select always run the default case when the first case actually is executed?

I'm trying to get a better understanding of golang channels. While reading this article I'm toying with non-blocking sends and have come up with the following code:
package main
import (
"fmt"
"time"
)
func main() {
stuff := make(chan int)
go func(){
for i := 0; i < 5; i ++{
select {
case stuff <- i:
fmt.Printf("Sent %v\n", i)
default:
fmt.Printf("Default on %v\n", i)
}
}
println("Closing")
close(stuff)
}()
time.Sleep(time.Second)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
}
This will print:
Default on 0
Default on 1
Default on 2
Default on 3
Default on 4
Closing
0
0
0
0
0
While I do understand that only 0s will get printed I do not really understand why the first send does still trigger the default branch of the select?
What is the logic behind the behavior of a select in this case?
Example at the Go Playground
You never send any values to stuff, you execute all the default cases before you get to any of the receive operations in the fmt.Println statements. The default case is taken immediately if there is no other operation than can proceed, which means that your loop will execute and return as quickly as possible.
You want to block the loop, so you don't need the default case. You don't need the close at the end either, because you're not relying on the closed channel unblocking a receive or breaking from a range clause.
stuff := make(chan int)
go func() {
for i := 0; i < 5; i++ {
select {
case stuff <- i:
fmt.Printf("Sent %v\n", i)
}
}
println("Closing")
}()
time.Sleep(time.Second)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
fmt.Println(<-stuff)
https://play.golang.org/p/k2rmRDP38f
Notice also that the last "Sent" and the "Closing" line aren't printed, because you have no other synchronization waiting for the goroutine to finish, however that doesn't effect the outcome of this example.
Since you're using a non-blocking 'send', the stuff <- i will really only be executed if there's a reader already waiting to read things on the channel, or if the channel has some buffer. If not, the 'send' would have to block.
Now since you have a time.Sleep(time.Second) before the print statements that read from the channel, there are no readers for the channel till after 1 second has passed. The goroutine on the other hand finishes executing within that time and doesn't send anything.
You're seeing all zeroes in the output because the fmt.Println(...) statements are reading from a closed channel.
Your first case isn't executing.
Here's what your program does:
Start a goroutine.
Attempt to send 0 through 4 on the channel, which all block, because there is nothing reading the channel, so fall through to the default.
Meanwhile, in the main goroutine, you're sleeping for one second...
Then after the second has elapsed, attempt to read from the channel, but it is closed, so you get 0 every time.
To get your desired behavior, you have two choices:
Use a buffered channel, which can hold all of the data you send:
stuff := make(chan int, 5)
Don't use default in your select statement, which will cause each send to wait until it can succeed.
Which is preferred depends on your goals. For a minimal example like this, either is probably no better or worse.
It only executes the default case, because the for loop runs 5 times before anything starts reading from the channel. Each time through, because nothing can read from the channel, it goes to the default case. If something could read from the channel, it would execute that case.

Instantiating of numerous function block in CODESYS (IEC61131)

I have made a function block using CODESYS to perform energy metering. The inputs of the function block is current and voltage and the output is energy.
Now, we need to have 1000 instances of this function block to run the code for 1000 meter we have. Writing (and possibly copy and pasting) of these instances doesn't seem to be the most interesting work.
Wondering if anybody has a smarter way of doing this numerous instantiation.
For example, here is how the code (in CODESYS) looks like for 2 instances:
meter_instance1(CURRENT:=I1, VOTAGE:=V2);
Energy1:= meter_instance1.ENERGY;
meter_instance2(CURRENT:=I2, VOTAGE:=V2);
Energy2:= meter_instance2.ENERGY;
And we like to have 1000 instances of it.
Any idea is highly appreciated.
Just make an array of the function block:
aEnergyMeter : array [0..999] of FB_EnergyMeter;
Also make arrays of the voltage and the current:
aVoltage : array [0..999] of INT; //if you use INT as type
aCurrent : array [0..999] of INT;
Then you can use it like that:
aEnergyMeter[0](CURRENT:= aCurrent[0], VOLTAGE := aVoltage[0]);
As you use different arrays with the same size, I would prefer to define some global constant:
VAR GLOBAL CONSTANT
firstDevice : UINT := 0;
lastDevice : UINT := 999;
END_VAR
Then you can define the array like that:
aEnergyMeter : array [firstDevice..lastDevice] of FB_EnergyMeter;
I agree with Arndt that you should use an array of function blocks and an array for voltage and current. Depending on your scan rate of your task you should be able to use a for loop to scan through all of your function blocks in a couple lines of code
var
meterInstance : array[1..1000] of FB_EnergyMeter;
voltage : array[1..1000] of int;
current : array[1..1000] of int;
energy : array[1..1000] of int;
end_var
for i:=1 to 1000 by 1 do
meterInstance[i](Voltage := voltage[i],Current:= current[i]);
energy[i] := meterInstance.Energy;
end_for;
in that for loop you could also combine some error checks while you're at it
for i:=1 to 1000 by 1 do
meterInstance[i](Voltage := voltage[i],Current:= current[i]);
if meterInstance.Energy > MaxEnergy then
//sound alarm
end_if;
end_for;
The only caveat with this approach is if the scan time is too fast for your task. You could possibly get a watch dog error as the task would overrrun. However since you are only doing 1000 elements and I am assuming your function block is not extremely complex you should be ok with this approach. If you have problems try extending scan time or watch error time.

Sporadic Task will be run at a specific time

I have a sporadic task that print something. I want to run this task at specified time i.e. 4:40PM.
I have an idea to create cyclic task, that checks time and when the time is equal to my specified time, this cyclic task realeases the barrier of sporadic task. But how to check if my specified time (represented as Unix timestamp or String) is equal to time at now? How to convert my specified time to Ada.Real_Time.Time according to Ravenscar Profile?
How can I achieve that in Ada?
Here is my code:
sprinkler.ads
pragma Profile(Ravenscar);
with GNATCOLL.Ravenscar.Simple_Cyclic_Task;
with GNATCOLL.Ravenscar.Simple_Sporadic_Task;
package Sprinkler is
Counter : Integer := 0;
MyTime : String := "4:45";
procedure My_Cyclic_Operation;
package My_Cyclic_Task is new GNATCOLL.Ravenscar.Simple_Cyclic_Task
(Task_Priority => 10,
Phase => 0,
Period => 1000,
Cyclic_Operation => My_Cyclic_Operation);
procedure My_Sporadic_Operation;
package My_Sporadic_Task is new GNATCOLL.Ravenscar.Simple_Sporadic_Task
(Task_Priority => 10,
Minimum_Interelease_Time => 1_000,
Protocol_Ceiling => 15,
Sporadic_Operation => My_Sporadic_Operation);
end Sprinkler;
sprinkler.adb
pragma Profile(Ravenscar);
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Real_Time; use Ada.Real_Time;
package body Sprinkler is
procedure My_Cyclic_Operation is
begin
-- Here I want to run My_Sporadic_Operation when time at now is equal to my specified time
null;
end My_Cyclic_Operation;
procedure My_Sporadic_Operation is
begin
Put_Line("Sporadic at specified time!");
end;
end Sprinkler;
The GNATCOLL.Ravenscar packages obey the restrictions of the Ravenscar profile, but they can be compiled into a partition that has an unrestricted profile. So, unless you’re compiling for a system that only supports Ravenscar, you can (with a small error because the calls take finite time)
convert your 4:40 pm to Ada.Calendar.Time
subtract Ada.Calendar.Clock to obtain a Duration
use Ada.Real_Time.To_Time_Span to convert to Time_Span