Arduino Bibliothek um Steuerungsaufgaben und das Handling von Ein- und Ausgabe zu vereinfachen

OP #7514803
Lesenswert?

Ich entwickele zurzeit eine Arduino Bibliothek um Steuerungsaufgaben und das Handling von Ein- und Ausgabe zu vereinfachen. Vielleicht habt Ihr ein paar Anregungen bevor ich das Teil bei github veröffentlichen werde.

Dadurch das die Programmlogik von den Ein- und Ausgabefunktionen durch die IO Controller strikt getrennt sind, läuft das untere Codebeispiel ohne Änderungen sowohl auf einem Arduino als auch auf dem PC (Visualstudio c++).

Aus diesem Grund habe ich z.B. Serial durch den Namen printStream ersetzt. Auf dem Arduino wird dann die UART Ein-/Ausgabe genutzt und auf dem Windows PC die Console. Die Bibliothek bildet auf dem Windows PC einige Arduino Funktionen(Streams,Print,String e.t.c.) nach, falls diese auf dem PC nicht verfügbar sind.


Das folgende Beispiel benutzt die Bibliothek:

1
#include "PlcIncludeAll.h"
2
#include "PlcSample.h"
3

4
PlcStdIO* io;
5
PlcMachine machine;
6

7

8
class Example : public PlcProcess
9
{
10
public:
11
  enum commands {  run , stop  , list,last};
12
  int btn1;
13
  int stopwatchTempValue;
14
  int measuredTime;
15
  int clockRun;
16
  int clockTick;
17
  int clockResult;
18
  void onRegisterIO(PlcIO* io)
19
  {  
20
    io->addCommands(command, last,  "run", "stop","list");  
21
    io->add(btn1, "btn1");
22
    io->add(measuredTime, "measured time");
23
    io->add(clockTick, "clock tick");
24
  }
25
  Example()
26
  {  
27
    clockTick = 0; clockRun = 0; measuredTime = 0;
28
  }
29
  void onMessage(PlcIO* plcIO, PlcAdapter* adapter, int index)
30
  {
31
    if (adapter->isCommand() && adapter->origin == true)
32
    {
33
      switch (adapter->getIntValue())
34
      {
35
      case run:
36
        clockRun = true;
37
        this->println("clock start");
38
        break;
39
      case stop:
40
        clockRun = false;
41
        this->println("clock stop");
42
        break;
43
      case list:
44
        syncInit();
45
        break;
46
      }
47
    }
48
    int u = 0;
49
  }
50
  void onTask()
51
  {
52
    /*PLC Node Calls e.g.clock, ... must not be used within if statemnts or loops.
53
    Only linear sequences are permitted !!!!
54
    forbidden example:
55
     if (condition)
56
     {
57
        a = clock(1000);
58
        b = clock(2000);
59
     } 
60
    */
61
    stopwatchTempValue = stopwatch(btn1);
62
    if (fall(btn1))
63
    {
64
      measuredTime = stopwatchTempValue;
65
      clockRun = true;
66
    }
67
    clockResult=  clock(measuredTime);
68
    clockTick = clockRun & clockResult;
69
  }
70
} example;
71
void setup() {
72
  printStream.begin(9600);
73
  io = new PlcStdIO(&printStream);
74
  example.registerIO(io);
75
  machine.registerProcess(&example);
76
  PlcStream *st;
77
  PlcBugStream* m;
78
}
79
void loop() {
80
  machine.tick();
81
}

Die Klasse Example macht folgendes:

Wenn ein Button gedrückt und losgelassen wird, wird die Zeit zuwichen den beiden Tastenzuständen gemessen und angezeigt (measured time=). Im Anschluss gibt ein periodischer Timer das gemessene Zeitinterval als High und Low Flanken aus (clock tick=1, clock tick=0 ..... siehe Screenshot).

Um die Zeitmessung zu starten wird auf dem Windows PC in der Konsole btn1=1 und btn1=0 hintereinander eingegeben, um den IO Port des Arduinos zu simulieren.

Zusätzlich kann mit den Kommandos run und stop die Ausgabe gestartet und beendet werden.

