Messwerte schnell berechnen und ausgeben

Hallo,
für mein Projekt, einen Laderegler für ein Windrad per arduino anzusteuern und gleichzeitig einige Messwerte zu erfassen und diese auf einem LCD auszugeben, bastle ich gerade an den einzelnen code-Abschnitten.
Da im gesamten Programm u.a. auch die Umdrehung pro Minute über eine Interruptfunktion gemessen wird und noch andere Dinge zeitkritisch geschehen, suche ich nach Lösungen, die möglichst wenig Rechenleistung und somit Zeit erfordern. Dabei geht es mir NICHT um schmalen Programmcode, sondern um Schnelligkeit.

Jetzt die konkrete Frage:

  • Spannungsmessung (funktioniert und die Werte stimmen)
    Um auf float-Operationen verzichten zu können habe ich Werte für die Berechnung so skaliert, dass ich mit long rechnen kann und die Division durch 1023 durch die Funktion bitshift right (>> 10, entspricht Division durch 1024) ersetzt und den geringen Unterschied zwischen den Divisoren in der Messwerterfassung kompensiert.

Das sieht dann so aus:

/*Spannungsmessung mit Arduino für Scheibengenerator
 
 Schaltung:
 +12V----10k---4k7---10k Poti-----|
 |-------------- + Arduino
 GND---------------10k------------|
 |---GND Arduino
*/

#include <LiquidCrystal.h>
#define voltPin A0    // inputpin für Spannungsmessung an Batterie
const long lVoltConst = 16147; // Konstante für Umrechnung analogen Messwert in Volt
// muss durch Messung und Berechnung in Abstimmung mit dem Poti errechnet werden Formel:
// Messwert Multimeter (Volt) * 1023 / analogIn-Wert am Arduino (Messreihe erforderlich)
long lVoltSensor = 0;  // Spannungsmessung an analogIn Pin A0
long lVolt = 0; //gemessene Spannung
void setup()
{
  pinMode(voltPin, INPUT);   // Pinmode für A0
  Serial.begin(9600); // +++Serial initialisieren - nur debug
  delay(2000);
}
void loop() 
{
  lVoltSensor = analogRead(voltPin);  //Auslesen Messwert A0  
  lVolt = lVoltSensor * lVoltConst; // Berechnung Volt 1.
  lVolt = lVolt >> 10; // Berechnung Volt 2
  Serial.println(lVolt); // nur debug
  delay(1000); // +++durch Timefunktion without delay ersetzen                 
}

Wie gesagt, die Berechnung stimmt auf die zweite Kommastelle mit den Parallelmessungen per Multimeter überein.
Mein Problem ist, dass der Berechnete Wert auf dem Display (bzw. dem serial Monitor) als long ausgegeben wird und real zwischen 5000 und 16000 liegt. Das entspricht zwar genau der Ausgabe in mV, aber ich brauche die Ausgabe in Volt und das nur mit nur zwei Nachkommastellen.

Mit der Lösung 1, den Wert durch 1000 zu dividieren und in einer float-Variable zu speichern, bin ich aber wieder da, dass die Berechnung zeitintensiver wird.
Lösung 2, den long-Wert in einen String umzuwandeln und nach Längenprüfung den Dezimalpunkt einzufügen erfordert aber auch wieder einige Schritte.

Wie kann mann das am optimalsten hinkriegen, dass z.B. der Wert 12534 (mV) als 12,53 (V) ausgegeben (oder gleich so berechnet) wird und dabei möglichst wenig Prozessorleistung abverlangt wird?

Gruß Klaus

...hab mich vielleicht etwas umständlich ausgedrückt, die Frage sollte lauten:

Was verlangt weniger Rechenleistung (Zeit), die Formatierung des Wertes als String oder gleich die Berechnung als float per Division?

