Guten Tag,
es kommt ja immer wieder vor das man zB einen mit dem ADC gemessene
Spannung in eine Andere Physikalische Größe umrechnen muss.
Dazu habe ich Folgende einfache Funktion geschrieben.
1
// input = Wert der in umgerechnet wird.(zb ADC_Ergebnis)
braumeister schrieb:> wie kann man diese Funktion noch verbessern bzw sparsamer oder> effizienter machen?
Indem man sich überlegt, ob man wirklich Float braucht oder man nicht
genauso gut mit Fixkomma-Werten fährt.
Die Dynamik von Float stellt dir der ADC sowieso nicht zur Verfügung.
an dem ersten Term sind nur integer beteiligt, durch die Division
verlierst du also Genauigkeit:
1
((dispHi-dispLo)/(inHi-inLo))
Danach wird das auf double augeblasen wo ein float reichen würde.
Auch wenn auf AVR der double einem float entspricht finde ich es ist
eine Unsitte da pauschal double zu nehmen, in unzähligen Arduino Libs
findet man das auch. Wenn man die auf einen Cortex-M portiert erlebt man
dann erstmal eine schöne Überraschung.
Im wesentlichen entspricht das der map() funktion aus der Arduino-Welt.
Dort wird mit "long" gerechnet, und alle deine Sicherheits-Prüfungen
werden weggelassen. Man muss halt sinnvolle input/output ranges
verwenden.
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) +
out_min;
}
Johannes S. schrieb:> an dem ersten Term sind nur integer beteiligt, durch die Division> verlierst du also Genauigkeit:((dispHi-dispLo)/(inHi-inLo))
Nicht nur Genauigkeit, sondern insbesondere die Auflösung geht flöten.
braumeister schrieb:> wie kann man diese Funktion noch verbessern bzw sparsamer oder> effizienter machen?
Indem man sie wegläßt und die entsprechende Rechnung einfach direkt an
der jeweiligen Stelle im Programm einsetzt.
Mal so ein paar Punkte:
1. die Fehlerprüfung in der Funktion ist unsinnig. Setzt du ernsthaft
jeden Aufruf der Funktion in ein if() und prüfst nachher, ob du
eventuell zu blöd warst, die Funktion richtig aufzurufen? Und wenn ja,
was macht dein Programm dann an dieser Stelle?
2. wenn man 1. beachtet, dann fällt der Krampf mit dem simulierten
"return by reference" weg.
3. und der Rumpf der Funktion läßt sich in einer einzigen Zeile
hinschreiben.
4. in der Praxis werden die Grenzen (dispLo, dispHi, inLo, inHi) in
99.9% der Fälle Konstanten sein. Wenn man überhaupt irgendeine
Fehlerprüfung machen will, dann zur Compilezeit und nicht zur Laufzeit.
5. wenn die Grenzen aber Konstanten sind, dann kann der Compiler einige
Rechenschritte schon zur Compilezeit durchführen. Folge: das Programm
läuft schneller.
Fazit: das ist ein typisches Beispiel für Overengineering.
Joe F. schrieb:> Im wesentlichen entspricht das der map() funktion aus der Arduino-Welt.> Dort wird mit "long" gerechnet
auch wenn man mit long long rechnen würde, es bleibt eine Integer
Division und die Nachkommastellen sind weg. Man müsste erst
raufskalieren, aber diese Integer rechnerei ist fehlerträchtig, die µC
haben heute kein Problem damit in float zu rechnen, spätestens beim CM4F
ist die Optimiererei nur noch Theorie.
1. Entscheide dich bei der Benennung von Variablen, Funktionen usw. für
eine Sprache, also entweder die englische oder die deutsche, aber
nicht alles durcheinander.
2. Es bringt nichts, die Berechnung (einschließlich der Division) in int
auszuführen und anschließend das Ergebnis in double zu casten. Das
braucht nur mehr Rechenzeit, ohne dass die Genauigkeit erhöht wird
(s. Kommentar von Johannes).
3. Wenn du ein double-Ergebnis möchtest, sollten sinnvollerweise auch
die Grenzen dispLo und dispHi vom Typ double sein.
4. Die Überprüfung, ob der Eingabewert im zulässigen Intervall liegt,
kann schon vor der eigentlichen Berechnung erfolgen, was ggf.
Rechenzeit einspart. Oder du lässt sie gleich komplett weg (s.
Kommentar von Axel).
5. Da in 99% aller Fälle die Grenzen inLo, inHi, dispLo, dispHi konstant
sind, sollte man dafür sorgen, dass in diesem Fall möglichst viel zur
Compilezeit berechnet werden kann, so dass zur Laufzeit nur noch 1
Multiplikation und 1 Addition (statt 1 Multiplikation, 1 Division, 1
Addition und 3 Subtraktionen) erforderlich sind. Zudem wird dann auch
die Überprüfung der Division durch 0 wegoptimiert.
6. Bei Fehlercodes, die mehr als einen Fehlerfall unterscheiden, ist es
üblich, dem OK-Fall den Code 0 zu geben.
Ich habe die obigen Punkte mal umgesetzt und dabei auch die Argumente
etwas konsistenter benannt:
1
typedefuint16_tinType;
2
typedefdoubleoutType;
3
4
staticint8_tnormalize(
5
inTypeinLo,inTypeinHi,inTypein,
6
outTypeoutLo,outTypeoutHi,outType*out)
7
{
8
if(in<inLo)
9
return-1;
10
if(in>inHi)
11
return-2;
12
if(inHi-inLo==0)
13
return-3;
14
constoutTypefactor=(outHi-outLo)/(inHi-inLo);
15
constoutTypeoffset=outLo-factor*inLo;
16
*out=factor*in+offset;
17
return0;
18
}
Anmerkung: Im Allgemeinen Fall, also wenn alle 4 Intervallgrenzen zur
Compilezeit noch nicht festgelegt sind, benötigt meine Funktion 1
Multiplikation mehr als deine und ist damit etwas weniger effizient.
Angesichts der 6 weiteren Rechenoperation fällt der Unterschied aber
nicht so arg ins Gewicht.
Edit:
Ich sehe gerade, dass man den Term für die Berechnung zumindest beim GCC
nicht auseinanderpflücken muss, da er dies von sich aus schon tut.
Man kann beim GCC also
1
constoutTypefactor=(outHi-outLo)/(inHi-inLo);
2
constoutTypeoffset=outLo-factor*inLo;
3
*out=factor*in+offset;
durch die ursprüngliche Schreibweise ersetzen:
1
*out=(outHi-outLo)/(inHi-inLo)*(in-inLo)+outLo;
Damit entfällt auch der Nachteil der zusätzlichen Multiplikation im
allgemeinen Fall (s.o.).
pista schrieb:> Dann ist dieser Ansatz ok?
Im Prinzip schon, aber er verleitet leicht zu "Bedienfehlern", denn
braumeister schrieb:> // dispHi = Ausgang oberes Limit
sollte z.B. beim ADC "1 mehr als Maxwert" sein.
Bsp 0..5V am 10-Bit ADC:
normierung(x, 0, 1024, 0.00, 5.00, &result)
bzw
normalize(0, 1024, 0.00, 5.00, &result)
Erklärung warum 1024 an der Stelle richtig und 1023 falsch ist:
AVR-Tutorial: ADC: Ein paar ADC-Grundlagen
Also sauber dokumentieren.
ggfs. auch die Tatsache, dass es nicht "den umgerechneten Wert"
zurückgibt, sondern "die untere Grenze des umgerechneten
Wertebereiches".
In den absolut seltensten Faellen werden echte Float Werte benoetigt.
Eigentlich nur fuer eine Anzeige, bei welcher man die Leute beeindrucken
will. Deswegen ist es auch weniger wichtig wie lange so eine Berechnung
dauert. Denn mehr wie vielleichr 3 mal pro Sekunde ist eh nichts mit
ablesen. Und das auch nur wenn das Geraet grad einen display hat. Sobald
ich Werte uebertragen kann, uebertrage ich als Ganzzahl und lasse ich
deren float Werte auf der PC Seite rechnen.
Speziell Logging-, oder Regelwerte laesst man am Besten als
Integerwerte.(Ganzzahl Werte)
Ein 32bit Integer Integrator hat uebrigens einen besseren Bereich wie
ein 32bit float Integrator.
Da die parameters (int16_t dispLo,int16_t dispHi,int16_t inLo,int16_t
inHi) oft gleich sind wuerde ich dafuer ein struct nehmen. Oder enum und
dann die berechnung hard-coded
Du konvertierst die Differenz der beiden double-Parameter in uint16_t,
so dass die anschließende Division in unsigned int ausgeführt wird.
Vielleicht wolltest du Klammern um den nach uint16_t zu konvertierenden
Ausdruck setzen:
plazebo schrieb:> Wie kann man die rechnung umschreiben das es weniger Ressourcen> verbraucht.
Auf einem Prozessor ohne FPU nicht mit Float rechnen. Sobald die
Bibliothek dafür eingebunden wird, schluckt das Resourcen.
Bei einem Sensor, der durch sein Messprinzip auf Ganzzahl festgelegt
ist, wird die Dynamik von Float meist sowieso nicht gebraucht.
Festkommarechnung und alles wird gut - wenn man weiss, was man tut.