Wenn das Programm auf einem Arduino laufen soll, wird zusätzlich ein HardwareIO Conroler registriert, welcher das Prozessabild der ein- und Ausgänge auf die IO Pins abbildet. Das eigentliche Programm hat keine Kenntnis darüber welche IO Hardware existieren.


Die virtuelle Funktion onRegister bindet Variablen an beliebige IO Controller. Zudem wird gleichzeitig ein Name zugewiesen wie die Aus- und Eingabe erfolgen soll. Propertys wie z.B. ReadOnly sind auch möglich. Außerdem könne hier auch Kommandos registriert werden.

Zurzeit existieren IO Controller für die Kommandozeilenein-/ausgabe und die Hardware IO Pin Ein/Ausgabe. Es folgen noch weitere für ModBus, I2C und mqtt.

Die virtuelle Funktion onMessage reagiert auf Änderungen von gebunden Variablen und Kommandos.

Die virtuelle Funktion onTask bildet SPS/PLC Funktionalität ab. Es wird im Hintergrund das Prozessabbild der Ein/-Ausgänge ausgewertet und die daraus erfolgten Variablenänderungen an die registrierten IOController weitergeleitet.

Es sind folgende SPS/Plc Standardfunktionen implementiert.

1
bool rs(bool set, bool reset);
2
  Int32 ctu(bool trigger, bool reset, Int32 pv, bool& q);
3
  bool sr(bool set, bool reset);
4
  // Ausschaltverzögerung
5
  bool tof(bool trigger, Int32 duration);
6
  // Einschaltverzögerung
7
  bool ton(bool trigger, Int32 duration);
8
  /// Impulsbildung
9
  bool tp(bool trigger, Int32 duration);
10
  bool clock(Int32 interval);
11
  bool gateBool(bool open, bool value);
12
  Int32 gate(bool open, Int32 value);
13
  Int32 stopwatch(bool run);

Bei jedem Funktionsaufruf werden falls notwendig, im Hintergrund automatisch Merker zugewiesen und dessen Zustände beim nächsten Funktionsaufruf abgerufen. Es können z.B. beliebig viele clock Aufrufe mit unterschiedlichen Zeiten verwendet werden. Alles läuft asynchron und das Hauptprogramm wird nicht blockiert wie es z.B. bei delay Aufrufen der Fall wäre


die setup Funktion:

  • serial wird durch printStream ersetzt

  • der PLCStdIO Controller ist ein IOController der ein Ausgaben für die Kommandozeile bereitstellt. Jede gebundene Variable kann mit Variablenname=Wert zugewiesen werden und jede Werteänderung wird als Variablennam=Wert ausgegeben. Der PlcStdIO Controller kann beliebige Arduino Streams wie z.B. die Serials oder bei einem ESP32 über Wifi einen Telnet Dienst bedienen.

  • die Maschine kann beliebig viele Prozesse mit unterschiedlichen Prioritäten registrieren.

die loop Funktion: zyklicher Aufruf der Bibliothek

viele Grüße

Angehängte Dateien:
#7514870
Lesenswert?

Vielleicht habt Ihr ein paar Anregungen

Ja, man benutzt einen Controller der bereits auf unterster Ebene dafuer passende Befehle bereitstellt. Dann reichen dafuer Assemblermakros. Der Restaufwand ist gerade noch ein Scheduler, der sich um Tasks, Queues und Timerevents kuemmert.

beliebig viele clock Aufrufe

Glaube ich nicht.

wird nicht blockiert wie es z.B. bei delay Aufrufen der Fall wäre

Das ist woanders der Normalfall.

auf einem Arduino als auch auf dem PC

Wozu soll das denn gut sein?

kann beliebige Arduino Streams wie z.B. die Serials oder bei einem ESP32 über Wifi einen Telnet Dienst bedienen

Die je nach Medium aber einige 100 bis einige 10000 ms fuer die Ausfuehrung brauchen. :)

stell es online und gut

Ja, da stoert es keinen.

OP #7514886
Lesenswert?

Motopick schrieb:

Glaube ich nicht.

Da gebe ich Dir recht. Ich korrigiere: Beliebig viele so lange es die Ressourcen der Hardware zulassen.

Natürlich wird für jeden Aufruf Speicherplatz auf dem Stack reserviert. Klar wenn der Speicher voll ist können auch keine Merker im Hintergrund reserviert werden.