Um die Zahl 12345 in "123,45" zu formatieren, muss sie durch alle möglichen Potenzen von 10 dividiert werden (oder z.B. pro iffer einmal modulo 10 und dann Division durch 10), dann läuft noch die eine oder andere Schleife - da kannst Du dann auch gleich ne Floating-Point-Operation durchführen. Dann allerdings gibst du aber möglicherweise auch wieder nen formatierten String aus - also nimmt sich das in der Menge sicher nix. Die Frage ist ja eher, welche Samplingfrequenz du beim Ermitteln deiner Meßwerte ansetzt, bzw. wieviel Zeit da zwischen den einzelnen Sample- und Berechnungsvorgängen vergeht. Oder anders - wenn Deine Augen vielleicht 30 Änderungen pro Sekunde wahrnehmen können (bei 25, also 25Hz, flimmerts) - d.h., wenn du z.B. 50 mal in der Sekunde nen Analogwert ausliest und anzeigst, dann sollte sich der Prozessor zwischendurch ziemlich langweilen, und die o.g. Berechnungen laufen eher nebenbei ... :slight_smile: und dann ist noch fraglich, mit welcher Frequenz das Display läuft, also wie schnell es die nächste Anzeige macht. Das alles gibt Dir ja dann nen Anhaltspunkt, wie oft du überhaupt sinnvollerweise die Meßwerte aktualisierst. Ich würde meinen, das steht in keinem Verhältnis zu dem, was der Prozessor leisten könnte. Vielleicht habe ich aber auch was mißverstanden :slight_smile:

Gruß

Axel

EDIT: Was ich da oben meinte ist, dass println() ja so oder so aus der Fließkommazahl wieder ne Zeichenette machen muss, um die dann irgendwo auszugeben. Ob Du nun also diese Zeichenkette erst erzeugst und die dann an println() übergibst, oder aber println() die Arbeit machen lässt, nimmt sich nix. Möglicherweise ist println() sehr gut optimiert, aber um das Erzeugen der Zeichenkette kommt es wohl nicht rum :slight_smile: Und die Division durch 100 (oder wieviel Nachkommastellen Du da hast) fällt dann im Verhältnis eh nicht mehr ins Gewicht. Will sagen, die Ausgabe des Messwerts steht aufwandsmäßig quasi in überhaupt keinem Verhältnis mehr zu seiner Berechnung :slight_smile:

@Axel
Deine Frage nach der Samplingfrequenz hat mir die Augen geöffnet. Meine Frage war in sofern blöd, dass ich nicht bedacht habe, dass ich eigentlich vor habe die analogIn-Spannungswerte über einige "Millis" zu mitteln, und das sicher mehr Rechenleistung erfordert, als das oben geschilderte Beispiel abverlangt.
Ich war nur dem immer wieder genannten Grundsatz: "vermeide floats und divisionen" aufgesessen und habe übersehen, dass schon bei der Messwertmittelung Division notwendig ist.

Trotzdem würde mich schon mal interessieren, ob die Umwandlung einer Zahl 12345 (Typ long) in einen String /charArray und der anschließenden Formatierung für die Ausgabe in 12,345 mehr oder weniger Prozessorleistung abverlangt, als eine Division mit float-Typen, die dieses als float zur Ausgabe direkt "bereit stellen würde"?

Naja :slight_smile:

Das kannste Dir folgendermaßen vorstellen. Die wahrscheinlich "sparsamste" Art und Weise ist es, das ganze so "umzurechnen", wenn Du also unbedingt die Floatingpoints vermeiden willst:

char displayZeile[1024];
int Messwert = liesMesswert(); // wir reden nicht davon, welcher Aufwand hier schon ensteht

int vorKomma = Messwert / 100; // zwei Nachkommastellen "abschneiden"
int nachKomma = Messwert % 100; // zwei Nachkommastellen "isolieren"

sprintf (displayZeile, "Messwert = %d,%d", vorKomma, nachKomma);

Serial.println (displayZeile);

Schön, schön - alles ohne explizite Fließkommaarithmetik, aber immerhin eine Integerdivision und die Modulo-Berechnung ist auch n komplexere, jenachdem wie gut der Compiler das optimiert. Letztere ist vor allem möglicherweise keine native Operation des atmega (kenne ich noch nicht so gut), sondern muss möglicherweise durch eine ganze Reihe separater Berechnungen abgebildet werden. Da ist schon ne ziemliche "Verrenkung" im Gegensatz zu:

sprintf (displayZeile, "Messwert = %l", (double)(Messwert/100));
Serial.println (displayZeile);

Nun müsste man mal wissen, wieviele Operationen des atmel das sind, bzw. wieviele Zyklen da verbraten werden. Und das setz dann mal ins Verhältnis zu dem Drumrum. Der richtige Aufwand entsheht wahrscheinlich eher beim Einlesen des Messwerts und bei der Ausgabe der Displayzeile - da werden Schleifen durchlaufen und da ist pro Schleifendurchlauf (entsprechend der Anzahl der zu lesenden bzw. zu schriebenden Bytes) mindestens eine Vergleichs- und eine Additionsoperation fällig. Nicht zu reden von der ganzen Protokollsteuerung der seriellen Schnittstelle :slight_smile: Und da machst Du dir um die eine kleine Fließkommaoperation Sorgen? :slight_smile:

