Forum: Mikrocontroller und Digitale Elektronik Wo ist in meiner Berechnung der Fehler?


von Tobi S. (tobias_n524)


Lesenswert?

Hallo,

ich habe mir ein kleines Programm geschrieben. "Eigentlich" macht es 
auch das was es soll.
1
#include <SPI.h>
2
#include <Wire.h>
3
#include <Adafruit_GFX.h>
4
#include <Adafruit_SSD1306.h>
5
6
#define SCREEN_WIDTH 128
7
#define SCREEN_HEIGHT 64
8
9
#define OLED_RESET     -1
10
#define SCREEN_ADDRESS 0x3D
11
12
#define pv_max_volt 40
13
#define pv_adc_steps 1023
14
15
#define bat_max_volt 14
16
#define bat_adc_steps 1023
17
18
float pv_volt_per_step = (pv_max_volt * 100 ) / pv_adc_steps;
19
float bat_volt_per_step = (bat_max_volt * 100 ) / bat_adc_steps;
20
21
int pvPin = A1;
22
int batPin = A2;
23
int pvAdcVal = 0;
24
int batAdcVal = 0;
25
26
float pvval = 0;
27
float batval = 0;
28
29
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
30
31
void setup() {
32
  Serial.begin(115200);
33
  pixels.begin();
34
35
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
36
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
37
    Serial.println(F("SSD1306 allocation failed"));
38
    for(;;); // Don't proceed, loop forever
39
  }
40
41
}
42
43
44
void updateDisplay() {
45
  display.clearDisplay();
46
  
47
  display.setTextSize(1);
48
  display.setTextColor(SSD1306_WHITE);
49
  display.setCursor(0,0);
50
  display.println(F("SolarInfo v1.0"));
51
52
  display.setCursor(0,17);
53
  display.println(F("PV In:"));
54
55
  display.setCursor(65,17);
56
  display.println(pvval);
57
58
  display.setCursor(100,17);
59
  display.println(F("Volt"));
60
61
  display.setCursor(0,34);
62
  display.println(F("PV Store:"));
63
64
  display.setCursor(65,34);
65
  display.println(batval);
66
67
  display.setCursor(100,34);
68
  display.println(F("Volt"));
69
70
  display.setCursor(0,54);
71
  display.println(F("PV"));
72
73
  display.setCursor(57,54);
74
  display.println(F("Store"));
75
76
  display.display();
77
}
78
79
void loop() {
80
  pvAdcVal = analogRead(pvPin);
81
  batAdcVal = analogRead(batPin);
82
83
  pvval = (pv_volt_per_step * pvAdcVal) / 100;
84
  batval = (bat_volt_per_step * batAdcVal) / 100;
85
86
  updateDisplay();
87
  //pixels.setPixelColor(0, pixels.Color(20,0,0)); // Moderately bright green color.
88
  //pixels.show();
89
  delay(1000);
90
  
91
}

Erklärung:

Ich setze zuerst Feste werte
1
#define pv_max_volt 40
2
#define pv_adc_steps 1023

Danach berechne ich darauf viel V ein ADC Wert beträgt
1
float pv_volt_per_step = (pv_max_volt * 100 ) / pv_adc_steps;

In der Loop dann die Berechnung
1
pvAdcVal = analogRead(pvPin);
2
3
pvval = (pv_volt_per_step * pvAdcVal) / 100;

Wenn der ADC Wert nun 0 ist, dann bekomme ich auch 0V. Allso richtig. 
Ist dieser aber max, also 1023, dann ist pvval 30.69 Volt

durch #define pv_max_volt 40 müsste das ganze jedoch 40Volt sein.. 
Irgendwie, gehen da 10 Volt "verloren".

Kann mir wer da helfen?

: Verschoben durch Moderator
von M. D. (derdiek)


Lesenswert?

Lass dir am besten mal die Zwischenergebnisse der Berechnungen ausgeben. 
Dann findest du den Fehler bestimmt.

von Udo S. (urschmitt)


Lesenswert?

Tobi S. schrieb:
> float pv_volt_per_step = (pv_max_volt * 100 ) / pv_adc_steps;

