Forum: Mikrocontroller und Digitale Elektronik Durchflussensor auslesen Arduino


von Paul H. (pauleheisster)


Lesenswert?

Hallo Zusammen,

ich bin dabei einen Durchflusssensor der Firma Biotech mit dem Arduino 
Mega auszulesen.
Es handelt es sich um das gute Stück:
https://www.conrad.de/de/durchfluss-sensor-1-st-fch-c-pa-2-x-g-12-ag-bio-tech-ek-betriebsspannung-bereich-5-24-vdc-messbereich-05-503591.html

Datenblatt:
http://www.produktinfo.conrad.com/datenblaetter/500000-524999/503591-da-01-ml-DURCHFLUSSMESSER_FCH_C_PA_de_en.pdf

Angeschlossen wie folgt:
+Ub an 5V von Arduino
GND an GND des Arduino
S an Pin 2 des Arduino

Im Datenblatt steht: 435 Imp./L. Also im Programmcode mittels Interrupt 
die Impulse gezählt und ab bestimmter Messzeit das ganze ausgerechnet.
Soweit klappt das alles auch und ich bekomme wohl realistische Werte von 
bis zu 16,5 l/min.
Allerdings stimmt mein Wert nicht mehr wenn der Arduino im Loop mit 
beispielsweise einer For Schleife für eine gewisse längere Zeit 
beschäftigt ist. Je länger ein Delay in dem Loop desto mehr sinkt der 
Wert. Das ganze verwundert mich weil ich für die Berechnung die 
tatsächliche Zeit (differenceMillis) seit der letzten Rechnung bis zur 
aktuellen Rechnung als Referenz nehme.

Hab ich ein Denkfehler?

Code:
1
double sensorCount, periode = 5000; //5 sec Imp. zählen
2
float durchfluss;
3
unsigned long startMillis;
4
unsigned long currentMillis;
5
unsigned long differenceMillis;
6
7
void setup() {
8
Serial1.begin(9600);
9
attachInterrupt(digitalPinToInterrupt(2), isr_durchfluss, CHANGE); //Datenkabel an Pin 2
10
startMillis = millis(); 
11
}
12
13
void loop() {
14
15
currentMillis = millis();
16
differenceMillis = currentMillis - startMillis; 
17
if (currentMillis - startMillis >= periode)
18
  {
19
  durchfluss = (sensorCount/435)*(60000/differenceMillis); //435 Imp./L, 1 min = 60 000 ms
20
  startMillis = millis();
21
  sensorCount =0;
22
  }
23
24
//Hier Bspw.:
25
//delay(6000); //je höher desto mehr sinkt der Durchfluss
26
}
27
28
void isr_durchfluss()
29
{
30
sensorCount++;
31
}

: Bearbeitet durch User
von Stefan F. (Gast)


Lesenswert?

Das hier riecht nach Problemen:

> durchfluss = (sensorCount/435)*(60000/differenceMillis);

Da treten bestimmt mehrere Rundungsfehler auf. bedenke auch, dass 
double=float ist. Doppelte Genauigkeit ist nämlich einfach nicht 
implementiert.

Ich würde das ganze auf Integer Arithmetik umstellen, aber dabei auf die 
Bereiche (gültige Werte) achten.

von Paul H. (pauleheisster)


Lesenswert?

Stefanus F. schrieb:
> Ich würde das ganze auf Integer Arithmetik umstellen, aber dabei auf die
> Bereiche (gültige Werte) achten.

Stefanus danke für dein Hinweis. Kannst du das nochmal genauer 
ausführen? Hab noch nicht verstanden wie ich das mit Integer lösen soll.

von Wolfgang (Gast)


Lesenswert?

Paul H. schrieb:
> double sensorCount, periode = 5000; //5 sec Imp. zählen

> if (currentMillis - startMillis >= periode)

Es ist keine gute Idee, die Variablentypen nach dem Zufallsprinzip zu 
verteilen, sozusagen frei nach dem Motto: "Der Compiler wird's schon 
richten".

millis() liefert unsigned long und alles was damit rechnet, sollte gerne 
dazu passen ;-)

von Stefan F. (Gast)


Lesenswert?