Motopick schrieb:

Wozu soll das denn gut sein?

Debuggen auf einen PC in Visualstudio ist um einiges effektiver als auf dem yC.

Motopick schrieb:

Die je nach Medium aber einige 100 bis einige 10000 ms fuer die Ausfuehrung brauchen. :)

Lass es mal bei einem Stream für die Serial Ausgabe 10-50 ms sein . 100-10000 ms ist schon sehr übertrieben. Für Hardware IO's werden in den IO Controllern natürlich KEINE Streams benutzt. Da liegen die Latenzzeiten unter 1 ms.

#7514911
Lesenswert?

Also ich gucke da rein, wie ein Schwein ins Uhrwerk. Da ist wohl der Großteil der Funktionalität in den nicht gezeigten h-Files versteckt. Ich mag es bei Steueraufgaben, wenn alle Abläufe einfach zu verfolgen sind. Also wann wird was und in welcher Reihenfolge ausgeführt. Und nicht über 1000 Instanzen hinweg von hinten durch die Brust ins Auge. Z.B. wodurch die Anzeige von measuredTime getriggert wird, ist mir vollkommen unklar.

OP #7514929
Lesenswert?

Peter D. schrieb:

Also ich gucke da rein, wie ein Schwein ins Uhrwerk. Da ist wohl der Großteil der Funktionalität in den nicht gezeigten h-Files versteckt.

Bei einem solchem Framework ist es normal das Funktionalität abstrahiert und hinter einer Fassade versteckt wird. Genau das ist Sinn und Zweck der Sache. Letztendlich dient es dazu Dinge schneller und fehlerfreier zu entwickeln.

Die ganze Bibliothek hat ca. 85 KB Quellcode. g

Peter D. schrieb:

Z.B. wodurch die Anzeige von measuredTime getriggert wird, ist mir vollkommen unklar.

Ein Zeiger auf die Variable measuredTime wird mit der Funktion add(int& ref, const char* name) dem IO Controller gemeldet. Beim Aufruf der add Funktion wird im Controller zusätzlich eine Kopie der Speicherzelle erzeugt. Bei jedem Zyklus wird die referenzierte Variable mit der Kopie verglichen. Wenn die beiden Werte unterschiedlich sind, wird eine Triggerfunktion ausgelöst und die Kopie aktualisiert.

Gast #7514943
Lesenswert?

Wir koennen ja nicht kommentieren, weil wir nicht die Quellen und Examples gesehen haben.

  • Vergleich Performance/Umfang z.B. zu FreeRTOS oder Azure? IO (Disk/Netzwerk)

  • Unterstuetzte Prozessoren ("Arduino" ist ein Framework, keine Hardware)

  • Ist das ein Ersatz fuer ein RTOS?

Du solltest versuchen, Deine SW in einem anderen Forum vorzustellen (hier wird es zerredet) oder ein Abo fuer "Asbestos Underwear" haben.

#7515562
Lesenswert?

Thomas K. schrieb:

Bei jedem Zyklus wird die referenzierte Variable mit der Kopie verglichen. Wenn die beiden Werte unterschiedlich sind, wird eine Triggerfunktion ausgelöst und die Kopie aktualisiert.

Das wäre mir zu aufwendig, haufenweise Schattenspeicher einzurichten und zu vergleichen. Ich habe auch die Erfahrung gemacht, daß es sicherer ist, alle Ausgänge zyklisch neu zu setzen, egal, ob sie sich geändert haben. Es können ja Störungen auf SPI, I2C usw. einstrahlen, ein DAC kippt um, eine Anzeige zeigt Müll an usw. Oder es passiert ein kurzer Spannungseinbruch, den die CPU gar nicht mitkriegt.

Thomas K. schrieb:

clockTick = clockRun & clockResult;

Ich würde diese Variablen aber als bool definieren und mit && verknüpfen. Oder ist das & überladen und was bewirkt es hier?

OP #7515710
Lesenswert?

Peter D. schrieb:

Ich habe auch die Erfahrung gemacht, daß es sicherer ist, alle Ausgänge zyklisch neu zu setzen, egal, ob sie sich geändert haben.