Bei der Berechnung macht der Compiler die Berechnung als Ganzzahl, und 
schneidet alles nach dem Komma ab. Erst dann wird die Zahl bei der 
Zuweisung in eine Float gewandelt.
Gib mal pv_volt_per_step aus.

Lösung dürfte sein ein Define als float zu definieren:
#define pv_max_volt 40.0

von Tobi S. (tobias_n524)


Lesenswert?

Udo S. schrieb:
> Tobi S. schrieb:
>> float pv_volt_per_step = (pv_max_volt * 100 ) / pv_adc_steps;
>
> Bei der Berechnung macht der Compiler die Berechnung als Ganzzahl, und
> schneidet alles nach dem Komma ab. Erst dann wird die Zahl bei der
> Zuweisung in eine Float gewandelt.
> Gib mal pv_volt_per_step aus.
>
> Lösung dürfte sein ein Define als float zu definieren:
> #define pv_max_volt 40.0

Super perfekt. Genau das war es. Super. vielen vielen Dank.

von Jens M. (schuchkleisser)


Lesenswert?

Wenn man die Berechnung in Integer packt, ist das ganze wesentlich 
schneller und kleiner, wenn der Controller nicht gerade eine FPU hat.
Dazu kann man die Skala ja vorausberechnen (40/1023=0,0391) und den Wert 
z.B. als 391 speichern, passt in einen uint_16 (geht man über 
"scale=40*10000/1023" muss es ein uint_32 sein!).
Dann "ADC*scale", was maximal 1023*391=399993 ergibt (ein uint_32), was 
man einfach als "39,9993" mit eingefügtem Komma ausgeben kann.
Wenn das zuviele Nachkommastellen sind, kann man die auch einfach 
weglassen, eine Division ist unnötig.

: Bearbeitet durch User
von Torsten B. (butterbrotstern)


Lesenswert?

Wenn Du auf float umsteigst, kannst Du die /100 und *100 gleich 
weglassen.
Entweder alles mit Integern - oder mit Floats, dann aber richtig.

Ich würde - da 40000/1024=39,0625 - alles int16 in Millivolt rechnen.
Oder (wenn die 40V-full-scale-Auflösung nicht ausreicht) mit int32 in 
Mikrovolt: 40000000/1024=39062,5

Außerdem sind beide xxx_adc_steps  1024 und nicht 1023.

: Bearbeitet durch User
von Harald K. (kirnbichler)



Lesenswert?

Das muss ja wirklich unglaublich schwierig sein.

Muss man diesen Text jetzt auch in "einfacher Sprache" ergänzen?

von Klaus H. (hildek)


Lesenswert?

Harald K. schrieb:
> Das muss ja wirklich unglaublich schwierig sein.

Ja, ist es wohl. Ich begegne ähnlichem auch in anderen Foren.

von Joachim B. (jar)


Lesenswert?

Jens M. schrieb:
> Wenn man die Berechnung in Integer packt, ist das ganze wesentlich
> schneller und kleiner, wenn der Controller nicht gerade eine FPU hat.

und wenn es platzsparender sein soll, gleich alles in centiVolt mit 2 
Nachkommerstellen rechen oder in deziVolt mit einer Nachkommastelle.
Milli- oder Micro-volt gibt eh der ADC meist nicht her. (vorgetäusche 
Auflösung)
Rechnen tut man mit Konstante als int16 +- oder uint16 und ausgeben 
durch Komma einsetzen als Text.

: Bearbeitet durch User
von Torsten B. (butterbrotstern)


Lesenswert?

Joachim, im Prinzip hast Du recht, aber um auf einigermaßen genaue 
Zenti-Volt zu kommen, muss man den Faktor, mit dem man den ADC-Wert 
multipliziert, genauer angeben, als dass 16 Bit beim Produkt ausreichen 
(z.B. (1023 * 313) >> 3 = 40024  braucht 19 Bits).
Man kann also, um z.B. auf 40V bei ADC 1023 zu kommen, mit 39 
multiplizieren, das trifft es ziemlich genau, aber die umliegenden 
Schritte liegen dann 2,5% auseinander:
1023 * 38 = 38874
1023 * 39 = 39897
1023 * 40 = 40920
Man kann das Ergebnis nach der Multiplikation korrigieren, indem man den 
ADC nach rechts schiebt, um "Nachkomma-Bits" zu erlangen:
1023 * 39 + (1023 >> 3) = 40024
Dann könnte man gleich ganz ohne Multiplikation schreiben:
wert  = adc  << 3; // *8
wert += wert << 2; // *32
wert -= adc;       // *(-1)
wert += adc >> 3;  // *0,125
Was einer Mul mit 39,125 (bei 1023 40024, also 40,02) entspricht. So 
hätte man es früher gemacht, der Code ist halt nicht so leicht wartbar. 
Aber noch effektiver (kleiner und schneller) kann man es nicht rechnen 
(16 shifts und 6 adds  mit 8 Bit).

