Wie bereits erwähnt, bin ich befasse ich mich momentan mit der Thematik Spannung mit dem Arduino ProMini möglichst genau zu messen. Ich verwende für meine Nodes sehr gerne einen Arduino ProMini in der 3.3 Volt Variante. Dieser verwendet einen ATmega328p, weshalb ich auch nur auf diesen Chip direkt eingehen werde.
Den Profis unter euch ist das hier dargestellt sicherlich nicht neu, da es vielerorts bereits erwähnt wird. Ich hatte damit bereits mal Versuche gestartet, bin dabei aber komplett vom Weg abgekommen.
Warum sind genaue Spannungsmessungen wichtig?
Viele Sensoren liefen an ihrem Ausgang eine Spannung, proportional zur messenden Größe. Wir müssen also eine Spannung messen um auf den eigentlich gemessenen Wert umrechnen zu können. Je genauer also unsere Spannungsmessung ist, desto genauer ist auch unser Messergebnis. Wer also genau messen möchte, ist auch darauf angewiesen.
Was ist jedoch das Problem bei Spannungsmessungen?
Normalerweise verwenden wir analogRead() um die Werte an einem analogen Eingang auszulesen. Dabei wird die Eingangsspannung in Werten zwischen 0 und 1023 ausgegeben, in Abhängigkeit von der Versorgungsspannung Vcc. Daraus lässt sich dann auch die anliegende Spannung berechnen:
int ADC;
float Voltage;
ADC = analogRead(A0);
Voltage = (ADC / 1024.0) * 3.3;
Hier liegt das eigentliche Problem. Meistens kennen wir die Versorgungsspannung Vcc nicht genau und nehmen meistens den ideellen Wert 3,3V an. Damit verfälschen wir dann unsere Messergebnisse. Ich nutze zwar Spannungsregler um meine Nodes zu betreiben, aber auch die haben eine 5% Toleranz. So können aus 3,3V schnell 3,4V werden. Dazu kommt noch das die Spannung schwanken kann. Wir müssen also immer unsere Versorgungsspannung wissen, sobald wir analogRead() nutzen.
Präziser messen mit der Bandgab Voltage
Glücklicherweise besitzt der 328p eine eigene 1,1V Referenzspannung, die so genannte Bandgap Voltage. Diese ist sehr genau und kaum abhängig von Temperatur oder Versorgungsspannung. Durch diese Referenz können wir relativ genau unsere Versorgungsspannung messen und eben auch die Spannung an unseren Eingängen berechnen. Folgernder Code hilft dabei:
long readVcc() {
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(10); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
result = 1125300L / result; // Back-calculate AVcc in mV
return result;
}
Unser Code zur Berechnung der Spannung kann dann so aussehen:
int ADC;
float Vcc;
float Voltage;
Vcc = readVcc()/1000.0;
ADC = analogRead(A0);
Voltage = (ADC / 1024.0) * Vcc;
Damit sind wir eine genauen Messung schon um einiges näher gekommen.
Darf es noch genauer sein?
Die Bandgap Voltage kann zwischen 1.0 und 1.2 V liegen, ist dort aber stabil. Wir können nun unseren ideellen Wert 1125300L noch anpassen um diese Schwankung zu kompensieren. Dazu müssen wir, wie oben beschrieben, unsere Versorgungsspannung messen und zusätzlich noch mit einem Multimeter. Anschließend können wir den Korrekturfaktor berechnen:
Vmultimeter / Varduino = Korrekturfaktor
Den Faktor multipliziert ihr nun mit 1125300 und tauscht den Wert in readVcc() aus. Dieser Faktor ist für jeden Chip einmalig, ihr müsst ihn also für jeden neu bestimmen. Der Lohn eurer Arbeit ist jedoch eine noch genauere Spannungsmessung mit eurem ProMini. Der ganze Sketch kann so aussehen:
long readVcc() {
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(10); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
result = 1125300L / result; // Back-calculate AVcc in mV
return result;
}
void setup() {
Serial.begin(9600);
}
void loop() {
Serial.println( readVcc(), DEC );
delay(1000);
}
Das ganze Thema ist natürlich nicht meinem Hirn entsprungen, sondern wurde z.B. hier bereits beschrieben.
Nun nochmals alles zusammen
Wenn man nun alles zusammen nimmt, kann ein Sketch zum auslesen von A0 so aussehen:
float Vcc;
float Vbatt;
int AdcBatt;
long readVcc() {
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(10); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
result = 1125300L / result; // Back-calculate AVcc in mV
return result;
}
void setup() {
Serial.begin(9600);
}
void loop() {
Vcc = readVcc()/1000.0;
AdcBatt = analogRead(A0);
Vbatt = (AdcBatt / 1024.0) * Vcc;
Serial.println(Vbatt,2);
delay(1000);
}
Es gibt auch noch Quellen, die empfehlen auf den Wert ADC noch 0,5 aufzuaddieren um möglichst genau zu sein:
float Voltage = ((ADC + 0.5) / 1024.0) * Vcc;
Um die Werte noch etwas zu glätten, kann man nun auch noch den Mittelwert aus mehreren Messungen berechnen. Dafür habe ich alles um die Funktion readVoltage() ergänzt:
float Vcc;
float Vbatt;
float AdcBatt;
long readVcc() {
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(10); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
result = 1125300L / result; // Back-calculate AVcc in mV
return result;
}
long readVoltage(uint8_t pin){
long result;
int i = 0;
for (i = 0; i < 10; i++) {
AdcBatt += analogRead(pin);
delay(2);
}
AdcBatt += (i+1) * 0.5;
AdcBatt /= (i+1);
result = AdcBatt;
return result;
}
void setup() {
Serial.begin(9600);
}
void loop() {
Vcc = readVcc()/1000.0;
AdcBatt = readVoltage(0);
Vbatt = (AdcBatt / 1024.0) * Vcc;
Serial.println(Vbatt, 2);
delay(1000);
}
Was bringt es?
Ich habe einen kleinen Testaufbau zusammengestellt und dabei die einzelnen Messwerte ermittelt. Ich messe dabei die Spannung einer LiIon-Zelle über einen Spannungsteiler. :
Art der Messung | Spannung |
Batterie direkt | 3,63 |
Messpunkt Spannungsteiler | 1,81 (3,62) |
Messung über Vcc=3,3V | 3,54 |
Messung über readVcc() | 3,60 |
Messung über readVcc() angepasst | 3,62 |
Anfänglich haben wir noch 0,09V unterschied und später nur noch 0,01V. Das ist jedenfalls schon ein großer Unterschied, der viel ausmachen kann. Betrachtet man auch die Spannung am Messpunkt des Spannungsteilers, sind die Werte sogar genau.
Vielen Dank für die tolle, verständliche Zusammenstellung.
So habe auch ich es tatsächlich kapiert. Bin gerade dabei einen kleinen Datenlogger zu bauen, um mal meine, in die Jahre gekommenen Akkus zu testen.
In der Funktion „readVoltage“ ist, glaube ich ein kleiner Fehler.
Der Mittelwert wird mit einem „i“ zu wenig gerechnet.
Ich habe die Zeile „AdcBatt /= i;“ durch „AdcBatt /= (i + 1);“ ersetzt.
Das Rauschen hält sich auch in Grenzen.
Für den Privatgebrauch ist das wohl okay.
https://imgur.com/CSi9QD9
Schöne Grüße,
Patrick
Hallo Patrick,
danke für den Hinweis, ich werde es später anpassen. In der Zeile davor ist der Fehler auch drin.
Grüße,
Björn
Hallo Björn,
ich der Zeile davor habe ich eigentlich nur die “ * 0.5″ entfernt.
Komme so näher an meine Referenz.
Also im Prinzip sieht es bei mir jetzt so aus:
long readVoltage(uint8_t pin){
long result;
int i = 0;
for (i = 0; i < 10; i++) {
AdcBatt += analogRead(pin);
delay(5);
}
AdcBatt += i;
AdcBatt /= (i + 1);
result = AdcBatt;
return result;
}
Schöne Grüße,
Patrick
Hallo,
wie hoch ist dein Spannungsteiler.
Also was für Wiederstandswerte?
Ich hab das selbe vor für Li ion Akkus.
Danke
Ich nehme immer zwei 10K oder zwei 100K Widerstände. Dadurch muss ich einfach die gemessene Spannung mal zwei rechnen. Somit kann ich bis 6,6 Volt messen.
Grüße,
Björn