I’ve been thinking about a way to monitor the battery supply voltage of my weather station remote node, as I was thinking of buying some LiFePO4 or LiPo batteries which don’t cope well with over-discharging.

Its all a bit moot as in the end I decided to stick with regular NiMH/NiCd rechargeable batteries and get some of Pololu’s new miniature boost regulators in 3.3v and 5.0v varieties. They’re pretty nifty as not only can they make 3.3/5.0v from as little as 0.5v, but they can also automatically regulate down from 5.5v 1.2A, so not only can you run off of two AA batteries, you could also run off of four, all from about 1cm2!

Anyway, back to measuring voltages. I found that the AVR chips have an internal voltmeter that compares its 1.1v analog reference with the AVcc:

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    // Read 1.1V reference against AVcc
    // set the reference to Vcc and the measurement to the internal 1.1V reference
    #if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
        ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    #elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
        ADMUX = _BV(MUX5) | _BV(MUX0);
    #elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
        ADMUX = _BV(MUX3) | _BV(MUX2);
    #else
        ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    #endif 

    delay(2); // Wait for Vref to settle
    ADCSRA |= _BV(ADSC); // Start conversion
    while (bit_is_set(ADCSRA,ADSC)); // measuring

    uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH 
    uint8_t high = ADCH; // unlocks both

    long result = (high<<8) | low;
    result = 1126400L / result; // Calculate Vcc (in mV);

    Serial.println(result);
    delay(1000);
}

The problem there is that its measuring the internal voltage across the chip (which could be the output from a regulator) not the supply voltage to the circuit. For that we need to make a voltage divider from a couple of resistors, to lower the maximum probably voltage to under 1.1v so we can compare it again to Aref. The resistors I chose were R2=4.7k and R1=10k, VIN=3.3v (from a regulated supply, the Mega was powered via USB) which gave me VOUT=1.055v. I also tried 470k/1M with a 100nF smoothing capacitor, which would drain the battery less but be less accurate.

The problem I found at this point was like all things Arduino, the hardware is wildly inaccurate! The 1.1v Aref on my Mega2560r3 for example is more like 1.09v, so I had to fix that in software, which then gave me spot-on results:

// vout = rb/(ra+rb)*vin
// 1.055 = 4.7k/(10k+4.7k)*3.3

// for mega2560, recalibrate for 328p
float aref_fix = 1.09;

void setup()
{
    // init serial monitor
    Serial.begin(9600);
    
    // choose 1.1v aref based on chip
    #if defined(__AVR_ATmega2560__)
        analogReference(INTERNAL1V1);
    #else
        analogReference(INTERNAL);
    #endif 
}

void loop()
{
    // read from voltage divider
    int reading = analogRead(A0);
    Serial.print("reading: ");
    Serial.println(reading);

    // convert 0-1023 reading to fraction of aref
    float vout = (reading*aref_fix) / 1023.0;
    Serial.print("vout: ");
    Serial.println(vout);

    // vin to divider = (r2+r1)/r2 * vout of divider
    float vin = (14.7/4.7)*vout;
    Serial.print("vin: ");
    Serial.println(vin);
    
    delay(2000);
}

So I’m going to re-wire, or possibly redesign from scratch my weather node to be powered from 2xAA (giving 2.4-2.8v maximum approximately) and the boost converter instead of a coincell, and also record Vbatt in the packet I send to the Pi.