: Bearbeitet durch User
von Jens M. (schuchkleisser)


Lesenswert?

Torsten B. schrieb:
> Was einer Mul mit 39,125 (bei 1023 40024, also 40,02) entspricht.

Oh, der ist auch schick. Schnellst und kürzest für dieses 
Multiplikation, würd ich sagen. Gefällt mir!

von Rainer W. (rawi)


Lesenswert?

Joachim B. schrieb:
> und wenn es platzsparender sein soll, gleich alles in centiVolt mit 2
> Nachkommerstellen rechen oder in deziVolt mit einer Nachkommastelle.
> Milli- oder Micro-volt gibt eh der ADC meist nicht her.

cV mit zwei Nachkommastellen kannst du besser als 100µV Ganzzahl 
rechnen.
Das spart die Float-Rechnerei, deren Dynamik wegen der begrenzten 
Auflösung eines ADC gar nicht sinnvoll, allenfalls bei Denkfaulheit 
bequem ist ;-)

Schon bei einem 10Bit ADC, bei man ein Ergebnis mit 3 gültigen Stellen 
erwartet, reicht die direkte Angabe des LSB in Einheiten von 100µV 
allerdings nicht.

von Falk B. (falk)


Lesenswert?

Das Ganze steht seit Jahren im Artikel Festkommaarithmetik.

von Joachim B. (jar)


Lesenswert?

Falk B. schrieb:
> Das Ganze steht seit Jahren im Artikel Festkommaarithmetik.

man muß nicht immer alles gelesen haben, manches konnte man sich auch 
schon vorher selbst erarbeiten.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jens M. schrieb:
> Wenn man die Berechnung in Integer packt, ist das ganze wesentlich
> schneller und kleiner,

Nicht automatisch, z.B. auf ATmega328:

uint32_t Division: ca. 615 Cycles
float Division: weniger als 500 Cycles

von Weingut P. (weinbauer)


Lesenswert?

und wenn man mit dem Kehrwert multiplizieren würde?

von Joachim B. (jar)


Lesenswert?

Johann L. schrieb:
> Nicht automatisch, z.B. auf ATmega328:
>
> uint32_t Division: ca. 615 Cycles
> float Division: weniger als 500 Cycles

und was ist mit flash und sram Verbrauch, manchmal ist es nicht die 
Geschwindigkeit die bremst, sondern das der SRAM ausgeht oder weniger in 
den FLASH paßt.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Joachim B. schrieb:
> Johann L. schrieb:
>> Nicht automatisch, z.B. auf ATmega328:
>>
>> uint32_t Division: ca. 615 Cycles
>> float Division: weniger als 500 Cycles
>
> und was ist mit flash und sram Verbrauch, manchmal ist es nicht die
> Geschwindigkeit die bremst, sondern das der SRAM ausgeht oder weniger in
> den FLASH paßt.

Ja natürlich.  Ich wollte nur zeigen, dass "float ist immer und überall 
schlechter als int" nicht automatisch zutrifft.  Wirklich nachmessen tut 
wohl keiner.

Zum Beispiel ist u64 Division in manchen Beispielen schneller als u32 
Division für die gleichen Eingaben (mit avr-gcc).

von Norbert (der_norbert)


Lesenswert?

Joachim B. schrieb:
> und was ist mit flash und sram Verbrauch, manchmal ist es nicht die
> Geschwindigkeit die bremst, sondern das der SRAM ausgeht oder weniger in
> den FLASH paßt.

Ja und oftmals hat man SRAM im Überfluß, FLASH im Überfluß, CPU cycles 
im Überfluß. Und nutzt FP-Arithmetik weil's im Forum halt so gefordert 
wird.

