häufige Zeitberechnung, hohe CPU Auslastung

Minehunter

Level-1
Beiträge
25
Reaktionspunkte
3
Zuviel Werbung?
-> Hier kostenlos registrieren
Hallo,

ich benötige einmal eure Hilfe bezüglich der Performance eines CP6606. (Beckhoff)
im Bezug auf eine Zeitmessung.


Situation:
an dem CP hängt eine EK1101 klemme welche über EtherCat angesprochen wird.
An dieser Klemme hängen wiederum Zählerkarten des Typs EL1512 (6 Karten)

Programm:
- Einlesen der Eingänge
- CASE
0: - speichere Time1: Systemzeit (GETSYSTEMTIME()) CASE=>1
1: - Berechne: Zählerdifferenz := (aktuellen Zählerstand) - (alten Zählerstand), Ist dieser Größer als 100, dann
- Speichere Time2 (Systemzeit) bereche differenz der beiden Zeiten, Time2-Time1,
- weitere Berechnungen mit der Zeit und des Zählers
- CASE=>2

Dieser Funktionsblock zur Berechnung der Zeiten wird am Anfang des Programms in ein Array initialisiert, sodass es für jeden Messkanal zur verfügung steht.
also für jeden Messkanal eine eigene Instanz.

Nun wird in einer FOR-schleife die einzelnen Funktionsblöcke abgearbeitet uind die Werte in eine Struktur geschrieben.

An sich funktioniert das ganze auch, bis auf diese Kleinigkeit.


timing.jpg

Bis die einzelnen Zähler ca. den Wert 100 erreicht haben, vergehen ca. 5 Sekunden.
Der CPU leerlauf iegt bei ca. 15%.
Wenn nun die Berechnung erfolgt, liegt dieser bei 60%
Kann man an der Performance noch etwas verbessern?
Ziel ist es bis zu 100 solcher Karten abzufragen und zu berechnen.



MAIN
VAR
fbCalc: ARRAY [0..11] OF FB_Calc;
END_VAR

FOR idx := 0 TO 11 BY 1 DO
fbCalc[idx]()
END_FOR

FB_CALC
VAR
ActSystemTime1 : T_ULARGE_INTEGER ;
ActSystemTime2 : T_ULARGE_INTEGER ;
UInt64TimeDif : T_ULARGE_INTEGER ;
TimeDifSec : LREAL;
fbReadSystemTime: GETSYSTEMTIME;
END_VAR


CASE iState OF
0: // setze Zeit
fbReadSystemTime(timeLoDW=> ActSystemTime1.dwLowPart, timeHiDW=> ActSystemTime1.dwHighPart);
iMeas_PulsAlt := iPlsInputVal; // Speicher aktuellen Zählerstand
iState := 1;

1: //Lese Zählerstand bis differenz über 100 ist

iDifPuls := iPlsInputVal - iMeas_PulsAlt;
IF iDifPuls >= 100 THEN
fbReadSystemTime(timeLoDW=> ActSystemTime2.dwLowPart,timeHiDW=> ActSystemTime2.dwHighPart); // Speicher aktuelle Zeit in zweiten Speicher
UInt64TimeDif:= UInt64Sub64(ActSystemTime2, ActSystemTime1); // Berechne die Differenz
TimeDifSec:= UINT64_TO_LREAL(UInt64TimeDif) / LREAL#10000000; // errechne sekundenwerte bsp. 10.12

rMeas_Act := ( (UDINT_TO_REAL( iDifPuls) * LREAL_TO_REAL(TimeDifSec) ) / 100) ; // Berechnungen
// ....

iState :=0;
END_IF

END_CASE


Vielen Dank im Voraus
 
Hallo Minehunter,

du braucht die Systemzeit doch nur einmal pro Zyklus. Dazu musst du sie ja nicht in jedem Baustein neu aufrufen. Lese sie einmal zu Zyklusbeginn aus und übergebe das Ergebnis den ganzen FB's. Das sollte einiges an Zeit sparen.

Grüße
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Velen Dank für den Tipp,

ich wahr der Ansicht, dass ich dies lieber alles in einem FB machen sollte, um nicht abhängig von äußeren Zeiten zu sein.

So habe nun ein meinem FB die Start-Zeit übergeben.
Diese wird nun im CASE 0: zwischengespeichert. dann springe ich gleich in CASE 1,
Dort wird nun gewartet bis der Zähler auf 100 gestiegen ist, die Systemzeit wird jedesmal aufs neuem dem FB übergeben, sodass innerhlab des FB keine Zeiten mehr ausgelesen werden müssen.
Ist der Zähler nun 100 wird einfach die gespeicherte Zeit aus CASE 0 mit der aktuellen Zeit vergleichen

timimg2.PNG


so richtig ist da aber eine verbesserung nicht zu erkennen.
Das auslesen der aktuellen Zeit erfolgt nun nach jedem Zyklus.
Nicht aber dann, wenn die Zähler den Stand von 100 erreicht haben. also ein definiertes auslesen

Das system muss also ständig die Zeit auslesen, obwöhl es eventuell 5 sekunden zeit gehabt hätte.



Vorteil der Neuen Variante:
- Da die Systemzeit noch außerhalb der FOR-Schleife vom Systemabgefragt wird, kann diese innerhlab der FOR-Schleife an alle Funktionsblöcke übergeben werden.
- das erspart aktuell 12mal den Zeit-Aufruf.

Habt ihr weitere Ideen?

VAR_INPUT
ActSystemTime : T_ULARGE_INTEGER ; // Übergabe der Systemzeit, welche vor dem Funktionsblock, außerhlab der FORschleife einmal gelesen wird
VAR_END


CASE iState OF
0: // setze Zeit
//fbReadSystemTime(timeLoDW=> ActSystemTime1.dwLowPart, timeHiDW=> ActSystemTime1.dwHighPart);

// NEU
ActSystemTime1 := ActSystemTime;
...

1: //Lese Zählerstand bis differenz über 100 ist

iDifPuls := iPlsInputVal - iMeas_PulsAlt;
IF iDifPuls >= 100 THEN
//fbReadSystemTime(timeLoDW=> ActSystemTime2.dwLowPart,timeHiDW=> ActSystemTime2.dwHighPart); // Speicher aktuelle Zeit in zweiten Speicher
//UInt64TimeDif:= UInt64Sub64(ActSystemTime2, ActSystemTime1);

// NEU
UInt64TimeDif:= UInt64Sub64(ActSystemTime, ActSystemTime1);
TimeDifSec:= UINT64_TO_LREAL(UInt64TimeDif) / LREAL#10000000;
.....
iState :=0;
END_IF
 
Zuletzt bearbeitet:
Dann vielleicht:
Alle FB's abfragen ob sie eine Zeit benötigen. Wenn ja Zeit auslesen und Zeit noch im selben zyklus noch einmal den FB's übergeben.
Dann rufst du die Zeit wirklich nur dann auf wenn du sie brauchst und maximal einmal pro zyklus.

Ansonsten kann ich deine Laufzeit nicht testen. Ich habe hier nur eine Lenze PLC mit 1,6 Ghz. da dauert das Zeit auslesen ca 10 µs.
 
Hallo,

ich würde das CASE ganz weg lassen. Im Main rufst du einmal die Zeit ab. Dann rufst du die FB's auf und machst deine Berechnung. Das CASE kostet nur Zeit.

Grüße
 
Zuviel Werbung?
-> Hier kostenlos registrieren
An sich funktioniert das ganze auch, bis auf diese Kleinigkeit.


Anhang anzeigen 31922

Bis die einzelnen Zähler ca. den Wert 100 erreicht haben, vergehen ca. 5 Sekunden.
Der CPU leerlauf iegt bei ca. 15%.
Wenn nun die Berechnung erfolgt, liegt dieser bei 60%
Kann man an der Performance noch etwas verbessern?
Ziel ist es bis zu 100 solcher Karten abzufragen und zu berechnen.
Was stört Dich eigentlich?
Wenn Du gleichmäßig ausgelastete Zyklen willst, dann mach doch die Berechnungen immer.

Wenn Du mit nur 12 Kanälen schon bei 60% CPU-Auslastung bist, dann wird das mit 100 Kanälen mit der Hardware womöglich nix.
Oder kannst Du die Berechnungsschritte auf mehrere Zyklen verteilen? (Du zeigst uns ja offensichtlich nicht das ganze Programm)
Du könntest in jedem Zyklus die Berechnungen für nur 1 Kanal machen (dann hättest Du theoretisch max 50ms Zeit je Kanal): dazu in jedem Zyklus nur die Zählerstands-Berechnung machen und bei >=100 die Zeit merken und den Index der Instanz in einen FIFO (oder Ringpuffer) schreiben. Die Instanz schaut in den FIFO, ob der erste/älteste Eintrag der eigene Index ist und macht nur dann die komplette Berechnung.

Verbessert sich etwas, wenn Du die 12 Instanzen sequentiell hintereinander aufrufst anstatt in der FOR-Schleife?

Harald
 
Also ich würde das gar nicht über die Systemzeit machen, sondern einfach nur über TIME(). Das ist die Zeit die nach einschalten der PLC hochläuft damit kannst du einfacher rechnen und brauchst keine LREAL usw.

Einfach:
Code:
CASE step OF
0:
tStartTime := TIME();
step := 1;