Ungeprüfter Vorschlag:
1
#define PERIODE 5000
2
unsigned long lastTime;
3
volatile unsigned long sensorCount;
4
unsigned long lastSensorCount;
5
6
void setup() {
7
    attachInterrupt(digitalPinToInterrupt(2), isr_durchfluss, CHANGE); 
8
    sensorCount=0;
9
    lastSensorCount=0;
10
    lastTime=millis();
11
}
12
13
void loop() {
14
    unsigned long elapsedTime = millis()-lastTime;
15
    if (elapsedTime >= PERIODE)
16
    {
17
        lastTime=millis();
18
        cli();
19
        unsigned long tmp = sensorCount;
20
        sei();
21
        unsigned long count = tmp-lastSensorCount;
22
        lastSensorCount = tmp;
23
24
        unsigned long milliLiters = count*1000/435;
25
        unsigned long milliLitersPerHour = milliLiters*60000/elapsedTime;
26
    }
27
28
    delay(...);
29
}
30
31
void isr_durchfluss()
32
{
33
    sensorCount++;
34
}

Folgendes habe ich mir dabei gedacht:
- Die PERIODE habe ich als Konstante definiert, das kann der Compiler 
nutzen, um den Code zu optimieren. Ist hier nicht wirklich wichtig.
- Die Variable sensorCount ist volatile, das muss man mit allen 
variablen machen, die in einer ISR beschrieben und außerhalb der ISR 
gelesen werden. Ansonsten wird sie eventuelll falsch in einem CPU 
register gecached weil der Compiler "denkt" dass sie sich 
zwischenzeitlich nicht ändern wird.
- Die doppelte Berechnung von "currentMillis - startMillis" habe ich 
optimiert.
- SensorCount wird niemals auf 0 zurück gesetzt. Stattdessen berechne 
ich die Differenz zum vorher verwendeten Wert. Dadurch vermeide ich das 
Verlieren von Counts, wenn Interrupts während der Ausführung der 
Berechnung auftreten.
- Beim Lesen von sensorCount sperre ich die Interrupts, da it die 
variable nicht verändert wird, während sie gelesen wird. Das muss man 
immer machen, sobald die Variable größer als ein Byte ist, sonst kommt 
im Konfliktfall Schrott heraus.
- Bei der Berechnung verwende ich ausschließlich Integer Arithmetik. Bei 
beiden Zeilen multipliziere ich zuerst, bevor ich dividiere, um 
Rundungsfehler zu minimieren. Hierbei muss man aber aufpassen, den 
Wertebereich nicht zu überschreiten. In diesem Fall ist es relativ 
einfach, weil unsigned long groß genug ist (sonst hätte ich noch casten 
müssen).

Wenn der gemessene Wert zur Anzeige gebracht wird, kann man ja immer 
noch durch 1000 dividieren, um Milliliter in Liter umzurechnen. Mit dem 
Modulus Operator käme man dann an die Nachkommastellen.
1
vorDemKomma  = milliLitersPerHour / 1000;
2
nachDemKomma = milliLitersPerHour % 1000;

Wegen dem Casten, folgende Rechnung würde nicht funktionieren:
1
unsigned int a=30000;
2
unsigned int b=40000;
3
unsigned int c=(a+b)/1000;

Weil a+b nicht mehr in einen Integer rein passt. Aber durch casten kann 
man erzwingen, dass die Zwischenergebnisse einen größeren Typ bekommen:
1
unsigned int c= ((unsigned long) a+b)/1000;

a+b passt in einen unsigned long rein, und das kann man danach auch 
korrekt durch 1000 teilen.

Nachdem man das mit dem Casten verstanden hat, kann man die Berechnung 
so umstellen, damit sie genauer wird (weniger Rundungsfehler):
1
unsigned long milliLitersPerHour = (unsigned long long) count*1000*60000/435/elapsedTime;

Das wäre dann aber eine 64 Bit Operation - für den kleinen 8 Bitter 
ziemlich teuer.

von Paul H. (pauleheisster)


Lesenswert?

Stefanus F. schrieb:
> Ungeprüfter Vorschlag:

