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.
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.
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);
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.
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 !
>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...
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.
>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
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;
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
StartwertvonVariable=0
2
StartwertvonTempVar=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.
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 :(
>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
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.
>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
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.
>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
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.
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 !
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.
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.
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 ;)
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.
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
StartwertvonVariable=0
2
StartwertvonTempVar=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.
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
>StartwertvonVariable=0
2
>StartwertvonTempVar=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.
>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
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.
>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
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.
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
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 :-)
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;