Wenn, dann musste gleich mit Assembler anfangen - ich würde mein sparsames Augenmerk eher auf andere Dinge richten, wie z.B. das Display nur so oft anzusteuern, wie es überhaupt drauf reagieren und das menschliche Auge die Änderungen wahrnehmen kann :slight_smile:

Die Mittelwertberechnung kann man ja relativ simpel machen - sofern ich das richtig verstanden haben, gibt es Timer bzw. interruptgesteuerte Funktionen. Da würde ich mir nen Timer setzen - vielleicht alle 10 ms - und dann folgende Funktion ausführen:

int anzahlMessungen = 0;
long summeMessungen = 0;

void setup () { 
  timerStarten ();
}

void loop () {
  // irgendwann mal
  zeigeMittelwert ();
}

void zeigeMittelwert () {
  timerStoppen ();
  Serial.println ("Messwert = %l", (double)(summeMessungen / anzahlMessungen));
  timerStarten ();
}

void timerStarten () {
  summeMessungen = 0;
  anzahl Messungen = 0;
  // hier den code zum starten des Timers, der die Funktion naechsterMesswert() ruft
}

void stopTimer () {
  // hier den code zum stoppen des Timers
}

void naechsterMesswert () {
  summeMessungen += liesMesswert ();
  anzahlMessungen++;
}

Du könntest auch noch die Funktion zur Ausgabe auf Timer oder Interrupt setzen - falls mehrere TImer und Interrupts zur Verfügung stehen...

Gruß aus B nach B :slight_smile:

Axel

Mir ist noch was eingefallen - man könnte den Messwert auch nur dann ausgeben, wenn er sich im Vergleich zur vorherigen Ausgabe geändert hat - vielleicht sogar so, dass es ein gewisses Toleranzintervall gibt, das über- oder unterschritten werden muss. :slight_smile:

Ich grinse gerade so in mich rein. Das erinnert mich alles an Zeiten vor 20 Jahren, als man noch tierisch mit Prozessorzyklen, Speicher und so weiter gegeizt hat... Das waren Zeiten kurz nach meinem C64, als ich nen sündhaft schnellen (386er mit 6MHz oder sowas) und wahnsinnig gewaltig ausgestattetenn (8MB RAM - die hab ich noch in 8 Einzelchips - und ne 125MB-Festplatte!) PC kaufte... :slight_smile: Der hat damals (1991) 5000DM gekostet und der Verkäufer hat dicke Backen gemacht. Aber auf der Gurke lief immerhin OS/2 1.3 (zu installieren von 22HD-Disketten oder sowas, große Freude!!!) - und der atmel hat nun doch etwas bessere Leistungsdaten :slight_smile:

:grin:

berlin1109:
Trotzdem würde mich schon mal interessieren, ob die Umwandlung einer Zahl 12345 (Typ long) in einen String /charArray und der anschließenden Formatierung für die Ausgabe in 12,345 mehr oder weniger Prozessorleistung abverlangt, als eine Division mit float-Typen, die dieses als float zur Ausgabe direkt "bereit stellen würde"?

Nochmal ich - Du bist da möglicherweise einem Denkfehler aufgesessen :slight_smile: println() gibt nach außen immer eine Bytefolge bzw. eben eine Zeichenfolge aus. Wie Du es nun drehst und wendest - der ganze Kram muss eben in eine Zeichenkette (also Array von Bytes) umgewandelt werden. Eine serielle Schnittstelle ist Byte-orientiert - na, eigentlich bit-orientiert. Und das Display kann mit int, float und sonstwas gar nichts anfangen - es erwartet Bytes (bzw. bits, die dann zu einer Bytefolge zusammengesetzt werden). Das Display wandelt nicht eine Fließkommazahl in eine solche Zeichenkette um - das macht eben println() :slight_smile: So gesehen ist diese Konvertierung irgendwann vor der Ausgabe sowieso fällig! :slight_smile: float, int, long, double, char, byte sind nur interne Repräsentationen von Daten - nach außen müssen sie so aufbereitet werden, dass sie weiter verarbeitet werden können.

:.