Danke dir vielmals für dein Vorschlag Stefanus. Ich merke jetzt erst wie 
oberflächlich meine C++ (oder C#?)-Kenntnisse sind. Da ist noch viel zu 
lernen.

Ich werde dein Vorschlag gleich mal testen und berichte dann!

von Tobias S. (x12z34)


Lesenswert?

<offtopic>

Ich bin mal so frei und klinke mich ein, da mich das Thema ebenfalls 
interessiert...

Ich hätte da mal eine Frage zu einem Teilaspekt:

Stefanus F. schrieb:
> - Beim Lesen von sensorCount sperre ich die Interrupts, da it die
> variable nicht verändert wird, während sie gelesen wird. Das muss man
> immer machen, sobald die Variable größer als ein Byte ist, sonst kommt
> im Konfliktfall Schrott heraus.

Ich bin über die gleiche Problematik gestolpert, ein Bekannter empfahl 
(ohne Begründung) die Verwendung des ATOMIC_BLOCK.

Du verwendet jedoch cli()/sei()

Kannst Du / könnt Ihr mir sagen, ob das eine deutliche Vorteile 
gegenüber dem anderen hat bzgl. Geschwindigkeit / Speicherverbrauch oder 
ist das "gehupft wie gesprungen"?

</offtopic>

von STK500-Besitzer (Gast)


Lesenswert?

Tobias S. schrieb:
>Ich bin über die gleiche Problematik gestolpert, ein Bekannter empfahl
> (ohne Begründung) die Verwendung des ATOMIC_BLOCK.

> Du verwendet jedoch cli()/sei()
>
> Kannst Du / könnt Ihr mir sagen, ob das eine deutliche Vorteile
> gegenüber dem anderen hat bzgl. Geschwindigkeit / Speicherverbrauch oder
> ist das "gehupft wie gesprungen"?

Das kommt am Ende beides auf das Gleiche hinaus.

> ATOMIC_BLOCK
ist ein Makro, das auf cli/sei basiert.
Irgendwo müsste die Definition (".h"-Datei) zu finden sein.

von Paul H. (pauleheisster)


Lesenswert?

Klappt alles hervorragend!

Stefanus F. schrieb:
> Wenn der gemessene Wert zur Anzeige gebracht wird, kann man ja immer
> noch durch 1000 dividieren, um Milliliter in Liter umzurechnen. Mit dem
> Modulus Operator käme man dann an die Nachkommastellen.
> vorDemKomma  = milliLitersPerHour / 1000;
> nachDemKomma = milliLitersPerHour % 1000;

Für die Anzeige auf einem Nextion Display benötige ich das Ergebnis 
zwingend in Float.

Ich habe das ganze jetzt so gelöst:
1
   float durchfluss = (float)milliLitersPerHour;
2
   durchfluss = durchfluss/1000;
3
4
   Serial1.print("t6.txt=");
5
   Serial1.write(0x22);  // \"
6
   Serial1.print(durchfluss2);
7
   Serial1.write(0x22);
8
   Serial1.write(0xFF);
9
   Serial1.write(0xFF);
10
   Serial1.write(0xFF);

Sollte das Ergebnis auch nicht mehr verfälschen oder?

: Bearbeitet durch User
von Wolfgang (Gast)


Lesenswert?

Paul H. schrieb:
> float durchfluss = (float)milliLitersPerHour;
>    durchfluss = durchfluss/1000;

Du verwendest du Variablentypen schon wieder nach dem Zufallsprinzip
durchfluss ist ein float, 1000 ein int ;-)
1000.0 wäre ein float-Konstante

von Paul H. (pauleheisster)


Lesenswert?

Wolfgang schrieb:
> Paul H. schrieb:
>> float durchfluss = (float)milliLitersPerHour;
>>    durchfluss = durchfluss/1000;
>
> Du verwendest du Variablentypen schon wieder nach dem Zufallsprinzip
> durchfluss ist ein float, 1000 ein int ;-)
> 1000.0 wäre ein float-Konstante

Ganz getreu dem Motto "Der Compiler wirds schon richten".
Spaß beiseite. Recht hast du. Dankeschön Wolfgang!

von Stefan F. (Gast)


Lesenswert?

So macht man das ohne float, ist etwas effizienter:
1
unsigned long vorDemKomma  = milliLitersPerHour / 1000;
2
unsigned long nachDemKomma = milliLitersPerHour % 1000;
3
char buffer[10];
4
sprintf(buffer,"%lu,%03lu",vorDemKomma,nachDemKomma);
5
Serial1.print(buffer);

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.