Forum: Mikrocontroller und Digitale Elektronik Normierung von Sensorenwerten


von braumeister (Gast)


Lesenswert?

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)
2
// dispLo  = Ausgang unteres Limit
3
// dispHi  = Ausgang oberes Limit
4
// inLo    = Eingang Minimum
5
// inHi    = Eingang Maximum
6
// aktualValue= Normierter Ausgang.
7
8
9
int8_t normierung (uint16_t input, int16_t dispLo,int16_t dispHi,int16_t inLo,int16_t inHi, double *aktualValue)
10
{
11
  if ((inHi-inLo)==0) return(-3);
12
  *aktualValue=(double)((dispHi-dispLo)/(inHi-inLo))* (input-inLo)+ dispLo;
13
  if (input<inLo) return(-1);
14
  if (input>inHi) return(-2);
15
  return(1);
16
}

wie kann man diese Funktion noch verbessern bzw sparsamer oder 
effizienter machen?

von Felix (Gast)


Lesenswert?

Erstmal deine Division durch Null bei deinem return(-3) entfernen.

((dispHi-dispLo)/(inHi-inLo))

mit (inHi-inLo) == 0.

von Felix (Gast)


Lesenswert?

Ah, vergiss es. Da kommt man dann ja nicht hin.

von Wolfgang (Gast)


Lesenswert?

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.

von Johannes S. (Gast)


Lesenswert?

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.

von Joe F. (easylife)


Lesenswert?

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;
}

: Bearbeitet durch User
von Wolfgang (Gast)


Lesenswert?

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.

von Axel S. (a-za-z0-9)


Lesenswert?

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.

von Johannes S. (Gast)


Lesenswert?

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
int8_t normierung (uint16_t input, int16_t dispLo,int16_t dispHi,int16_t inLo,int16_t inHi, double *aktualValue)
2
{
3
  if ((inHi-inLo)==0) return(-3);
4
  *aktualValue=(((float)dispHi-dispLo)/(inHi-inLo))* (input-inLo)+ dispLo;
5
  if (input<inLo) return(-1);
6
  if (input>inHi) return(-2);
7
  return(1);
8
}
9
10
int8_t normierung2 (long input, long dispLo, long dispHi, long inLo, long inHi, double *aktualValue)
11
{
12
  if ((inHi-inLo)==0) return(-3);
13
  *aktualValue=(double)((dispHi-dispLo)/(inHi-inLo))* (input-inLo)+ dispLo;
14
  if (input<inLo) return(-1);
15
  if (input>inHi) return(-2);
16
  return(1);
17
}
18
19
// main() runs in its own thread in the OS
20
int main() {
21
    uint32_t x = 0;
22
23
    volatile uint16_t adcValue = 100;
24
    double result;
25
26
    normierung(adcValue, 0, 10000, 0, 1024, &result);
27
    printf("result (float): %f\n", result);
28
29
    normierung2(adcValue, 0, 10000, 0, 1024, &result);
30
    printf("result (long): %f\n", result);

liefert:
1
result (float): 976.562500
2
result (long): 900.000000

von Yalu X. (yalu) (Moderator)


Lesenswert?

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
typedef uint16_t  inType;
2
typedef double   outType;
3
4
static int8_t normalize(
5
     inType  inLo,  inType  inHi,  inType   in,
6
    outType outLo, outType outHi, 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
  const outType factor = (outHi - outLo) / (inHi - inLo);
15
  const outType offset = outLo - factor * inLo;
16
  *out = factor * in + offset;
17
  return 0;
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
  const outType factor = (outHi - outLo) / (inHi - inLo);
2
  const outType offset = 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.).

: Bearbeitet durch Moderator
von pista (Gast)


Lesenswert?

Dann ist dieser Ansatz ok?

von Billigheimer (Gast)


Lesenswert?

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".

von Weg mit dem Troll (Gast)


Lesenswert?

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.

von Patrick C. (pcrom)


Lesenswert?

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

von pista (Gast)


Lesenswert?

Wie kann ich mit chasts die berchnung mit Double machen und das 
ergebniss dann mit uint16_t zurückgeben?

normalize(0, 1024, 0.00, 5000.0, &result)
1
typedef uint16_t  inType;
2
typedef double   outType;
3
4
int8_t normalize(inType  inLo,  inType  inHi,  inType   in,outType outLo, outType outHi, uint16_t *out)
5
{
6
  if (in < inLo)
7
    return -1;
8
  if (in > inHi)
9
    return -2;
10
  if (inHi - inLo == 0)
11
    return -3;
12
  *out = (uint16_t)(double)(outHi - outLo) / (inHi - inLo) * (in - inLo) + outLo;
13
  return 0;
14
}

von pista (Gast)


Lesenswert?

Ich bekomme leider statt 5000 4092 zurück :(

von Yalu X. (yalu) (Moderator)


Lesenswert?

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:

1
 *out = (uint16_t)((double)(outHi - outLo) / (inHi - inLo) * (in - inLo) + outLo);

von plazebo (Gast)


Lesenswert?

Wie kann man die rechnung umschreiben das es weniger Ressourcen 
verbraucht.

von Wolfgang (Gast)


Lesenswert?

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.

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.