von Norbert (der_norbert)


Lesenswert?

Johann L. schrieb:
> Wirklich nachmessen tut wohl keiner.

Doch, ich zum Beispiel schon vor langer Zeit. Darum nutze ich FP auch 
nur wenn es mir wirklich etwas bringt.

von Joachim B. (jar)


Lesenswert?

Norbert schrieb:
> Ja und oftmals hat man SRAM im Überfluß, FLASH im Überfluß, CPU cycles
> im Überfluß. Und nutzt FP-Arithmetik weil's im Forum halt so gefordert
> wird.

und manchmal hat man sich auf dem 328p mühsam sparsame eigene Routinen 
geschaffen und ist froh das man diese auch auf einem ESP32 nutzen kann, 
wozu als den Überfluß SRAM und FLASH ohne Not nutzen?

: Bearbeitet durch User
von Norbert (der_norbert)


Lesenswert?

Joachim B. schrieb:
> und manchmal hat man sich auf dem 328p mühsam sparsame eigene Routinen
> geschaffen und ist froh das man diese auch auf einem ESP32 nutzen kann,
> wozu als den SRAM und FLASH ohne Not nutzen?

Aha, jetzt also die Strohmann Argumente.

von Rainer W. (rawi)


Lesenswert?

Johann L. schrieb:
> Nicht automatisch, z.B. auf ATmega328:
>
> uint32_t Division: ca. 615 Cycles

Deswegen ist es klug, die Division ganz zu vermeiden. Wenn der Divisor 
eine 2er-Potenz ist, machen die meisten Compiler das von sich aus und 
verwenden statt dessen eine Shift-Operation.

von Falk B. (falk)


Lesenswert?

Rainer W. schrieb:
>> uint32_t Division: ca. 615 Cycles
>
> Deswegen ist es klug, die Division ganz zu vermeiden. Wenn der Divisor
> eine 2er-Potenz ist, machen die meisten Compiler das von sich aus und
> verwenden statt dessen eine Shift-Operation.

In der Tat. Steht auch alles im Artikel Festkommaarithmetik.

von Michi S. (mista_s)


Lesenswert?

Joachim B. schrieb:
> wozu als den Überfluß SRAM und FLASH ohne Not nutzen?

Weil man für nicht genutztes selbiges trotzdem kein Geld zurück kriegt.

von Εrnst B. (ernst)


Lesenswert?

Um noch die Grundlagen für das eigentliche Verständnisproblem 
nachzureichen:

Ein 10-Bit ADC unterteilt seinen Messbereich in 1024 gleichgroße 
Bereiche.
Der Rückgabewert von "analogRead" ist die "Nummer" des Bereichs, in dem 
der Messwert liegt.
Die übliche (im Datenblatt genannte) Umrechnungs-Formel (mit 1024) 
berechnet daraus die Spannung für die "untere Kante" vom Bereich. Nicht 
den Mittelpunkt vom Bereich und auch nicht die obere Kante.

Tobi S. schrieb:
> Wenn der ADC Wert nun 0 ist, dann bekomme ich auch 0V. Allso richtig.

Nein.

ADC-Wert von 0 bedeutet: Spannung liegt zwischen 0V und 0.039V.
ADC-Wert von 1 bedeutet: Spannung liegt zwischen 0.039V und 0.078V
...
ADC-Wert von 1023 bedeutet: Spannung ist größer als 39.96V

Wenn du nun so tust, als hätte dein 10-Bit ADC nur 9.9986 Bits (also: 
Als würde der nur 1023 Bereiche unterscheiden können) stimmt die 
Rechnung halt nicht mehr mit der Hardware überein, du kriegst aber an 
den Grenzen schönere Zahlen angezeigt...

Kann man machen, wenn man sich im klaren ist was man tut.

von Peter D. (peda)


Lesenswert?

Jens M. schrieb:
> Wenn man die Berechnung in Integer packt, ist das ganze wesentlich
> schneller und kleiner, wenn der Controller nicht gerade eine FPU hat.

Nur kann man sich oft genau 0,nix dafür kaufen.
Ich hatte mich auch lange mit Rundungsfehlern und Überläufen abgeplagt. 
Dann hatte ich die Nase voll und benutze einfach float.
Selbst bei Software-FP ist das fast nie ein Flaschenhals. Z.B. für die 
Anzeige von Meßwerten ist eine Rate von 2..5 Werten/s ergonomisch. 
Darüber lacht jede FP-Lib nur.

von Gerald K. (geku)


Lesenswert?

Dein Problem liegt in der Berechnung von pv_volt_per_step. Aktuell 
berechnest du pv_volt_per_step wie folgt:
1
float pv_volt_per_step = (pv_max_volt * 100 ) / pv_adc_steps;

Dabei teilst du pv_max_volt durch pv_adc_steps, jedoch multiplizierst du 
es zuerst mit 100, was zu einem falschen Wert führt. Du solltest einfach 
pv_max_volt durch pv_adc_steps teilen, ohne die Multiplikation mit 100:
1
float pv_volt_per_step = pv_max_volt / pv_adc_steps;

Hier ist die angepasste Version deines Codes:
1
#include <SPI.h>
2
#include <Wire.h>
3
#include <Adafruit_GFX.h>
4
#include <Adafruit_SSD1306.h>
5
6
#define SCREEN_WIDTH 128
7
#define SCREEN_HEIGHT 64
8
9
#define OLED_RESET -1
10
#define SCREEN_ADDRESS 0x3D
11
12
#define pv_max_volt 40
13
#define pv_adc_steps 1023
14
15
#define bat_max_volt 14
16
#define bat_adc_steps 1023
17
18
float pv_volt_per_step = pv_max_volt / pv_adc_steps;
19
float bat_volt_per_step = bat_max_volt / bat_adc_steps;
20
21
int pvPin = A1;
22
int batPin = A2;
23
int pvAdcVal = 0;
24
int batAdcVal = 0;
25
26
float pvval = 0;
27
float batval = 0;
28
29
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
30
31
void setup() {
32
  Serial.begin(115200);
33
  // pixels.begin(); // Auskommentiert, da nicht benötigt
34
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
35
    Serial.println(F("SSD1306 allocation failed"));
36
    for(;;); // Don't proceed, loop forever
37
  }
38
}
39
40
void updateDisplay() {
41
  display.clearDisplay();
42
43
  display.setTextSize(1);
44
  display.setTextColor(SSD1306_WHITE);
45
  display.setCursor(0, 0);
46
  display.println(F("SolarInfo v1.0"));
47
48
  display.setCursor(0, 17);
49
  display.println(F("PV In:"));
50
51
  display.setCursor(65, 17);
52
  display.println(pvval);
53
54
  display.setCursor(100, 17);
55
  display.println(F("Volt"));
56
57
  display.setCursor(0, 34);
58
  display.println(F("PV Store:"));
59
60
  display.setCursor(65, 34);
61
  display.println(batval);
62
63
  display.setCursor(100, 34);
64
  display.println(F("Volt"));
65
66
  display.setCursor(0, 54);
67
  display.println(F("PV"));
68
69
  display.setCursor(57, 54);
70
  display.println(F("Store"));
71
72
  display.display();
73
}
74
75
void loop() {
76
  pvAdcVal = analogRead(pvPin);
77
  batAdcVal = analogRead(batPin);
78
79
  pvval = pv_volt_per_step * pvAdcVal;
80
  batval = bat_volt_per_step * batAdcVal;
81
82
  updateDisplay();
83
  // pixels.setPixelColor(0, pixels.Color(20,0,0)); // Auskommentiert, da nicht benötigt
84
  // pixels.show(); // Auskommentiert, da nicht benötigt
85
  delay(1000);
86
}

Durch diese Anpassung wird pvval bei einem ADC-Wert von 1023 korrekt 40 
Volt ergeben.

von Εrnst B. (ernst)


Lesenswert?

Gerald K. schrieb:
> Durch diese Anpassung wird pvval bei einem ADC-Wert von 1023 korrekt 40
> Volt ergeben.

40V ist an der Stelle nicht korrekt. 40V ist der aufgerundete 
Wunsch-Anzeigewert.
Der ADC hat garkeine Möglichkeit, 40V zu erkennen, die Hardware gibt nur 
eine Erkennung "Größer als 39.96V" her.

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.