Bei direkten IO's ist das sicherlich die Standardvorgehensweise, die Ports immer wieder zyklisch neu zu setzen. Aber nehmen wir einmal an, Du hast einen Zyklus der 10 ms dauert und Du möchtest keinen direkten IO setzen, sondern irgendetwas das sich außerhalb des Controllers befindet. Zum Beispiel eine SCADA Anzeige über Modbus RS485. Dann würden in dieser Situation (alle Ausgänge zyklisch neu setzen)alle 10 ms der gesamte Status der Ausgänge über den Bus verschickt. Wenn 32 solcher Ausgänge übermittelt werden sollen und jeder genau 1 byte an Informationen hat, dann wären das 256 Bit's netto + Overhead. Mit diesen 256 Bit's alleine wären bei einer Zyklusrate von 10 ms schon 25.600 Baud komplett ausgelastet. Den Overhead noch gar nicht mit einberechnet. Da erstens das menschliche Auge eine solche Anzeigerate auf der SCADA GUI gar nicht benötigt und zweitens es auch durchaus Maximalbegrenzung einer Bandbreite bei einem Datenbus gibt, finde ich das neu senden aller Daten bei jedem Zyklus für keine so gute Lösung. Und wenn Du dann noch Pesch hast, läuft Dir der Sendebuffer über oder der Prozess blockiert, weil alles verstopft. Ja kann man so machen, natürlich. Hier finde ich es besser nach jedem Zyklus die Änderungen zu erfassen und nur die Änderungen zu übertragen. ggf. alle 100 Zyklen zusätzlich einmal eine Komplettsynchronisation. Das ganze zusammen mit einem Bandbreitenmanagement das im Hintergrund läuft und notfalls den Datenstrom abbremst.

Peter D. schrieb:

Ich würde diese Variablen aber als bool definieren und mit && verknüpfen. Oder ist das & überladen und was bewirkt es hier?

Fehler von mir, müßte eine boolsche Verknüpfung sein und keine mathematische AND Operation auf den Integerwert. Obwohl, solange der Integer nur 1 und 0 als Wert annehmen kann müsste sich das Programm gleich verhalten.

Vax W. schrieb:

  • Ist das ein Ersatz fuer ein RTOS?

Wenn ich die Frage an dieser Stelle mit ja beantworten würde, wäre das formell falsch, weil es kein Multithreading gibt. Aber es gibt auf der Anwendungsebene eine Art "gefakte Paralellität", weil vieles in möglich kleine Slices aufgeteilt wird. Der Parser z.B. der die String Eingaben parst, liest pro Zyklus eine gewisse feste Anzahl an Bytes aus dem Stream und speichert den Status zwischen, bis der Ausdruck vollständig gelesen und interpretiert ist. Das Gegenteil wäre z.B. alles zeilenweise zu lesen und die Zeile dann komplett auf einmal auszuwerten. Aus meiner Erfahrung führt das dann manchmal dazu, das der Zyklus zu lange unterbrochen wird und über die Schwelle der maximal gewünschten Zyklusdauer geht.

Vax W. schrieb:

  • Unterstuetzte Prozessoren ("Arduino" ist ein Framework, keine Hardware)

Das stimmt. Aber die Adrdunio Plattform hat klar definierte Schnittstellen für die Bibliotheken und einen HAL Layer für die darunterliegende Hardware. Solange es eine Arduino kompatible HAL Implementation und einen C++ Compiler für den Prozessor gibt, sollte es darauf laufen. Ich habe bis jetzt noch nicht alle getestet, aber Arduino MEGA 2560, Arduino DUE, Arduino Zero, ESP32, ESP8266 laufen damit, weil ich die selber zu Hause habe

(Firma: Gast) #7515740
Lesenswert?

Thomas K. schrieb:

...

Da du klug bist, ist dir auch klar, dass ich das nicht testen/beurteilen kann. Und da du schlau bist, sorgst du absichtlich dafür. (warum auch immer)

Als alter PLC/SPS Liebhaber/Nutzer verstehe ich dein Vorhaben, habe allerdings gänzlich andere Wege genutzt um das für meine Belange zu realisieren. Z.B. mich Für die Datenfluss Variante entschieden, wie sie z.B. von den Koppelplänen bekannt ist.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren