Forum: Compiler & IDEs Runden beim Teilen macht nicht was es soll


von Gastschreiber01 (Gast)


Lesenswert?

Hallo,

normalerweise reicht mir die Suchfunktion des Forums um bei Problemem 
weiter zu kommen, aber diesmal bin ich am Ende :(

Hier meine
Hardware: ATMEGA168
Software: AVR-Studio 4.16 , Optimierung ausgeschaltet

Folgende Zeilen dienen dazu, einen ADC-Wert (hier als Beispiel 100) zu 
summieren und gewichtet zu mitteln. Dadurch sollen sich Spikes am 
Eingang nicht sofort bemerkbar machen und einen Fehler erzeugen, in 
folge dessen ich das Gerät ausschalten würde. Also es soll wie eine Art 
Filter wirken.

Startwert von Variable = 0
1
Variable = Variable*63;
2
Variable = Variable + 100; // 100 als Beispiel für einen ADC-Messwert
3
Variable = Variable/64;

Das geht auch alles ganz prima, bis zur dezimal 37. Dann passiert 
folgendes:
37*63 = 2331
2331+100 = 2431
2431/64 wäre 37,98 ... der Controller rundet aber ab zu 37.

Das bedeutet, solange der Messwert wie im Beispiel 100 wäre und sich 
nicht ändert, was in der Praxis durchaus vorkommt. Das Resultat bliebe 
also immer bei 37 wenn der Messwert 100 ist, bei anderen Messwerten 
würde er wahrscheinlich woanders hängen bleiben.

Wie würdet Ihr das Problem angehen ? Fliesskomma wollte ich eigentlich 
vermeiden. Wie bekomme ich den Controller zum aufrunden ?

Die Hardware ist mir vorgegeben und auch das ich obige Vorgehensweise 
anwenden soll. Ist keine Studienaufgabe, nur ein hirntauber 
Chefingenieur ... Geschichten könnt ich Euch erzählen.

Mikrocontroller sind immer noch Neuland für mich.

von Klaus W. (mfgkw)


Lesenswert?

In C wird bei Division ganzer Zahlen nicht gerundet, sondern 
abgeschnitten.

Abhilfe: z.B. 32 addieren, dann durch 64 teilen; kommt etwa aufs Gleiche 
raus.

von Mark B. (markbrandis)


Lesenswert?

Hm, normal würde ich sagen

wert = (int) (wert + 0.5);

zum Runden. Aber geht ja nicht ohne Fließkomma. Wie wäre das 
(ungetestet):

wert = (int) ((2*wert + 1)/2);

von yalu (Gast)


Lesenswert?

Zu nächsten ganzen Zahl auf- bzw. abgerundet wird dann, wenn du vor der
Division zum Dividenden den halben Divisor addierst, was auf etwa das
Gleiche hinausläuft wie der Vorschlag von Mark Brandis. Ersetzt du die
letzte Zeile durch
1
Variable = (Variable+32)/64;

sollte das Gewünschte herauskommen.

So richtig gut funktioniert die Filterung der Werte aber immer noch
nicht: Ist bspw. der Startwert von Variable 0 und der ADC-Wert konstant
31, wird das Ergebnis wegen der Rundung auch nach beliebig vielen
Durchläufen 0 bleiben, obwohl man erwarten würde, dass das Ergebnis
asymptotisch gegen 31 geht. Um dieses Problem einzudämmen, muss die
Rechnung mit höherer Genauigkeit durchgeführt werden, so dass der
Rundungsfehler nicht so stark ins Gewicht fällt.

von Gastschreiber01 (Gast)


Lesenswert?

Vielen Dank für die Antworten. In einem anderen Thread habe ich noch 
gefunden (von yalu) Beitrag "Mikrocontroller und runden?"  :

Division der positiven Ganzzahlen a und b mit Aufrunden:

(a+b-1)/b

Das scheint eine leichte Abwandlung der vorgeschlagenen Lösungen zu 
sein. Es funktionieren wahrscheinlich alle Eure Vorschläge ... das 
Prinzip scheint zu sein, den Dividend entscheidend zu vergrössern, so 
dass immer aufgerundet wird ?

Ich probier jetzt noch mal die Varianten durch.

Besten Dank !

von Oliver (Gast)


Lesenswert?

>Mikrocontroller sind immer noch Neuland für mich.

Mach dir mal klar, was Integer-Zahlen sind, und wie damit gerechnet 
wird. Da exisitiert kein 37,98.
Bei der Division gibt es bei deiner Rechnung als Ergebnis 2431/64 = 37 
Rest 63, und der Rest wird vernachlässigt.

Das ist in übrigen so ziemlich auf jeder Hardware und in jeder 
Programmiersprache so. Irgendwann hat dann mal jemand Festkommazahlen 
und Fliesskommazahlen erfunden. Ausserdem kann man Integerwerte 
skalieren. Frag mal deinen Chefingenieur, was der dazu meint.

Oliver
P.S. Nimm mal an Stelle von 100 als ADC-Wert 62, und schau, was dann 
passiert...

von Oliver (Gast)


Angehängte Dateien:

Lesenswert?

So siehts aus:

violett: Fliesskomma
gelb: Deine Originalversion
blau: mit Variable = (Variable+32)/64;

Alles eher subiotimal :-)

Oliver

von Gastschreiber01 (Gast)


Lesenswert?

Mir ist schon klar dass der Controller keine 37,98 kennt und einfach auf 
37 abschneidet. Hab ich auch eindeutig oben beschrieben. Solche dummen 
Kommentare kannst Du Dir sparen ... hab hier schon öfter solchen 
Zynismus von Dir gelesen der keinem weiter hilft.

von Gastschreiber01 (Gast)


Lesenswert?

(a+b-1)/b funktioniert bislang gut ... ich teste das jetzt weiter.

Vielen Dank trotzdem für alle Antworten.

von Oliver (Gast)


Lesenswert?

>Mir ist schon klar dass der Controller keine 37,98 kennt ...

Aber die Folgen sind dir nicht klar. Rechnen mit Integer-Werten ist 
nicht trivial (siehe Bild).

>Solche dummen Kommentare kannst Du Dir sparen ...

:-)

Oliver

von Benedikt K. (benedikt)


Lesenswert?

Oliver schrieb:
>>Mir ist schon klar dass der Controller keine 37,98 kennt ...
>
> Aber die Folgen sind dir nicht klar. Rechnen mit Integer-Werten ist
> nicht trivial (siehe Bild).

Wenn man es richtig macht schon: Bau mal folgende Lösung in dein Bild 
ein:

Variable = Variable-Variable/64+ADCWert;
Ergebnis=Variable/64;

von Edi (Gast)


Lesenswert?

Es gibt durchaus eine Lösungsmöglichkeit für Dein Problem. Der 
eigentliche Fehler entsteht nämlich nicht, wie man vermuten würde, bei 
der Division durch 64 und der folgenden Rundung, sondern beim 
Multiplizieren des gerundeten Wertes mit 63. Hier wird quasi der 
Rundungsfehler um den Faktor 63 vergrößert.

In solchen Fällen arbeite ich mit einer Hilfsgröße, ich nenne sie mal 
phantasiereich "TempVar". Der Algorithmus schaut dann so aus:
1
Startwert von Variable = 0
2
Startwert von TempVar = 0
3
4
TempVar = TempVar - Variable + 100; // 100 als Beispiel für einen ADC-Messwert
5
Variable = (Variable + 32) / 64;

Es wird in "Tempvar" also immer der 64fache Wert von "Variable" 
gehalten, intern werden also alle "Nachkommastellen", die bei der 
Division durch 64 verschwinden würden, in "TempVar" aufbewahrt. Nach 
außen hin wird die geteilte und gerundete "Variable" verwendet. Statt 
der Multiplikation mit 63 wird vom 64fachen Wert von "Variable", also 
von "TempVar", einfach einmal "Variable" abgezogen, dann hat man in 
"TempVar" nur noch den 63fachen Wert von "Variable". Diesem Wert addiert 
man den neuen Messwert.

Mit diesem Verfahren läuft Dein "Variable" langsam aber sicher auf den 
gewünschten Endwert von 100.

von Gastschreiber01 (Gast)


Lesenswert?

Variable = Variable-Variable/64+ADCWert;
Ergebnis=Variable/64;

Danke, Oliver ... probier ich gleich ... sorry dass ich eben so 
ungehalten war ... mein Chefingenieur macht mich wahnsinnig ... bin nahe 
dem Nervenzusammenbruch :(

von Oliver (Gast)


Lesenswert?

>Bau mal folgende Lösung in dein Bild ein:

>Variable = Variable-Variable/64+ADCWert;
>Ergebnis=Variable/64;

Da Variable anfangs kleiner 64 ist, bleibt Variable/64 bei ADC-Wert = 
100 immer Null, und das Ergebnis immer 1.

Solange da mit Integern gerechnet wird, muss der Zahlenbereich so 
skaliert werden, daß die Rundungsfehler bei allen zu erwartenden 
ADC-Werten vernachlässigbar werden. Ob das überhaupt geht, ist fraglich. 
Das ist ein rekursives Filter, da summieren sich die Rundungsfehler auf.

Am besten umformulieren in ein nicht-rekursives Filter. Ansonsten mit 
hochskalierten 32-Bit-Integern, oder Fliesskomma.

Oliver

von Benedikt K. (benedikt)


Lesenswert?

Oliver schrieb:
>>Bau mal folgende Lösung in dein Bild ein:
>
>>Variable = Variable-Variable/64+ADCWert;
>>Ergebnis=Variable/64;
>
> Da Variable anfangs kleiner 64 ist, bleibt Variable/64 bei ADC-Wert =
> 100 immer Null, und das Ergebnis immer 1.

Nein, denn in jedem Schritt wird immer 100 aufaddiert -> baus ein, dann 
siehst du es. Das Ergebnis dürfte der Fließkommaversion sehr ähnlich 
sein.

von Oliver (Gast)


Lesenswert?

>Nein, denn in jedem Schritt wird immer 100 aufaddiert -> baus ein, dann
>siehst du es.

Mein Fehler, Ergebnis != Variable.

Sehen tut man im Bild da nichts, die Kurve liegt so gut wie auf der 
Flieskommakurve.

So gehts.

Oliver

von Oliver (Gast)


Angehängte Dateien:

Lesenswert?

Hier das Bild mit ADC=10. Da sieht man eher was.

Oliver

von Gastschreiber01 (Gast)


Lesenswert?

Ich bin einfach zu dämlich um das zu begreifen:

habe jetzt geschrieben:
1
Variable = Variable*63;
2
Variable = Variable - (Variable/64) + 100; // ADC-Wert = 100 als Beispiel
3
Variable = Variable/64;

Variable ist dann mein Ergebnis.

Jedenfalls bleibt es jetzt bei 19 stehen.

Ich habe die Mathematik dahinter noch nicht begriffen ... ist ganz 
sicher falsch programmiert.

von Oliver (Gast)


Lesenswert?

>Variable ist dann mein Ergebnis.

Da bin ich ja auch erst drauf reingefallen:

Ergebnis und Variable sind zwei unterschiedliche Werte.

Schreib genau das hin, was Benedikt vorgegeben hat. Deine Variante ist 
jetzt was völlig anderes.
1
Variable = Variable-Variable/64+ADCWert;
2
Ergebnis=Variable/64;

Und wenn dein Chefingenieur nicht glauben will, daß das mathematisch das 
gleiche ist, wie die o.a. Ausgangsformel, dann schick den hier vorbei 
:-)

Oliver

von Michael G. (linuxgeek) Benutzerseite


Lesenswert?

Gastschreiber01 schrieb:

> 2431/64 wäre 37,98 ... der Controller rundet aber ab zu 37

Was vollkommen korrekt ist. Denn der implizite cast auf Integer 
schneidet Dir den Nachkomma-Anteil ab. Fertig. Wenn Du Runden willst 
brauchst Du entsprechende Funktionen, besser aber man erledigt es dann 
alles mit Integers, das spart viel Code.

von Gastschreiber01 (Gast)


Lesenswert?

Jaaaa ... es geht ... vielen, vieln Dank an alle, v.a. Oliver u. 
Benedikt ..... jetzt versuch ich das ganze mal auf Papier bzw. Excel 
nach zu vollziehen damit ich auch verstehe was da passiert !

von Benedikt K. (benedikt)


Lesenswert?

Streng genommen dürfte diese Lösung eine Festkommarechnung sein, denn 
der Wert 64 entspricht einer 1.

Der Trick ist also, wie weiter oben schon geschrieben, mit einer höheren 
Auflösung zu rechnen (hier der 64x), was ganz einfach dadurch geschieht 
das erst beim Endergebnis geteilt wird. Dadurch addieren sich die 
Rundungsfehler nicht nso stark auf. Man könnte das ganze noch verbessern 
wenn man auch hier richtig runden würde, aber das ist meist unnötig.

von Karl H. (kbuchegg)


Lesenswert?

Gastschreiber01 schrieb:
> Jaaaa ... es geht ... vielen, vieln Dank an alle, v.a. Oliver u.
> Benedikt ..... jetzt versuch ich das ganze mal auf Papier bzw. Excel
> nach zu vollziehen damit ich auch verstehe was da passiert !

Im Grunde ist das sehr simpel.

Du willst addieren:
 2 Euro 50; 3 Euro 80; 5 Euro 15

Also: 2.50 + 3.80 + 5.15
Blöd ist nur, dass du keine Kommastellen benutzen darfst, also addierst 
du

  2 + 3 + 5  = 10

(ist auch ok so, denn du deinen Kunden interessieren nur ganze Euros)

Das richtige Ergebnis wär aber gewesen:

  2.50 + 3.80 + 5.15 = 11.45

und das ist ganz was anderes. Selbst als ganze Zahl sind das immer noch 
11 und nicht 10

Aber du bist ja nicht blöd.
Wenn du nur mit ganzen Zahlen rechnen darfst, dann rechnest du eben in 
Cent anstatt mit Euros

  250 + 380 + 515 = 1145

Voila, nur ganze Zahlen.
Wenn du damit weiterrechnen musst, dann machst du das auch weiterhin in 
Cent. Nur deinem Kunden gegenüber dividierst du schnell durch 100, weil 
du ja weißt, dass deine ganzen Zahlen einfach das Hundertfache des 
Eurobetrags sind.
Deinem Kunden sagst du also, das Ergebnis sei 11 und der wundert sich 
die ganze Zeit, weil er sich fraget wie du das bloss machst, dass du 
ohne Fliesskommaarithmetik ein Ergebnis hinzauberst, dass so gar nicht 
möglich ist wenn er das mit ganzen Zahlen nachrechnet :-) Nur rechnet er 
eben in Euros und du in Cent.

von Gastschreiber01 (Gast)


Lesenswert?

OK ... das was Karl Heinz beschrieben hat leuchtet mir ein.

In der Vorgehensweise von Benedikt und Oliver finde ich jedoch die 
"Umrechnung in Euro-Cent" ... um beim Beispiel zu bleiben nicht wieder 
... kann es sein, dass das Beispiel von Benedikt ein wenig wie das 
schriftliche Dividieren in der Schule ist ? (wo ich besser hätte 
aufpassen sollen, dann bräuchte ich hier nicht meine Dämlichkeit zur 
Schau zu stellen ;)

von Karl H. (kbuchegg)


Lesenswert?

Gastschreiber01 schrieb:

> In der Vorgehensweise von Benedikt und Oliver finde ich jedoch die
> "Umrechnung in Euro-Cent" ... um beim Beispiel zu bleiben nicht wieder

Jetzt enttäuscht du mich aber :-(

Variable = Variable-Variable/64+ADCWert;
Ergebnis=Variable/64;

Dein 'Euro' hat 64 'Cent'.

In Variable ist die Summe in 'Cent', Ergebnis ist das 'Euro'-Ergebnis, 
welches der Kunde zu sehen kriegt.

von Michael W. (retikulum)


Lesenswert?

>Dein 'Euro' hat 64 'Cent'.

galoppierende Inflation :-)

von Oliver (Gast)


Lesenswert?

>In der Vorgehensweise von Benedikt und Oliver

Nimm mich da ruhig raus, die Lösung kam alleine von Benedikt.

Oliver

von Gastschreiber01 (Gast)


Lesenswert?

Ich glaube meine Gehirnzelle gaukelt mir Verständnis vor.
1
Variable = Variable - Variable/64 + ADCWert;
2
Ergebnis=Variable/64;

Angenommen die Messwerte sind konstant, erreicht "Variable" irgendwann 
das 64-fache des Messwertes. Beim Subtrahieren von Variable/64 wird dann 
wieder ein Betrag in Höhe des Messwertes abgezogen. Bleiben die 
Messwerte konstant, addiert man den eben gerade abgezogenen Wert wieder 
dazu und Ergebnis bleibt unverändert solange die ADC-werte konstant 
bleiben. Tritt eine Störung und daher ein anderer ADC-Wert auf, kommt es 
zu einem neuen Ergebnis, in welches der ADC-Wert gewichtet einfliesst. 
Je nachdem welchen Teiler man nimmt, hier 64 ... fällt eine 
Messwertänderung mehr oder weniger ins Gewicht.

Wenn Variable und Ergebnis beide static mit 0 initialisiert werden, 
könnte man bestimmt auch schreiben:
1
Variable = Variable - Ergebnis + ADCWert;
2
Ergebnis=Variable/64;
?

Wobei ich nicht sicher bin was platzsparender ist, eine weitere 
statische Variable oder das 2 Mal durch 64 teilen ?

Edi hat weiter oben vorgeschlagen:
1
Startwert von Variable = 0
2
Startwert von TempVar = 0
3
4
TempVar = TempVar - Variable + 100; // 100 als ADC-Messwert
5
Variable = (Variable + 32) / 64;

^^Das müsste auch funktionieren.

In einem anderen Thread habe ich gefunden:
Zitat: Beitrag "gleitender Mittelwert im Atmel"
"
Autor: Lothar Miller (lkmiller)
Datum: 28.11.2008 14:55

Ich mach das immer gern mit 2er-Potenzen, dann hats der Compiler
leichter:

unsigned long avg_val, avgmem, new_val;
:
avgmem -= avgmem/128; // Filterbreite 128 Abtastungen, alten Wert 
abziehen
avgmem += new_val;    // neuen Wert aufaddieren
avg_val = avgmem/128; // Mittelwert berechnen

Und keine Unterscheidungen zwischen Startup und Betrieb.
Das macht ein "normales" analoges RC-Glied auch nicht  ;-)
" Zitat Ende.

Das ganze scheint sich "gleitender Mittelwert" zu nennen.

von Karl H. (kbuchegg)


Lesenswert?

Gastschreiber01 schrieb:

> Das ganze scheint sich "gleitender Mittelwert" zu nennen.

Gratulation an deine kleinen grauen Zellen :-)

von Benedikt K. (benedikt)


Lesenswert?

Gastschreiber01 schrieb:

> Je nachdem welchen Teiler man nimmt, hier 64 ... fällt eine
> Messwertänderung mehr oder weniger ins Gewicht.

Ja, genauso ist es.

> Wenn Variable und Ergebnis beide static mit 0 initialisiert werden,
> könnte man bestimmt auch schreiben:
>
>
1
> Variable = Variable - Ergebnis + ADCWert;
2
> Ergebnis=Variable/64;
3
>
> ?

Ja. Das verbraucht halt wieder etwas RAM, da Ergebnis immer gespeichert 
werden muss für den nächsten Durchlauf.
Ich lasse das ganze im ADC Interrupt laufen. Die Variable enthält dann 
den gefilterten, 64x ADC Wert. Diesen lade und teile ich nur wenn ich 
ihn benötige. Auf diese Art bekommt man wunderbar stabile ADC Werte.

> Edi hat weiter oben vorgeschlagen:
>
1
> Startwert von Variable = 0
2
> Startwert von TempVar = 0
3
> 
4
> TempVar = TempVar - Variable + 100; // 100 als ADC-Messwert
5
> Variable = (Variable + 32) / 64;
6
>
>
> ^^Das müsste auch funktionieren.

Ist letzendlich das gleiche, nur dass hier noch korrekt gerundet wird. 
Das ist denke ich aber vernachlässigbar, denn der ADC selbst ist mit 
Sicherheit ungenauer als dieser kleine Fehler.

> unsigned long avg_val, avgmem, new_val;
> :
> avgmem -= avgmem/128; // Filterbreite 128 Abtastungen, alten Wert
> abziehen
> avgmem += new_val;    // neuen Wert aufaddieren
> avg_val = avgmem/128; // Mittelwert berechnen

Das ist anders geschrieben, aber macht genau das gleiche wie obige 
Lösungen.

> Das macht ein "normales" analoges RC-Glied auch nicht  ;-)
> " Zitat Ende.
>
> Das ganze scheint sich "gleitender Mittelwert" zu nennen.

Ja, bzw. es ist ein IIR Filter. Die Wirkung ist identisch mit der eines 
RC Glieds, wobei der Teilerfaktor in etwa der Zeitkonstanten entspricht.

von Oliver (Gast)


Lesenswert?

>Variable = Variable - Ergebnis + ADCWert;
>Ergebnis=Variable/64;

Funktioniert

>Edi hat weiter oben vorgeschlagen:Startwert von Variable = 0
>>Startwert von TempVar = 0

>>TempVar = TempVar - Variable + 100; // 100 als ADC-Messwert
>>Variable = (Variable + 32) / 64;

Das ist bis auf den Rundungs-Summanden "+ 32" das gleiche, und 
funktioniert daher auch.

Oliver

von Gastschreiber01 (Gast)


Lesenswert?

Vielen Dank Benedikt und alle anderen ... hab heute wieder viel dazu 
gelernt !
Jetzt hab ich saubere Messwerte und kann anfangen das eigentliche 
Programm zu schreiben.

von Oliver (Gast)


Lesenswert?

>Ja, bzw. es ist ein IIR Filter.

Wobei die Realisierung mit Integern und deren Rundungsfehlern daraus 
dann doch wieder ein FIR macht - die Impulsantwort wird nach endlicher 
Zeit zu Null.

Oliver

von Edi (Gast)


Lesenswert?

Der Vollständigkeit halber:

>Ist letzendlich das gleiche, nur dass hier noch korrekt gerundet wird.
>Das ist denke ich aber vernachlässigbar, denn der ADC selbst ist mit
>Sicherheit ungenauer als dieser kleine Fehler.

Um ehrlich zu sein, habe ich mir bisher überhaupt noch keine Gedanken 
darüber gemacht. Aber jetzt habe ich mir die beiden Fälle genauer 
angeschaut, und  Benedikt K. (benedikt) hat vollkommen recht: Die 
Unterschiede zwischen der Lösung mit korrekter Rundung und Abrunden sind 
so unbedeutend, dass ich in Zukunft einfach nur abrunde und auf das +32 
verzichte.

von sebastian (Gast)


Lesenswert?

Hier mal was ganz einfaches um ADC Werte von 0 - 1023 zu mitteln und zu 
runden.

Gemittelt werden sollen 4 Werte.

u8_ADC_Summe >>= 1;
u8_ADC_Summe ++;
u8_ADC_Summe >>= 1;

Voila! Durch 4 geteilt und sogar ab 0.5 hoch und unter 0.5 nach unten 
gerundet.

Beispiel:

Der (errechnete) Wert von 346.75 würde zu 437 gerundet.
Der (errechnete) Wert von 123.25 würde zu 123 gerundet.

Ciao

von sebastian (Gast)


Lesenswert?

Der (errechnete) Wert von 346.75 würde zu 437 gerundet.

muss natürlich

Der (errechnete) Wert von 346.75 würde zu 347 gerundet.

heissen. Kleiner Dreher drin :-)

von Simon K. (simon) Benutzerseite


Lesenswert?

sebastian schrieb:
> Hier mal was ganz einfaches um ADC Werte von 0 - 1023 zu mitteln und zu
> runden.
>
> Gemittelt werden sollen 4 Werte.
>
> u8_ADC_Summe >>= 1;
> u8_ADC_Summe ++;
> u8_ADC_Summe >>= 1;
>
> Voila! Durch 4 geteilt und sogar ab 0.5 hoch und unter 0.5 nach unten
> gerundet.

Was aber mathematisch als auch von der Anzahl der Taktzyklen her 
identisch sein sollte mit dem

u8_ADC_Summe += 2;
u8_ADC_Summe /= 4;

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.