Mitteln von Messwerten geht als Integeroperation, vor allem dann wenn man immer 2,4,8 oder 16, ... mittelt und dann per Bit Shift dividiert.

Für die Ausgabeaufbereitung fällt eine float Operation garantiert nicht ins Gewicht. Bei 16Mhz ist dauert das nicht so lange. Das Display und seine Ansteuerung verbrauchen garantiert mehr Zeit.

Udo

Hallo,
erst mal vielen Dank, besonders an Axel. Deine ausführlichen Erklärungen haben mir sehr viel weiter geholfen und ich finde es toll, dass Du neben Lösungsansätzen auch gleich die Erklärung dazu lieferst. Dadurch habe ich so einiges besser verstanden, obwohl ich sicher nie über den "Bastlerstatus" hinaus kommen werde :slight_smile: - Danke

Hier mal das Programm-Modul für die Drehzalmessung des Repellers. Es läuft bereits an meinem Windrad seit einigen Wochen ohne Probleme. Ich habe jedoch noch einige Änderungen am code vorgenommen (u.a. per shift right dividiert - danke Uwe). In dieser Version habe ich es aber noch nicht getestet.
Dazu kommt dann die Spannungsmessung und eine Strommessung mit dem Allegromodul ACS715 von Watterott.

/* Generator - LED, Datenlogger und Steuerung
 2. Umdrehungsmesser
 +++ Dreiphasen-Wechselstrom-Scheibengenerator
 +++ Messung zwischen zwei Phasen
 +++ pro Umdrehung 6Phasenwechsel 
 */
#include <LiquidCrystal.h>
// Umdrehungsmesser PIN 2 = Interrupt 0
// Display-PINs:
LiquidCrystal lcd (8, 9, 13, 12, 11, 10);
// Umdrehungsmesser
volatile unsigned long l_pulse = 0; // speichert gemessene Impulse
unsigned long l_totalTime = 0; // Gesamte abgelaufene Zeit
unsigned long l_lastTime_pulse = 0; //vergangene Zeit Kurzmessung
unsigned int i_lastPulse = 0; // Runde=Umdrehungen bis zum letzen Durchlauf kurz
unsigned int i_rpm = 0; // Runden = gezählte Umdrehungen
unsigned int i_rpm_max = 0; // maximaler RPM-Wert
const int i_messZeit_pulse = 2000; // Messzeitraum in ms RPM
const int i_GenPhasen = 6; // Anzahl der Phasenwechsel pro Umdrehung

void setup()
{
  attachInterrupt(0 , pulse, RISING); //Interrupt auslösen bei Phasenwechsel (an PIN 2)
  lcd.begin(16, 2);
  lcd.print("Drehzahlmessung");
}
void loop()
{
  // --- U/min berechnen im Zeitraum von (const int i_messZeit_pulse) 2000 ms
  if ((l_totalTime - l_lastTime_pulse) >= i_messZeit_pulse)
  {
    i_rpm = l_pulse - i_lastPulse;   // neuer Wert, gemessene Pulswechsel
    i_lastPulse = l_pulse;    // Zwischenspeichern des aktuellen Wertes
    i_rpm *= 10;   // Umrechnung: eine Umdrehung = (6 Pulswechsel/s = 1 U/s =  60 U/min = 6 Pulswechsel * 10 = 60)
    i_rpm = i_rpm >> 1;   // Durch Messzeit (2 s) = RPM
   if (i_rpm > i_rpm_max)
    {
      i_rpm_max = i_rpm;
    }
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Uges: ");
    lcd.print(l_pulse / i_GenPhasen); //Umdrehungen gesamt auf LCD ausgeben
    lcd.setCursor(0, 1);
    lcd.print("RPM:  ");
    lcd.print(i_rpm); //RPM ausgeben
    lcd.setCursor(11, 1);
    lcd.print(" max: "); //RPMmax ausgeben
    lcd.print(i_rpm_max);
    l_lastTime_pulse = l_totalTime; //Zeit nach letztem Durchlauf speichern
  }
}
//+++++++++ Funktionen
void pulse() //Interruptfunktion - Pulsewechsel hochzählen
{
  l_pulse++; // hochzählen
}

Das hier verwendete 16/2 Display werde ich dann durch ein 16/4-Zeilendisplay ersetzen, um die anderen Messwerte zusätzlich anzeigen zu können.

Jetzt werde ich mir nochmal die Tipps von Axel ansehen und dann die beisen Kodeteile zusammenbasteln.

Gruss Klaus