1:
[COLOR=#333333][I]iDifPuls := iPlsInputVal - iMeas_PulsAlt;[/I][/COLOR]
[COLOR=#333333][I]IF iDifPuls >= 100 THEN 


tDiffTime := TIME() - tStartTime;
step := 0;
END_IF;

END_CASE;[/I][/COLOR]

Wenn ich dein Code richtig gelesen habe interessiert dich ja nicht die tatsächliche Uhrzeit also Montag, Tag, Jahr, Stunde, Minute..., sondern nur die Zeitdifferenz. Das kannst du mit Time auch gut machen. Wenn du eine CPU hast die mit Gleitkommawerten nicht so schnell rechnen kann gibt es auch noch eine Möglichkeit gar nicht über Zeiten zu rechnen, sondern in Zyklen (vorausgesetzt du hast deine Task auf Zyklisch gestellt und nicht freilaufend!)
Dann würde ich das so machen:

Code:
VAR
iInit : INT := 0;
tCycleTime  : TIME;
iCycleTimeInMS : INT := 0;
step : INT:=0;
iCycleCounter : INT := 0;
END_VAR

// einmalige Ermittlung der Zykluszeit
case iInit of
0:
tCycleTime := TIME();
iInit := 1;

1:
tCycleTime := TIME() - tTaskTime;
iCycleTimeInMS := TIME_TO_INT(tCycleTime); // Zykluszeit als Int und in MS als Mulitplikationsbasis
iInit := 999;

END_CASE;

// erst rest durchlaufen wenn die Zykluszeit ermittelt wurde
IF iCycleTimeInMS  = 0 THEN
RETURN;
END_IF;


// Nun dein Kram machen
CASE step OF
0:
iCycleCounter  := 0;
step := 1;

1:
iCycleCounter := iCycleCounter  +1; // jeden Zyklus + 1 dazuzählen 

[COLOR=#333333][I]iDifPuls := iPlsInputVal - iMeas_PulsAlt;[/I][/COLOR]
[COLOR=#333333][I]IF iDifPuls >= 100 THEN 


iDiffTime := iCycleCounter * iCycleTimeInMS;
step := 0;
END_IF;

END_CASE;[/I][/COLOR]

Dann bist du (fast) komplett von den Zeiten, LongReals,... weg.
 
Also erstmal:
Der Baustein GETSYSTEMTIME trägt so gut wie garnichts zu deiner Systemlast bei. Ich habe den zum Test mal 1000 mal in einer FOR Schleife auf meinem CX5010 laufen lassen und die CPU-Last ist nichtmal um 1% gestiegen.

Was bei dir aber ordentlich Rechenleistung zieht ist dein rum gemähre mit 64 Bit Werten die du garnicht brauchst. Du rechnest einen Zeitstempel mit 100ns Auflösung auf eine Auflösung von 1 Sekunde runter. Allerding sind die Last-Spitzen auch bei 500 INT64 Operationen und Konvertierungen zu LREAL pro Zyklus noch lange nicht so krass wie bei dir. Und die CP6606 ist so langsam auch nicht.

Wenn du bloss 1s Ticks brauchst, dann schaff dir deine eigene Zeitbasis. Z.B. du addierst die Sekunden timergesteuert auf etc.
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Hab vor kurzen einen Artikel gelesen um den es ging "EINFACH" zu denken. Fällt mir so spontan bei deinem Code ein.

GetSystemTime gibt dir zwar Zeiten im 100nsec Format aber so what. Die Systemgenauigkeit des CP6606 liegt deutlich darüber.
LREAL-Arithemetik ist somit nett aber in dem Fall für die Katz und zudem Resourcenfressend.
Dich interessiert das die EL1512 möglichst äquidistant abgefragt. Diese Äquidistanz kannst du dann für die Berechnung nutzen. Wenn du mehr brauchst dann bist du bei Distributed Clocks und die PC-Uhrzeit wird irrelevant. Dein CP6606 wäre dann auch nicht mehr die geeignete Platform.

Also
1) IO am Taskanfang definieren -> beim Zeitinterrupt wird zuerst der EtherCAT gelesen/geschrieben, danach erst deine SPS. Hintergrund: Du hast eine bessere Synchronität des EtherCATs
2) Berechne dein Gesamtzeit einfach mit "Anzahl der Zyklen" mit Zykluszeit der PLC. Anzahl der Zyklen wäre nur ein Zähler den du jeweils Inkrementierst.
Die Zykluszeit kannst du dir auch auslesen und somit Konfigurationsunabhängig nutzen.

Guga
 
Hallo,

vielen Dank für die Vielen Antworten. Alles konnte ich noch nicht umsetzen und testen, bin aber denke auf einem Gutem Weg. Danke im Voraus.

ich möchte natürlich auf die ein oder andere Frage noch Antworten und meine ergebnisse kurz mitteilen.

(Du zeigst uns ja offensichtlich nicht das ganze Programm)
Ich habe die restlichen Programmteile auskommentiert. Im MAIN läuft nur noch der FB mit der schleife und abfrage von 12 EL1512, wie im ersten Beitrag beschrieben

Was bei dir aber ordentlich Rechenleistung zieht ist dein rum gemähre mit 64 Bit Werten die du garnicht brauchst. Du rechnest einen Zeitstempel mit 100ns Auflösung auf eine Auflösung von 1 Sekunde runter. Allerding sind die Last-Spitzen auch bei 500 INT64 Operationen und Konvertierungen zu LREAL pro Zyklus noch lange nicht so krass wie bei dir. Und die CP6606 ist so langsam auch nicht.

Wenn du bloss 1s Ticks brauchst, dann schaff dir deine eigene Zeitbasis. Z.B. du addierst die Sekunden timergesteuert auf etc.

Ja die berechnungen werde ich noch etwas überarbeiten müssen, das habe ich auch mitbekommen, Dank für den Tipp

Wenn ich dein Code richtig gelesen habe interessiert dich ja nicht die tatsächliche Uhrzeit also Montag, Tag, Jahr, Stunde, Minute..., sondern nur die Zeitdifferenz. Das kannst du mit Time auch gut machen. Wenn du eine CPU hast die mit Gleitkommawerten nicht so schnell rechnen kann gibt es auch noch eine Möglichkeit gar nicht über Zeiten zu rechnen, sondern in Zyklen (vorausgesetzt du hast deine Task auf Zyklisch gestellt und nicht freilaufend!)
Dann würde ich das so machen:

Ich werde mir mal die TIME FUnktion anschauen.
Die voraussetzung war, die Zyklenzeit zwischen den Pulsen so genau zu bestimmen, wie möglich. Daher habe ich ersteinmal diese Systemzeit mit 100ns genommen. Wollte erst auf Distributed Clocks/DC gehen, mit 1ns, aber das fande ich eher zu übertrieben.
Die umrechnung auf Sekunden hatte ich langsam hochgeschraubt, um zu sehen wie genau ich werden muss.
Werde im nächsten Schritt die TIME Funktion einmal ausprobieren.
bisher reicht mir auch die Millisekunde fast aus, wenn ich noch weiter optimieren kann, sieht es gut aus.

Verbessert sich etwas, wenn Du die 12 Instanzen sequentiell hintereinander aufrufst anstatt in der FOR-Schleife?

Anbei das ergebnis :DIch habe es nicht in einer FOR-Schleife gemacht, sondern den Zähler hochgezählt, sobald die Berechnung fertig ist.
timing3.PNG

Wenn ich dies so mache, brauche ich auch keine 12 Instanzen, welche gleichzeitig abgearbeitet werden, sondern nur ein. Vllt war auch dies das Problem, das innerhalb der schleife 12 berechnungen durchgeführt wurden.



Also vielen Dan noch einmal.
Ich werde mir noch einmal die Umrechnungen der Zahlen anschauen und ein anderes Zeitformat wählen, um von den LREAL's weg zukommen.


vllt auch noch einmal zum Hintergrund des ganzen.
Im erten schritt habe ich die EL1512 mit hilf eines TON jede 1Sekunde abgefragt und den aktuellen Zähler mit dem vorherigen zähler verglichen.
Hierbei gibt es aber das Prblem, das s sich immer wieder einmal ein Zähler dazugeschummelt hat.
also : 100,100,100,100,101,100,100

Wenn man diese Werte immer mit der Festen Zeitbasis von 1 Sekunde verechnet, gibt es eine Fehler im bezug auf diesen einen Puls.
Daher nun das ganze mit der Zeitbestimmung nach den Pulsen.

Damit ist egal im es nun 100 Pulse oder 102 Puls sind, da die Zeit ensprechen 1 Sekunde oder 1,1 Sekunde ist. Die Berechnung bleibt somit gleich.
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Was Zählst du denn überhaupt mit den Zählerklemmen?
Dir sollte schon bewusst sein, dass die Prozessdaten immer nur am Anfang eines Zyklus eingelesen werden. D.h. während dein Programm abgearbeitet wird, bekommst du die Änderungen in den Zählern garnicht mit.
Wenn deine Zykluszeit z.B. 10ms ist, dann bekommst du nur alle 10ms einen neuen Wert. Da kannst du im Programm noch so toll mit µs, ns oder der Plank-Zeit rechnen.
Mal abgesehen davon, das die von dir verwendeten EL1512 eh nur eine Zählfrequenz von 1kHz haben. Wenn du den genauen zeitlichen Abstand zw. 2 Impulsen brauchst, dann ist diese Klemme sicher nicht das optimale Werkzeug. Da wäre eine EL1252 in Verbindung mit einer schnellen Zykluszeit der SPS die bessere Wahl.
 
Verbessert sich etwas, wenn Du die 12 Instanzen sequentiell hintereinander aufrufst anstatt in der FOR-Schleife?
Anbei das ergebnis :DIch habe es nicht in einer FOR-Schleife gemacht, sondern den Zähler hochgezählt, sobald die Berechnung fertig ist.
Anhang anzeigen 31937

Wenn ich dies so mache, brauche ich auch keine 12 Instanzen, welche gleichzeitig abgearbeitet werden, sondern nur ein. Vllt war auch dies das Problem, das innerhalb der schleife 12 berechnungen durchgeführt wurden.
Ich meinte nicht, daß Du nun in jedem Zyklus nur 1 Instanz bearbeiten sollst. Ich meinte, Du sollst die 12 Calls der 12 Instanzen einfach hintereinanderweg ins Programm hinschreiben, anstatt nur 1 indizierter Call in einer FOR-Schleife, und somit in jedem Zyklus die 12 Instanzen auch bearbeiten.
Code:
fbCalc[0]();
fbCalc[1]();
...
fbCalc[11]();

Du mußt schon in jedem Zyklus (bzw. E/A-Buszyklus) den Zählerstand aller Klemmen auswerten. Nur die mehr oder weniger aufwendigen Berechnungen sollst Du je Zyklus nur für eine Klemme machen. Deshalb der FIFO.


btw: Wie machst Du das eigentlich, daß die 12 Instanzen mit verschiedenen Klemmen/Kanälen arbeiten? Oder berechnest Du 12 mal exakt das gleiche?

Harald
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Die EA-Variablen der Instanzen werden im Systemmanger ganz normal verknüpft.
Ich habe kein Twincat, ich kann mir das irgendwie nicht vorstellen. :(

Was meinst Du mit EA-Variablen der Instanzen - Bausteinparameter IN/OUT? Der TE hat uns leider diesen Teil seines Programmcodes nicht gezeigt, deshalb frage ich nach.

Harald
 
Wenn du in einem FB (FB_Foo) Prozess Ein- und Ausgangsvariablen anlegst
Code:
I_iZaehler  AT%I* :INT; (*Zaehlerwert von Klemme*)
und den FB instanzierst
Code:
fbFoo1 :FB_Foo;
fbFoo2 :FB_Foo;

Dann erscheint im Systemmanager für jede Instanz von FB_Foo die Eingangsvariable I_iZaehler, die du mit dem passenden Prozessdatum einer beliebigen Zaehlerklemme (oder jeder anderen Klemme, die passende Datentypen liefert) in deinem System verknüpfen kannst.

MAIN.fbfoo1.I_iZaehler -> Klemme17_Kanal1_Daten_Ein
MAIN.fbfoo2.I_iZaehler -> Klemme28_Kanal2_Daten_Ein
 
TwinCat hat getrennte I/O-Abbilder für die HW und die SW-Geräte (PLC 1..4, NC,...). Im Systemmanager werden die HW-I/Os mit den SW-I/Os verknüpft. Zusätzlich kann man auch FB-Variablen (scheinbar) im I/O-Abbild eines SW-Gerätes anlegen.
Tatsächlich liegen diese Variablen natürlich weiterhin im Datenbereich des FBs, die Daten werden anhand einer Konfig-Tabelle zwischen SW-I/O-Abbild und FB hin und her kopiert. So kann jede FB-Instanz mit eigenen I/Os arbeiten. Da der Systemmanager die Konfig-Tabelle automatisch anlegt, ist diese Adressierung aber auch bei PRGs oder FBs mit nur einer Instanz gang und gäbe. Die durch das doppelte Kopieren verschwendete Rechenleistung trägt sicher erheblich zur Erderwärmung bei, aber es ist halt bequem.
 
Zuviel Werbung?
-> Hier kostenlos registrieren
Geht doch auch in sonst wo z.B. Codesys...


fbCalc[0].bInput1 := bla;
fbCalc[1].bInput1 := blub;


// Call FB Instanz
for i := 0 TO 11 DO
fbCalc(); // Nur Aufruf
END_FOR;


Geht natürlich nicht auf VAR_IN_OUT, die MÜSSEN immer direkt beim Aufruf angegeben werden...
 
Wenn die Übergabeparameter für jede Instanz extra zugewiesen werden dann ist aber der schöne faule Instanzaufruf in der Schleife irgendwie sinnlos.

Harald
Wieso? Wenn du deine FB Instanzen in einem Array hast, dann hindert dich doch niemand die Parameter auch in einem Array zu organisieren. Und wenn es dir noch zu viele Parameter sind, dann bündelst du sie in einer Struktur.

Code:
FOR i := 1 to 100 DO
  fbFooInstanz[i](E_Inputs := Inputs[i], A_Outputs => Outputs[i]);
END_FOR

EDIT
Ich glaube wir kommen hier gerade leicht vom Thema ab ........
 
Zuletzt bearbeitet:
Zurück
Oben